標籤:
1. 關於new Socket()中參數的理解
Server端:
- 調用ServerSocket serverSocket = new ServerSocket(1287,2);後Server端開啟了指定的連接埠1287,並綁定了PID 5449。
Client端:
- 調用Socket socket = new Socket(remoteAddress, 1287);後,Client端會將Client的PID綁定到一個隨機未使用的連接埠上;
- 調用Socket socket = new Socket(remoteAddress, 1287, localAddress, 1288);後,Client端會將Client的PID綁定到指定的連接埠1288上;
2. 關於backlog,connectiontimeout,SO_TIMEOUT參數的理解2.1. 測試過程及現象
經過測試發現,測試程式在Windows上和在Linux上的表現不同。
2.1.1. Linux平台上(Server和Client都在Linux上)
在eclipse中先啟動Server,然後啟動第1個Client C1,此時C1正常串連到Server並返回:
Server端也列印出了相應的資訊:
啟動第2個Client C2,此時C2一直等待Server的“hi”回應,Server端的排隊數為1;
啟動第3個Client C3,此時C3一直等待Server的“hi”回應,Server端的排隊數為2;
啟動第4個Client C4,此時C4一直等待Server的“hi”回應,Server端的排隊數為3;
啟動第5個Client C5,此時C5一直等待Server的“hi”回應,Server端的排隊數為4;
啟動第6個Client C6,此時C6一直等待Server的“hi”回應,Server端的排隊數為5;
等待大約100s之後,C5和C6都返回了報錯資訊:(Client端均沒有設定socket.setSoTimeout(5000);)
而C2、C3和C4依然處於等待狀態,沒有任何輸出:
此時Server端的排隊數為3,比Server端的設定(ServerSocket serverSocket = new ServerSocket(1287,2))的排隊數2多1。事實上,經過進一步的測試,Server端設定的排隊數為1或3時,Server端的實際排隊數都比所設定的值多1。
2.1.2. Windows平台上(Server和Client都在Windows上)
在cmd中先啟動Server,然後啟動第1個Client C1,此時C1正常串連到Server並返回:
Server端也列印出了相應的資訊:
啟動第2個Client C2,此時C2一直等待Server的“hi”回應,Server端的排隊數為1;
啟動第3個Client C3,此時C3一直等待Server的“hi”回應,Server端的排隊數為2;
啟動第4個Client C4,此時C4立刻返回了報錯資訊:(Client端均沒有設定socket.setSoTimeout(5000);)
而C2、C3依然處於等待狀態,沒有任何輸出:
此時Server端的排隊數為2,與Server端的設定(ServerSocket serverSocket = new ServerSocket(1287,2))的排隊數2相等。事實上,經過進一步的測試,Server端設定的排隊數為1或3時,Server端的實際排隊數都與所設定的值相等。
2.2. Client串連Server的過程
(1). Server端初始化ServerSocket並監聽連接埠
ServerSocket serverSocket = new ServerSocket(1287,2);
Socket socket = serverSocket.accept(); // 此時若無Client端串連上來,即Server端的等待隊列為空白,那麼Server端在此阻塞
(2). Client端初始化Socket,並串連Server
Socket socket = new Socket ();
socket.connect(new InetSocketAddress(InetAddress.getByName("22.11.143.60"),1287), 1000);
(socket.setSoTimeout(5000);)
(3). Server端accept()調用從等待隊列中取出Client,並返回
Socket socket = serverSocket.accept();
(4). Server端通過返回的Socket與Client端互動
2.3. 猜測結論
根據以上現象猜測,所謂“排隊隊列”應該是在TCP層控制的,具體如:
【解釋圖中的要素】
串連隊列(connection隊列):所有的用戶端TCP串連請求,首先進入串連隊列,進行三向交握,三向交握成功標誌著串連建立(established),串連建立後用戶端請求被從隊列中取出並進入ACCEPT隊列。三向交握建立串連的過程一般很快,區域網路中1ms以內。
connectiontimeout是指從進入串連隊列、進行三向交握、建立串連到從隊列中被取出這段時間的逾時時間,一般這個不會逾時。connectiontimeout可以在用戶端通過socket.connect(new InetSocketAddress(InetAddress.getByName("22.11.143.60"),1287), 1000);來設定。
ACCEPT隊列:串連建立後,用戶端請求被放入ACCEPT隊列,等待服務端應用調用serverSocket.accept(),從而將用戶端請求從ACCEPT隊列中取出並與其互動。當然,若服務端應用調用Socket socket = serverSocket.accept();時ACCPET隊列為空白,那麼服務端應用在此阻塞,直到ACCEPT隊列不為空白。
ACCEPT隊列的最大排隊數backlog控制著這個隊列最多能存放多少個用戶端請求,當用戶端請求個數大於backlog時將拒絕為多餘的用戶端服務。服務端應用可以通過調用ServerSocket serverSocket = new ServerSocket(1287,2);來設定backlog。
在ACCEPT隊列中,每個用戶端請求都有一個SO_TIMEOUT的參數,用來控制該用戶端在這段時間內的逾時——從進入ACCEPT隊列開始到從中取出、服務端accept()返回、服務端向socket寫入回複資料、用戶端收到服務端的回複。如果用戶端在SO_TIMEOUT時間內沒有收到來自服務端的回複,則用戶端在is.readLine()報Read timed out異常,如。在用戶端可以通過調用socket.setSoTimeout(5000);來設定SO_TIMEOUT。
port-PID映射表:用戶端請求從ACCEPT隊列中被取出後,需要交給服務端應用,因此需要知道應該交給哪個服務端應用,它的PID是多少,所以需要一個類似port-PID映射表的映射關係。當服務端應用(PID=1234)啟動時,調用ServerSocket serverSocket = new ServerSocket(1287,2);時,意味著該服務綁定了1287這個連接埠,此時應在port-PID映射表中添加一條記錄“port:1287, PID=1234”。這樣,當用戶端請求被從port1287的ACCEPT隊列中取出時,就知道應該交給PID為1234的服務端應用。
【描述C1-C6串連Server的過程】
在服務端,當應用程式層應用1調用ServerSocket serverSocket = new ServerSocket(1287,2);時,TCP層在port-PID映射表中加入一條記錄“port1,PID1”以示綁定;並且TCP層為port1開闢新的空間,用來儲存port、串連隊列和ACCEPT隊列,並設定ACCPET隊列的backlog。然後應用1調用Socket socket = serverSocket.accept();來監聽ACCEPT隊列。
當C1調用socket.connect(new InetSocketAddress(InetAddress.getByName("22.11.143.60"),1287), 1000);發起請求時,C1會路由到服務端機器,從物理層、鏈路層、IP層到達TCP層。到達後首先進入串連隊列1,設定connectiontimeout,進行三向交握,成功後進入ACCEPT隊列,並調用socket.setSoTimeout(5000);設定SO_TIMEOUT。由於應用1正阻塞在accept(),所以C1被從排隊隊列中取出,然後查詢port-PID映射表,決定PID1,從而將C1發送給PID1進程應用1,然後應用1的accpet()返回並產生Socket來與C1互動。
當C2調用socket.connect(new InetSocketAddress(InetAddress.getByName("22.11.143.60"),1287), 1000);時,C2在串連隊列中進行三向交握後進入ACCEPT隊列,並調用socket.setSoTimeout(5000);設定SO_TIMEOUT。由於此時應用1沒有調用accept(),所以C2在排隊隊列中等待。C3、C4、C5、C6重複此過程。若排隊隊列中的元素個數大於最大排隊數backlog1,那麼TCP拒絕為多餘的Client服務。
【解釋測試現象】
因此,下面可以解釋測試現象(ps. 在測試中Client端均沒有設定socket.setSoTimeout(5000);)
l 若設定backlog1 = 2,那麼,在Linux環境下C5和C6會被拒絕,ACCPET隊列長度為3,比backlog1大1;而在windows環境下C4、C5和C6會被拒絕,ACCEPT隊列長度為2,等於backlog1。這可能是由於不同的作業系統對TCP的參數設定不同導致。
l 在不同的作業系統下,串連被拒絕時的報錯Exception也不同。windows下報connnection refused異常,異常位置在socket.connect(***);,這是由於串連被拒絕,或者說沒有串連上;而Linux下報connection reset異常,異常位置在is.readLine()(這個異常一般是由於一端socket被關閉,而另一端仍在進行讀寫操作引起的),這是可能是由於服務端斷開多於backlog+1的串連而用戶端仍在readLine()導致。這可能是由於不同作業系統對TCP的實現不同導致。
l 在測試過程中Client端只設定了connectiontimeout,沒有設定SO_TIMEOUT。在Linux環境下C5和C6會在約100s後被拒絕,而在windows環境下C4、C5和C6會立刻被拒絕。這個應該和connectiontimeout無關。由於用戶端都沒有設定SO_TIMEOUT,所以估計跟這個參數也無關。應該除了connectiontimeout和SO_TIMEOUT以外還有一個作業系統預設的參數來控制是100s還是立刻,目前尚不清楚。
l 如果在Client端設定了SO_TIMEOUT,例如為5s,那麼在Linux上的現象是C2及其之後的Client會在串連上服務端等待其回複5s後報Read timed out,異常位置在is.readLine();在windows上的現象是C4及其之後的Client依然立即報Connection refused異常,異常位置在socket.connect(***),而C2和C3是在串連上服務端等待其回複5s後報Read timed out異常,異常位置在is.readLine()。
如果在Client端設定了SO_TIMEOUT,例如為110s,那麼在Linux上的現象是C5及其之後的Client會在100s後報Connection reset異常,異常位置在is.readLine(),而C2、C3和C4會在110s後報Read timed out異常,異常位置在is.readLine();在windows上的現象是C4及其之後的Client依然立即報Connection refused異常,異常位置在socket.connect(***),而C2和C3是在串連上服務端等待其回複110s後報Read timed out異常,異常位置在is.readLine()。
這就是SO_TIMEOUT這個參數的含義。兩個作業系統在這一點上的理解是一致的。
3. 附件
測試使用的代碼:
Client.java
1 package socket; 2 3 import java.io.*; 4 import java.net.*; 5 import java.util.Date; 6 7 public class Client { 8 9 /**10 * @param args11 */12 public static void main(String[] args) {13 // TODO Auto-generated method stub14 try {15 System.out.println(new Date());16 InetAddress remoteAddress = InetAddress.getByName("127.0.0.1");17 // InetAddress localAddress = InetAddress.getByName("127.0.0.1");18 // Socket socket = new Socket(remoteAddress, 1287, localAddress, 1288);19 // Socket socket = new Socket(remoteAddress, 1287);20 Socket socket = new Socket ();21 socket.connect(new InetSocketAddress(remoteAddress,1287), 1000);22 socket.setSoTimeout(5000);23 System.out.println(new Date());24 PrintWriter os = new PrintWriter(socket.getOutputStream());25 BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));26 String msg = "hello";27 os.println(msg);28 os.flush();29 System.out.println("Client: " + msg);30 // Thread.sleep(1000);31 System.out.println("Server: " + is.readLine());32 System.out.println(new Date());33 Thread.sleep(1000000);34 System.out.println("end!");35 os.close();36 is.close();37 socket.close();38 } catch (UnknownHostException e) {39 // TODO Auto-generated catch block40 System.out.println(new Date());41 e.printStackTrace();42 } catch (IOException e) {43 // TODO Auto-generated catch block44 System.out.println(new Date());45 e.printStackTrace();46 }47 catch (InterruptedException e) {48 // TODO Auto-generated catch block49 System.out.println(new Date());50 e.printStackTrace();51 }52 53 }54 55 }
Server.java
1 package socket; 2 3 import java.io.*; 4 import java.net.*; 5 6 public class Server { 7 8 /** 9 * @param args10 */11 public static void main(String[] args) {12 // TODO Auto-generated method stub13 try {14 ServerSocket serverSocket = new ServerSocket(1287,2);15 Socket socket = serverSocket.accept();16 17 PrintWriter os = new PrintWriter(socket.getOutputStream());18 BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));19 while (true){20 System.out.println("Client: " + is.readLine());21 String msg = "hi";22 os.println(msg);23 os.flush();24 System.out.println("Server: " + msg);25 }26 // os.close();27 // is.close();28 // socket.close();29 // serverSocket.close();30 } catch (IOException e) {31 // TODO Auto-generated catch block32 e.printStackTrace();33 }34 // catch (InterruptedException e) {35 // // TODO Auto-generated catch block36 // e.printStackTrace();37 // }38 39 }40 41 }
關於java socket