動手學習TCP:4種定時器

上一篇中介紹了TCP數據傳輸中涉及的一些基本知識點。本文讓咱們看看TCP中的4種定時器。html

TCP定時器

對於每一個TCP鏈接,TCP管理4個不一樣的定時器,下面看看對4種定時器的簡單介紹。編程

  • 重傳定時器使用於當但願收到另外一端的確認。
    • 該定時器是用來決定超時和重傳的。
    • 因爲網絡環境的易變性,該定時器時間長度確定不是固定值;該定時器時間長度的設置依據是RTT(Round Trip Time),根據網絡環境的變化,TCP會根據這些變化並相應地改變超時時間。
  • 堅持定時器(persist)使窗口大小信息保持不斷流動,即便另外一端關閉了其接收窗口。
  • 保活定時器(keepalive)可檢測到一個空閒鏈接的另外一端什麼時候崩潰或重啓。
  • 2MSL定時器測量一個鏈接處於TIME_WAIT狀態的時間。

下面就介紹一下堅持定時器和保活定時器。瀏覽器

堅持定時器

TCP經過讓接收方指明但願從發送方接收的數據字節數(即窗口大小)來進行流量控制。服務器

若是窗口大小爲 0會發生什麼狀況呢?這將有效地阻止發送方傳送數據,直到窗口變爲非0爲止。網絡

可是,因爲TCP不對ACK報文段進行確認(TCP只確認那些包含有數據的ACK報文段),若是上圖中通知發送方窗口大於0的[ACK]丟失了,則雙方就有可能由於等待對方而使鏈接死鎖。接收方等待接收數據(由於它已經向發送方通告了一個非0的窗口),而發送方在等待容許它繼續發送數據的窗口更新。app

爲防止這種死鎖狀況的發生,發送方使用一個堅持定時器 (persist timer)來週期性地向接收方查詢,以便發現窗口是否已增大。這些從發送方發出的報文段稱爲窗口探查(window probe)。socket

實驗代碼

下面經過Python socket實現一個快的發送端和慢的接收端,而後經過Wireshark抓包來看看窗口更新通知和窗口探查。tcp

客戶端代碼以下,用戶輸入字符,客戶端將用戶輸入重複1000次而後發送給服務端,經過這種簡單的重複來模擬一個快的發送端:學習

from socket import *
import time

HOST = "192.168.56.102"
PORT = 8081
ADDR = (HOST, PORT)

client = socket(AF_INET, SOCK_STREAM)
client.connect(ADDR)

while True:
    input = raw_input()
    
    if input:
        client.send(input*1000)
    else:
        client.close()
        break

對於服務端,經過制定一個小的接收BUFFER,以及一個延時來模擬一個慢的接收端:測試

import sys
from socket import *
import time

HOST = "192.168.56.102"
PORT = 8081
BUFSIZ = 100
ADDR = (HOST, PORT)

server = socket(AF_INET, SOCK_STREAM)
print "Socket created"
try:
    server.bind(ADDR)
except error, msg:
    print 'Bind failed. Error Code : ' + str(msg[0]) + ' Message ' + msg[1]
    sys.exit()

server.listen(1)
print 'Socket now listening'
conn, addr = server.accept()

while True:
    time.sleep(3)
    try:
        data = conn.recv(BUFSIZ)
        if data:
            print data
        else:
            conn.close()
            break
    except Exception, e:
        print e
        break

在開始運行代碼以前還須要進行一些設置,默認狀況下接收端的window size很大,實驗中很難耗盡。

因此,爲了看到實驗效果,須要對系統進行一些設置。打開虛擬機中的註冊表設置"regedit",而後找到選項"HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters",設置"TcpWindowSize"爲4096Bytes。

注意,實驗結束後,必定要恢復"TcpWindowSize"的原始設置,否則可能會影響正常的網絡訪問。

關於更多TCP相關的註冊表設置,能夠參考這個連接

運行效果

下面運行代碼,分別輸入兩個字符"a"和"b",經過Wireshark能夠看到,在進行鏈接確認的時候,接收端已經給出了咱們跟新後的可用窗口4096Bytes。

通過第一輪發送後,接收方的window size減小了1000;當兩個數據包都處理完成後,window size又恢復到了4096。

第二輪測試中,發送端發送"1234567890"十個字符,從接收端的最後一個[ACK]包能夠看到,最後接收端window size爲1393,這次傳輸到此結束。

過了一段時間,當慢接收端處理完數據以後,接收端會發送窗口更新,通知發送端能夠窗口爲4096Bytes。

第三輪測試中,發送端發送更多的字符"1234567890987654321",此次接收端的可用窗口就被耗盡了,而後接收端發送一個[TCP ZeroWindow]的通知;這時,發送端中止發送,而後經過發送窗口探查。

當接收端有可用窗口的時候,接收端會發送窗口更新,數據傳輸繼續。

注意,[TCP ZeroWindowProbe]和[TCP ZeroWindowProbeAck]的Seq和Ack號。

糊塗窗口綜合症

基於窗口的流量控制方案,會致使一種"糊塗窗口綜合症SWS(Silly Window Syndrome)"的情況。

當發送端應用進程產生數據很慢、或接收端應用進程處理接收緩衝區數據很慢,或兩者兼而有之;就會使應用進程間傳送的報文段很小,特別是有效載荷很小。 極端狀況下,有效載荷可能只有1個字節;而傳輸開銷有40字節(20字節的IP頭+20字節的TCP頭),加上物理幀頭後,有效的數據傳輸比例就更小了,這就浪費了網絡帶寬,表現爲糊塗窗口綜合症。

糊塗窗口綜合症可能由接收端或者發送端引發,不一樣的原由須要不一樣的解決方案,更多內容能夠參考此處

保活定時器

跟據TCP協議,當發送端和接收端都不主動釋放一個TCP鏈接的時候,該鏈接將一直保持。即便一端出現了故障,因爲另外一端沒有收到任何通知,TCP鏈接也會一直保持,這樣就會形成TCP鏈接資源的浪費。

TCP keepalive

爲了解決這個問題,大多數的實現中都是使服務器設置保活計時器。

保活計時器一般設置爲2小時。若服務器過了2小時尚未收到客戶的信息,它就發送探測報文段。若發送了10個探測報文段(每個相隔75秒)尚未響應,就假定客戶出了故障,於是就終止該鏈接。

在Linux系統中,有三個跟TCP keepalive相關的參數:

tcp_keepalive_intvl (integer; default: 75; since Linux 2.4)
       The number of seconds between TCP keep-alive probes.

tcp_keepalive_probes (integer; default: 9; since Linux 2.2)
       The  maximum  number  of  TCP  keep-alive  probes  to send before giving up and killing the connection if no
       response is obtained from the other end.

tcp_keepalive_time (integer; default: 7200; since Linux 2.2)
       The number of seconds a connection needs to be idle before TCP begins sending out keep-alive probes.   Keep-
       alives  are  sent only when the SO_KEEPALIVE socket option is enabled.  The default value is 7200 seconds (2
       hours).  An idle connection is terminated after approximately an additional 11 minutes (9 probes an interval
       of 75 seconds apart) when keep-alive is enabled.

在Socket編程中,能夠經過設置"TCP_KEEPCNT","TCP_KEEPIDLE"和"TCP_KEEPINTVL"選項來更改上述的三個系統參數:

from socket import *
import time

HOST = "192.168.56.102"
PORT = 8081
ADDR = (HOST, PORT)

client = socket(AF_INET, SOCK_STREAM)

#TCP_KEEPCNT  overwrite  tcp_keepalive_probes,默認9(次)
#TCP_KEEPIDLE overwrite  tcp_keepalive_time,默認7200(秒)
#TCP_KEEPINTVL overwrite  tcp_keepalive_intvl,默認75(秒)
client.setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1)
client.setsockopt(SOL_TCP, TCP_KEEPCNT, 5)
client.setsockopt(SOL_TCP, TCP_KEEPINTVL, 5)
client.setsockopt(SOL_TCP, TCP_KEEPIDLE, 10)
client.connect(ADDR)

while True:
    input = raw_input()
    
    if input:
        client.send(input*1000)
    else:
        client.close()
        break

TCP keepalive 包

下面是一段網絡上抓取的TCP keepalive包,接下來看看TCP keepalive包的內容。

  • 根據規範,TCP keepalive保活包不該該包含數據,但也能夠包含1個無心義的字節,好比0x0。
  • TCP保活探測包Seq號是將前一個TCP包的Seq號減去1。

固然,也有人認爲保活定時器不合理,給出了不使用保活定時器的理由:

  • 在出現短暫差錯的狀況下,這可能會使一個很是好的鏈接釋放掉
  • 耗費了沒必要要的帶寬
  • 在按分組計費的狀況下會在互聯網上花掉更多的錢

HTTP Keep-Alive

在HTTP早期 ,每一個HTTP請求都要求打開一個TCP鏈接,而且使用一次以後就斷開這個TCP鏈接。

這種方式會帶來一些問題,尤爲是包含圖片,JS,CSS的複雜網頁,一個完整的頁面須要不少個請求才能完成,若是每個HTTP請求都須要新建並斷開一個TCP,這樣就會消耗不少服務器的TCP鏈接資源。

爲了緩解這個問題,HTTP 1.1中出現了Keep-Alive這個特性,開啓HTTP Keep-Alive以後,能複用已有的TCP連接,當前一個請求已經響應完畢,服務器端沒有當即關閉TCP連接,而是等待一段時間接收瀏覽器端可能發送過來的第二個請求,開啓Keep-Alive能節省的TCP創建和關閉的消耗。

下面看看我訪問一個網頁後,經過Wireshark抓取的數據包。

HTTP/1.1以後默認開啓Keep-Alive, 在HTTP的頭域中增長Connection選項。當設置爲"Connection:keep-alive"表示開啓,設置爲"Connection:close"表示關閉。

在上圖中,服務器通過了大概2分鐘的時間,而後發出關閉TCP鏈接的請求。

如今,基本全部的應用服務器都支持設置打開Keep-Alive,以及Keep-Alive timeout的設置。

總結

本文介紹了TCP中的4種定時器,並詳細的介紹了堅持定時器和保活定時器。

在保活定時器的介紹中,對比介紹了HTTP的Keep-Alive特性。HTTP協議的Keep-Alive意圖在於鏈接複用;TCP的keepalive機制在於保活、心跳,檢測鏈接錯誤,二者的做用徹底不一樣。

由於TCP keepalive不能知足實時性的要求,不少應用程序會在應用層實現heart beat(心跳包)來確認TCP鏈接的可用性。

相關文章
相關標籤/搜索