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