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