TCP知識片段

文章來源:http://blog.chinaunix.net/uid-29075379-id-3896840.html

TCP之ACK發送情景

我現在的理解,在有以下幾種情景,TCP會把ack包發出去:

1.收到1個包,啓動200ms定時器,等到200ms的定時器到點了(第二個包沒來),於是對這個包的確認ack被髮送。這叫做“延遲發送”。

2.收到1個包,啓動200ms定時器,200ms定時器還沒到,第二個數據包又來了(兩個數據包一個ack)。

3.收到1個包,啓動200ms定時器,還沒超時,正好要給對方發點內容。於是對這個包的確認ack就跟着捎過去。這叫做“捎帶發送”。

4.每當TCP接收到一個超出期望序號的失序數據時,它總是發送一個確認序號爲其期望序號的ACK

5.窗口更新或者也叫做打開窗口(接收端窗口達到最大的時候,接收緩存中的數據全部推向進程導致接收緩存爲空),通知發送端可以繼續發送。

6.正常情況下對對方保活探針的響應,詳見TCP keepalive


TCP之RST發送場景  

1.connect一個不存在的端口;

2.向一個已經關掉的連接send數據;

3.向一個已經崩潰的對端發送數據(連接之前已經被建立);

4.close(sockfd)時,直接丟棄接收緩衝區未讀取的數據,並給對方發一個RST。這個是由SO_LINGER選項來控制的;

5.a重啓,收到b的保活探針,a發rst,通知b。

TCP socket在任何狀態下,只要收到RST包,即可進入CLOSED初始狀態。


值得注意的是RST報文段不會導致另一端產生任何響應,另一端根本不進行確認。收到RST的一方將終止該連接。程序行爲如下:
阻塞模型下,內核無法主動通知應用層出錯,只有應用層主動調用read()或者write()這樣的IO系統調用時,內核纔會利用出錯來通知應用層對端RST。

非阻塞模型下,select或者epoll會返回sockfd可讀,應用層對其進行讀取時,read()會報錯RST。


TCP之異常關閉的意義 

終止一個連接的正常方式是發送FIN。在發送緩衝區中所有排隊數據都已發送之後才發送FIN,正常情況下沒有任何數據丟失。

但我們有時也有可能發送一個RST報文段而不是FIN來中途關閉一個連接。這稱爲異常關閉

進程關閉socket的默認方式是正常關閉,如果需要異常關閉,利用SO_LINGER選項來控制。


異常關閉一個連接對應用程序來說有兩個優點:

(1)丟棄任何待發的已經無意義的數據,並立即發送RST報文段;

(2)RST的接收方利用關閉方式來區分另一端執行的是異常關閉還是正常關閉。


值得注意的是RST報文段不會導致另一端產生任何響應,另一端根本不進行確認。收到RST的一方將終止該連接。程序行爲如下:

阻塞模型下,內核無法主動通知應用層出錯,只有應用層主動調用read()或者write()這樣的IO系統調用時,內核纔會利用出錯來通知應用層對端RST。

非阻塞模型下,select或者epoll會返回sockfd可讀,應用層對其進行讀取時,read()會報錯RST。

haproxy的實現中用到了這個選項。


TCP選項之TCP_KEEPALIVE 


KEEPALIVE機制,是TCP協議規定的TCP層(非應用層業務代碼實現的)檢測TCP本端到對方主機的TCP連接的連通性的行爲。避免服務器在客戶端出現各種不良狀況時無法感知,而永遠等在這條TCP連接上。

 

該選項可以設置這個檢測行爲的細節,如下代碼所示:

int keepAlive = 1;    // 0值,開啓keepalive屬性

int keepIdle = 60;    // 如該連接在60秒內沒有任何數據往來,則進行此TCP層的探測

int keepInterval = 5; // 探測發包間隔爲5

int keepCount = 3;        // 嘗試探測的次數.如果第1次探測包就收到響應了,則後2次的不再發
setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepAlive, sizeof(keepAlive));
setsockopt(sockfd, SOL_TCP, TCP_KEEPIDLE, (void*)&keepIdle, sizeof(keepIdle));
setsockopt(sockfd, SOL_TCP, TCP_KEEPINTVL, (void *)&keepInterval, sizeof(keepInterval));
setsockopt(sockfd, SOL_TCP, TCP_KEEPCNT, (void *)&keepCount, sizeof(keepCount));

 

設置該選項後,如果60秒內在此套接口所對應連接的任一方向都沒有數據交換,TCP層就自動給對方發一個保活探測分節(keepalive probe)。這是一個對方必須響應的TCP分節。它會導致以下三種情況:

    對方接收一切正常:以期望的ACK響應。60秒後,TCP將重新開始下一輪探測。

    對方已崩潰且已重新啓動:以RST響應。套接口的待處理錯誤被置爲ECONNRESET。

    對方無任何響應:比如客戶端那邊已經斷網,或者客戶端直接死機。以設定的時間間隔嘗試3次,無響應就放棄。套接口的待處理錯誤被置爲ETIMEOUT。

 

全局設置可更改/etc/sysctl.conf,加上:

net.ipv4.tcp_keepalive_intvl = 5
net.ipv4.tcp_keepalive_probes = 3
net.ipv4.tcp_keepalive_time = 60

 

在程序中表現爲:

阻塞模型下,當TCP層檢測到對端socket不再可用時,內核無法主動通知應用層出錯,只有應用層主動調用read()或者write()這樣的IO系統調用時,內核纔會利用出錯來通知應用層。

非阻塞模型下,select或者epoll會返回sockfd可讀,應用層對其進行讀取時,read()會報錯。



一點經驗:
實際上我們在做服務器程序的時候,對客戶端的保活探測基本上不依賴於這個TCP層的keepalive探測機制。
而是我們自己做一套應用層的請求應答消息,在應用層實現這樣一個功能。


TCP選項之SO_RCVBUF和SO_SNDBUF 

SO_RCVBUF SO_SNDBUF

先明確一個概念:每個TCP socket在內核中都有一個發送緩衝區和一個接收緩衝區,TCP的全雙工的工作模式以及TCP的滑動窗口便是依賴於這兩個獨立的buffer以及此buffer的填充狀態。接收緩衝區把數據緩存入內核,應用進程一直沒有調用read進行讀取的話,此數據會一直緩存在相應socket的接收緩衝區內。再囉嗦一點,不管進程是否讀取socket,對端發來的數據都會經由內核接收並且緩存到socket的內核接收緩衝區之中。read所做的工作,就是把內核緩衝區中的數據拷貝到應用層用戶的buffer裏面,僅此而已。進程調用send發送的數據的時候,最簡單情況(也是一般情況),將數據拷貝進入socket的內核發送緩衝區之中,然後send便會在上層返回。換句話說,send返回之時,數據不一定會發送到對端去(和write寫文件有點類似),send僅僅是把應用層buffer的數據拷貝進socket的內核發送buffer中。後續我會專門用一篇文章介紹read和send所關聯的內核動作。每個UDP socket都有一個接收緩衝區,沒有發送緩衝區,從概念上來說就是只要有數據就發,不管對方是否可以正確接收,所以不緩衝,不需要發送緩衝區。

接收緩衝區被TCP和UDP用來緩存網絡上來的數據,一直保存到應用進程讀走爲止。對於TCP,如果應用進程一直沒有讀取,buffer滿了之後,發生的動作是:通知對端TCP協議中的窗口關閉。這個便是滑動窗口的實現。保證TCP套接口接收緩衝區不會溢出,從而保證了TCP是可靠傳輸。因爲對方不允許發出超過所通告窗口大小的數據。 這就是TCP的流量控制,如果對方無視窗口大小而發出了超過窗口大小的數據,則接收方TCP將丟棄它。 UDP:當套接口接收緩衝區滿時,新來的數據報無法進入接收緩衝區,此數據報就被丟棄。UDP是沒有流量控制的;快的發送者可以很容易地就淹沒慢的接收者,導致接收方的UDP丟棄數據報。
以上便是TCP可靠,UDP不可靠的實現。
這兩個選項就是來設置TCP連接的兩個buffer尺寸的。



深入淺出TCP之半關閉與CLOSE_WAIT 


 終止一個連接要經過4次握手。這由TCP的半關閉(half-close)造成的。既然一個TCP連接是全雙工(即數據在兩個方向上能同時傳遞,可理解爲兩個方向相反的獨立通道),因此每個方向必須單獨地進行關閉。這原則就是當一方完成它的數據發送任務後就能發送一個FIN來終止這個方向連接。當一端收到一個FIN內核讓read返回0通知應用層另一端已經終止了向本端的數據傳送。發送FIN通常是應用層對socket進行關閉的結果。

例如:TCP客戶端發送一個FIN,用來關閉從客戶到服務器的數據傳送。


    半關閉對服務器究竟有什麼影響呢?先看看下面的TCP狀態轉化圖

                                  tcp狀態裝換圖

    客戶端主動關閉時,發出FIN包,收到服務器的ACK,客戶端停留在FIN_WAIT2狀態。而服務端收到FIN,發出ACK後,停留在COLSE_WAIT狀態。
    這個CLOSE_WAIT狀態非常討厭,它持續的時間非常長,服務器端如果積攢大量的COLSE_WAIT狀態的socket,有可能將服務器資源耗盡,進而無法提供服務
    那麼,服務器上是怎麼產生大量的失去控制的COLSE_WAIT狀態的socket呢?我們來追蹤一下。
    一個很淺顯的原因是,服務器沒有繼續
發FIN包給客戶端。
    服務器爲什麼不發FIN,可能是業務實現上的需要,現在不是發送FIN的時機,因爲服務器還有數據要發往客戶端,發送完了自然就要通過系統調用發FIN了,這個場景並不是上面我們提到的持續的COLSE_WAIT狀態,這個在受控範圍之內。
    那麼究竟是什麼原因呢,咱們引入兩個系統調用close(sockfd)和shutdown(sockfd,how)接着往下分析。
    在這兒,需要明確的一個概念---- 一個進程打開一個socket,然後此進程再派生子進程的時候,此socket的sockfd會被繼承。socket是系統級的對象,現在的結果是,此socket被兩個進程打開,此socket的引用計數會變成2。

    繼續說上述兩個系統調用對socket的關閉情況。
    調用close(sockfd)時,內核檢查此fd對應的socket上的引用計數。如果引用計數大於1,那麼將這個引用計數減1,然後返回。如果引用計數等於1,那麼內核會真正通過發FIN來關閉TCP連接。
    調用shutdown(sockfd,SHUT_RDWR)時,內核不會檢查此fd對應的socket上的引用計數,直接通過發FIN來關閉TCP連接。

     現在應該真相大白了,可能是服務器的實現有點問題,父進程打開了socket,然後用派生子進程來處理業務,父進程繼續對網絡請求進行監聽,永遠不會終止。客戶端發FIN過來的時候,處理業務的子進程的read返回0,子進程發現對端已經關閉了,直接調用close()對本端進行關閉。實際上,僅僅使socket的引用計數減1,socket並沒關閉。從而導致系統中又多了一個CLOSE_WAIT的socket。。。

如何避免這樣的情況發生?
子進程的關閉處理應該是這樣的:
shutdown(sockfd, SHUT_RDWR);

close(sockfd);
這樣處理,服務器的FIN會被髮出,socket進入LAST_ACK狀態,等待最後的ACK到來,就能進入初始狀態CLOSED。


補充一下shutdown()的函數說明

linux系統下使用shutdown系統調用來控制socket的關閉方式

int shutdown(int sockfd,int how);

參數 how允許爲shutdown操作選擇以下幾種方式:

SHUT_RD:關閉連接的讀端。也就是該套接字不再接受數據,任何當前在套接字接受緩衝區的數據將被丟棄。進程將不能對該套接字發出任何讀操作。對TCP套接字該調用之後接受到的任何數據將被確認然後被丟棄。

SHUT_WR:關閉連接的寫端。

SHUT_RDWR:相當於調用shutdown兩次:首先是以SHUT_RD,然後以SHUT_WR

注意:

在多進程中如果一個進程中shutdown(sfd, SHUT_RDWR)後其它的進程將無法進行通信. 如果一個進程close(sfd)將不會影響到其它進程.



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章