李檢輝



關鍵詞:Java;TCP;三次握手;accept隊列;多線程
中圖分類號:TP393 文獻標識碼:A
文章編號:1009-3044(2023)20-0103-03
在網絡分層里,基于Java的套接字(Socket) 網絡編程應用屬于應用層。網絡通信雙方的兩個應用程序在應用層面上各創建一個Socket,并通過Socket建立一個雙向的通信連接以實現應用層面數據的交換[1]。應用程序的Socket并不是直接訪問主機通信模塊進行數據交換,而是通過調用操作系統提供的Socket API接口來請求數據傳輸,并通過這個接口選擇傳輸層提供的TCP協議或UDP協議來完成通信。TCP協議要求在真正的數據傳輸前雙方先完成三次“預通信”連接過程,即三次握手,用于確認傳輸通道是否正常,并建立一條虛連接,然后傳送數據,且通信結束時還需要拆除連接[2]。由于UDP是不面向連接的協議,這里不進行分析。
在Java網絡編程通信模型里,TCP虛連接請求由客戶端(Client)發起,如圖1所示。
1) 在第一次握手中,由客戶端發起SYN報文,服務端收到該報文后,第一次握手成功;
2) 接著,服務端發起第二次握手,向客戶端發送SYN+ACK 報文,客戶端收到該報文后,第二次握手成功;
3) 最后,由客戶端發起第三次握手,向服務端發送ACK報文,服務端收到該報文,便完成三次握手,雙方建立一條TCP連接。
那么,在Java Socket通信程序中,三次握手是否由Socket應用程序完成,三次握手是何時開始,何時結束,程序是如何完成這些步驟的?這些是本文要探討的主要問題。
1 服務端監聽客戶端發起連接
Java程序通過類ServerSocket創建服務端,并通過調用bind()方法綁定服務器地址。一臺主機可以同時提供多個服務,這些不同服務的IP地址是相同的,因此需要通過不同的端口來區別不同的服務[3]。創建服務器的代碼如下:
SocketAddress server_addr =new InetSocketAddress“( 192.168.1.2”,5000);
ServerSocket ss=new ServerSocket();
ss.bind(server_addr,4);
或者直接通過構造方法new ServerSocket(5000,4) 綁定本地端口,其中“192.168.1.2”為服務端的IP 地址,5000為端口。
構造方法及bind()方法中的數值4 是形式參數backlog的實值,表示最大連接數為4。操作系統將綁定某個指定端口的入站連接請求,存儲在一個先進先出的隊列中。后期,服務器在處理隊列中的連接請求時會調用accept()方法,所以,這個隊列也被稱為ac?cept隊列,不同的操作系統對accept隊列的長度設定會有所不同。設置backlog值是在應用程序層面設定accept 隊列的大小。在SeverSocket 類的源碼中, 對backlog設定了默認值50,代碼如下:
public void bind(SocketAddress endpoint) throwsIOException {
bind(endpoint, 50);
}
如果在綁定端口時沒有給定這個參數值,即調用另一個重載方法ss.bind(server_addr),程序則會將backlog的值自動設定為默認值50。或者綁定端口時給定的backlog4的值小于1時,程序也會將這個值重新設置為50。方法void bind(SocketAddress endpoint,int backlog)中的相關代碼如下:
if (backlog < 1)
backlog = 50;
bind()方法在執行時,首先會綁定服務端地址(IP 地址和端口),接著會調用listen()方法傳遞backlog的值,以設置最大連接數,并開始進入端口監聽狀態,以等待客戶端的連接。此時,服務端操作系統會開始響應到達該端口連接請求。當有客戶端連接服務端并完成三次握手后,服務端則會將此連接放入accept隊列,等待服務端調用accept()方法從該隊列中取出并進行后期通信。如果accept隊列中存儲的未處理的連接數目達到設定的backlog值,即隊列滿了,那么,服務端將會拒絕新的客戶端的連接請求。
2 客戶端觸發第一次握手
客戶端可以通過如下代碼連接服務端:
Socket s = new Socket();
SocketAddress server_addr=new InetSocketAddress“( 192.168.1.2”,5000);
s.connect(server_addr); 或者直接使用Socket 的構造方法連接服務端,例如:
Socket s = new Socket(host_name,port);
從Socket類的源碼可知,該構造方法在執行時會自動調用connect()方法連接服務端。
客戶端在開始執行connect()方法時,首先觸發TCP的第一次握手。如果此時服務端未啟動,或者服務端的連接隊列滿了,那么connect()方法會拋出Socket異常,提示“異常信息:Connection refused: con?nec“t 的錯誤。如果服務端正常響應,接下來會進行第二次及第三次的握手,當三次握手成功時則connect() 方法會正常返回。
3 三次握手與程序之間的關系
基于TCP協議的Java Socket程序通信的過程可以通過三個層面進行解析,如圖2所示應用程序(Cli?ent與Server) 、Socket、操作系統(OS) 之間的關系。三次握手在操作系統層面進行,客戶端與服務端之間通過操作系統提供Socket API 接口完成通信連接。
1) 服務端創建ServerSocket對象,調用bind()綁定和監聽端口,并創建一個先進先出的accept隊列;
2) 客戶端創建Socket對象后,通過調用其connect()方法觸發了三次握手。連接成功后,系統將該連接放入accept隊列。客戶端同時通過這個Socket創建后續與服務器通信的輸入輸出流(in、out) ;
3) 服務端調用accept()方法監聽accept隊列是否成功連接。如果有,則取出并返回一個與對應客戶端通信的Socket對象,并創建與該客戶端通信的輸入輸出流(in、out) ;
4) 在客戶端調用connect()方法成功返回后,如果服務端并沒有執行accept()方法將這個客戶端的請求從accept隊列中取出處理,那么客戶端并不能真正地和服務端進行應用層面上的通信。但是,客戶端已經可以通過Socket建立輸入輸出流,并開始向服務端發送數據,而服務端操作系統也會在TCP協議層面回復ACK包,并將數據保存在指定的緩沖區中,等待服務端程序的后期處理。
因此,三次握手與accept()方法并無直接關系。通過模擬實驗可以驗證,在未執行accept()方法的情況下,已經完成三次握手,如圖3所示。當accept隊列已滿時,客戶端的第一次握手請求(SYN) 后,會收到RST 包,表示重置連接,即該連接請求被服務端面拒絕,接著客戶端會連續嘗試發送SYN包,如果仍是收到RST 包,則結束連接請求,如圖4所示。只要完成了三次握手,客戶端便可以向服務端發送數據,并且這些數據會在服務端的操作系統層面被接收并存儲在臨時空間。
4 Accept 方法處理要點分析
從服務端資源的安全使用方面考慮,服務端程序需要設定合適的最大連接數。然后,一旦設定了最大連接數,如果程序沒有及時調用accept方法對取出ac?cept隊列的請求進行處理,則會帶來如下兩個問題。
1) 如果在短時間內有較多的客戶端發起連接請求,而服務端不能夠及時地將請求從accept隊列取出進行處理,accept隊列很快會溢出,致使其他客戶端的連接都會被拒絕;
2) 客戶端一旦完成了三次握手,則可以通過Socket創建輸出流發送數據給服務端,如果服務端程序不及時處理,客戶端可能因為沒有及時得到回復而進入異常狀態,同時服務端相應的緩存會被大量占用。
因此,程序中如何調用accept()方法顯得非常重要。在單線程程序中,當服務器調用accept()方法接收到第一個客戶請求時,便創建輸入輸出流,開始與客戶端進行通信[4]。在通信結束前,不會再次調用accept()方法接收其他客戶的連接請求,而越來越多的未被接收的連接請求就會占滿整個accept隊列。解決這個問題的方式有兩種,一種是應用非阻塞I/O技術,另一種是應用多線程技術。由于非阻塞I/O技術比較復雜,這里不展開分析。
多線程技術可以實現分開執行不同的任務或者分段執行程序代碼,從而顯著提高程序的運行效率[5]。應用多線程解決問題的關鍵在于將接收請求與處理請求分離成兩個任務。服務端程序的主線程用于執行接收請求任務,循環地調用accept()方法,每當ac?cept()成功返回相應Socket對象時,創建一個新的線程(子線程)并傳遞Socket對象,啟動這個新線程。代碼如下:
while (true){
Socket clientSocket = listenSocket.accept();
Thread t = new ClientThread(clientSocket);
t.start();
}
其中,ClientThread 類為子線程類(class Client?Thread extends Thread)。服務端主線程通過調用Cli?entThread類的構造方法創建子線程,并調用start()方法啟動子線程用于執行與客戶端通信的任務。這樣,主線程啟動子線程后便可以快速地返回并進入下一次的循環,繼續調用accept()方法接收accept隊列中下一個客戶端的請求。在這個子線程中,通過傳遞的Socket對象創建通信的輸入輸出流,開始與客戶端進行數據通信。
應用多線程響應客戶端請求時,應考慮服務器的資源狀況。由于服務端可以快速地處理accept隊列中的請求,將會產生大量的子線程用于各個客戶端的通信。如果不做任何控制,過多的子線程也有可能影響系統的性能。因此,最好是應用線程池技術對這些子線程進行管理。
5 結束語
文章從應用程序、Socket和操作系統三個層面分析Java Socket程序,可以看出,TCP三次握手是由程序通過調用操作系統的API接口,并在操作系統內核層面完成的。三次握手是在服務端調用bind()方法后,由客戶端調用connect()方法觸發完成,服務端的ac?cept()方法只是用于處理已完成的連接請求,并不參與三次握手的過程。但是,如果服務端在與客戶端連接成功后,沒有及時處理accept隊列,也會影響新的客戶端的請求,致使三次握手失敗。