java網路編程學習之路(2)

第二章  Socket用法詳解

在客戶端和服務器端進行通信時,客戶端需要主動創建與服務器端連接的Socket,服務器端收到了客戶端的連接請求後,也會創建一個與客戶端連接的Socket。Socket可以看做是通信連接兩端的收發器,服務器與客戶端都通過Socket來收發數據。


2.1 構造Socket

Socket的構造方法見下圖

除了第一個不帶參數的構造方法以外,其他的方法都試圖與服務器建立連接,如果連接成功則返回Socket對象;如果連接失敗則拋出相應的異常。

我們可以利用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 設定服務器的地址

我們都知道一臺主機既有當前系統的主機名,又有IP地址作爲地址的標識。一般情況下我們知道其中某一項就可以連接到服務器。
public Socket(InetAddress address,
              int port)
       throws IOException             
address - IP 地址。port - 端口號。
public Socket(String host,
              int port)
       throws UnknownHostException,
              IOException
host - 主機名,或者爲 null,表示回送地址。
port - 端口號。 

InetAddress類表示服務器的IP地址,InetAddress類提供了一系列的靜態工廠方法,用於構造自身的實例。
//返回本地主機的IP地址
InetAddress addr1 = InetAddress.getLocalHost();
//返回代表"222.34.5.7"的IP地址, 其實就是"222.34.5.7"
InetAddress addr2 = InetAddress.getByName("222.34.5.7");
//返回代表域名爲"www.baidu.com"的IP地址
InetAddress addr3 = InetAddress.getByName("www.baidu.com"); //結果:www.baidu.com/220.181.111.148

2.1.3  設定客戶端地址

在一個Socket對象中,既包含遠程服務器的IP地址和端口信息,也包含本地客戶端的IP地址和端口信息。默認情況下,客戶端的IP地址來自於客戶程序所在的主機,客戶端的端口則由操作系統隨機分配。上一篇日誌中寫的服務器端accept接受到的socket就包含客戶端的信息,可以輸出對應信息進行查看。
另外Socket類還有兩個構造方法顯示地設置客戶端的IP地址和端口。

//參數localAddr和localPort用來設置客戶端的IP地址和端口
詳解上面Socket構造方法第4個和最後一個

如果一個主機同時屬於兩個以上的網絡,它就有可能擁有兩個以上的IP地址。例如,一個主機在訪問外網時的IP地址是222.67.1.34,在局域網中的IP地址是112.5.4.3.假使這個主機的客戶程序希望和同一個局域網上的一個服務器程序(112.5.4.45:8000)進行通信。客戶端可以按照如下方式構造Socket對象:
InetAddress remoteAddr = InetAddress.getByName("112.5.4.45");
InetAddress localAddr = InetAddress.getByName("112.5.4.3");
Socket socket = new Socket(remoteAddr,8000,localAddr,6666);//客戶端使用6666端口

2.1.4   客戶端連接服務器時可能拋出的異常

● UnknowHostException:  無法識別主機的名字或IP地址
●ConnectException:沒有服務器進程監聽指定的端口,或者服務器進程拒絕連接

SocketTimeoutException:等待連接超時,上面有詳細介紹
BindException:無法把Socket對象與指定的本地IP地址或端口綁定

以上4中通常都是IOException的直接或間接子類:

2.2  獲取Socket的信息

關於Socket的相關信息,可以通過以下方法去獲取

● getInetAddress();獲得遠程服務器的IP地址

● getPort();獲得遠程服務器的端口

● getLocalPort();獲得客戶端本地的端口

● getLocalAddress();獲得客戶本地的IP地址

● getInputStream();獲得輸入流

● getOutputStream();獲得輸出流

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服務器發回的相應結果。
如果訪問頁面發回的數據較多,則用BufferedReader來接受讀取的數據並輸出
        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

當客戶端與服務器的通信結束時,應該及時關閉Socket,以釋放相應的資源。Socket有個close()就是用來關閉Socket對象的。需要注意的是,當一個Socket對象被關閉時,就不能再對其進行相關流操作,否則會拋出異常。
爲了確保關閉Socket的操作總是被執行,最後將其放到finally中

			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();
					}
				}
			}


Socket類提供了3個狀態檢測的方法
● isClosed():如果Socket已經連接到遠程主機,並且還沒有關閉,則返回true,否則返回false。
● isConnected():如果Socket曾經連接到遠程主機,則返回true,否則返回false。
●isBound():如果Socket已經與一個本地端口綁定,則返回true,否則返回false。
如果要判斷一個Socket對象是否處於連接狀態,可以採用以下方式:
boolean isConnected = socket.isConnected()&&!socked.isClosed();

2.4  半關閉Socket

進程A和進程B通過Socket通信,假設進程A輸出數據,進程B讀入數據。進程A如何告訴進程B數據已經輸出完了呢?有以下幾種方法可以使用:
(1).進程之間可以通過事先約定的一個特殊的字符串作爲結束標誌,如以字符串“bye”作爲結束標誌。在第一章的例子中用的就是這個方法。當進程B讀到標誌字符串時就停止讀數據了。

(2).進程A先發一個消息,告訴進程B它一共要傳給B的數據的長度是多少,當進程B讀完該長度的內容後就停止讀數據。

(3).進程A發完所有的數據後,關閉Socket。此時進程B再讀入進程A發送的數據時read()方法會返回-1值,這時進程B就知道已經讀完了。

(4).當調用Socket()的Close()方法時,往往將輸出流和輸入流都關閉了。有時候,我們只希望關閉其中之一。這時候,我們可以採用Socket類的半關閉方法。

● shutdownInput():關閉輸入流。
● shutdownOutput():關閉輸出流。

上面2.2中就用到了瓣關閉流
OutputStream socketOut = socket.getOutputStream();
socketOut.write(str.getBytes());
socket.shutdownOutput();//關閉輸出流

進程B再讀入數據時,如果進程A的輸出流已經關閉,進程B讀入所有數據後,就會讀到輸入流的末尾。
要注意的是,先後調用以上2個方法關閉輸入輸出流並不等同於調用了Socket的close()方法。所以通信結束後,爲了釋放佔用的資源,仍需調用close()方法。

Socket類還提供了2個狀態測試的方法,來判斷輸入流輸出流是否關閉
● public boolean isInputShutdown();  如果關閉返回true,否則返回false

● public boolean isOutputShutdown(); 同上

當客戶端與服務器端通信時,如果有一方突然關閉時會產生什麼影響。我們來通過一個例子瞭解:

Sender.java
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();
		}
	}
}


Receiver.java
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值來觀察不同的結果。

1.自然結束Sender和Receiver通信
先運行Receiver,再運行Sender,Sender會發生20行字符串,然後自然結束運行,Receiver會接受20行字符串,然後也自然結束運行。
2.提前終止Receiver
設置Receiver裏面的stopWay值爲2,3,4,5並運行,然後再運行Sender,Receiver接受了3行字符串後,就結束運行。但是Sender仍會發生完20行字符串,才自然結束運行。因爲,儘管Receiver已經結束運行,但底層的Socket並沒有立即釋放本地端口,操作系統探測到還有發送給該Socket的數據,會使底層ocket繼續佔用本地端口一段時間。後面2.5.2(SO_RESUSEADDR選項)會做進一步解釋。
3突然中止Sender
先運行Receiver,再運行Sender,Sender發送了3行字符串後,在沒有關閉Socket的情況下,就結束了運行。Receiver在第4次執行BufferedReader的readLine()方法時會拋出異常。java.net.SocketException:Connection reset
4.關閉或半關閉Sender的Socket
先運行Receiver,再運行Sender,設置Sender的stopWay=3或者4,。Sender發送了3行字符串後,會關閉Socket或者關閉Socket的輸出流,然後結束運行。Receiver在第4次執行BufferedReader的readLine方法時讀到輸入流的末尾。

2.5 設置Socket的選項

Socket有以下幾個選項:
● TCP_NODELAY:表示立即發送數據
● SO_RESUSEADDR:表示是否允許重用Socket所綁定的本地地址。
 SO_TIMEOUT:表示接受數據時的等待超時時間
 SO_LINGER:表示當執行Socket的close()方法時,是否立即關閉底層的Socket。
 SO_SNFBUF:表示發送數據的緩衝區的大小
 SO_RCVBUF:表示接受數據的緩衝區的大小
 SO_KEEPALIVE:表示對於長時間處於空閒狀態的Socket,是否要自動把它關閉。
 OOBINLINE:表示是否支持發送一個字節的TCP緊急數據。

2.5.1 TCP_NODELAY選項

設置該選項:public void setTcpNoDelay(boolean on)throws SocketException
讀取該選項:public boolean getTcpNodelay() throws SocketException

默認情況下,發送數據採用Negale算法。該算法是指發送方發送的數據不會立刻發出,而是先放在緩存區內,等待緩衝區慢了在發生。發送完一批數據後,會等待接受放對這批數據的迴應,然後再發送下一批數據。Negale算法適用於發送方需要發送大批量數據,並且接收方會及時作出迴應的場合,這種算法通過減少傳輸數據的次數來提高通信效率。
如果發送方是發送小批量數據並且接收方不會作出快速回應,則使用Negale算法會使發送方運行很慢。對於GUI程序,如網絡遊戲程序,服務器端需要實時跟蹤客戶端鼠標的移動,採用Negale算法會採用緩衝,所以會降低實時響應速度,所以該算法在這裏就不適合用了。
TCP_NODELAY的默認值是false,表示採用Negale算法。如果調用setTcpNoDelay(true)方法,就會關閉Socket緩衝,確保數據及時發送:
if(!socket.getTcpNodelay())socket.setTcpNoDelay(true);
 如果Socket的底層不支持該選項,那麼使用那兩個方法會拋出異常。

2.5.2 SO_RESUSEADDR選項

 設置該選項:public void setResuseAddress(boolean on)throws SocketException
   讀取該選項:public boolean getResuseAddress() throws SocketException

當接收方通過Socket的close方法關閉Socket時,如果網絡上海有發送到這個Socket的數據,那麼底層的Socket不會立刻釋放本地端口,而是會等待一段時間,確保接收到網絡上發送過來的延遲數據,然後再釋放端口。Socket接收到延遲數據後,不會對這些數據作出任何處理。Socket接收延遲數據的目的是確保這些數據不會被其他碰巧綁定到同樣端口的進程收到。
客戶程序一般採用隨機端口,因此出現兩個客戶程序綁定到同樣端口的可能性不大。許多服務器程序都使用固定的端口。當服務器程序關閉後,有可能它的端口還會被佔用一段時間,如果此時立刻在同一個主機上重啓服務器程序,由於端口已經被佔用,導致啓動失敗。
爲了確保一個進程關閉Socket後,即使它還沒有釋放端口,同一個主機上的其他進程還可以立刻重用該端口,可以調用Socet的setResuseAddress(true)方法
if(!socket.getResuseAddress())socket.setResuseAddress(true);
值得注意的是socket.setResuseAddress(true)必須在Socket還沒有綁定到一個本地端口之前調用,否則該方法不起作用
		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);//連接遠程服務器
此外,兩個共用同一個端口的進程必須都調用setResuseAddress(true)方法,才能達到一個進程關閉Socket後,即使它還沒有釋放端口,同一個主機上的其他進程還可以立刻重用該端口這種效果。

2.5.3 SO_TIMEOUT選項

 設置該選項:public void setSoTimeout(int milliseconds)throws SocketException
   讀取該選項:public int getSoTimeout() throws SocketException

Socket類的SO_TIMEOUT選項用於設定接受數據的等待超時時間,單位毫秒,它的默認值爲0表示會無限等待,永不會超市。以下代碼表示把接受數據的超時時間設置爲3分鐘
if(!socket.getTimeout()==0)socket.setTimeout(60000*3);
注意Socket的setTimeout方法必須在接受數據之前調用纔有效。

2.5.4 SO_LINGER選項

 設置該選項:public void setSoLinger(boolean on,int seconds) throws SocketException //注意這裏時間單位是秒
   讀取該選項:public boolean getSoLinger() throws SocketException

SO_LINGER選項用來控制Socket關閉時的行爲。默認情況下,執行Socket的close方法,該方法會立即返回,但底層的Socket實際上並不立即關閉,它會延遲一段時間知道發送完所有的數據。
如果執行以下方法:
socket.setSoLinger(true,0);
那麼執行Socket的close方法時,該方法會立即返回並且底層的Socket會立即關閉,未發送完的數據會被丟棄。
如果執行以下方法
socket.setSoLinger(true,3600);
那麼執行Socket的close方法時,該方法不會立即返回,而是進入了阻塞狀態。同時,底層Socket正在發生未發送完的數據。只有滿足以下兩個條件之一,close方法纔會返回.
(1).底層的socket已經發送完了所有的剩餘數據;
(2).阻塞時間到了3600秒,也會返回,未發送完的數據會被丟棄。
值得一提的是,在以上兩種情況內,close返回後,底層Socket會被關閉,斷開連接。

2.5.5 SO_RCVBUF選項

 設置該選項:public void setReceiveBufferSize(int size) throws SocketException
   讀取該選項:public int getReceiveBufferSize() throws SocketException

 SO_RCVBUF表示Socket的用於輸入數據的緩衝區的大小。一般來說,傳輸大的數據塊(基於HTTP或FTP協議的通信)可以使用較大的緩衝區,以便於減少傳輸數據的次數,提高傳輸小姑。而對於交互頻繁且單次傳輸數據較小的(例如網路遊戲)可以用較小的緩衝區,以便於確保小批量數據及時傳送給對方,降低延遲。
如果底層Socket不支持該選項,那麼在調用以上兩個方法時會拋出異常。

2.5.6 SO_SNDBUF選項

 設置該選項:public void setSendBufferSize(int size) throws SocketException
   讀取該選項:public int getSendBufferSize() throws SocketException

SO_SNFBUF表示Socket的用於輸出數據的緩衝區的大小。緩衝區大小選定方法跟SO_RCVBUF類似。
如果底層Socket不支持該選項,那麼在調用以上兩個方法時會拋出異常。

2.5.7 SO_KEEPALIVE選項

 設置該選項:public void setKeepAlive(boolean on) throws SocketException
   讀取該選項:public int getKeepAlive() throws SocketException

當該選項爲true時,表示底層的TCP實現會監視連接是否有效。當連接處於空閒時間超過2小時時,本地的TCP會發送一個數據包給遠程的Socket。如果遠程Socket沒有及時發回響應,TCP實現就會持續嘗試11分鐘,直到接收到響應爲止。如果再等到了12分鐘還未收到響應,那麼TCP實現就會自動關閉本地Socket,斷開連接。在不同的網絡平臺上,TCP實現嘗試與遠程Socket對象的時限會有所差別。
該選項的默認值是false,即不會進行上述監視操作,不活動的客戶端會永久存在,甚至不會注意到服務器已經崩潰。
if(!socket.getKeepAlive()) socket.setKeepAlive(true); //設置該選項值爲true

2.5.8 OOBINLINE選項

 設置該選項:public void setOOBInline(boolean on) throws SocketException
   讀取該選項:public int getOOBInline() throws SocketException
當該選項值爲true時,表示支持發送一個字節的TCP緊急數據。Socket類的sendUrgentDate(int data )方法用於發生一個字節的TCP緊急數據。
該選項默認值爲false,即如果接收方收到緊急數據會將其廢棄。
socket.setOOBInline(true); //設置該選項值爲true
此時接收方會將緊急數據與普通數據放在同樣隊列中,除非使用一些更高層次的協議,否則接收方很難區分並處理特殊緊急數據。

2.5.9 服務器類型選項

IP規定了4種服務器的類型,用來描述服務的質量
●低成本:發送成本低。
●高可靠性:保證把數據可靠地送達目的地。
最高吞吐量:一次可以接收或發送大批量的數據。
最小延遲:傳輸數據的速度快,把數據快速送達目的地。
這4種服務類型可以進行組合使用,例如,獲取最高可靠性和最小延遲的。
Socket類中使用服務類型的方法如下:

●設置服務類型: public void setTrafficClass(int trafficClass) throws SocketException;
●讀取服務類型: public int getTrafficClass() throws SocketException;

Socket類用4個整數(十六進制)表示服務類型
●低成本:0x02 (轉換爲二進制的倒數第二位爲1)
●高可靠性:0x04(倒數第三位爲1)
最高吞吐量:0x08(倒數第四位爲1)
最小延遲:0x10(倒數第五位爲1)

例如,以下代碼請求高可靠性傳輸服務:
	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 設定連接時間、 延遲和帶寬的相對重要性

在JDK1.5中,爲Socket類提供了一個setPerformancePreferences()方法:
public void setPerformancePreferences(int connectionTime,int latency,int bandwidth)

以上方法三個參數表示網絡傳輸數據的3項指標:
● 參數connectionTime :表示用最少時間建立連接
● 參數latency:表示最小延遲
● 參數bandwidth:表示最高帶寬

用法爲,這3個參數值可以取任意整數,整數的大小決定他們之間的權重。例如
setPerformancePreferences(1,2,3);表示帶寬最重要,其次是最小延遲,最後爲最少連接時間。

2.6 發送郵件的SMTP客戶程序

第一章有介紹SMTP協議是簡單郵件傳輸協議。SMTP協議規定了把郵件從一方發送到另一方的規則。

主要的SMTP命令
SMTP命令 說明
HELO/EHLO 指明郵件發送者的主機地址
MAIL FROM 指明郵件發送者的郵件地址
PCRT TO 指明郵件接收者的郵件地址
DATA 表示接下來要發送的郵件內容
QUIT 結束通信
HELP 查詢服務器支持的命令

主要的SMTP應答碼
應答碼 說明
214 幫助信息
220 服務就緒
221 服務關閉
250 郵件操作完成
354 開始輸入郵件內容,以"."結束
421 服務未就緒,關閉傳輸通道
501 命令參數格式錯誤
502 命令不支持
503 錯誤的命令序列
504 命令參數不支持


下面給一個SMTP客戶程序與SMTP服務一次會話過程的例子。SMTP服務器程序所在的主機名稱叫smtp.mydomain.com,服務器的響應數據(以"Server>開頭")客戶端的發送數據(以"Client>開頭")

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




如果\n\r不加在Subject和content中間的話就會默認全爲標題了。\n\r是標題和內容的分界線。
本來smtp服務器想用QQ的,可是需要SSL認證什麼的,所以弄不了,改用163的了。
是不是有點意思,趕快去試試吧,運行一次發送一次,打成jar程序,無限刷別人的郵箱有木有。。


2.7 總結

主要學習了Socket的詳細用法,其中包含發送數據效率之類的等等。這一章學習了SMTP協議的大致內容,並且通過SMTP協議可以用java程序去發送郵件。
歡迎大家評論交流,共同學習。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章