?前言:建議看著圖片,根據(jù)文字描述走一遍TCP通訊過程,加深理解。
目錄
TCP通信時(shí)序:
1)建立連接(三次握手)的過程:
2)數(shù)據(jù)傳輸?shù)倪^程:
3)關(guān)閉連接(四次揮手)的過程:
滑動(dòng)窗口 (TCP流量控制):
TCP狀態(tài)轉(zhuǎn)換:
半關(guān)閉:
2MSL:
程序設(shè)計(jì)中的問題:
端口復(fù)用:
TCP異常斷開:
心跳檢測機(jī)制
1)Heart-Beat線程
2)設(shè)置TCP屬性
TCP通信時(shí)序:
下圖是一次TCP通訊的時(shí)序圖。TCP連接建立斷開。包含大家熟知的三次握手和四次揮手。
在這個(gè)例子中:
- 首先客戶端主動(dòng)發(fā)起連接、發(fā)送請(qǐng)求;
- 然后服務(wù)器端響應(yīng)請(qǐng)求;
- 然后客戶端主動(dòng)關(guān)閉連接。
-
? ??兩條豎線表示通訊的兩端,從上到下表示時(shí)間的先后順序,注意,數(shù)據(jù)從一端傳到網(wǎng)絡(luò)的另一端也需要時(shí)間,所以圖中的箭頭都是斜的。
- ????????雙方發(fā)送的段按時(shí)間順序編號(hào)為1-10,各段中的主要信息在箭頭上標(biāo)出,例如段2的箭頭上標(biāo)著SYN, 8000(0), ACK1001, ,表示該段中的SYN位置1,32位序號(hào)是8000,該段不攜帶有效載荷(數(shù)據(jù)字節(jié)數(shù)為0),ACK位置1,32位確認(rèn)序號(hào)是1001,帶有一個(gè)mss(Maximum Segment Size,最大報(bào)文長度)選項(xiàng)值為1024。
-
1)建立連接(三次握手)的過程:
1.客戶端發(fā)送一個(gè)帶SYN標(biāo)志的TCP報(bào)文到服務(wù)器。這是三次握手過程中的段1。
????????客戶端發(fā)出段1,SYN位表示連接請(qǐng)求。序號(hào)是1000,這個(gè)序號(hào)在網(wǎng)絡(luò)通訊中用作臨時(shí)的地址,每發(fā)一個(gè)數(shù)據(jù)字節(jié),這個(gè)序號(hào)要加1,這樣在接收端可以根據(jù)序號(hào)排出數(shù)據(jù)包的正確順序,也可以發(fā)現(xiàn)丟包的情況,另外,規(guī)定SYN位和FIN位也要占一個(gè)序號(hào),這次雖然沒發(fā)數(shù)據(jù),但是由于發(fā)了SYN位,因此下次再發(fā)送應(yīng)該用序號(hào)1001。
????????mss表示最大段尺寸,如果一個(gè)段太大,封裝成幀后超過了鏈路層的最大幀長度,就必須在IP層分片,為了避免這種情況,客戶端聲明自己的最大段尺寸,建議服務(wù)器端發(fā)來的段不要超過這個(gè)長度。
2.服務(wù)器端回應(yīng)客戶端,是三次握手中的第2個(gè)報(bào)文段,同時(shí)帶ACK標(biāo)志和SYN標(biāo)志。它表示對(duì)剛才客戶端SYN的回應(yīng);同時(shí)又發(fā)送SYN給客戶端,詢問客戶端是否準(zhǔn)備好進(jìn)行數(shù)據(jù)通訊。
????????服務(wù)器發(fā)出段2,也帶有SYN位,同時(shí)置ACK位表示確認(rèn),確認(rèn)序號(hào)是1001,表示“我接收到序號(hào)1000及其以前所有的段,請(qǐng)你下次發(fā)送序號(hào)為1001的段”,也就是應(yīng)答了客戶端的連接請(qǐng)求,同時(shí)也給客戶端發(fā)出一個(gè)連接請(qǐng)求,同時(shí)聲明最大尺寸為1024。
3.客戶必須再次回應(yīng)服務(wù)器端一個(gè)ACK報(bào)文,這是報(bào)文段3。
????????客戶端發(fā)出段3,對(duì)服務(wù)器的連接請(qǐng)求進(jìn)行應(yīng)答,確認(rèn)序號(hào)是8001。在這個(gè)過程中,客戶端和服務(wù)器分別給對(duì)方發(fā)了連接請(qǐng)求,也應(yīng)答了對(duì)方的連接請(qǐng)求,其中服務(wù)器的請(qǐng)求和應(yīng)答在一個(gè)段中發(fā)出,因此一共有三個(gè)段用于建立連接,稱為“三方握手(three-way-handshake)”。在建立連接的同時(shí),雙方協(xié)商了一些信息,例如雙方發(fā)送序號(hào)的初始值、最大段尺寸等。
????????在TCP通訊中,如果一方收到另一方發(fā)來的段,讀出其中的目的端口號(hào),發(fā)現(xiàn)本機(jī)并沒有任何進(jìn)程使用這個(gè)端口,就會(huì)應(yīng)答一個(gè)包含RST位的段給另一方。例如,服務(wù)器并沒有任何進(jìn)程使用8080端口,我們卻用telnet客戶端去連接它,服務(wù)器收到客戶端發(fā)來的SYN段就會(huì)應(yīng)答一個(gè)RST段,客戶端的telnet程序收到RST段后報(bào)告錯(cuò)誤
Connection refused:
$ telnet 192.168.0.200 8080
Trying 192.168.0.200...
telnet: Unable to connect to remote host: Connection refused
2)數(shù)據(jù)傳輸?shù)倪^程:
- 客戶端發(fā)出段4,包含從序號(hào)1001開始的20個(gè)字節(jié)數(shù)據(jù)。
- 服務(wù)器發(fā)出段5,確認(rèn)序號(hào)為1021,對(duì)序號(hào)為1001-1020的數(shù)據(jù)表示確認(rèn)收到,同時(shí)請(qǐng)求發(fā)送序號(hào)1021開始的數(shù)據(jù),服務(wù)器在應(yīng)答的同時(shí)也向客戶端發(fā)送從序號(hào)8001開始的10個(gè)字節(jié)數(shù)據(jù),這稱為piggyback。
- 客戶端發(fā)出段6,對(duì)服務(wù)器發(fā)來的序號(hào)為8001-8010的數(shù)據(jù)表示確認(rèn)收到,請(qǐng)求發(fā)送序號(hào)8011開始的數(shù)據(jù)。
????????在數(shù)據(jù)傳輸過程中,ACK和確認(rèn)序號(hào)是非常重要的,應(yīng)用程序交給TCP協(xié)議發(fā)送的數(shù)據(jù)會(huì)暫存在TCP層的發(fā)送緩沖區(qū)中,發(fā)出數(shù)據(jù)包給對(duì)方之后,只有收到對(duì)方應(yīng)答的ACK段才知道該數(shù)據(jù)包確實(shí)發(fā)到了對(duì)方,可以從發(fā)送緩沖區(qū)中釋放掉了,如果因?yàn)榫W(wǎng)絡(luò)故障丟失了數(shù)據(jù)包或者丟失了對(duì)方發(fā)回的ACK段,經(jīng)過等待超時(shí)后TCP協(xié)議自動(dòng)將發(fā)送緩沖區(qū)中的數(shù)據(jù)包重發(fā)。
3)關(guān)閉連接(四次揮手)的過程:
????????由于TCP連接是全雙工的,因此每個(gè)方向都必須單獨(dú)進(jìn)行關(guān)閉。這原則是當(dāng)一方完成它的數(shù)據(jù)發(fā)送任務(wù)后就能發(fā)送一個(gè)FIN來終止這個(gè)方向的連接。收到一個(gè) FIN只意味著這一方向上沒有數(shù)據(jù)流動(dòng),一個(gè)TCP連接在收到一個(gè)FIN后仍能發(fā)送數(shù)據(jù)。首先進(jìn)行關(guān)閉的一方將執(zhí)行主動(dòng)關(guān)閉,而另一方執(zhí)行被動(dòng)關(guān)閉。
- 客戶端發(fā)出段7,F(xiàn)IN位表示關(guān)閉連接的請(qǐng)求。
- 服務(wù)器發(fā)出段8,應(yīng)答客戶端的關(guān)閉連接請(qǐng)求。
- 服務(wù)器發(fā)出段9,其中也包含F(xiàn)IN位,向客戶端發(fā)送關(guān)閉連接請(qǐng)求。
- 客戶端發(fā)出段10,應(yīng)答服務(wù)器的關(guān)閉連接請(qǐng)求。
????????建立連接的過程是三方握手,而關(guān)閉連接通常需要4個(gè)段,服務(wù)器的應(yīng)答和關(guān)閉連接請(qǐng)求通常不合并在一個(gè)段中,因?yàn)橛羞B接半關(guān)閉的情況,這種情況下客戶端關(guān)閉連接之后就不能再發(fā)送數(shù)據(jù)給服務(wù)器了,但是服務(wù)器還可以發(fā)送數(shù)據(jù)給客戶端,直到服務(wù)器也關(guān)閉連接為止。
滑動(dòng)窗口 (TCP流量控制):
????????UDP 有這樣的問題:如果發(fā)送端發(fā)送的速度較快,接收端接收到數(shù)據(jù)后處理的速度較慢,而接收緩沖區(qū)的大小是固定的,就會(huì)丟失數(shù)據(jù)。TCP協(xié)議通過“滑動(dòng)窗口(Sliding Window)”機(jī)制解決這一問題??聪聢D的通訊過程:
- 發(fā)送端發(fā)起連接,聲明最大段尺寸是1460,初始序號(hào)是0,窗口大小是4K,表示“我的接收緩沖區(qū)還有4K字節(jié)空閑,你發(fā)的數(shù)據(jù)不要超過4K”。接收端應(yīng)答連接請(qǐng)求,聲明最大段尺寸是1024,初始序號(hào)是8000,窗口大小是6K。發(fā)送端應(yīng)答,三方握手結(jié)束。
- 發(fā)送端發(fā)出段4-9,每個(gè)段帶1K的數(shù)據(jù),發(fā)送端根據(jù)窗口大小知道接收端的緩沖區(qū)滿了,因此停止發(fā)送數(shù)據(jù)。
- 接收端的應(yīng)用程序提走2K數(shù)據(jù),接收緩沖區(qū)又有了2K空閑,接收端發(fā)出段10,在應(yīng)答已收到6K數(shù)據(jù)的同時(shí)聲明窗口大小為2K。
- 接收端的應(yīng)用程序又提走2K數(shù)據(jù),接收緩沖區(qū)有4K空閑,接收端發(fā)出段11,重新聲明窗口大小為4K。
- 發(fā)送端發(fā)出段12-13,每個(gè)段帶2K數(shù)據(jù),段13同時(shí)還包含F(xiàn)IN位。
- 接收端應(yīng)答接收到的2K數(shù)據(jù)(6145-8192),再加上FIN位占一個(gè)序號(hào)8193,因此應(yīng)答序號(hào)是8194,連接處于半關(guān)閉狀態(tài),接收端同時(shí)聲明窗口大小為2K。
- 接收端的應(yīng)用程序提走2K數(shù)據(jù),接收端重新聲明窗口大小為4K。
- 接收端的應(yīng)用程序提走剩下的2K數(shù)據(jù),接收緩沖區(qū)全空,接收端重新聲明窗口大小為6K。
- 接收端的應(yīng)用程序在提走全部數(shù)據(jù)后,決定關(guān)閉連接,發(fā)出段17包含F(xiàn)IN位,發(fā)送端應(yīng)答,連接完全關(guān)閉。
????????上圖在接收端用小方塊表示1K數(shù)據(jù),實(shí)心的小方塊表示已接收到的數(shù)據(jù),虛線框表示接收緩沖區(qū),因此套在虛線框中的空心小方塊表示窗口大小,從圖中可以看出,隨著應(yīng)用程序提走數(shù)據(jù),虛線框是向右滑動(dòng)的,因此稱為滑動(dòng)窗口。
??????? 從這個(gè)例子還可以看出,發(fā)送端是一K一K地發(fā)送數(shù)據(jù),而接收端的應(yīng)用程序可以兩K兩K地提走數(shù)據(jù),當(dāng)然也有可能一次提走3K或6K數(shù)據(jù),或者一次只提走幾個(gè)字節(jié)的數(shù)據(jù)。也就是說,應(yīng)用程序所看到的數(shù)據(jù)是一個(gè)整體,或說是一個(gè)流(stream),在底層通訊中這些數(shù)據(jù)可能被拆成很多數(shù)據(jù)包來發(fā)送,但是一個(gè)數(shù)據(jù)包有多少字節(jié)對(duì)應(yīng)用程序是不可見的,因此TCP協(xié)議是面向流的協(xié)議。而UDP是面向消息的協(xié)議,每個(gè)UDP段都是一條消息,應(yīng)用程序必須以消息為單位提取數(shù)據(jù),不能一次提取任意字節(jié)的數(shù)據(jù),這一點(diǎn)和TCP是很不同的。
TCP狀態(tài)轉(zhuǎn)換:
????????這個(gè)圖N多人都知道,它排除和定位網(wǎng)絡(luò)或系統(tǒng)故障時(shí)大有幫助,但是怎樣牢牢地將這張圖刻在腦中呢?那么你就一定要對(duì)這張圖的每一個(gè)狀態(tài),及轉(zhuǎn)換的過程有深刻的認(rèn)識(shí),不能只停留在一知半解之中。下面對(duì)這張圖的11種狀態(tài)詳細(xì)解析一下,以便加強(qiáng)記憶!不過在這之前,先回顧一下TCP建立連接的三次握手過程,以及關(guān)閉連接的四次握手過程。
CLOSED:表示初始狀態(tài)。
LISTEN:該狀態(tài)表示服務(wù)器端的某個(gè)SOCKET處于監(jiān)聽狀態(tài),可以接受連接。
SYN_SENT:這個(gè)狀態(tài)與SYN_RCVD遙相呼應(yīng),當(dāng)客戶端SOCKET執(zhí)行CONNECT連接時(shí),它首先發(fā)送SYN報(bào)文,隨即進(jìn)入到了SYN_SENT狀態(tài),并等待服務(wù)端的發(fā)送三次握手中的第2個(gè)報(bào)文。SYN_SENT狀態(tài)表示客戶端已發(fā)送SYN報(bào)文。
SYN_RCVD: 該狀態(tài)表示接收到SYN報(bào)文,在正常情況下,這個(gè)狀態(tài)是服務(wù)器端的SOCKET在建立TCP連接時(shí)的三次握手會(huì)話過程中的一個(gè)中間狀態(tài),很短暫。此種狀態(tài)時(shí),當(dāng)收到客戶端的ACK報(bào)文后,會(huì)進(jìn)入到ESTABLISHED狀態(tài)。
ESTABLISHED:表示連接已經(jīng)建立。
FIN_WAIT_1:? FIN_WAIT_1和FIN_WAIT_2狀態(tài)的真正含義都是表示等待對(duì)方的FIN報(bào)文。區(qū)別是:
????????FIN_WAIT_1狀態(tài)是當(dāng)socket在ESTABLISHED狀態(tài)時(shí),想主動(dòng)關(guān)閉連接,向?qū)Ψ桨l(fā)送了FIN報(bào)文,此時(shí)該socket進(jìn)入到FIN_WAIT_1狀態(tài)。
????????FIN_WAIT_2狀態(tài)是當(dāng)對(duì)方回應(yīng)ACK后,該socket進(jìn)入到FIN_WAIT_2狀態(tài),正常情況下,對(duì)方應(yīng)馬上回應(yīng)ACK報(bào)文,所以FIN_WAIT_1狀態(tài)一般較難見到,而FIN_WAIT_2狀態(tài)可用netstat看到。
FIN_WAIT_2:主動(dòng)關(guān)閉鏈接的一方,發(fā)出FIN收到ACK以后進(jìn)入該狀態(tài)。稱之為半連接或半關(guān)閉狀態(tài)。該狀態(tài)下的socket只能接收數(shù)據(jù),不能發(fā)。
TIME_WAIT: 表示收到了對(duì)方的FIN報(bào)文,并發(fā)送出了ACK報(bào)文,等2MSL后即可回到CLOSED可用狀態(tài)。如果FIN_WAIT_1狀態(tài)下,收到對(duì)方同時(shí)帶 FIN標(biāo)志和ACK標(biāo)志的報(bào)文時(shí),可以直接進(jìn)入到TIME_WAIT狀態(tài),而無須經(jīng)過FIN_WAIT_2狀態(tài)。
CLOSING: 這種狀態(tài)較特殊,屬于一種較罕見的狀態(tài)。正常情況下,當(dāng)你發(fā)送FIN報(bào)文后,按理來說是應(yīng)該先收到(或同時(shí)收到)對(duì)方的 ACK報(bào)文,再收到對(duì)方的FIN報(bào)文。但是CLOSING狀態(tài)表示你發(fā)送FIN報(bào)文后,并沒有收到對(duì)方的ACK報(bào)文,反而卻也收到了對(duì)方的FIN報(bào)文。什么情況下會(huì)出現(xiàn)此種情況呢?如果雙方幾乎在同時(shí)close一個(gè)SOCKET的話,那么就出現(xiàn)了雙方同時(shí)發(fā)送FIN報(bào)文的情況,也即會(huì)出現(xiàn)CLOSING狀態(tài),表示雙方都正在關(guān)閉SOCKET連接。
CLOSE_WAIT: 此種狀態(tài)表示在等待關(guān)閉。當(dāng)對(duì)方關(guān)閉一個(gè)SOCKET后發(fā)送FIN報(bào)文給自己,系統(tǒng)會(huì)回應(yīng)一個(gè)ACK報(bào)文給對(duì)方,此時(shí)則進(jìn)入到CLOSE_WAIT狀態(tài)。接下來呢,察看是否還有數(shù)據(jù)發(fā)送給對(duì)方,如果沒有可以 close這個(gè)SOCKET,發(fā)送FIN報(bào)文給對(duì)方,即關(guān)閉連接。所以在CLOSE_WAIT狀態(tài)下,需要關(guān)閉連接。
LAST_ACK: 該狀態(tài)是被動(dòng)關(guān)閉一方在發(fā)送FIN報(bào)文后,最后等待對(duì)方的ACK報(bào)文。當(dāng)收到ACK報(bào)文后,即可以進(jìn)入到CLOSED可用狀態(tài)。
半關(guān)閉:
????????當(dāng)TCP鏈接中A發(fā)送FIN請(qǐng)求關(guān)閉,B端回應(yīng)ACK后(A端進(jìn)入FIN_WAIT_2狀態(tài)),B沒有立即發(fā)送FIN給A時(shí),A方處在半鏈接狀態(tài),此時(shí)A可以接收B發(fā)送的數(shù)據(jù),但是A已不能再向B發(fā)送數(shù)據(jù)。
從程序的角度,可以使用API來控制實(shí)現(xiàn)半連接狀態(tài)。
#include <sys/socket.h>
int shutdown(int sockfd, int how);
sockfd: 需要關(guān)閉的socket的描述符
how:?? 允許為shutdown操作選擇以下幾種方式:?
????????SHUT_RD(0):關(guān)閉sockfd上的讀功能,此選項(xiàng)將不允許sockfd進(jìn)行讀操作。該套接字不再接收數(shù)據(jù),任何當(dāng)前在套接字接受緩沖區(qū)的數(shù)據(jù)將被無聲的丟棄掉。
? ? ? ? SHUT_WR(1):??關(guān)閉sockfd的寫功能,此選項(xiàng)將不允許sockfd進(jìn)行寫操作。進(jìn)程不能在對(duì)此套接字發(fā)出寫操作。
??? ????SHUT_RDWR(2): 關(guān)閉sockfd的讀寫功能。相當(dāng)于調(diào)用shutdown兩次:首先是以?SHUT_RD,然后以SHUT_WR。?
????????使用close中止一個(gè)連接,但它只是減少描述符的引用計(jì)數(shù),并不直接關(guān)閉連接,只有當(dāng)描述符的引用計(jì)數(shù)為0時(shí)才關(guān)閉連接。
????????shutdown不考慮描述符的引用計(jì)數(shù),直接關(guān)閉描述符。也可選擇中止一個(gè)方向的連接,只中止讀或只中止寫。
注意:
- 如果有多個(gè)進(jìn)程共享一個(gè)套接字,close每被調(diào)用一次,計(jì)數(shù)減1,直到計(jì)數(shù)為0時(shí),也就是所用進(jìn)程都調(diào)用了close,套接字將被釋放。
- 在多進(jìn)程中如果一個(gè)進(jìn)程調(diào)用了shutdown(sfd, SHUT_RDWR)后,其它的進(jìn)程將無法進(jìn)行通信。但,如果一個(gè)進(jìn)程close(sfd)將不會(huì)影響到其它進(jìn)程。
2MSL:
2MSL (Maximum Segment Lifetime) TIME_WAIT狀態(tài)的存在有兩個(gè)理由:
(1)讓4次握手關(guān)閉流程更加可靠;4次握手的最后一個(gè)ACK是是由主動(dòng)關(guān)閉方發(fā)送出去的,若這個(gè)ACK丟失,被動(dòng)關(guān)閉方會(huì)再次發(fā)一個(gè)FIN過來。若主動(dòng)關(guān)閉方能夠保持一個(gè)2MSL的TIME_WAIT狀態(tài),則有更大的機(jī)會(huì)讓丟失的ACK被再次發(fā)送出去。
(2)防止lost duplicate對(duì)后續(xù)新建正常鏈接的傳輸造成破壞。lost duplicate在實(shí)際的網(wǎng)絡(luò)中非常常見,經(jīng)常是由于路由器產(chǎn)生故障,路徑無法收斂,導(dǎo)致一個(gè)packet在路由器A,B,C之間做類似死循環(huán)的跳轉(zhuǎn)。IP頭部有個(gè)TTL,限制了一個(gè)包在網(wǎng)絡(luò)中的最大跳數(shù),因此這個(gè)包有兩種命運(yùn),要么最后TTL變?yōu)?,在網(wǎng)絡(luò)中消失;要么TTL在變?yōu)?之前路由器路徑收斂,它憑借剩余的TTL跳數(shù)終于到達(dá)目的地。但非??上У氖荰CP通過超時(shí)重傳機(jī)制在早些時(shí)候發(fā)送了一個(gè)跟它一模一樣的包,并先于它達(dá)到了目的地,因此它的命運(yùn)也就注定被TCP協(xié)議棧拋棄。
????????另外一個(gè)概念叫做incarnation connection,指跟上次的socket pair一摸一樣的新連接,叫做incarnation of previous connection。lost uplicate加上incarnation connection,則會(huì)對(duì)我們的傳輸造成致命的錯(cuò)誤。
????????TCP是流式的,所有包到達(dá)的順序是不一致的,依靠序列號(hào)由TCP協(xié)議棧做順序的拼接;假設(shè)一個(gè)incarnation connection這時(shí)收到的seq=1000, 來了一個(gè)lost duplicate為seq=1000,len=1000, 則TCP認(rèn)為這個(gè)lost duplicate合法,并存放入了receive buffer,導(dǎo)致傳輸出現(xiàn)錯(cuò)誤。通過一個(gè)2MSL TIME_WAIT狀態(tài),確保所有的lost duplicate都會(huì)消失掉,避免對(duì)新連接造成錯(cuò)誤。
該狀態(tài)為什么設(shè)計(jì)在主動(dòng)關(guān)閉這一方:
(1)發(fā)最后ACK的是主動(dòng)關(guān)閉一方。
(2)只要有一方保持TIME_WAIT狀態(tài),就能起到避免incarnation connection在2MSL內(nèi)的重新建立,不需要兩方都有。
如何正確對(duì)待2MSL TIME_WAIT?
????????RFC要求socket pair在處于TIME_WAIT時(shí),不能再起一個(gè)incarnation connection。但絕大部分TCP實(shí)現(xiàn),強(qiáng)加了更為嚴(yán)格的限制。在2MSL等待期間,socket中使用的本地端口在默認(rèn)情況下不能再被使用。
????????若A 10.234.5.5 : 1234和B 10.55.55.60 : 6666建立了連接,A主動(dòng)關(guān)閉,那么在A端只要port為1234,無論對(duì)方的port和ip是什么,都不允許再起服務(wù)。這甚至比RFC限制更為嚴(yán)格,RFC僅僅是要求socket pair不一致,而實(shí)現(xiàn)當(dāng)中只要這個(gè)port處于TIME_WAIT,就不允許起連接。這個(gè)限制對(duì)主動(dòng)打開方來說是無所謂的,因?yàn)橐话阌玫氖桥R時(shí)端口;但對(duì)于被動(dòng)打開方,一般是server,就悲劇了,因?yàn)閟erver一般是熟知端口。比如http,一般端口是80,不可能允許這個(gè)服務(wù)在2MSL內(nèi)不能起來。
????????解決方案是給服務(wù)器的socket設(shè)置SO_REUSEADDR選項(xiàng),這樣的話就算熟知端口處于TIME_WAIT狀態(tài),在這個(gè)端口上依舊可以將服務(wù)啟動(dòng)。當(dāng)然,雖然有了SO_REUSEADDR選項(xiàng),但sockt pair這個(gè)限制依舊存在。比如上面的例子,A通過SO_REUSEADDR選項(xiàng)依舊在1234端口上起了監(jiān)聽,但這時(shí)我們?nèi)羰菑腂通過6666端口去連它,TCP協(xié)議會(huì)告訴我們連接失敗,原因?yàn)锳ddress already in use.
????????RFC 793中規(guī)定MSL為2分鐘,實(shí)際應(yīng)用中常用的是30秒,1分鐘和2分鐘等。
????????RFC (Request For Comments),是一系列以編號(hào)排定的文件。收集了有關(guān)因特網(wǎng)相關(guān)資訊,以及UNIX和因特網(wǎng)社群的社交文件。
程序設(shè)計(jì)中的問題:
做一個(gè)測試,首先啟動(dòng)server,然后啟動(dòng)client,用Ctrl-C終止server,馬上再運(yùn)行server,運(yùn)行結(jié)果:
itcast$ ./server
bind error: Address already in use
????????這是因?yàn)椋m然server的應(yīng)用程序終止了,但TCP協(xié)議層的連接并沒有完全斷開,因此不能再次監(jiān)聽同樣的server端口。我們用netstat命令查看一下:
itcast$ netstat -apn |grep 6666
tcp?? ?????1????? 0 192.168.1.11:38103????? 192.168.1.11:6666?????? CLOSE_WAIT? 3525/client????
tcp??????? 0????? 0 192.168.1.11:6666?????? 192.168.1.11:38103????? FIN_WAIT2?? -???
?????server終止時(shí),socket描述符會(huì)自動(dòng)關(guān)閉并發(fā)FIN段給client,client收到FIN后處于CLOSE_WAIT狀態(tài),但是client并沒有終止,也沒有關(guān)閉socket描述符,因此不會(huì)發(fā)FIN給server,因此server的TCP連接處于FIN_WAIT2狀態(tài)。
現(xiàn)在用Ctrl-C把client也終止掉,再觀察現(xiàn)象:
itcast$ netstat -apn |grep 6666
tcp??????? 0????? 0 192.168.1.11:6666?????? 192.168.1.11:38104????? TIME_WAIT?? -
itcast$ ./server
bind error: Address already in use
? ? ? ?client終止時(shí)自動(dòng)關(guān)閉socket描述符,server的TCP連接收到client發(fā)的FIN段后處于TIME_WAIT狀態(tài)。TCP協(xié)議規(guī)定,主動(dòng)關(guān)閉連接的一方要處于TIME_WAIT狀態(tài),等待兩個(gè)MSL(maximum segment lifetime)的時(shí)間后才能回到CLOSED狀態(tài),因?yàn)槲覀兿菴trl-C終止了server,所以server是主動(dòng)關(guān)閉連接的一方,在TIME_WAIT期間仍然不能再次監(jiān)聽同樣的server端口。
????????MSL在RFC 1122中規(guī)定為兩分鐘,但是各操作系統(tǒng)的實(shí)現(xiàn)不同,在Linux上一般經(jīng)過半分鐘后就可以再次啟動(dòng)server了。至于為什么要規(guī)定TIME_WAIT的時(shí)間,可參考UNP 2.7節(jié)。
端口復(fù)用:
????????在server的TCP連接沒有完全斷開之前不允許重新監(jiān)聽是不合理的。因?yàn)?,TCP連接沒有完全斷開指的是connfd(127.0.0.1:6666)沒有完全斷開,而我們重新監(jiān)聽的是lis-tenfd(0.0.0.0:6666),雖然是占用同一個(gè)端口,但I(xiàn)P地址不同,connfd對(duì)應(yīng)的是與某個(gè)客戶端通訊的一個(gè)具體的IP地址,而listenfd對(duì)應(yīng)的是wildcard address。解決這個(gè)問題的方法是使用setsockopt()設(shè)置socket描述符的選項(xiàng)SO_REUSEADDR為1,表示允許創(chuàng)建端口號(hào)相同但I(xiàn)P地址不同的多個(gè)socket描述符。
在server代碼的socket()和bind()調(diào)用之間插入如下代碼:
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
TCP異常斷開:
心跳檢測機(jī)制
????????在TCP網(wǎng)絡(luò)通信中,經(jīng)常會(huì)出現(xiàn)客戶端和服務(wù)器之間的非正常斷開,需要實(shí)時(shí)檢測查詢鏈接狀態(tài)。常用的解決方法就是在程序中加入心跳機(jī)制。
1)Heart-Beat線程
????????這個(gè)是最常用的簡單方法。在接收和發(fā)送數(shù)據(jù)時(shí)個(gè)人設(shè)計(jì)一個(gè)守護(hù)進(jìn)程(線程),定時(shí)發(fā)送Heart-Beat包,客戶端/服務(wù)器收到該小包后,立刻返回相應(yīng)的包即可檢測對(duì)方是否實(shí)時(shí)在線。
????????該方法的好處是通用,但缺點(diǎn)就是會(huì)改變現(xiàn)有的通訊協(xié)議!大家一般都是使用業(yè)務(wù)層心跳來處理,主要是靈活可控。
????????UNIX網(wǎng)絡(luò)編程不推薦使用SO_KEEPALIVE來做心跳檢測,還是在業(yè)務(wù)層以心跳包做檢測比較好,也方便控制。
2)設(shè)置TCP屬性
????????SO_KEEPALIVE 保持連接檢測對(duì)方主機(jī)是否崩潰,避免(服務(wù)器)永遠(yuǎn)阻塞于TCP連接的輸入。設(shè)置該選項(xiàng)后,如果2小時(shí)內(nèi)在此套接口的任一方向都沒有數(shù)據(jù)交換,TCP就自動(dòng)給對(duì)方發(fā)一個(gè)保持存活探測分節(jié)(keepalive probe)。這是一個(gè)對(duì)方必須響應(yīng)的TCP分節(jié).它會(huì)導(dǎo)致以下三種情況:對(duì)方接收一切正常:以期望的ACK響應(yīng)。
????????2小時(shí)后,TCP將發(fā)出另一個(gè)探測分節(jié)。對(duì)方已崩潰且已重新啟動(dòng):以RST響應(yīng)。套接口的待處理錯(cuò)誤被置為ECONNRESET,套接 口本身則被關(guān)閉。對(duì)方無任何響應(yīng):源自berkeley的TCP發(fā)送另外8個(gè)探測分節(jié),相隔75秒一個(gè),試圖得到一個(gè)響應(yīng)。在發(fā)出第一個(gè)探測分節(jié)11分鐘 15秒后若仍無響應(yīng)就放棄。套接口的待處理錯(cuò)誤被置為ETIMEOUT,套接口本身則被關(guān)閉。如ICMP錯(cuò)誤是“host unreachable(主機(jī)不可達(dá))”,說明對(duì)方主機(jī)并沒有崩潰,但是不可達(dá),這種情況下待處理錯(cuò)誤被置為EHOSTUNREACH。
????????根據(jù)上面的介紹我們可以知道對(duì)端以一種非優(yōu)雅的方式斷開連接的時(shí)候,我們可以設(shè)置SO_KEEPALIVE屬性使得我們?cè)?小時(shí)以后發(fā)現(xiàn)對(duì)方的TCP連接是否依然存在。
keepAlive = 1;
setsockopt(listenfd, SOL_SOCKET, SO_KEEPALIVE, (void*)&keepAlive, sizeof(keepAlive));
????????如果我們不能接受如此之長的等待時(shí)間,從TCP-Keepalive-HOWTO上可以知道一共有兩種方式可以設(shè)置,一種是修改內(nèi)核關(guān)于網(wǎng)絡(luò)方面的 配置參數(shù),另外一種就是SOL_TCP字段的TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT三個(gè)選項(xiàng)。
- The tcp_keepidle parameter specifies the interval of inactivity that causes TCP to generate a KEEPALIVE transmission for an application that requests them. tcp_keepidle defaults to 14400 (two hours).
/*開始首次KeepAlive探測前的TCP空閉時(shí)間 */
- The tcp_keepintvl parameter specifies the interval between the nine retriesthat are attempted if a KEEPALIVE transmission is not acknowledged. tcp_keep ntvldefaults to 150 (75 seconds).
/* 兩次KeepAlive探測間的時(shí)間間隔 */
- The tcp_keepcnt option specifies the maximum number of keepalive probes tobe sent. The value of TCP_KEEPCNT is an integer value between 1 and n, where n s the value of the systemwide tcp_keepcnt parameter.
/* 判定斷開前的KeepAlive探測次數(shù)*/
int keepIdle = 1000;
int keepInterval = 10;
int keepCount = 10;
Setsockopt(listenfd, SOL_TCP, TCP_KEEPIDLE, (void *)&keepIdle, sizeof(keepIdle));
Setsockopt(listenfd, SOL_TCP,TCP_KEEPINTVL, (void *)&keepInterval, sizeof(keepInterval));
Setsockopt(listenfd,SOL_TCP, TCP_KEEPCNT, (void *)&keepCount, sizeof(keepCount));
????????SO_KEEPALIVE設(shè)置空閑2小時(shí)才發(fā)送一個(gè)“保持存活探測分節(jié)”,不能保證實(shí)時(shí)檢測。對(duì)于判斷網(wǎng)絡(luò)斷開時(shí)間太長,對(duì)于需要及時(shí)響應(yīng)的程序不太適應(yīng)。文章來源:http://www.zghlxwxcb.cn/news/detail-437092.html
????????當(dāng)然也可以修改時(shí)間間隔參數(shù),但是會(huì)影響到所有打開此選項(xiàng)的套接口!關(guān)聯(lián)了完成端口的socket可能會(huì)忽略掉該套接字選項(xiàng)。文章來源地址http://www.zghlxwxcb.cn/news/detail-437092.html
到了這里,關(guān)于TCP通訊(三次握手、四次揮手;滑動(dòng)窗口;TCP狀態(tài)轉(zhuǎn)換;端口復(fù)用;TCP心跳檢測機(jī)制)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!