天下为公:TCP堵塞控制
在 TCP 協議中,我們使用連接記錄 TCP 兩端的狀態,使用編號和分段實現了 TCP 傳輸的有序,使用廣播窗口來實現了發送方和接收方處理能力的匹配,還使用重復發送機制來實現 TCP 傳輸的可靠性。最初的 TCP 協議就是由上述的幾個方面構成。1980 年代,TCP 協議又引入了流量控制機制。
令人頭痛的堵車
從 1980 年代開始,網絡變得繁忙。許多網絡中出現了大量的堵塞(congestion)。堵塞類似于現實中的堵車。網絡被稱為“信息高速公路”。許多 IP 包在網絡中行駛,并經過一個一個像十字路口一樣的路由器,直到到達目的地。一個路由器如果過度繁忙,會丟棄一些 IP 包。UDP 協議不保證傳輸的可靠性,所以丟失就丟失了。而 TCP 協議需要保證傳輸的可靠性,當包含有 TCP 片段的 IP 包丟失時,TCP 協議會重復發送 TCP 片段。于是,更多的“汽車”進入到公路中,原本繁忙的路由器變得更加繁忙,更多的 IP 包丟失。這樣就構成了一個惡性循環。這樣的情況被稱為堵塞崩潰(congestion collapse)。每個發送方為了保證自己的發送質量而亂發車,是造成堵塞崩潰的主要原因。當時的網絡中高達 90%的傳輸資源可能被堵塞崩潰所浪費。
為了彌補這一缺陷,從 1980 年代開始,TCP 協議中開始加入堵塞控制(congestion control)的功能,以避免堵塞崩潰的出現。多個算法被提出并實施,大大改善了網絡的交通狀況。流量控制類似于生活中的真實交通。現實中,當我們遇到堵車,可能就會希望興建立交橋和高架,或者希望有一位交警來疏導交通。而 TCP 協議的堵塞控制是通過約束自己實現的。當 TCP 的發送方探測到網絡交通擁堵時,會控制自己發送片段的速率,以緩解網絡的交通狀況,避免堵塞崩潰。簡言之,TCP 協議規定了發送方需要遵守的“公德”。
我們先來說明堵塞是如何探測的。在 TCP 重新發送中,我們已經總結了兩種推測 TCP 片段丟失的方法:ACK 超時和重復 ACK。一旦發送方認為 TCP 片段丟失,則認為網絡中出現堵塞。另一方面,TCP 發送方是如何控制發送速率呢?TCP 協議通過控制滑窗大小來控制發送速率。在 TCP 滑窗管理中,我們已經見到了一個窗口限制,就是廣播窗口大小,以實現 TCP 流量控制。TCP 還會維護一個阻塞窗口大小,以根據網絡狀況來調整滑窗大小。真實滑窗大小取這兩個滑窗限制的最小值,從而同時滿足流量控制和堵塞控制的限制。我們下面就來看一看阻塞窗口。
阻塞窗口
阻塞窗口總是處于兩種狀態的一個。這兩種狀態是慢起動(slow start)和堵塞避免(congestion avoidance)。下面是它們的示意圖:
上圖是概念性的。實際的實施要比上圖復雜,而且根據算法不同會有不同的版本。cwnd 代表阻塞窗口大小(congestion window size)。我們以片段的個數為單位,來表示阻塞窗口大小 。實際應用中會以字節為單位,但并不影響這里的講解。
阻塞窗口從慢啟動狀態開始。慢啟動的特點是初始速率低,但速率不斷倍增。每次進入到慢啟動狀態時,阻塞窗口大小都需要重置為初始值 1。發送方每接收到一個正確的 ACK,就會將阻塞窗口大小增加 1,從而實現速率的倍增。需要注意的是,由于累計 ACK,速率增長可能會小于倍增。
當阻塞窗口大小達到閾值(圖中 ssthresh)時,阻塞窗口進入到阻塞避免狀態。發送速率會繼續增長。發送方在每個窗戶所有片段成功傳輸后,將窗口尺寸增加 1,等效于每個往返時間增加 1。所以在阻塞避免模式下,阻塞窗口大小線性增長,增長速率慢。如果在阻塞避免下有片段丟失,重新回到慢啟動狀態,并將閾值更新為阻塞窗口大小的一半。
我們看到,觸及閾值是從慢啟動到 c 阻塞避免的切換點。而片段丟失是阻塞避免到慢啟動的切換點。一開始閾值一般比較大,所以慢啟動可能在切換成阻塞避免之前就丟失片段。這種情況下,慢啟動會重新開始,而閾值更新為阻塞窗口大小的一半。
總的來說,發送速率總是在增長。如果片段丟失,則重置速率為 1,并快速增長。增長到一定程度,則進入到慢性增長。快速增長和慢性增長的切換點會隨著網絡狀況更新。通過上面的機制,讓發送速率處于動態平衡,不斷的嘗試更大值。初始時增長塊,而接近飽和時增長慢。但一旦嘗試過度,則迅速重置,以免造成網絡負擔。阻塞控制有效的提高了互聯網的利用率,但依然不完善。一個常見的問題是阻塞窗口小在接近飽和時線性增長,因此對新增的網絡帶寬不敏感。
TCP 實踐
TCP 協議利用流量控制機制來實現整個網絡的總體效率。到現在為止,已經講解了 TCP 的幾大模塊:分段與流,滑窗,連接,流量控制,重新發送,堵塞控制。現在,我們可以在編程中實際利用一下 TCP 協議。
在 Python 中,我們可以用標準庫中的 socket 包來建立 TCP 連接。這個包已經用于 UDP 協議的套接字編程,它同樣可以用于 TCP 協議的套接字編程。我們需要寫兩個程序,分別用于 TCP 連接的服務器端和客戶端。一旦連接建立,雙方可以相互通信。下面是服務器端的程序。我們用 bind()方法來賦予套接字以固定的地址和端口,用 listen()方法來被動的監聽該端口。當有客戶嘗試用 connect()方法連接的時候,服務器使用 accept()接受連接,從而建立一個 TCP 連接:
# Written by Vamei
# Server side
import socket
# Address
HOST = ''
PORT = 8000
reply = 'Yes'
# Configure socket
s ? ? ?= socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
# passively wait, 3: maximum number of connections in the queue
s.listen(3)
# accept and establish connection
conn, addr = s.accept()
# receive message
request ? ?= conn.recv(1024)
print 'request is: ',request
print 'Connected by', addr
# send message
conn.sendall(reply)
# close connection
conn.close()
上面的程序中,socket.socket()創建一個 socket 對象,并說明 socket 使用的是 IPv4(AF_INET,IP version 4)和 TCP 協議(SOCK_STREAM)。服務器寫好了,下面是客戶。在客戶的程序中,我們主動使用 connect()方法來搜索服務器端的 IP 地址和端口,以便客戶可以找到服務器,并建立連接:
# Written by Vamei
# Client side
import socket
# Address
HOST = '172.20.202.155'
PORT = 8000
request = 'can you hear me?'
# configure socket
s ? ? ? = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
# send message
s.sendall(request)
# receive message
reply ? = s.recv(1024)
print 'reply is: ',reply
# close connection
s.close()
TCP 協議是雙向的。因此,我們在連接兩端都可以調用 recv()方法來接收信息,調用 sendall()方法來發送信息。這樣,我們就可以在分處于兩臺計算機的兩個進程間進行通信了。當通信結束的時候,我們使用 close()方法來關閉 TCP 連接。為了展示網絡通信,上面程序最好運行于兩臺電腦。但如果沒有兩臺計算機做實驗,也可以將客戶端的 IP 改為 127.0.0.1。這是個特殊的 IP 地址,指向主機自身。這樣,我們在同一臺電腦上建立了 TCP 連接。
總結
流量控制要求參與 TCP 通信的各方守公德,從而提高了 TCP 通信的整體效率。這一篇的最后,還有一個實際建立 TCP 連接的例子,用 Python 語言實現。到了這里,TCP 協議的介紹就可以告一段落了。下一章我們將進入應用層。
? ?哈爾莫斯:怎樣做數學研究
? ?扎克伯格2017年哈佛大學畢業演講
? ?線性代數在組合數學中的應用
? ?你見過真的菲利普曲線嗎?
? ?支持向量機(SVM)的故事是這樣子的
? ?深度神經網絡中的數學,對你來說會不會太難?
? ?編程需要知道多少數學知識?
? ?陳省身——什么是幾何學
? ?模式識別研究的回顧與展望
? ?曲面論
? ?自然底數e的意義是什么?
? ?如何向5歲小孩解釋什么是支持向量機(SVM)?
? ?華裔天才數學家陶哲軒自述
? ?代數,分析,幾何與拓撲,現代數學的三大方法論
算法數學之美微信公眾號歡迎賜稿
稿件涉及數學、物理、算法、計算機、編程等相關領域。
稿件一經采用,我們將奉上稿酬。
投稿郵箱:math_alg@163.com
商務合作:微信號hengzi5809
總結
以上是生活随笔為你收集整理的天下为公:TCP堵塞控制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: lastindexof php,java
- 下一篇: 数学不好,如何转行人工智能?