OSI七層模型與TCP/IP四層模型
很多同學(xué)知道在大學(xué)課程中,我們學(xué)習(xí)的《計算機網(wǎng)絡(luò)》一書采用的是OSI七層網(wǎng)絡(luò)模型(OSI Model),但是OSI 七層模型是一種抽象模型,在操作系統(tǒng)實際實現(xiàn)中,采用的是TCP/IP四層網(wǎng)絡(luò)模型,四層模型將七層模型合并為了應(yīng)用層(Application Layer)、傳輸層(Transport Layer)、網(wǎng)絡(luò)層(Internet Layer)、鏈路層(Link Layer),使得網(wǎng)絡(luò)系統(tǒng)在具體實現(xiàn)中更加簡化,OSI七層網(wǎng)絡(luò)模型與TCP/IP四層網(wǎng)絡(luò)模型以及協(xié)議對應(yīng)關(guān)系如下表所示。在計算機系統(tǒng)中,分層是一種很重要的編程思想,分層思想將系統(tǒng)的功能與責(zé)任進(jìn)行了層次化劃分,基本上所有系統(tǒng)的架構(gòu)設(shè)計,都是按照層次架構(gòu)作為基本架構(gòu)來設(shè)計的。在計算機網(wǎng)絡(luò)中,相同層次具有相同的協(xié)議處理方式,下層協(xié)議上層提供服務(wù),上層協(xié)議的行為控制著下層的工作狀態(tài),層層之間責(zé)任單一,目的明確。
Java對于TCP/IP協(xié)議的實現(xiàn)
在網(wǎng)絡(luò)程序開發(fā)中,操作系統(tǒng)都為我們提供了全面 方便的應(yīng)用層網(wǎng)絡(luò)操作類與接口,使得程序員在使用過程中無需考慮協(xié)議棧的細(xì)節(jié),而專心于數(shù)據(jù)的傳輸處理過程中。當(dāng)然,操作系統(tǒng)也為程序員提供了可以掌控協(xié)議細(xì)節(jié)的機會,例如使用原始套接字(Raw Socket)可以控制TCP的三次握手的細(xì)節(jié)實現(xiàn)TCP SYN掃描(注意部分 Windows 7系統(tǒng)不支持原始套接字的半開掃描)。但是在大多常規(guī)網(wǎng)絡(luò)應(yīng)用開發(fā)中,我們都直接使用系統(tǒng)提供的應(yīng)用層接口來實現(xiàn)網(wǎng)絡(luò)程序。這里我們羅列出在Java中常見的TCP/IP協(xié)議的實現(xiàn)類或方法,如下下表所示:
TCP協(xié)議為什么需要三次握手
首先我們來看一下TCP協(xié)議三次握手的具體過程(本圖選自網(wǎng)絡(luò)):
第一次握手:建立連接??蛻舳税l(fā)送連接請求報文段,將SYN位置為1,Sequence Number為x;然后,客戶端進(jìn)入SYN_SEND狀態(tài),等待服務(wù)器的確認(rèn);
第二次握手:服務(wù)器收到SYN報文段。服務(wù)器收到客戶端的SYN報文段,需要對這個SYN報文段進(jìn)行確認(rèn),設(shè)置Acknowledgment Number為x+1(Sequence Number+1);同時,自己自己還要發(fā)送SYN請求信息,將SYN位置為1,Sequence Number為y;服務(wù)器端將上述所有信息放到一個報文段(即SYN+ACK報文段)中,一并發(fā)送給客戶端,此時服務(wù)器進(jìn)入SYN_RECV狀態(tài);
第三次握手:客戶端收到服務(wù)器的SYN+ACK報文段。然后將Acknowledgment Number設(shè)置為y+1,向服務(wù)器發(fā)送ACK報文段,這個報文段發(fā)送完畢以后,客戶端和服務(wù)器端都進(jìn)入ESTABLISHED狀態(tài),完成TCP三次握手。
那么,為什么TCP協(xié)議需要三次握手?在謝希仁的《計算機網(wǎng)絡(luò)》中是這樣說的:
已失效的連接請求報文段會的產(chǎn)生在這樣一種情況下:client發(fā)出的第一個連接請求報文段并沒有丟失,而是在某個網(wǎng)絡(luò)結(jié)點長時間的滯留了,以致延誤到連接釋放以后的某個時間才到達(dá)server。本來這是一個早已失效的報文段。但server收到此失效的連接請求報文段后,就誤認(rèn)為是client再次發(fā)出的一個新的連接請求。于是就向client發(fā)出確認(rèn)報文段,同意建立連接。假設(shè)不采用“三次握手”,那么只要server發(fā)出確認(rèn),新的連接就建立了。由于現(xiàn)在client并沒有發(fā)出建立連接的請求,因此不會理睬server的確認(rèn),也不會向server發(fā)送數(shù)據(jù)。但server卻以為新的運輸連接已經(jīng)建立,并一直等待client發(fā)來數(shù)據(jù)。這樣,server的很多資源就白白浪費掉了。采用“三次握手”的辦法可以防止上述現(xiàn)象發(fā)生。例如剛才那種情況,client不會向server的確認(rèn)發(fā)出確認(rèn)。server由于收不到確認(rèn),就知道client并沒有要求建立連接。
換句話說,TCP之所以采用三次握手建立連接的機制,是為了防止已失效的連接請求報文段突然又傳送到了服務(wù)端,因而產(chǎn)生錯誤。在網(wǎng)絡(luò)中不能確保數(shù)據(jù)包的一定可以發(fā)送成功,也不能確保數(shù)據(jù)包的發(fā)送順序和到達(dá)順序一致,三次握手機制避免了客戶端與服務(wù)器之間在建立連接時可能因丟包而造成的一端 無法感知另一端狀態(tài)的現(xiàn)象。關(guān)于TCP協(xié)議的四次揮手過程與原因與三次握手完全類同,這里就不說明了,關(guān)于TCP連接的關(guān)閉,后文將分析一個相關(guān)問題:TCP半關(guān)閉現(xiàn)象(Half-Close),這里我們先來看一下Java中如何使用Socket類建立一個TCP連接的過程。
Java中TCP通信的相關(guān)實現(xiàn)
在上節(jié)我們分析了TCP協(xié)議建立連接,數(shù)據(jù)傳輸以及關(guān)閉連接的具體實現(xiàn)方式,而在實際的開發(fā)中,程序員只需了解TCP協(xié)議是一種可靠的傳輸協(xié)議即可實現(xiàn)數(shù)據(jù)在客戶端與服務(wù)器之間穩(wěn)定的傳輸。在Java中,提供了Socket與SocketServer類來實現(xiàn)TCP服務(wù)器與客戶端的相關(guān)功能,一次正常的TCP通信其大致流程可以分為四步(BIO模式):
-
服務(wù)器端(ServerSocket)綁定監(jiān)聽端口,等待客戶端的TCP的連接(ServerSocket.accept())
-
客戶端(Socket)通過IP地址與端口連接服務(wù)器監(jiān)聽端口(Socket.Connect()),連接成功后服務(wù)器端返回表示該TCP連接的Socket對象。
-
客戶端與服務(wù)器通過打開Socket對象的InputStream和OutputStream數(shù)據(jù)流,實現(xiàn)數(shù)據(jù)的傳輸工作(Java將I/O相關(guān)的操作都提供了流操作接口,網(wǎng)絡(luò)接口操作方式也一樣)。
-
客戶端與服務(wù)器完成數(shù)據(jù)傳輸,關(guān)閉數(shù)據(jù)流,關(guān)閉TCP連接(Socket.close())。
下圖是Socket的通行模型圖:
這里有個問題,TCP的三次握手是在Socket的哪一步中實現(xiàn)?在ServerSocket.accept()與Socket.Connect()的過程中實現(xiàn)的,在客戶端通過Connect()接口連接服務(wù)器時,操作系統(tǒng)底層的tcp/ip協(xié)議棧便開始了發(fā)送SYN包,回復(fù)SYN+1等TCP的三次握手過程,只有三次握手成功,ServerSocket.accept()才會返回一個合法的Scoket對象,客戶端Socket.Connect()函數(shù)會正常返回,如果三次握手失敗,客戶端Socket.Connect()會拋出IOException異常。
這里需要注意,具體的三次握手協(xié)議細(xì)節(jié)的實現(xiàn),也不是在java中實現(xiàn)的,java只是運行在jvm虛擬機上的語言,具體的實現(xiàn)是由宿主主機的TCP/IP協(xié)議棧實現(xiàn)的,java只是通過虛擬機調(diào)用了這些宿主主機提供的方法而已。
TCP半關(guān)閉現(xiàn)象(Half-Close)
有TCP服務(wù)器與客戶端應(yīng)用程序開發(fā)經(jīng)驗的同學(xué)應(yīng)該遇到過一個問題,那就是服務(wù)器端突然崩潰(kill 掉服務(wù)器進(jìn)程),查看系統(tǒng)中的網(wǎng)絡(luò)連接時,發(fā)現(xiàn)TCP客戶端的狀態(tài)還是處于連接狀態(tài)(ESTABLISHED),而該TCP連接實際已經(jīng)失效了,這就是TCP的Half-Close 現(xiàn)象。如果應(yīng)用程序判斷與服務(wù)器連接狀態(tài)的方法依賴于TCP的連接狀態(tài),客戶端將會一直認(rèn)為與服務(wù)器的TCP連接是正常的,只有在客戶端向服務(wù)器發(fā)送數(shù)據(jù)時,才能發(fā)現(xiàn)TCP連接對應(yīng)的套接字失效了。之所以產(chǎn)生Half-Close現(xiàn)象就是由于客戶端與服務(wù)器之間沒有通過四次揮手的方式關(guān)閉TCP連接,在服務(wù)器的突然下線會造成客戶端無法立即感知的問題。有同學(xué)會問,不是有超時時間嗎,一旦超時,客戶端不就可以感知到服務(wù)器已經(jīng)下線了嗎?不錯,系統(tǒng)的協(xié)議棧實現(xiàn)具有超時時間的機制,但是在windows系統(tǒng)下,這個超時時間默認(rèn)是2小時(作者未考證linux下的keepAlive時間)。
那么如何避免Half-Close現(xiàn)象?
-
首先進(jìn)行再使用完tcp連接后,一定要將套接字close掉。
-
添加心跳包機制,服務(wù)器與客戶端之間的連接保持機制不應(yīng)該依賴套接字的狀態(tài),而應(yīng)該在TCP協(xié)議之上設(shè)計心跳包機制,例如每5分鐘,客戶端與服務(wù)器之間通過發(fā)送心跳包來感知對方的存在。
-
TCP Server應(yīng)該實現(xiàn)JVM的關(guān)閉鉤子(Runtime.addShutdownHook()),主動關(guān)閉所有TCP連接,清理占用資源,JVM關(guān)閉鉤子的使用方式如下所:
總結(jié)
TCP協(xié)議作為可靠傳輸協(xié)議,是所有協(xié)議中最常用的協(xié)議,什么?最常用的協(xié)議不是HTTP嗎?HTTP協(xié)議只是TCP協(xié)議的應(yīng)用協(xié)議而已。TCP協(xié)議相關(guān)的開發(fā)難點在于服務(wù)器端的開發(fā),需要考慮并發(fā)性能,本文以講解了TCP的協(xié)議為主,因此只采用了BIO模式進(jìn)行分析,在之后的文章中將會分析高并發(fā)的TCP服務(wù)器的實現(xiàn)原理。