uTorrent 傳輸協議(uTP)由 Ludvig Strigeus,Greg Hazel,Stanislav Shalunov,Arvid Norberg 和 Bram Cohen 設計。
設計原因#
uTP 協議的動機是讓 BitTorrent 客戶端不會中斷互聯網連接,同時仍然充分利用未使用的帶寬。
BitTorrent 流量通常是後台傳輸,其優先級應該低於檢查電子郵件、辦公和瀏覽網頁,但是當使用常規 TCP 連接時,BitTorrent 會快速填滿發送緩衝區,為所有互動式流量增加數秒的延遲。 BitTorrent 使用多個 TCP 連接的事實使其在與其他服務競爭帶寬時具有不公平的優勢,這誇大了 BitTorrent 占用上傳帶寬的效果。這樣做的原因是 TCP 在連接之間均勻分配可用帶寬,並且一個應用程序使用的連接越多,它獲得的帶寬份額就越大。
這個問題的傳統解決方案是將 BitTorrent 客戶端的上傳速率限制在上行帶寬容量的 80%。為其餘上下行流量留下了一些空間。此解決方案的主要缺點是:
- 用戶需要配置他 / 她的 BitTorrent 客戶端,它不會即開即用。
- 用戶需要知道他 / 她的互聯網連接的上限容量。此容量可能會發生變化,尤其是在可能連接到大量不同網絡的筆記本電腦或手機上。
- 20% 的餘量是比較隨意的,會浪費帶寬。每當沒有互動式流量與 BitTorrent 競爭時,額外的 20% 就會被浪費掉。每當存在競爭的互動式流量時,它不能只需要使用 20% 的容量。
uTP 通過使用調製解調器隊列大小作為其發送速率的控制器來解決此問題。當隊列變得太大時,它會控制流量。這允許它在沒有競爭時利用全部上傳容量,並且在有大量互動式流量時允許它減少到幾乎為零。
概述#
本文檔假定讀者對 TCP 和基於窗口的堵塞控制的工作原理有一定的了解。 uTP 是一種分層在 UDP 之上的傳輸協議。因此,它必須(並且有能力)實現自己的網絡堵塞控制。與 TCP 相比,主要區別在於基於延遲的堵塞控制。
uTP 是建立在 UDP 之上的傳輸協議,因此它需要自己實現擁塞控制機制。與 TCP 相比,uTP 的主要區別在於基於延遲的擁塞控制。具體細節可參考擁塞控制部分的描述。類似於 TCP,uTP 採用基於窗口的擁塞控制。每個套接字都有一個 max_window,用於確定套接字在任何給定時間內可同時傳輸的最大字節數。已發送但尚未確認的任何數據包都被認為是在傳輸過程中。
- cur_window 表示當前傳輸過程中的字節數。只有當 cur_window + packet_size 小於等於 min (max_window, wnd_size) 時,套接字才能發送數據包。 packet_size 表示數據包的大小,可能會有不同的取值。
- wnd_size 是對方端口所廣告的窗口大小。它設置了傳輸中的數據包數量的上限。
- 如果 max_window 小於數據包大小,並且通過調整數據包傳輸速率使得平均 cur_window 小於等於 max_window,實現可能違反上述規則。
- 每個套接字保存了與其他端點的最後一次延遲測量狀態(reply_micro)。每當接收到一個數據包時,通過將時間戳(以微秒為單位)減去主機當前時間來更新該狀態。
- 每次發送數據包時,套接字的 reply_micro 值將放置在數據包頭部的 timestamp_difference_microseconds 字段中。
- 與 TCP 不同,uTP 中的序列號和 ACK 是基於數據包而不是字節的。這意味著在重新發送數據時,uTP 無法對其進行重新封裝。
- 每個套接字都保持著下一個用於發送數據包的序列號(seq_nr)和上次接收到的數據包的序列號(ack_nr)的狀態。最老的未確認數據包序列號為 seq_nr – cur_window 。
header 格式#
版本 1 標頭:
0 4 8 16 24 32
+-------+-------+---------------+---------------+---------------+
| type | ver | extension | connection_id |
+-------+-------+---------------+---------------+---------------+
| timestamp_microseconds |
+---------------+---------------+---------------+---------------+
| timestamp_difference_microseconds |
+---------------+---------------+---------------+---------------+
| wnd_size |
+---------------+---------------+---------------+---------------+
| seq_nr | ack_nr |
+---------------+---------------+---------------+---------------+
所有字段均按網絡字節順序(大端序)排列。
版本#
這是協議版本。當前版本為 1 。
connection_id#
這是一個隨機的唯一數字,用於標識屬於同一連接的所有數據包。每個套接字都有一個用於發送數據包的連接 ID 和一個用於接收數據包的不同連接 ID 。啟動連接的終結點決定使用哪個 ID,返回路徑具有相同的 ID + 1 。
timestamp_microseconds#
這是發送此數據包的時間戳的 “微秒” 部分。這是在 posix 上使用 gettimeofday()和在 windows 上使用 QueryPerformanceTimer()設置的。此時間戳的分辨率越高越好。設置的越接近實際傳輸時間越好。
timestamp_difference_microseconds#
這是本地時間與上次接收數據包(在收到最後一個數據包時)中的時間戳之間的差異。這是從遠程對等體到本地計算機的鏈路的最新單向延遲測量。當套接字是新打開的並且還沒有任何延遲樣本時,必須將其設置為 0 。
wnd_size#
播發的接收窗口。這是 32 位寬,以字節為單位指定。窗口大小是當前正在進行的字節數,即已發送但未確認的字節數。通告的接收窗口允許另一端限制窗口大小,如果它不能更快地接收,如果它的接收緩衝區正在填滿。發送數據包時,應將其設置為套接字接收緩衝區中剩餘的字節數。
extension#
擴展標頭鏈接列表中第一個擴展的類型。 0 表示無擴展名。
目前有一個擴展:
- 選擇性確認
擴展是鏈接的,就像 TCP 選項一樣。如果擴展字段不為零,則緊跟在 uTP 標頭後面的兩個字節:
0 8 16
+---------------+---------------+
| extension | len |
+---------------+---------------+
其中 extension 指定鏈表中下個擴展名的類型,0 終止列表。並 len 指定此擴展的字節數。未知擴展可以通過簡單地前進 len bytes 來跳過。
SELECTIVE ACK#
選擇性 ACK 是一種擴展,可以非順序地選擇性地 ACK 數據包。其有效負載是至少 32 位的位掩碼,以 32 位的倍數表示。每个位表示发送窗口中的一个数据包。发送窗口之外的位将被忽略。设置位指定数据包已接收,清除位指定数据包尚未接收。标题如下所示:
0 8 16
+---------------+---------------+---------------+---------------+
| extension | len | bitmask
+---------------+---------------+---------------+---------------+
|
+---------------+---------------+
請注意,擴展的 len 字段引用字節,在此擴展中,字節必須至少為 4,並且是 4 的倍數。
僅當接收的流中至少跳過一個序列號時,才會發送選擇性 ACK 。因此,掩碼中的第一位表示 ack_nr + 2 。 ack_nr + 1 假定在發送此數據包時已被丟棄或丟失。設置位表示已接收的數據包,清除位表示尚未接收的數據包。
位掩碼的字節順序相反。第一個字節以相反的順序表示數據包 [ack_nr + 2, ack_nr + 2 + 7] 。字節中最低有效位表示 ack_nr + 2,字節中最高有效位表示 ack_nr + 2 + 7 。掩碼中的下一個字節以相反的順序表示 [ack_nr + 2 + 8,ack_nr + 2 + 15],依此類推。位掩碼不限於 32 位,但可以是任何大小。
下面是位掩碼的佈局,表示選擇性 ACK 位域中表示的前 32 個數據包確認:
0 8 16
+---------------+---------------+---------------+---------------+
| 9 8 ... 3 2 | 17 ... 10 | 25 ... 18 | 33 ... 26 |
+---------------+---------------+---------------+---------------+
圖中的數字將位掩碼中的位映射到要添加到 ack_nr 的偏移量,以便計算位正在確認的序列號。
type#
類型字段描述數據包的類型。它可以是以下之一:
ST_DATA = 0
常規數據包。套接字處於連接狀態,並且有要發送的數據。 ST_DATA 數據包始終具有數據有效負載。
ST_FIN = 1
完成連接。這是最後一個數據包。它關閉連接,類似於 TCP FIN 標誌。此連接的序列號永遠不會大於此數據包中的序列號。套接字將此序列號記錄為 eof_pkt 。這允許套接字等待可能仍然丟失的數據包,即使在收到 ST_FIN 數據包後也會無序到達。
ST_STATE = 2
狀態數據包。用於傳輸沒有數據的 ACK 。不包含任何有效負載的數據包不會增加 seq_nr 。
ST_RESET = 3
強制終止連接。類似於 TCP RST 標誌。遠程主機沒有任何此連接的狀態。它是過時的,應該終止。
ST_SYN = 4
與 TCP SYN 標誌類似,此數據包啟動連接。序列號初始化為 1 。連接 ID 初始化為隨機數。 syn 數據包是特殊的,在此連接上發送的所有後續數據包(ST_SYN 的重新發送除外)都以連接 ID + 1 發送。連接 ID 是另一端應在其響應中使用的 ID 。
收到 ST_SYN 時,應使用數據包標頭中的 ID 初始化新套接字。套接字的發送 ID 應初始化為 ID + 1 。返回通道的序列號初始化為隨機數。另一端需要 ST_STATE 數據包(僅 ACK)作為響應。
seq_nr#
這是此數據包的序列號。與 TCP 相反,uTP 序列號不是指字節,而是數據包。序列號告訴另一端數據包應以何種順序返回應用層。
ack_nr#
這是數據包的發送方上次在另一個方向上收到的序列號。
連接設置#
下圖說明了啟動連接的交換和狀態。 c.* 表示套接字本身中的狀態,pkt.* 表示數據包標頭中的字段。
initiating endpoint accepting endpoint
| c.state = CS_SYN_SENT |
| c.seq_nr = 1 |
| c.conn_id_recv = rand() |
| c.conn_id_send = c.conn_id_recv + 1 |
| |
| |
| ST_SYN |
| seq_nr=c.seq_nr++ |
| ack_nr=* |
| conn_id=c.rcv_conn_id |
| >-------------------------------------------> |
| c.receive_conn_id = pkt.conn_id+1 |
| c.send_conn_id = pkt.conn_id |
| c.seq_nr = rand() |
| c.ack_nr = pkt.seq_nr |
| c.state = CS_SYN_RECV |
| |
| |
| |
| |
| ST_STATE |
| seq_nr=c.seq_nr++ |
| ack_nr=c.ack_nr |
| conn_id=c.send_conn_id |
| <------------------------------------------< |
| c.state = CS_CONNECTED |
| c.ack_nr = pkt.seq_nr |
| |
| |
| |
| ST_DATA |
| seq_nr=c.seq_nr++ |
| ack_nr=c.ack_nr |
| conn_id=c.conn_id_send |
| >-------------------------------------------> |
| c.ack_nr = pkt.seq_nr |
| c.state = CS_CONNECTED |
| |
| | connection established
.. ..|.. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..|.. ..
| |
| ST_DATA |
| seq_nr=c.seq_nr++ |
| ack_nr=c.ack_nr |
| conn_id=c.send_conn_id |
| <------------------------------------------< |
| c.ack_nr = pkt.seq_nr |
| |
| |
V V
連接由其 conn_id 標頭標識。如果新連接的連接 ID 與現有連接衝突,則連接嘗試將失敗,因為 ST_SYN 數據包在現有流中將是意外的,並被忽略。
丟包#
如果序列號為( seq_nr – cur_window )的數據包尚未確認(這是發送緩衝區中最早的數據包,下個數據包預計被確認),但已通過該數據包 3 個或更多數據包(通過選擇性 ACK),則假定該數據包已丟失。同樣,當收到 3 個重複的確認時, ack_nr 假定 + 1 已丟失(如果已發送具有該序列號的數據包)。
這也適用於選擇性確認。在選擇性確認消息中確認的每個數據包都計為一個重複的確認,如果為 3 個或更多,則應觸發重新發送至少包含 3 個數據包的數據包。
當數據包丟失時, max_window 乘以 0.5 以模擬 TCP 。
超時#
每個被確認的數據包,無論是落在範圍(last_ack_nr 、 ack_nr] 還是被選擇性 ACK 消息顯式確認,都應用於更新(往返時間)和 rtt rtt_var (rtt 方差)測量值。 last_ack_nr 這裡是當前數據包之前在套接字上收到的最後一個 ack_nr,ack_nr 是當前接收的數據包中的字段。
僅針對 rtt 僅發送一次的數據包更新 和 rtt_var 。這避免了確定哪個數據包被確認的問題,第一个还是第二个。
rtt 并通过 rtt_var 以下公式计算,每次确认数据包时:
delta = rtt - packet_rtt
rtt_var += (abs(delta) - rtt_var) / 4;
rtt += (packet_rtt - rtt) / 8;
與套接字關聯的數據包的默認超時也會每次更新 rtt 并 rtt_var 更新。它設置為:
timeout = max(rtt + rtt_var * 4, 500);
其中以毫秒為單位指定超時。即數據包的最小超時為 1/2 秒。
每次套接字發送或接收數據包時,它都會更新其超時計數器。如果在上次超時計數器重置後的毫秒 timeout 內沒有數據包到達,套接字將觸發超時。它會將其 packet_size and max_window 設置為最小的數據包大小(150 字節)。這允許它再發送一個數據包,如果窗口大小降至零,這就是套接字再次啟動的方式。
初始超時設置為 1000 毫秒,稍後根據上述公式進行更新。對於超時的每個連續數據包,超時將加倍。
數據包大小#
為了盡可能減少對慢速擁塞鏈路的影響,uTP 將其數據包大小調整為每個數據包 150 字節。使用這麼小的數據包的好處是不會阻塞慢速上行鏈路,並且序列化延遲較長。使用這麼小的數據包的代價是數據包標頭的開銷變得很大。在高速率下,使用大數據包大小,在慢速率下,使用小數據包大小。
擁塞控制#
uTP 擁塞控制的整體目標是使用單向緩衝區延遲作為主要擁塞測量,以及數據包丟失(如 TCP)。關鍵是要避免在發送數據時使用完整的發送緩衝區運行。對於 DSL / 電纜調製解調器來說,這是一個特別的問題,其中調製解調器中的發送緩衝區通常具有容納數秒數據的空間。 uTP(或任何後台流量協議)的理想緩衝區利用率是以 0 字節緩衝區利用率運行。即任何其他流量可以隨時發送,而不會受到後台流量阻塞發送緩衝區的阻礙。實際上,uTP 目標延遲設置為 100 毫秒。每個套接字的目標是永遠不會在發送鏈接上看到超過 100 毫秒的延遲。如果是這樣,它將節流回去。
這有效地使 uTP 屈服於任何 TCP 流量。
這是通過在通過 uTP 發送的每個數據包中包含高分辨率時間戳來實現的,接收端計算其自己的高分辨率計時器與其接收的數據包中的時間戳之間的差異。然後將此差異反饋給數據包的原始發送方(timestamp_difference_microseconds)。此值作為絕對值沒有意義。機器中的時鐘很可能不同步,尤其是沒有達到微秒級的分辨率,並且數據包的傳輸時間也包含在這些時間戳的差異中。但是,與以前的值相比,該值很有用。
每個套接字在最後兩分鐘內保持最低值的滑動最小值。此值稱為 base_delay,用作基準,即主機之間的最小延遲。從每個數據包的時間戳差異中減去 base_delay 時,您可以測量套接字上的當前緩衝延遲。這種測量稱為 our_delay 。它有很多噪音,但用作驅動程序來確定是增加還是減少發送窗口(控制發送速率)。
CCONTROL_TARGET 是 uTP 在上行鏈路上接受的緩衝延遲。目前,延遲目標設置為 100 毫秒,off_target 實際測量的延遲與目標延遲的距離(根據 CCONTROL_TARGET – our_delay 計算)。
套接字結構中的窗口大小指定了我們在連接上總共可能具有的運行(未確認)字節數。發送速率與此窗口大小直接相關。傳輸中的字節越多,發送速率越快。在代碼中,窗口大小稱為 max_window 。它的大小大致由以下表達式控制:
delay_factor = off_target / CCONTROL_TARGET;
window_factor = outstanding_packet / max_window;
scaled_gain = MAX_CWND_INCREASE_PACKETS_PER_RTT * delay_factor * window_factor;
其中,第一個因素將 off_target 縮放到目標延遲單位。
然後將 scaled_gain 添加到 max_window:
max_window += scaled_gain;
如果 off_target 大於 0,這將使窗口變小,如果偏離目標小於 0,則窗口增大。
如果 max_window 小於 0,則設置為 0 。窗口大小為零表示套接字可能不會發送任何數據包。在此狀態下,套接字將觸發超時並強制窗口大小為一個數據包大小,並發送一個數據包。關於詳細信息,請參閱有關超時的部分。