第二章 Socket用法詳解
在客戶端和服務器端進行通信時,客戶端需要主動創建與服務器端連接的Socket,服務器端收到了客戶端的連接請求後,也會創建一個與客戶端連接的Socket。Socket可以看做是通信連接兩端的收發器,服務器與客戶端都通過Socket來收發數據。
2.1 構造Socket
import java.io.IOException;
import java.net.Socket;
public class PortScanner {
public static void main(String[] args) {
String host = "localhost";
new PortScanner().scan(host);
}
public void scan(String host){
Socket socket = null;
for(int port = 1024;port < 1600;port++){//檢測1024-1600端口
try {
socket = new Socket(host, port);
System.out.println("有一個服務器在"+port+"端口上運行");
} catch (IOException e) {
System.out.println("端口"+port+"正在被佔用");
}finally{
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}}}
2.1.1 設定等待建立連接的超時時間
Socket socket = new Socket();
SocketAddress remoAddress = new InetSocketAddress("localhost", 8000);
socket.connect(remoAddress, 60000);
以上代碼用於連接本地的8000端口,如果1分鐘內連接成功,則connect方法順利返回;如果在1分鐘內出現某些異常,則拋出該異常,如果連接時間超過1分鐘,則既沒有連接成功也沒有拋出其他出錯的異常,那麼會拋出一個SocketTimeoutException,超時異常。Socket的connect(SocketAddress endpoint,int timeout)方法負責連接服務器,參數endpoint表示服務器地址,參數timeout表示設置的超時時間,單位爲毫秒。2.1.2 設定服務器的地址
public Socket(InetAddress address, int port) throws IOException
address
- IP 地址。port
- 端口號。
public Socket(String host, int port) throws UnknownHostException, IOException
host
- 主機名,或者爲null
,表示回送地址。
port
- 端口號。
2.1.3 設定客戶端地址
2.1.4 客戶端連接服務器時可能拋出的異常
2.2 獲取Socket的信息
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class HTTPClient {
String host = "www.baidu.com";
int port = 80;
Socket socket =null;
public void createSocket()throws Exception{
socket = new Socket(host,port);
}
public void communicate()throws Exception{
String str ="GET /index.html HTTP/1.1\n"
+"Accept: */*\n"
+ "Accept-Language: zh-cn;q=0.5\n"
+ "User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; CIBA; .NET CLR 2.0.50727)\n"
+ "Host: www.baidu.com\n" + "Connection: Keep-Alive\n"
+ "\n";
//上面都是HTTP的一些頭部信息
//發出HTTP請求
OutputStream socketOut = socket.getOutputStream();
socketOut.write(str.getBytes());
socket.shutdownOutput();//關閉輸出流
//接受響應結果
InputStream socketIn = socket.getInputStream();
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] buff = new byte[2048];
int len = -1;
while((len=socketIn.read(buff))!=-1){
buffer.write(buff,0,len);
}
System.out.println(new String(buffer.toByteArray()));
socket.close();
}
public static void main(String[] args) throws Exception {
HTTPClient client = new HTTPClient();
client.createSocket();
client.communicate();
}
}
以上代碼用於訪問www.baidu.com/index.html網頁,並且接受從HTTP服務器發回的相應結果。 InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is,
"utf-8"));
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
2.3 關閉Socket
try {
socket = new Socket(host, port);
System.out.println("有一個服務器在"+port+"端口上運行");
} catch (IOException e) {
System.out.println("端口"+port+"正在被佔用");
}finally{
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.4 半關閉Socket
socketOut.write(str.getBytes());
socket.shutdownOutput();//關閉輸出流
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
public class Sender {
private String host = "localhost";
private int port = 8000;
private Socket socket;
private static int stopWay = 1;// 結束通信的方式
private final int NATURL_STOP = 1;// 自然結束
private final int SUDDEN_STOP = 2;// 突然終止程序
private final int SOCKET_STOP = 3;// 關閉socket,再結束程序
private final int OUTPUT_STOP = 4;// 關閉輸出流,再結束程序
public Sender() throws Exception {
socket = new Socket(host, port);
}
public static void main(String[] args) throws Exception {
stopWay = 2;
new Sender().send();
}
private PrintWriter getWriter(Socket socket) throws Exception {
OutputStream socketOut = socket.getOutputStream();
return new PrintWriter(socketOut, true);// true表示自動將中間緩存flush到接受數據端
}
private void send() throws Exception {
PrintWriter pw = getWriter(socket);
for (int i = 0; i < 20; i++) {
String msg = "hello:" + i;
pw.println(msg);
System.out.println("send:" + msg);
Thread.sleep(500);
if(i == 2){
if(stopWay == SUDDEN_STOP){
System.out.println("突然終止程序");
System.exit(0);
}else if(stopWay == SOCKET_STOP){
System.out.println("關閉socket,再結束程序");
System.exit(0);
}else if(stopWay == OUTPUT_STOP){
socket.shutdownOutput();
System.out.println("關閉輸出流,在結束程序");
break;
}
}
}
if(stopWay == NATURL_STOP){
socket.close();
}
}
}
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class Receiver {
private int port = 8000;
private ServerSocket serverSocket;
private static int stopWay = 1;// 結束通信的方式
private final int NATURL_STOP = 1;// 自然結束
private final int SUDDEN_STOP = 2;// 突然終止程序
private final int SOCKET_STOP = 3;// 關閉socket,再結束程序
private final int INPUT_STOP = 4;// 關閉輸入流,再結束程序
private final int SERVERSOCKET_STOP = 5;// 關閉ServerSocket,再結束程序
public Receiver() throws Exception{
serverSocket = new ServerSocket(port);
System.out.println("服務器已經啓動");
}
private BufferedReader getReader(Socket socket) throws Exception{
InputStream socketIn = socket.getInputStream();
return new BufferedReader(new InputStreamReader(socketIn));
}
public void receive() throws Exception{
Socket socket = null;
socket = serverSocket.accept();
BufferedReader br = getReader(socket);
for(int i = 0;i < 20;i++){
String msg = br.readLine();
System.out.println("receive:"+msg);
Thread.sleep(1000);
if(i == 2){
if(stopWay == SUDDEN_STOP){
System.out.println("突然終止程序");
System.exit(0);
}else if(stopWay == SOCKET_STOP){
System.out.println("關閉socket,再結束程序");
System.exit(0);
}else if(stopWay == INPUT_STOP){
socket.shutdownInput();
System.out.println("關閉輸入流,在結束程序");
break;
}else if(stopWay == SERVERSOCKET_STOP){
System.out.println("關閉ServerSocket,再結束程序");
serverSocket.close();
System.exit(0);
}
}
if(stopWay == NATURL_STOP){
socket.close();
serverSocket.close();
}
}
}
public static void main(String[] args) throws Exception {
stopWay = 3;
new Receiver().receive();
}
}
可以通過改變上面的topWay值來觀察不同的結果。2.5 設置Socket的選項
2.5.1 TCP_NODELAY選項
2.5.2 SO_RESUSEADDR選項
Socket socket = new Socket(); //此時Socket對象未綁定本地端口,並且未連接遠程服務器
socket.setReuseAddress(true);
SocketAddress remoteAddr = new InetSocketAddress("remotehost",8000);
socket.connect(remoteAddr);//連接遠程服務器,並且綁定匿名的本地端口
// 或者
// Socket socket = new Socket(); //此時Socket對象未綁定本地端口,並且未連接遠程服務器
// socket.setReuseAddress(true);
// SocketAddress localAddr = new InetSocketAddress("localhost",9000);
// SocketAddress remoteAddr = new InetSocketAddress("remotehost",8000);
// socket.bind(localAddr);//與本地端口綁定
// socket.connect(remoteAddr);//連接遠程服務器
2.5.3 SO_TIMEOUT選項
2.5.4 SO_LINGER選項
2.5.5 SO_RCVBUF選項
2.5.6 SO_SNDBUF選項
2.5.7 SO_KEEPALIVE選項
●設置該選項:public void setKeepAlive(boolean on) throws SocketException2.5.8 OOBINLINE選項
2.5.9 服務器類型選項
Socket socket = new Socket("www.baidu.com",80);
socket.setTrafficClass(0x04);
如果要請求多項服務參數用或運算,轉換成2進制再進行或運算 Socket socket = new Socket("www.baidu.com",80);
socket.setTrafficClass(0x04|0x10);//或的結果是倒數第三五位都是1
2.5.10 設定連接時間、 延遲和帶寬的相對重要性
public void setPerformancePreferences(int connectionTime,int latency,int bandwidth)
以上方法三個參數表示網絡傳輸數據的3項指標:
setPerformancePreferences(1,2,3);表示帶寬最重要,其次是最小延遲,最後爲最少連接時間。
2.6 發送郵件的SMTP客戶程序
SMTP命令 | 說明 |
HELO/EHLO | 指明郵件發送者的主機地址 |
MAIL FROM | 指明郵件發送者的郵件地址 |
PCRT TO | 指明郵件接收者的郵件地址 |
DATA | 表示接下來要發送的郵件內容 |
QUIT | 結束通信 |
HELP | 查詢服務器支持的命令 |
主要的SMTP應答碼
應答碼 | 說明 |
214 | 幫助信息 |
220 | 服務就緒 |
221 | 服務關閉 |
250 | 郵件操作完成 |
354 | 開始輸入郵件內容,以"."結束 |
421 | 服務未就緒,關閉傳輸通道 |
501 | 命令參數格式錯誤 |
502 | 命令不支持 |
503 | 錯誤的命令序列 |
504 | 命令參數不支持 |
Server>220 smtp.mydomain.com SMTP service ready
Client>HELO ANGEL
Server>250 smtp.mydomain.com Hello ANGEL,pleased to meet you.
Client>MAIL FROM:<[email protected]>
Server>250 sender<[email protected]> OK
Client>RCPT TO:<[email protected]>
Server>250 recipient<[email protected]> OK
Client> DATA
Server>354 Enter mail,end with "." on a line by itself
Client> Subject:hello from haha
hi,I miss you very much
Client>.
Server>250 message sent
Client>QUIT
Server>221 goodbye
以上是一次SMTP的流程演示,我在windows下用telnet不知道爲什麼不行,在linux下用telnet應該可以,下面用java代碼來實現一個郵件發送功能
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import sun.misc.BASE64Encoder;
public class MailSender {
private String smtpServer = "smtp.163.com";//SMTP郵件服務器的主機名稱
private int port = 25;
public static void main(String[] args) {
Message msg = new Message("這裏寫你的163郵箱", "所要發送的目的郵箱", "hello", "hi,I miss you very much.");
new MailSender().sendMail(msg);
}
private PrintWriter getWriter(Socket socket) throws IOException{
OutputStream socketOut = socket.getOutputStream();
return new PrintWriter(socketOut,true);//true表示自動將中間緩存flush到接受數據端
}
private BufferedReader getReader(Socket socket) throws IOException{
InputStream socketIn = socket.getInputStream();
return new BufferedReader(new InputStreamReader(socketIn));
}
public void sendMail(Message msg){
Socket socket = null;
try {
socket = new Socket(smtpServer,port); //連接到郵件服務器
BufferedReader br = getReader(socket);
PrintWriter pw = getWriter(socket);
String localhost = InetAddress.getLocalHost().getHostName(); //客戶主機的名字
String username = ""; //寫你的163郵箱賬戶
String password = ""; //寫你的密碼
//對用戶名和密碼進行Base64編碼
username = new BASE64Encoder().encode(username.getBytes());
password = new BASE64Encoder().encode(password.getBytes());
sendAndReceive(null,br,pw); //僅僅是爲了接收服務器的響應數據
sendAndReceive("HELO "+localhost, br, pw);
sendAndReceive("AUTH LOGIN", br, pw); //認證命令
sendAndReceive(username, br, pw); //用戶名
sendAndReceive(password, br, pw); //密碼
sendAndReceive("MAIL FROM:<"+msg.from+">", br, pw);
sendAndReceive("RCPT TO:<"+msg.to+">", br, pw);
sendAndReceive("DATA", br, pw); //接下來開始發送郵件內容
pw.println(msg.data);
System.out.println("Client>"+msg.data);
sendAndReceive(".", br, pw); //郵件發送完畢
sendAndReceive("QUIT", br, pw);
} catch (IOException e) {
e.printStackTrace();
} finally{
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**發送一行字符串,並且接受一行服務器的響應數據
* @throws IOException */
private void sendAndReceive(String str,BufferedReader br,PrintWriter pw) throws IOException {
if(str != null){
System.out.println("Client>"+str);
pw.println(str); //發送完str字符串後,還會發送"\r\n"
}
String response;
if((response = br.readLine())!=null){
System.out.println("Server>"+response);
}
}
}
class Message{
String from; //發送者的郵件地址
String to; //接收者的郵件地址
String subject; //郵件標題
String content; //郵件正文
String data; //郵件內容,包括郵件標題和正文
public Message(String from,String to,String subject,String content){
this.from = from;
this.to = to;
this.subject = subject;
this.content = content;
data = "Subject:"+subject+"\n\r"+content; //注意這裏是\n\r,\r\n不行不知道爲什麼
}
}
注意以上的BASE64加密那個類可能用不了,因爲那個包總導不進去,如果你是用myeclipse或是eclipse編輯該項目,你只需要右擊jre system library,buildpath將jre system library包給remove掉然後再右擊項目buildpath重新加進來就可以了。
Server>220 163.com Anti-spam GT for Coremail System (163com[20121016])
Client>HELO linux_v-PC
Server>250 OK
Client>AUTH LOGIN
Server>334 dXNlcm5hbWU6
Client>
Server>334 UGFzc3dvcmQ6
Client>
Server>235 Authentication successful
Client>MAIL FROM:<>
Server>250 Mail OK
Client>RCPT TO:<>
Server>250 Mail OK
Client>DATA
Server>354 End data with <CR><LF>.<CR><LF>
Client>Subject:hello
hi,I miss you very much.
Client>.
Server>250 Mail OK queued as smtp11,D8CowED5+l7WI01TXkN6AQ--.890S2 1397564374
Client>QUIT