天天看點

熬夜為學弟學妹整理的網絡程式設計基礎知識(二)!

IP段格式

熬夜為學弟學妹整理的網絡程式設計基礎知識(二)!

IP資料報格式

IP資料報的首部長度和資料長度都是可變長的,但總是4位元組的整數倍。對于IPv4,4位版本字段是4。4位首部長度的數值是以4位元組為機關的,最小值為5,也就是說首部長度最小是4x5=20位元組,也就是不帶任何選項的IP首部,4位能表示的最大值是15,也就是說首部長度最大是60位元組。8位TOS字段有3個位用來指定IP資料報的優先級(目前已經廢棄不用),還有4個位表示可選的服務類型(最小延遲、最大吞吐量、最大可靠性、最小成本),還有一個位總是0。總長度是整個資料報(包括IP首部和IP層payload)的位元組數。每傳一個IP資料報,16位的辨別加1,可用于分片和重新組裝資料報。3位标志和13位片偏移用于分片。TTL(Time to live)是這樣用的:源主機為資料包設定一個生存時間,比如64,每過一個路由器就把該值減1,如果減到0就表示路由已經太長了仍然找不到目的主機的網絡,就丢棄該包,是以這個生存時間的機關不是秒,而是跳(hop)。協定字段訓示上層協定是TCP、UDP、ICMP還是IGMP。然後是校驗和,隻校驗IP首部,資料的校驗由更高層協定負責。IPv4的IP位址長度為32位。

UDP資料報格式

熬夜為學弟學妹整理的網絡程式設計基礎知識(二)!

UDP資料段

下面分析一幀基于UDP的TFTP協定幀。

以太網首部

0000: 00 05 5d 67 d0 b1 00 05 5d 61 58 a8 08 00

IP首部

0000: 45 00

0010: 00 53 93 25 00 00 80 11 25 ec c0 a8 00 37 c0 a8

0020: 00 01

UDP首部

0020: 05 d4 00 45 00 3f ac 40

TFTP協定

0020: 00 01 'c'':''''q'

0030: 'w''e''r''q''.''q''w''e'00 'n''e''t''a''s''c''i'

0040: 'i'00 'b''l''k''s''i''z''e'00 '5''1''2'00 't''i'

0050: 'm''e''o''u''t'00 '1''0'00 't''s''i''z''e'00 '0'

0060: 00以太網首部:源MAC位址是00:05:5d:61:58:a8,目的MAC位址是00:05:5d:67:d0:b1,上層協定類型0x0800表示IP。

IP首部:每一個位元組0x45包含4位版本号和4位首部長度,版本号為4,即IPv4,首部長度為5,說明IP首部不帶有選項字段。服務類型為0,沒有使用服務。16位總長度字段(包括IP首部和IP層payload的長度)為0x0053,即83位元組,加上以太網首部14位元組可知整個幀長度是97位元組。IP報辨別是0x9325,标志字段和片偏移字段設定為0x0000,就是DF=0允許分片,MF=0此資料報沒有更多分片,沒有分片偏移。TTL是0x80,也就是128。上層協定0x11表示UDP協定。IP首部校驗和為0x25ec,源主機IP是c0 a8 00 37(192.168.0.55),目的主機IP是c0 a8 00 01(192.168.0.1)。 UDP首部:源端口号0x05d4(1492)是用戶端的端口号,目的端口号0x0045(69)是TFTP服務的well-known端口号。UDP報長度為0x003f,即63位元組,包括UDP首部和UDP層pay-load的長度。UDP首部和UDP層payload的校驗和為0xac40。

TFTP是基于文本的協定,各字段之間用位元組0分隔,開頭的00 01表示請求讀取一個檔案,接下來的各字段是:

c:\qwerq.qwe

netascii

blksize 512

timeout 10

tsize 0

一般的網絡通信都是像TFTP協定這樣,通信的雙方分别是用戶端和伺服器,用戶端主動發起請求(上面的例子就是用戶端發起的請求幀),而伺服器被動地等待、接收和應答請求。用戶端的IP位址和端口号唯一辨別了該主機上的TFTP用戶端程序,伺服器的IP位址和端口号唯一辨別了該主機上的TFTP服務程序,由于用戶端是主動發起請求的一方,它必須知道伺服器的IP位址和TFTP服務程序的端口号,是以,一些常見的網絡協定有預設的伺服器端口,例如HTTP服務預設TCP協定的80端口,FTP服務預設TCP協定的21端口,TFTP服務預設UDP協定的69端口(如上例所示)。在使用用戶端程式時,必須指定伺服器的主機名或IP位址,如果不明确指定端口号則采用預設端口,請讀者查閱ftp、tftp等程式的man page了解如何指定端口号。/etc/services中列出了所有well-known的服務端口和對應的傳輸層協定,這是由IANA(Internet Assigned Numbers Authority)規定的,其中有些服務既可以用TCP也可以用UDP,為了清晰,IANA規定這樣的服務采用相同的TCP或UDP預設端口号,而另外一些TCP和UDP的相同端口号卻對應不同的服務。

很多服務有well-known的端口号,然而用戶端程式的端口号卻不必是well-known的,往往是每次運作用戶端程式時由系統自動配置設定一個空閑的端口号,用完就釋放掉,稱為ephemeral的端口号,想想這是為什麼?

前面提過,UDP協定不面向連接配接,也不保證傳輸的可靠性,例如:

發送端的UDP協定層隻管把應用層傳來的資料封裝成段交給IP協定層就算完成任務了,如果因為網絡故障該段無法發到對方,UDP協定層也不會給應用層傳回任何錯誤資訊。

接收端的UDP協定層隻管把收到的資料根據端口号交給相應的應用程式就算完成任務了,如果發送端發來多個資料包并且在網絡上經過不同的路由,到達接收端時順序已經錯亂了,UDP協定層也不保證按發送時的順序交給應用層。

通常接收端的UDP協定層将收到的資料放在一個固定大小的緩沖區中等待應用程式來提取和處理,如果應用程式提取和處理的速度很慢,而發送端發送的速度很快,就會丢失資料包,UDP協定層并不報告這種錯誤。

是以,使用UDP協定的應用程式必須考慮到這些可能的問題并實作适當的解決方案,例如等待應答、逾時重發、為資料包編号、流量控制等。一般使用UDP協定的應用程式實作都比較簡單,隻是發送一些對可靠性要求不高的消息,而不發送大量的資料。例如,基于UDP的TFTP協定一般隻用于傳送小檔案(是以才叫trivial的ftp),而基于TCP的FTP協定适用于 各種檔案的傳輸。TCP協定又是如何用面向連接配接的服務來代替應用程式解決傳輸的可靠性問題呢。

TCP資料報格式

熬夜為學弟學妹整理的網絡程式設計基礎知識(二)!

TCP資料段

與UDP協定一樣也有源端口号和目的端口号,通訊的雙方由IP位址和端口号辨別。32位序号、32位确認序号、視窗大小稍後詳細解釋。4位首部長度和IP協定頭類似,表示TCP協定頭的長度,以4位元組為機關,是以TCP協定頭最長可以是4x15=60位元組,如果沒有選項字段,TCP協定頭最短20位元組。URG、ACK、PSH、RST、SYN、FIN是六個控制位,本節稍後将解釋SYN、ACK、FIN、RST四個位,其它位的解釋從略。16位檢驗和将TCP協定頭和資料都計算在内。緊急指針和各種選項的解釋從略。

TCP協定

TCP通信時序

下圖是一次TCP通訊的時序圖。TCP連接配接建立斷開。包含大家熟知的三次握手和四次握手。

熬夜為學弟學妹整理的網絡程式設計基礎知識(二)!

TCP通訊時序

在這個例子中,首先用戶端主動發起連接配接、發送請求,然後伺服器端響應請求,然後用戶端主動關閉連接配接。兩條豎線表示通訊的兩端,從上到下表示時間的先後順序,注意,資料從一端傳到網絡的另一端也需要時間,是以圖中的箭頭都是斜的。雙方發送的段按時間順序編号為1-10,各段中的主要資訊在箭頭上标出,例如段2的箭頭上标着SYN, 8000(0), ACK1001, ,表示該段中的SYN位置1,32位序号是8000,該段不攜帶有效載荷(資料位元組數為0),ACK位置1,32位确認序号是1001,帶有一個mss(Maximum Segment Size,最大封包長度)選項值為1024。

建立連接配接(三次握手)的過程:

用戶端發送一個帶SYN标志的TCP封包到伺服器。這是三次握手過程中的段1。

用戶端發出段1,SYN位表示連接配接請求。序号是1000,這個序号在網絡通訊中用作臨時的位址,每發一個資料位元組,這個序号要加1,這樣在接收端可以根據序号排出資料包的正确順序,也可以發現丢包的情況,另外,規定SYN位和FIN位也要占一個序号,這次雖然沒發資料,但是由于發了SYN位,是以下次再發送應該用序号1001。mss表示最大段尺寸,如果一個段太大,封裝成幀後超過了鍊路層的最大幀長度,就必須在IP層分片,為了避免這種情況,用戶端聲明自己的最大段尺寸,建議伺服器端發來的段不要超過這個長度。

伺服器端回應用戶端,是三次握手中的第2個封包段,同時帶ACK标志和SYN标志。它表示對剛才用戶端SYN的回應;同時又發送SYN給用戶端,詢問用戶端是否準備好進行資料通訊。

伺服器發出段2,也帶有SYN位,同時置ACK位表示确認,确認序号是1001,表示“我接收到序号1000及其以前所有的段,請你下次發送序号為1001的段”,也就是應答了用戶端的連接配接請求,同時也給用戶端發出一個連接配接請求,同時聲明最大尺寸為1024。

客戶必須再次回應伺服器端一個ACK封包,這是封包段3。

用戶端發出段3,對伺服器的連接配接請求進行應答,确認序号是8001。在這個過程中,用戶端和伺服器分别給對方發了連接配接請求,也應答了對方的連接配接請求,其中伺服器的請求和應答在一個段中發出,是以一共有三個段用于建立連接配接,稱為“三方握手(three-way-handshake)”。在建立連接配接的同時,雙方協商了一些資訊,例如雙方發送序号的初始值、最大段尺寸等。

在TCP通訊中,如果一方收到另一方發來的段,讀出其中的目的端口号,發現本機并沒有任何程序使用這個端口,就會應答一個包含RST位的段給另一方。例如,伺服器并沒有任何程序使用8080端口,我們卻用telnet用戶端去連接配接它,伺服器收到用戶端發來的SYN段就會應答一個RST段,用戶端的telnet程式收到RST段後報告錯誤Connection refused:

$ telnet 192.168.0.200 8080

Trying 192.168.0.200...

telnet: Unable to connect to remote host: Connection refused

資料傳輸的過程:

用戶端發出段4,包含從序号1001開始的20個位元組資料。

伺服器發出段5,确認序号為1021,對序号為1001-1020的資料表示确認收到,同時請求發送序号1021開始的資料,伺服器在應答的同時也向用戶端發送從序号8001開始的10個位元組資料,這稱為piggyback。

用戶端發出段6,對伺服器發來的序号為8001-8010的資料表示确認收到,請求發送序号8011開始的資料。

在資料傳輸過程中,ACK和确認序号是非常重要的,應用程式交給TCP協定發送的資料會暫存在TCP層的發送緩沖區中,發出資料包給對方之後,隻有收到對方應答的ACK段才知道該資料包确實發到了對方,可以從發送緩沖區中釋放掉了,如果因為網絡故障丢失了資料包或者丢失了對方發回的ACK段,經過等待逾時後TCP協定自動将發送緩沖區中的資料包重發。

關閉連接配接(四次握手)的過程:

由于TCP連接配接是全雙工的,是以每個方向都必須單獨進行關閉。這原則是當一方完成它的資料發送任務後就能發送一個FIN來終止這個方向的連接配接。收到一個 FIN隻意味着這一方向上沒有資料流動,一個TCP連接配接在收到一個FIN後仍能發送資料。首先進行關閉的一方将執行主動關閉,而另一方執行被動關閉。

用戶端發出段7,FIN位表示關閉連接配接的請求。

伺服器發出段8,應答用戶端的關閉連接配接請求。

伺服器發出段9,其中也包含FIN位,向用戶端發送關閉連接配接請求。

用戶端發出段10,應答伺服器的關閉連接配接請求。

建立連接配接的過程是三方握手,而關閉連接配接通常需要4個段,伺服器的應答和關閉連接配接請求通常不合并在一個段中,因為有連接配接半關閉的情況,這種情況下用戶端關閉連接配接之後就不能再發送資料給伺服器了,但是伺服器還可以發送資料給用戶端,直到伺服器也關閉連接配接為止。

滑動視窗 (TCP流量控制)

介紹UDP時我們描述了這樣的問題:如果發送端發送的速度較快,接收端接收到資料後處理的速度較慢,而接收緩沖區的大小是固定的,就會丢失資料。TCP協定通過“滑動視窗(Sliding Window)”機制解決這一問題。看下圖的通訊過程:

熬夜為學弟學妹整理的網絡程式設計基礎知識(二)!

滑動視窗

發送端發起連接配接,聲明最大段尺寸是1460,初始序号是0,視窗大小是4K,表示“我的接收緩沖區還有4K位元組空閑,你發的資料不要超過4K”。接收端應答連接配接請求,聲明最大段尺寸是1024,初始序号是8000,視窗大小是6K。發送端應答,三方握手結束。

發送端發出段4-9,每個段帶1K的資料,發送端根據視窗大小知道接收端的緩沖區滿了,是以停止發送資料。

接收端的應用程式提走2K資料,接收緩沖區又有了2K空閑,接收端發出段10,在應答已收到6K資料的同時聲明視窗大小為2K。

接收端的應用程式又提走2K資料,接收緩沖區有4K空閑,接收端發出段11,重新聲明視窗大小為4K。

發送端發出段12-13,每個段帶2K資料,段13同時還包含FIN位。

接收端應答接收到的2K資料(6145-8192),再加上FIN位占一個序号8193,是以應答序号是8194,連接配接處于半關閉狀态,接收端同時聲明視窗大小為2K。

接收端的應用程式提走2K資料,接收端重新聲明視窗大小為4K。

接收端的應用程式提走剩下的2K資料,接收緩沖區全空,接收端重新聲明視窗大小為6K。

接收端的應用程式在提走全部資料後,決定關閉連接配接,發出段17包含FIN位,發送端應答,連接配接完全關閉。

上圖在接收端用小方塊表示1K資料,實心的小方塊表示已接收到的資料,虛線框表示接收緩沖區,是以套在虛線框中的空心小方塊表示視窗大小,從圖中可以看出,随着應用程式提走資料,虛線框是向右滑動的,是以稱為滑動視窗。

從這個例子還可以看出,發送端是一K一K地發送資料,而接收端的應用程式可以兩K兩K地提走資料,當然也有可能一次提走3K或6K資料,或者一次隻提走幾個位元組的資料。也就是說,應用程式所看到的資料是一個整體,或說是一個流(stream),在底層通訊中這些資料可能被拆成很多資料包來發送,但是一個資料包有多少位元組對應用程式是不可見的,是以TCP協定是面向流的協定。而UDP是面向消息的協定,每個UDP段都是一條消息,應用程式必須以消息為機關提取資料,不能一次提取任意位元組的資料,這一點和TCP是很不同的。

TCP狀态轉換

這個圖N多人都知道,它排除和定位網絡或系統故障時大有幫助,但是怎樣牢牢地将這張圖刻在腦中呢?那麼你就一定要對這張圖的每一個狀态,及轉換的過程有深刻的認識,不能隻停留在一知半解之中。下面對這張圖的11種狀态詳細解析一下,以便加強記憶!不過在這之前,先回顧一下TCP建立連接配接的三次握手過程,以及 關閉連接配接的四次握手過程。

熬夜為學弟學妹整理的網絡程式設計基礎知識(二)!

TCP狀态轉換圖

CLOSED:表示初始狀态。

LISTEN:該狀态表示伺服器端的某個SOCKET處于監聽狀态,可以接受連接配接。

SYN_SENT:這個狀态與SYN_RCVD遙相呼應,當用戶端SOCKET執行CONNECT連接配接時,它首先發送SYN封包,随即進入到了SYN_SENT狀态,并等待服務端的發送三次握手中的第2個封包。SYN_SENT狀态表示用戶端已發送SYN封包。

SYN_RCVD: 該狀态表示接收到SYN封包,在正常情況下,這個狀态是伺服器端的SOCKET在建立TCP連接配接時的三次握手會話過程中的一個中間狀态,很短暫。此種狀态時,當收到用戶端的ACK封包後,會進入到ESTABLISHED狀态。

ESTABLISHED:表示連接配接已經建立。

FIN_WAIT_1: FIN_WAIT_1和FIN_WAIT_2狀态的真正含義都是表示等待對方的FIN封包。差別是:

FIN_WAIT_1狀态是當socket在ESTABLISHED狀态時,想主動關閉連接配接,向對方發送了FIN封包,此時該socket進入到FIN_WAIT_1狀态。

FIN_WAIT_2狀态是當對方回應ACK後,該socket進入到FIN_WAIT_2狀态,正常情況下,對方應馬上回應ACK封包,是以FIN_WAIT_1狀态一般較難見到,而FIN_WAIT_2狀态可用netstat看到。

FIN_WAIT_2:主動關閉連結的一方,發出FIN收到ACK以後進入該狀态。稱之為半連接配接或半關閉狀态。該狀态下的socket隻能接收資料,不能發。

TIME_WAIT: 表示收到了對方的FIN封包,并發送出了ACK封包,等2MSL後即可回到CLOSED可用狀态。如果FIN_WAIT_1狀态下,收到對方同時帶 FIN标志和ACK标志的封包時,可以直接進入到TIME_WAIT狀态,而無須經過FIN_WAIT_2狀态。

CLOSING: 這種狀态較特殊,屬于一種較罕見的狀态。正常情況下,當你發送FIN封包後,按理來說是應該先收到(或同時收到)對方的 ACK封包,再收到對方的FIN封包。但是CLOSING狀态表示你發送FIN封包後,并沒有收到對方的ACK封包,反而卻也收到了對方的FIN封包。什麼情況下會出現此種情況呢?如果雙方幾乎在同時close一個SOCKET的話,那麼就出現了雙方同時發送FIN封包的情況,也即會出現CLOSING狀态,表示雙方都正在關閉SOCKET連接配接。

CLOSE_WAIT: 此種狀态表示在等待關閉。當對方關閉一個SOCKET後發送FIN封包給自己,系統會回應一個ACK封包給對方,此時則進入到CLOSE_WAIT狀态。接下來呢,察看是否還有資料發送給對方,如果沒有可以 close這個SOCKET,發送FIN封包給對方,即關閉連接配接。是以在CLOSE_WAIT狀态下,需要關閉連接配接。

LAST_ACK: 該狀态是被動關閉一方在發送FIN封包後,最後等待對方的ACK封包。當收到ACK封包後,即可以進入到CLOSED可用狀态。

半關閉

當TCP連結中A發送FIN請求關閉,B端回應ACK後(A端進入FIN_WAIT_2狀态),B沒有立即發送FIN給A時,A方處在半連結狀态,此時A可以接收B發送的資料,但是A已不能再向B發送資料。

從程式的角度,可以使用API來控制實作半連接配接狀态。

#include <sys/socket.h>

int shutdown(int sockfd, int how);

sockfd: 需要關閉的socket的描述符

how: 允許為shutdown操作選擇以下幾種方式:

SHUT_RD(0): 關閉sockfd上的讀功能,此選項将不允許sockfd進行讀操作。

    該套接字不再接收資料,任何目前在套接字接受緩沖區的資料将被無聲的丢棄掉。

SHUT_WR(1):  關閉sockfd的寫功能,此選項将不允許sockfd進行寫操作。程序不能在對此套接字發出寫操作。

SHUT_RDWR(2): 關閉sockfd的讀寫功能。相當于調用shutdown兩次:首先是以SHUT_RD,然後以SHUT_WR。

使用close中止一個連接配接,但它隻是減少描述符的引用計數,并不直接關閉連接配接,隻有當描述符的引用計數為0時才關閉連接配接。

shutdown不考慮描述符的引用計數,直接關閉描述符。也可選擇中止一個方向的連接配接,隻中止讀或隻中止寫。

注意:

如果有多個程序共享一個套接字,close每被調用一次,計數減1,直到計數為0時,也就是所用程序都調用了close,套接字将被釋放。

在多程序中如果一個程序調用了shutdown(sfd, SHUT_RDWR)後,其它的程序将無法進行通信。但,如果一個程序close(sfd)将不會影響到其它程序。

2MSL

2MSL (Maximum Segment Lifetime) TIME_WAIT狀态的存在有兩個理由:

(1)讓4次握手關閉流程更加可靠;4次握手的最後一個ACK是是由主動關閉方發送出去的,若這個ACK丢失,被動關閉方會再次發一個FIN過來。若主動關閉方能夠保持一個2MSL的TIME_WAIT狀态,則有更大的機會讓丢失的ACK被再次發送出去。

(2)防止lost duplicate對後續建立正常連結的傳輸造成破壞。lost duplicate在實際的網絡中非常常見,經常是由于路由器産生故障,路徑無法收斂,導緻一個packet在路由器A,B,C之間做類似死循環的跳轉。IP頭部有個TTL,限制了一個包在網絡中的最大跳數,是以這個包有兩種命運,要麼最後TTL變為0,在網絡中消失;要麼TTL在變為0之前路由器路徑收斂,它憑借剩餘的TTL跳數終于到達目的地。但非常可惜的是TCP通過逾時重傳機制在早些時候發送了一個跟它一模一樣的包,并先于它達到了目的地,是以它的命運也就注定被TCP協定棧抛棄。

另外一個概念叫做incarnation connection,指跟上次的socket pair一摸一樣的新連接配接,叫做incarnation of previous connection。lost uplicate加上incarnation connection,則會對我們的傳輸造成緻命的錯誤。

TCP是流式的,所有包到達的順序是不一緻的,依靠序列号由TCP協定棧做順序的拼接;假設一個incarnation connection這時收到的seq=1000, 來了一個lost duplicate為seq=1000,len=1000, 則TCP認為這個lost duplicate合法,并存放入了receive buffer,導緻傳輸出現錯誤。通過一個2MSL TIME_WAIT狀态,確定所有的lost duplicate都會消失掉,避免對新連接配接造成錯誤。

該狀态為什麼設計在主動關閉這一方:

(1)發最後ACK的是主動關閉一方。 (2)隻要有一方保持TIME_WAIT狀态,就能起到避免incarnation connection在2MSL内的重建立立,不需要兩方都有。

如何正确對待2MSL TIME_WAIT?

RFC要求socket pair在處于TIME_WAIT時,不能再起一個incarnation connection。但絕大部分TCP實作,強加了更為嚴格的限制。在2MSL等待期間,socket中使用的本地端口在預設情況下不能再被使用。

若A 10.234.5.5 : 1234和B 10.55.55.60 : 6666建立了連接配接,A主動關閉,那麼在A端隻要port為1234,無論對方的port和ip是什麼,都不允許再起服務。這甚至比RFC限制更為嚴格,RFC僅僅是要求socket pair不一緻,而實作當中隻要這個port處于TIME_WAIT,就不允許起連接配接。這個限制對主動打開方來說是無所謂的,因為一般用的是臨時端口;但對于被動打開方,一般是server,就悲劇了,因為server一般是熟知端口。比如http,一般端口是80,不可能允許這個服務在2MSL内不能起來。

解決方案是給伺服器的socket設定SO_REUSEADDR選項,這樣的話就算熟知端口處于TIME_WAIT狀态,在這個端口上依舊可以将服務啟動。當然,雖然有了SO_REUSEADDR選項,但sockt pair這個限制依舊存在。比如上面的例子,A通過SO_REUSEADDR選項依舊在1234端口上起了監聽,但這時我們若是從B通過6666端口去連它,TCP協定會告訴我們連接配接失敗,原因為Address already in use.

RFC 793中規定MSL為2分鐘,實際應用中常用的是30秒,1分鐘和2分鐘等。

RFC (Request For Comments),是一系列以編号排定的檔案。收集了有關網際網路相關資訊,以及UNIX和網際網路社群的軟體檔案。

程式設計中的問題

做一個測試,首先啟動server,然後啟動client,用Ctrl-C終止server,馬上再運作server,運作結果:

itcast$ ./server

bind error: Address already in use

這是因為,雖然server的應用程式終止了,但TCP協定層的連接配接并沒有完全斷開,是以不能再次監聽同樣的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終止時,socket描述符會自動關閉并發FIN段給client,client收到FIN後處于CLOSE_WAIT狀态,但是client并沒有終止,也沒有關閉socket描述符,是以不會發FIN給server,是以server的TCP連接配接處于FIN_WAIT2狀态。 現在用Ctrl-C把client也終止掉,再觀察現象:

tcp        0      0 192.168.1.11:6666       192.168.1.11:38104      TIME_WAIT   -

bind error: Address already in use

client終止時自動關閉socket描述符,server的TCP連接配接收到client發的FIN段後處于TIME_WAIT狀态。TCP協定規定,主動關閉連接配接的一方要處于TIME_WAIT狀态,等待兩個MSL(maximum segment lifetime)的時間後才能回到CLOSED狀态,因為我們先Ctrl-C終止了server,是以server是主動關閉連接配接的一方,在TIME_WAIT期間仍然不能再次監聽同樣的server端口。

MSL在RFC 1122中規定為兩分鐘,但是各作業系統的實作不同,在Linux上一般經過半分鐘後就可以再次啟動server了。至于為什麼要規定TIME_WAIT的時間,可參考UNP 2.7節。

端口複用

在server的TCP連接配接沒有完全斷開之前不允許重新監聽是不合理的。因為,TCP連接配接沒有完全斷開指的是connfd(127.0.0.1:6666)沒有完全斷開,而我們重新監聽的是lis-tenfd(0.0.0.0:6666),雖然是占用同一個端口,但IP位址不同,connfd對應的是與某個用戶端通訊的一個具體的IP位址,而listenfd對應的是wildcard address。解決這個問題的方法是使用setsockopt()設定socket描述符的選項SO_REUSEADDR為1,表示允許建立端口号相同但IP位址不同的多個socket描述符。

在server代碼的socket()和bind()調用之間插入如下代碼:

int opt = 1;

setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

有關setsockopt可以設定的其它選項請參考UNP第7章。

TCP異常斷開

「心跳檢測機制」

在TCP網絡通信中,經常會出現用戶端和伺服器之間的非正常斷開,需要實時檢測查詢連結狀态。常用的解決方法就是在程式中加入心跳機制。

Heart-Beat線程

這個是最常用的簡單方法。在接收和發送資料時個人設計一個守護程序(線程),定時發送Heart-Beat包,用戶端/伺服器收到該小包後,立刻傳回相應的包即可檢測對方是否實時線上。

該方法的好處是通用,但缺點就是會改變現有的通訊協定!大家一般都是使用業務層心跳來處理,主要是靈活可控。 UNIX網絡程式設計不推薦使用SO_KEEPALIVE來做心跳檢測,還是在業務層以心跳包做檢測比較好,也友善控制。

「設定TCP屬性」

SO_KEEPALIVE 保持連接配接檢測對方主機是否崩潰,避免(伺服器)永遠阻塞于TCP連接配接的輸入。設定該選項後,如果2小時内在此套接口的任一方向都沒有資料交換,TCP就自動給對方發一個保持存活探測分節(keepalive probe)。這是一個對方必須響應的TCP分節.它會導緻以下三種情況:對方接收一切正常:以期望的ACK響應。2小時後,TCP将發出另一個探測分節。對方已崩潰且已重新啟動:以RST響應。套接口的待處理錯誤被置為ECONNRESET,套接 口本身則被關閉。對方無任何響應:源自berkeley的TCP發送另外8個探測分節,相隔75秒一個,試圖得到一個響應。在發出第一個探測分節11分鐘 15秒後若仍無響應就放棄。套接口的待處理錯誤被置為ETIMEOUT,套接口本身則被關閉。如ICMP錯誤是“host unreachable(主機不可達)”,說明對方主機并沒有崩潰,但是不可達,這種情況下待處理錯誤被置為EHOSTUNREACH。

根據上面的介紹我們可以知道對端以一種非優雅的方式斷開連接配接的時候,我們可以設定SO_KEEPALIVE屬性使得我們在2小時以後發現對方的TCP連接配接是否依然存在。

keepAlive = 1;

setsockopt(listenfd, SOL_SOCKET, SO_KEEPALIVE, (void*)&keepAlive, sizeof(keepAlive));

如果我們不能接受如此之長的等待時間,從TCP-Keepalive-HOWTO上可以知道一共有兩種方式可以設定,一種是修改核心關于網絡方面的 配置參數,另外一種就是SOL_TCP字段的TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT三個選項。

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設定空閑2小時才發送一個“保持存活探測分節”,不能保證明時檢測。對于判斷網絡斷開時間太長,對于需要及時響應的程式不太适應。

當然也可以修改時間間隔參數,但是會影響到所有打開此選項的套接口!關聯了完成端口的socket可能會忽略掉該套接字選項。

程式員硬核書籍資源,點選檢視!

繼續閱讀