TCP協(xié)議
KCP是一個(gè)快速可靠協(xié)議抡句,能以比 TCP 浪費(fèi) 10%-20% 的帶寬的代價(jià)整胃,換取平均延遲降低 30%-40%角溃,且最大延遲降低三倍的傳輸效果拷获。純算法實(shí)現(xiàn),并不負(fù)責(zé)底層協(xié)議(如UDP)的收發(fā)减细,需要使用者自己定義下層數(shù)據(jù)包的發(fā)送方式匆瓜,以 callback的方式提供給 KCP。 連時(shí)鐘都需要外部傳遞進(jìn)來(lái),內(nèi)部不會(huì)有任何一次系統(tǒng)調(diào)用驮吱。
TCP 是為流量設(shè)計(jì)的(每秒鐘多少 KB)茧妒,KCP 是為流速設(shè)計(jì)的(RTT 時(shí)延多少毫秒)。KCP 參考 TCP 做了一些優(yōu)化, 犧牲了帶寬, 以換取更低的時(shí)延, 設(shè)計(jì)上大部分是通用的, 所以這里先介紹 TCP 協(xié)議的原理.
假設(shè)你已經(jīng)了解 TCP/IP 的基本概念, UDP 和 TCP 都屬于第四層傳輸層, 完成了進(jìn)程到另一個(gè)進(jìn)程通信的最后一步, 我們看看 TCP 的協(xié)議報(bào)
TCP 的協(xié)議報(bào)
5層網(wǎng)絡(luò)依次是: 物理層, 鏈路層, 網(wǎng)絡(luò)層, 傳輸層, 應(yīng)用層
網(wǎng)絡(luò)上的包都是完整的, 有了上層必須有下層, 所以下面也貼一下第二層鏈路層和第三層網(wǎng)絡(luò)層的協(xié)議報(bào)
第二層鏈路層的協(xié)議報(bào)
MAC 以 幀 為單位, 包含協(xié)議報(bào)(18字節(jié)), 數(shù)據(jù)(46-1500字節(jié))
協(xié)議報(bào)包含了源 MAC 地址(6字節(jié)), 目標(biāo) MAC 地址(6字節(jié)), 以太網(wǎng)類型(2字節(jié)), 循環(huán)冗余校驗(yàn)碼(4字節(jié))
- 以太網(wǎng)類型: 0x0800 表示 IP 協(xié)議, 0x0806 表示 ARP 協(xié)議, 0x8035 表示 RARP 協(xié)議
第三層網(wǎng)絡(luò)層的協(xié)議報(bào)
IP 層提供主機(jī)到主機(jī)之間的通信, 以 包 為單位, 包含協(xié)議報(bào)(20-60字節(jié)), 數(shù)據(jù)(0-65535 字節(jié))
協(xié)議報(bào)包含了版本(4位), 首部長(zhǎng)度(4位), 服務(wù)類型(8位), 總長(zhǎng)度(16位), 標(biāo)識(shí)(16位), 標(biāo)志(3位), 片偏移(13位), 生存時(shí)間(8位), 協(xié)議(8位), 首部校驗(yàn)和(16位), 源 IP 地址(32位), 目標(biāo) IP 地址(32位), 選項(xiàng)(0-40字節(jié))
- 版本: 4位, 4 表示 IPv4, 6 表示 IPv6
- 首部長(zhǎng)度: 4位, 表示首部/協(xié)議報(bào)的長(zhǎng)度, 以 32 位(4字節(jié))為單位, 也就是說(shuō), 首部的長(zhǎng)度最小是 5 個(gè) 32 位, 最大是 15 個(gè) 32 位, 也就是說(shuō), 最小是 20 字節(jié), 最大是 60 字節(jié)
- 服務(wù)類型/區(qū)分服務(wù): 8位, 用來(lái)標(biāo)識(shí)服務(wù)的類型, 一般不用, 只有在區(qū)分服務(wù)時(shí)候這個(gè)字段才有用
- 總長(zhǎng)度: 16位, 表示整個(gè)包的長(zhǎng)度, 以字節(jié)為單位, 最小是 20 字節(jié)(只包含協(xié)議報(bào)), 最大是 65535 字節(jié)
- 標(biāo)識(shí): 16位, IP軟件在存儲(chǔ)器中維持一個(gè)計(jì)數(shù)器左冬,每產(chǎn)生一個(gè)數(shù)據(jù)報(bào)桐筏,計(jì)數(shù)器就加1,并將此值賦給標(biāo)識(shí)字段拇砰。但這個(gè)“標(biāo)識(shí)”并不是序號(hào)梅忌,因?yàn)镮P是無(wú)連接服務(wù),數(shù)據(jù)報(bào)不存在按序接收的問(wèn)題除破。當(dāng)數(shù)據(jù)報(bào)由于長(zhǎng)度超過(guò)網(wǎng)絡(luò)的MTU而必須分片時(shí)牧氮,這個(gè)標(biāo)識(shí)字段的值就被復(fù)制到所有的數(shù)據(jù)報(bào)片的標(biāo)識(shí)字段中。相同的標(biāo)識(shí)字段的值使分片后的各數(shù)據(jù)報(bào)片最后能正確地重裝成為原來(lái)的數(shù)據(jù)報(bào)
- 標(biāo)志: 3位, 最低位表示是否分片, 0 表示不分片, 1 表示分片, 中間位表示是否是最后一個(gè)分片, 0 表示不是, 1 表示是, 最高位保留, 一般為 0
- 片偏移: 片偏移 占13位瑰枫。片偏移指出:較長(zhǎng)的分組在分片后蹋笼,某片在原分組中的相對(duì)位置。也就是說(shuō)躁垛,相對(duì)于用戶數(shù)據(jù)字段的起點(diǎn)剖毯,該片從何處開始。片偏移以8個(gè)字節(jié)為偏移單位教馆。這就是說(shuō)逊谋,每個(gè)分片的長(zhǎng)度一定是8字節(jié)(64位)的整數(shù)倍。
- 生存時(shí)間: 占8位土铺,生存時(shí)間字段常用的英文縮寫是TTL (Time To Live)胶滋,表明是數(shù)據(jù)報(bào)在網(wǎng)絡(luò)中的壽命。由發(fā)出數(shù)據(jù)報(bào)的源點(diǎn)設(shè)置這個(gè)字段悲敷。其目的是防止無(wú)法交付的數(shù)據(jù)報(bào)無(wú)限制地在因特網(wǎng)中兜圈子(例如從路由器R1轉(zhuǎn)發(fā)到R2究恤,再轉(zhuǎn)發(fā)到R3,然后又轉(zhuǎn)發(fā)到R1)后德,因而白白消耗網(wǎng)絡(luò)資源部宿。最初的設(shè)計(jì)是以秒作為TTL值的單位。每經(jīng)過(guò)一個(gè)路由器時(shí)瓢湃,就把TTL減去數(shù)據(jù)報(bào)在路由器所消耗掉的一段時(shí)間理张。若數(shù)據(jù)報(bào)在路由器消耗的時(shí)間小于1秒,就把TTL值減1绵患。當(dāng)TTL值減為零時(shí)雾叭,就丟棄這個(gè)數(shù)據(jù)報(bào).
- 協(xié)議: 8位, 表示下一層協(xié)議的類型,例如:
注意這里的 IP 指的是再次將 IP 報(bào)封裝到 IP 報(bào)中
- 校驗(yàn)和: 16位, 用來(lái)檢驗(yàn) IP 報(bào)頭的正確性, 由發(fā)送方計(jì)算, 接收方檢驗(yàn)
- 源地址: 32位, 表示發(fā)送方的 IP 地址
- 目的地址: 32位, 表示接收方的 IP 地址
- 選項(xiàng): 可選, 用來(lái)擴(kuò)展 IP 報(bào)文, 一般不使用
第四層TCP協(xié)議報(bào)
TCP 提供端到端的通信, 是進(jìn)程之間通信的最后一步, 以 段 為單位, 包含協(xié)議報(bào)(20-60字節(jié)), 數(shù)據(jù)(0-65535字節(jié))
- 源端口: 16位, 表示發(fā)送方的端口號(hào)
- 目的端口: 16位, 表示接收方的端口號(hào)
- 序列號(hào): 32位, 表示發(fā)送方發(fā)送的數(shù)據(jù)的第一個(gè)字節(jié)的序號(hào)
- 確認(rèn)號(hào): 32位, 表示接收方期望收到的下一個(gè)字節(jié)的序號(hào)
- 部首長(zhǎng)度: 4位, 表示 TCP 報(bào)頭的長(zhǎng)度, 一般為 5, 也就是 20 字節(jié)
- 保留: 6位, 保留, 一般為 0
- 標(biāo)志位: 6位, 用來(lái)標(biāo)識(shí) TCP 報(bào)文的各種狀態(tài), 例如: SYN, ACK, FIN 等
- URG(Urgent) 緊急標(biāo)志, 表示緊急指針 (16位) 生效 (比如遠(yuǎn)程ssh的Ctl+C中斷命令), 發(fā)送端不再按順序發(fā)送, 優(yōu)先發(fā)送和后面的緊急指針配合的緊急數(shù)據(jù), 接收端優(yōu)先接受, 不等待 buffer 滿, 讀取 start=序列號(hào), offset=緊急指針的數(shù)據(jù)
- ACK (Acknowledge) 確認(rèn)標(biāo)志, 確認(rèn)序列號(hào) (32位) 生效
- PUSH 推送標(biāo)志, 起到催促作用, 發(fā)送端不再按順序發(fā)送, 優(yōu)先創(chuàng)建 PUSH 報(bào)文, 接收端不等待 buffer 滿, 讀取整個(gè) buffer + 新報(bào)文的數(shù)據(jù)
- RST (Reset) 重置, 表示需要退出或者重新連接
- SYN (Synchronization) 同步 (3 次握手的 SYN 包)
- FIN (Finish) 結(jié)束 (4次揮手的 FIN 包)
- 窗口大小: 16位, 接受方 ACK 發(fā)送自己的接受窗口大小, 用來(lái)控制發(fā)送方的發(fā)送速率 (流量控制)
- 校驗(yàn)和: 16位, 用來(lái)檢驗(yàn) TCP 報(bào)頭的正確性, 由發(fā)送方計(jì)算, 接收方檢驗(yàn)
- 緊急指針: 16位, 只有緊急指針標(biāo)志位 URG 有效時(shí)候有效, 表示緊急數(shù)據(jù)的最后一個(gè)字節(jié)的序號(hào)
- 選項(xiàng): 可選, 用來(lái)擴(kuò)展 TCP 報(bào)文, 比如 SACK(Selective ACK), MSS(Maximum Segment Size), TS(Timestamp), WSOPT(Window Scale Option) 等
可靠傳輸?shù)墓ぷ髟?/h2>
我們知道,TCP發(fā)送的報(bào)文段是交給IP層傳送的落蝙。但I(xiàn)P層只能提供盡最大努力服務(wù)织狐,也就是說(shuō)暂幼,TCP下面的網(wǎng)絡(luò)所提供的是不可靠的傳輸。因此移迫,TCP必須采用適當(dāng)?shù)拇胧┎拍苁沟脙蓚€(gè)運(yùn)輸層之間的通信變得可靠旺嬉。
理想的傳輸條件有以下兩個(gè)特點(diǎn):
(1) 可靠傳輸: 傳輸信道不產(chǎn)生差錯(cuò)。
(2) 不管發(fā)送方以多快的速度發(fā)送數(shù)據(jù)起意,接收方總是來(lái)得及處理收到的數(shù)據(jù)。
可靠傳輸主要由 ACK + 重傳機(jī)制 保證, 有 停止等待協(xié)議 和 連續(xù)ARQ協(xié)議 兩種實(shí)現(xiàn)方式病瞳。
最簡(jiǎn)單的方案就是停止等待協(xié)議
停止等待協(xié)議
停止等待 就是每發(fā)送完一個(gè)分組就停止發(fā)送揽咕,等待對(duì)方的確認(rèn)。在收到確認(rèn)后再發(fā)送下一個(gè)分組套菜。
無(wú)差錯(cuò)情況
例如: A發(fā)送分組M1亲善,發(fā)完就暫停發(fā)送,等待B的確認(rèn)逗柴。B收到了M1就向A發(fā)送確認(rèn)蛹头。A在收到了對(duì)M1的確認(rèn)后,就再發(fā)送下一個(gè)分組M2戏溺。同樣渣蜗,在收到B對(duì)M2的確認(rèn)后,再發(fā)送M3旷祸。
有差錯(cuò)情況
例如: A發(fā)送分組M1耕拷,M1丟包了, A等待超時(shí)還沒(méi)有收到 M1 的確認(rèn), 就重傳 M1
停止等待協(xié)議的信道利用率問(wèn)題
關(guān)于停止等待協(xié)議的信道利用率, 如果A發(fā)送數(shù)據(jù)的時(shí)間是 Td, B接受和處理的時(shí)間忽略不計(jì), B 返回 ACK 的時(shí)間是 Ta, 數(shù)據(jù)在網(wǎng)絡(luò)中傳輸?shù)臅r(shí)間是 RTT, 那么信道的利用率是 Td/(Td+RTT+Ta)
比如 A, B 距離 200KM, RTT 是 20ms, 發(fā)送速率是 1MB/s, 平均 TCP 包的大小 1KB, 用時(shí) 1ms 發(fā)送 Ta 忽略不計(jì)
信道的利用率大概就是 1/21
停止等待協(xié)議的信道利用率實(shí)在是太低了, 這還沒(méi)有算上超時(shí)丟包等異常情況, 算上的話信道利用率會(huì)更低
流水線的傳輸 (連續(xù)的 ARQ 協(xié)議) 可以提高信道利用率, 如下圖所示
連續(xù)的 ARQ 協(xié)議(滑動(dòng)窗口協(xié)議)
滑動(dòng)窗口協(xié)議,可以將窗口內(nèi)的多個(gè)分組數(shù)據(jù)都發(fā)送出去, 而不需要等待對(duì)方的確認(rèn)托享。這樣骚烧,信道利用率就提高了.
TCP 什么時(shí)候發(fā)送端什么時(shí)候發(fā)送數(shù)據(jù), 接收端什么時(shí)候確認(rèn)數(shù)據(jù)呢? 這里也是使用緩存的 累積 的思想, 發(fā)送端累積發(fā)送, 接收端累積確認(rèn)
累積發(fā)送
應(yīng)用進(jìn)程把數(shù)據(jù)傳送到TCP的發(fā)送緩存后,剩下的發(fā)送任務(wù)就由TCP來(lái)控制了闰围≡甙恚可以用不同的機(jī)制來(lái)控制TCP報(bào)文段的發(fā)送時(shí)機(jī)。
- 第一種機(jī)制是TCP維持一個(gè)變量羡榴,它等于最大報(bào)文段長(zhǎng)度MSS碧查。只要緩存中存放的數(shù)據(jù)達(dá)到MSS字節(jié)時(shí),就組裝成一個(gè)TCP報(bào)文段發(fā)送出去校仑。
- 第二種機(jī)制是由發(fā)送方的應(yīng)用進(jìn)程指明要求立即發(fā)送報(bào)文段么夫,比如 PUSH 和 URG 標(biāo)志位。
- 第三種機(jī)制是發(fā)送方的一個(gè)計(jì)時(shí)器期限到了肤视,這時(shí)就把當(dāng)前已有的緩存數(shù)據(jù)裝入報(bào)文段(但長(zhǎng)度不能超過(guò)MSS)發(fā)送出去档痪。
其實(shí)什么時(shí)候發(fā)送數(shù)據(jù)是一個(gè)復(fù)雜的問(wèn)題, 因?yàn)?必須考慮傳輸效率, 后面會(huì)講到
累積確認(rèn)
同理累積發(fā)送, 接收方的TCP接收到一個(gè)報(bào)文段后,就把它放入接收緩存中, 剩下的確認(rèn)任務(wù)就由TCP來(lái)控制了邢滑「可以用不同的機(jī)制來(lái)控制TCP報(bào)文段的確認(rèn)時(shí)機(jī)愿汰。
- 第一種機(jī)制是TCP維持一個(gè)變量,它等于最大報(bào)文段長(zhǎng)度MSS乐纸。只要接收緩存中存放的數(shù)據(jù)達(dá)到MSS字節(jié)時(shí)衬廷,就把累積確認(rèn)標(biāo)志置1,發(fā)送確認(rèn)報(bào)文段汽绢。
- 第二種機(jī)制是由發(fā)送方的應(yīng)用進(jìn)程指明要求立即確認(rèn)的報(bào)文段吗跋,比如 PUSH 和 URG 標(biāo)志位。
- 第三種機(jī)制是接收方的一個(gè)計(jì)時(shí)器期限到了宁昭,這時(shí)就把累積確認(rèn)標(biāo)志置1跌宛,發(fā)送確認(rèn)報(bào)文段。
累積確認(rèn)只需要回復(fù)完整連續(xù)的數(shù)據(jù)塊的最大序號(hào), 不需要回復(fù)每一個(gè)數(shù)據(jù)塊的序號(hào), 大大減少了 ack 的數(shù)量
比如上圖 (a) 發(fā)送端將 1 2 3 4 5 分組都發(fā)送出去
- 如果接收端回復(fù)了收到了數(shù)據(jù) 1, 發(fā)送端就能將窗口向右移動(dòng) 1 位, 如上圖 (b) 所示
- 如果接收端回復(fù)了收到了數(shù)據(jù) 2, 發(fā)送端就能將窗口向右移動(dòng) 2 位, 如上圖 (c) 所示
注意: 上圖將窗口和緩存畫成線性數(shù)組, 其實(shí)緩存應(yīng)該是循環(huán)利用的環(huán)形 ringbuffer
窗口只能不動(dòng)或者右移動(dòng), 收到 ack 之后就右移, 并且窗口左邊的數(shù)據(jù)都不能再使用
剛剛我們介紹了正常情況下的 ack 機(jī)制, 我們?cè)倏纯?2 種異常情況下重傳機(jī)制, 超時(shí)重傳和快重傳
超時(shí)重傳
同理停止等待協(xié)議, 如果超時(shí)還沒(méi)有收到 ack (發(fā)送的數(shù)據(jù)包丟包或者 ack 的數(shù)據(jù)包丟包) 就重傳數(shù)據(jù)
例如: A發(fā)送分組M1积仗,M1丟包了, A等待超時(shí)還沒(méi)有收到 M1 的確認(rèn), 就重傳 M1
超時(shí)重傳時(shí)間是多少呢?
如果把超時(shí)重傳時(shí)間設(shè)置得太短疆拘,就會(huì)引起很多報(bào)文段的不必要的重傳,使網(wǎng)絡(luò)負(fù)荷增大寂曹。但若把超時(shí)重傳時(shí)間設(shè)置得過(guò)長(zhǎng)哎迄,則又使網(wǎng)絡(luò)的空閑時(shí)間增大,降低了傳輸效率隆圆。
TCP 使用自適應(yīng)算法(一種動(dòng)態(tài)規(guī)劃), 動(dòng)態(tài)計(jì)算超時(shí)重傳時(shí)間 RTO (RetransmissionTime-Out)
計(jì)算超時(shí)重傳時(shí)間 RTO
(1) 首先需要計(jì)算平均往返時(shí)間, 又叫做平滑往返時(shí)間 RTTS (Round Trip Time Smooth)
新RTTS = (1-α) * 舊RTTS + α * 新RTT
α 是平滑因子, 一般取 0.125
(2) 然后計(jì)算平均往返時(shí)間的偏差, 又叫做平滑往返時(shí)間偏差 RTTVAR (Round Trip Time Variation)
新RTTVAR = (1-β) * 舊RTTVAR + β * |新RTTS - 舊RTTS|
β 是平滑因子, 一般取 0.25
(3) 最后計(jì)算超時(shí)重傳時(shí)間 RTO
RTO = RTTS + 4 * RTTVAR
一般 RTO 的最小值是 1 秒, 最大值是 60 秒
為什么說(shuō)往返時(shí)間 RTT 是不準(zhǔn)確的?
往返時(shí)間 RTT 一般計(jì)算方式是: 發(fā)送端發(fā)送數(shù)據(jù)包, 接收端收到數(shù)據(jù)包后回復(fù) ack, 發(fā)送端收到 ack 后計(jì)算時(shí)間差, 得到往返時(shí)間 RTT
為什么說(shuō)往返時(shí)間 RTT 是不準(zhǔn)確的? 主要有 2 個(gè)原因:
- 接收端接受數(shù)據(jù)不會(huì)立即回復(fù), 而是等待延遲應(yīng)答定時(shí)器結(jié)束后再回復(fù)
- 如何判定此確認(rèn)報(bào)文段是對(duì)先發(fā)送的報(bào)文段的確認(rèn)漱挚,還是對(duì)后來(lái)重傳的報(bào)文段的確認(rèn)
針對(duì)第 2 點(diǎn), 比如發(fā)送端發(fā)送了一個(gè)數(shù)據(jù)包M1, 如果M1丟包了, 發(fā)送端會(huì)重傳M2, 接收端收到M2后, 會(huì)回復(fù)一個(gè) ack, 但是發(fā)送端收到的 ack 可能是對(duì) M1 的確認(rèn), 也可能是對(duì)重傳的 M2 的確認(rèn), 如果發(fā)送端用重傳的 M2 的確認(rèn)來(lái)計(jì)算發(fā)送時(shí)間, 得到的往返時(shí)間 RTT 就會(huì)比實(shí)際的往返時(shí)間 RTT 大, 從而導(dǎo)致超時(shí)重傳時(shí)間 RTO 計(jì)算不準(zhǔn)確
解決方法就是: 重傳的往返時(shí)間不參與 RTT 計(jì)算
只要一個(gè)包發(fā)生了重傳, 這個(gè)包就不參與 RTT 計(jì)算, 直接 RTO翻倍
也就是說(shuō)每次發(fā)生重傳 RTO 都會(huì)翻倍, 比如連續(xù)重傳 3 次, RTO 就會(huì)變成 8 倍 (對(duì)比 KCP 的翻 1.5 倍, 8 倍的翻倍是非常恐怖的)
快重傳
超時(shí)重傳需要重傳丟包位置開始后面的所有數(shù)據(jù), 比較浪費(fèi)資源
比如發(fā)送端發(fā)送 1 2 3 4 5 6幾組數(shù)據(jù), 只有 3 丟包了, 超時(shí)重傳需要重傳 3 4 5 6
快重傳: 接收方每收到一個(gè)失序的報(bào)文段后就立即發(fā)出重復(fù)確認(rèn), 發(fā)送端收到 3 個(gè)重復(fù)的確認(rèn)后立即重傳, 而不是等到發(fā)送端超時(shí)重傳 (快重傳一般和 TCP 擁塞控制的 快恢復(fù) 搭配使用, 后面介紹)
超時(shí)重傳的情況下: 接收方收到數(shù)據(jù)一般不會(huì)馬上回復(fù), TCP 會(huì)聚合收到的數(shù)據(jù), 比如每 200ms 回復(fù)一次
下面舉個(gè)例子介紹下快重傳的流程:
比如發(fā)送端發(fā)送 1 2 3 4 5 6幾組數(shù)據(jù), 3 丟包了
接收端收到 1 2 4 后, 因?yàn)槔鄯e確認(rèn)只能確認(rèn)收到完整的數(shù)據(jù), 所以立即回復(fù)收到了 2
接收端下次再收到數(shù)據(jù) 5, 還是沒(méi)有收到 3, 回復(fù)收到了 2
接收端下次再收到數(shù)據(jù) 6, 還是沒(méi)有收到 3, 回復(fù)收到了 2
發(fā)送端 3 次收到重復(fù)的 ack 后, 立即重傳 3
接收端湊齊了 1 2 3 4 5 6, 回復(fù)收到了 6
ack 表示了接下來(lái)發(fā)送的數(shù)據(jù), 所以回復(fù)收到了 x, 其實(shí) ack=x+1, 例如回復(fù)收到了 6, 其實(shí) ack=7
由于發(fā)送方能盡早重傳未被確認(rèn)的報(bào)文段渺氧,因此采用快重傳后可以使整個(gè)網(wǎng)絡(luò)的吞吐量提高約20%
SACK 選擇性確認(rèn)
快重傳主要是解決 累積確認(rèn) 如果中間數(shù)據(jù)包丟了導(dǎo)致的重傳丟包后所有數(shù)據(jù)的資源浪費(fèi)問(wèn)題
這主要是因?yàn)槔鄯e確認(rèn)只返回收到的完整的最大序號(hào), 例如: 1 2 3 4 5 6, 3 丟了, 累積確認(rèn)只能返回 2, 所以發(fā)送端只能重傳 3 4 5 6
其實(shí)如果我們能知道 3 丟了, 那么就可以只重傳 3, 而不是 3 4 5 6
如何知道 3 丟了呢? 這就是 SACK 選擇性確認(rèn)
SACK 選擇性確認(rèn): 接收方收到數(shù)據(jù)后, 不僅僅返回收到的最大序號(hào), 還會(huì)在報(bào)文的選項(xiàng)部分返回收到的數(shù)據(jù)塊, 例如: 1 2 3 4 5 6, 3 丟了, 在選項(xiàng)部分返回 1-2 4-6 表示收到的數(shù)據(jù)塊, 這樣發(fā)送端就知道 3 丟了, 只需要重傳 3
SACK 處于 TCP 頭部的選項(xiàng)部分, 需要發(fā)送方和接收方都支持
選項(xiàng)部分最多有 40 字節(jié), 標(biāo)記一個(gè)數(shù)據(jù)塊需要 2 個(gè)邊界, 也就是 2 * 4 = 8
字節(jié), 因?yàn)樾枰?1 個(gè)字節(jié)表示選項(xiàng)類型, 1 個(gè)字節(jié)表示選項(xiàng)長(zhǎng)度, 所以 SACK 最多標(biāo)記 4 個(gè)數(shù)據(jù)塊 (4 * 8 + 2 = 34
沒(méi)超過(guò), 5 * 8 + 2 = 42
超過(guò))
SACK文檔并沒(méi)有指明發(fā)送方應(yīng)當(dāng)怎樣響應(yīng)SACK棱烂。因此大多數(shù)的實(shí)現(xiàn)還是重傳所有未被確認(rèn)的數(shù)據(jù)塊。
流量控制
區(qū)分流量控制和擁塞控制
也許你聽說(shuō)過(guò) TCP 的流量控制, 擁塞控制, 我這里舉個(gè)例子來(lái)區(qū)分下他們
流量控制: 比如帶寬是 10Gb, A 向 B 以 1Gb 的速度發(fā)送數(shù)據(jù), 顯然網(wǎng)絡(luò)帶寬是足夠的不存在擁塞問(wèn)題, 但是流量控制是必須的, 因?yàn)?B 處理不過(guò)來(lái), 需要經(jīng)常停下來(lái) (這個(gè)時(shí)候 B 可以通過(guò)流量控制告訴 A, 你的速度太快了, 我處理不過(guò)來(lái), 你慢點(diǎn))
擁塞控制: 比如帶寬是 1Mb, 有 1000 臺(tái)機(jī)器用 100Kb 的速度向服務(wù)器發(fā)送數(shù)據(jù), 網(wǎng)絡(luò)接受不了這么多的數(shù)據(jù), 交換機(jī)和路由器處理不過(guò)來(lái)的數(shù)據(jù)就會(huì)丟棄, 導(dǎo)致大量丟包. (這個(gè)時(shí)候發(fā)送端丟包了就知道網(wǎng)絡(luò)可能不太好, 就使用擁塞控制減低發(fā)送速度)
流量控制防止數(shù)據(jù)將服務(wù)器的撐爆, 擁塞控制防止把網(wǎng)絡(luò)設(shè)備撐爆
基于滑動(dòng)窗口實(shí)現(xiàn)流量控制
流量控制是基于滑動(dòng)窗口實(shí)現(xiàn)的, 每次發(fā)送端發(fā)送數(shù)據(jù)后, 接收端會(huì)返回一個(gè)窗口大小, 發(fā)送端根據(jù)窗口大小來(lái)決定發(fā)送的數(shù)據(jù)量
舉個(gè)例子
- A 和 B 建立連接的時(shí)候 B 告訴 A, 我的接受窗口是 400 字節(jié), 發(fā)送端的發(fā)送窗口不要大于接收端的接受窗口
- 接收方 B 對(duì)于發(fā)送方 A 進(jìn)行了 3 次流量控制, 第一次將窗口減少到 300, 第二次減少到 100, 第三次減少到 0
我們又考慮一種情況, 如果窗口減少到 0 之后, B 將數(shù)據(jù)處理后又有了空間, 于是 B 向 A 發(fā)送窗口有 400 的報(bào)文, A 收到后將窗口增加到 400
但是如果 B 向 A 發(fā)送窗口有 400 的報(bào)文丟包了呢? A一直等待收到B發(fā)送的非零窗口的通知阶女,而B也一直等待A發(fā)送的數(shù)據(jù)颊糜。如果沒(méi)有其他措施,這種互相等待的死鎖局面將一直延續(xù)下去秃踩。
為了解決這個(gè)問(wèn)題衬鱼,TCP為每一個(gè)連接設(shè)有一個(gè)持續(xù)計(jì)時(shí)器(persistence timer)。只要TCP連接的一方收到對(duì)方的零窗口通知憔杨,就啟動(dòng)持續(xù)計(jì)時(shí)器鸟赫。若持續(xù)計(jì)時(shí)器設(shè)置的時(shí)間到期,就發(fā)送一個(gè)零窗口探測(cè)報(bào)文段(僅攜帶1字節(jié)的數(shù)據(jù))消别,而對(duì)方就在確認(rèn)這個(gè)探測(cè)報(bào)文段時(shí)給出了現(xiàn)在的窗口值抛蚤。如果窗口仍然是零,那么收到這個(gè)報(bào)文段的一方就重新設(shè)置持續(xù)計(jì)時(shí)器寻狂。如果窗口不是零岁经,那么死鎖的僵局就可以打破了。
必須考慮傳輸效率
其實(shí)什么時(shí)候發(fā)送數(shù)據(jù)是一個(gè)復(fù)雜的問(wèn)題, 在 累積發(fā)送 的基礎(chǔ)上, 必須考慮傳輸效率
我們來(lái)看一種極端情況
一個(gè)交互式用戶使用一條 ssh 連接(運(yùn)輸層為TCP協(xié)議)蛇券。假設(shè)用戶只發(fā)1個(gè)字符缀壤。加上20字節(jié)的首部后樊拓,得到21字節(jié)長(zhǎng)的TCP報(bào)文段。再加上20字節(jié)的IP首部塘慕,形成41字節(jié)長(zhǎng)的IP數(shù)據(jù)報(bào)筋夏。在接收方TCP立即發(fā)出確認(rèn),構(gòu)成的數(shù)據(jù)報(bào)是40字節(jié)長(zhǎng)(假定沒(méi)有數(shù)據(jù)發(fā)送)图呢。若用戶要求遠(yuǎn)地主機(jī)回送這一字符条篷,則又要發(fā)回41字節(jié)長(zhǎng)的IP數(shù)據(jù)報(bào)和40字節(jié)長(zhǎng)的確認(rèn)IP數(shù)據(jù)報(bào)。這樣蛤织,用戶僅發(fā)1個(gè)字符時(shí)線路上就需傳送總長(zhǎng)度為162字節(jié)共4個(gè)報(bào)文段赴叹。當(dāng)線路帶寬并不富裕時(shí),這種傳送方法的效率的確不高瞳筏。因此應(yīng)適當(dāng)推遲發(fā)回確認(rèn)報(bào)文稚瘾,并盡量使用捎帶確認(rèn)的方法牡昆。
在TCP的實(shí)現(xiàn)中廣泛使用Nagle算法姚炕。算法如下:若發(fā)送應(yīng)用進(jìn)程把要發(fā)送的數(shù)據(jù)逐個(gè)字節(jié)地送到TCP的發(fā)送緩存,則發(fā)送方就把第一個(gè)數(shù)據(jù)字節(jié)先發(fā)送出去丢烘,把后面到達(dá)的數(shù)據(jù)字節(jié)都緩存起來(lái)柱宦。當(dāng)發(fā)送方收到對(duì)第一個(gè)數(shù)據(jù)字符的確認(rèn)后,再把發(fā)送緩存中的所有數(shù)據(jù)組裝成一個(gè)報(bào)文段發(fā)送出去播瞳,同時(shí)繼續(xù)對(duì)隨后到達(dá)的數(shù)據(jù)進(jìn)行緩存掸刊。只有在收到對(duì)前一個(gè)報(bào)文段的確認(rèn)后才繼續(xù)發(fā)送下一個(gè)報(bào)文段。當(dāng)數(shù)據(jù)到達(dá)較快而網(wǎng)絡(luò)速率較慢時(shí)赢乓,用這樣的方法可明顯地減少所用的網(wǎng)絡(luò)帶寬忧侧。Nagle算法還規(guī)定,當(dāng)?shù)竭_(dá)的數(shù)據(jù)已達(dá)到發(fā)送窗口大小的一半或已達(dá)到報(bào)文段的最大長(zhǎng)度時(shí)牌芋,就立即發(fā)送一個(gè)報(bào)文段蚓炬。這樣做,就可以有效地提高網(wǎng)絡(luò)的吞吐量躺屁。
我們?cè)賮?lái)看一種極端情況, 叫做 "糊涂窗口綜合癥"
TCP接收方的緩存已滿肯夏,而交互式的應(yīng)用進(jìn)程一次只從接收緩存中讀取 1 個(gè)字節(jié), 接受方有 1 個(gè)接受窗口空余的時(shí)候, 向發(fā)送端告知還有一個(gè)接受窗口空余, 這樣發(fā)送方的發(fā)送窗口為 1, 發(fā)送端再發(fā) 1 個(gè)字節(jié)的數(shù)據(jù), 接受端收到數(shù)據(jù)后窗口又滿了... 這樣下去, 傳輸效率就很低了
要解決這個(gè)問(wèn)題, 可以有下面的方案
- 接收方的接受窗口有空余時(shí)候, 不要立即回復(fù)發(fā)送端, 等待一段時(shí)間累積回復(fù)
- 接收方的接受窗口有空余時(shí)候, 立即回復(fù)發(fā)送端, 累積一定數(shù)量再恢復(fù) (最大報(bào)文長(zhǎng)度 MSS)
- 發(fā)送端有發(fā)送數(shù)據(jù)的時(shí)候不要立即發(fā)送, 參考 累積發(fā)送 的機(jī)制
上述兩種方法可配合使用。使得在發(fā)送方不發(fā)送很小的報(bào)文段的同時(shí)犀暑,接收方也不要在緩存剛剛有了一點(diǎn)小的空間就急忙把這個(gè)很小的窗口大小信息通知給發(fā)送方驯击。
擁塞控制
擁塞控制和流量控制都可以減低發(fā)送端的發(fā)送速度, 他們的區(qū)別參考 區(qū)分流量控制和擁塞控制
擁塞控制是基于擁塞窗口實(shí)現(xiàn)的, 發(fā)送方維持一個(gè)叫做擁塞窗口 cwnd (congestion window)的狀態(tài)變量, 所以發(fā)送窗口的計(jì)算方式如下
發(fā)送窗口 = min(接收窗口, 擁塞窗口)
擁塞控制有四種算法,即慢開始(slow-start)耐亏、擁塞避免(congestion avoidance)徊都、快重傳(fast retransmit)和快恢復(fù)(fast recovery)
我們先假設(shè)接收窗口是無(wú)限大, 不會(huì)被流量控制限制, 我們只考慮網(wǎng)絡(luò)擁塞的情況
慢開始和擁塞避免
首先在 3 次握手建立連接的時(shí)候通信獲得最大報(bào)文段 MSS (Max Segment Size), 以及閾值 ssthresh (slow start threshold)的大小
- 如果 cwnd < sshthresh, 慢開始算法, 擁塞窗口指數(shù)遞增,
cwnd = cwnd * 2
- 如果 cwnd > sshthresh, 擁塞避免算法, 擁塞窗口線性遞增,
cwnd = cwnd + 1
- 如果 cwnd = sshthresh, 2 種算法都可以
舉個(gè)例子, 比如一開始的 ssthresh = 12 個(gè) MSS
- 剛開始發(fā)送數(shù)據(jù)時(shí), 先把擁塞窗口 cwnd 設(shè)置為 1
- 然后開始慢開始算法, 擁塞窗口指數(shù)遞增, 1 2 4 8 16 ...
- 當(dāng)遞增到 16 之后, cwnd > sshthresh, 擁塞避免算法, 擁塞窗口線性遞增
- 當(dāng)遞增到 24 之后丟包了, 發(fā)生了 超時(shí)重傳, 降低擁塞窗口
ssthresh = cwnd / 2 = 12, cwnd = 1
, 重新開始慢開始策略
快重傳和快恢復(fù)
快重傳和快恢復(fù)一般搭配使用, 可以參考上面的 快重傳, 快重傳算法首先要求接收方每收到一個(gè)失序的報(bào)文段后就立即發(fā)出重復(fù)確認(rèn)
發(fā)送端接受到 3 次相同的 ack 之后就馬上重傳確實(shí)的數(shù)據(jù), 然后執(zhí)行 快恢復(fù)算法 , 擁塞窗口 ssthresh = cwnd / 2 = 1, cwnd = ssthresh = 12
, 然后開始擁塞避免算法
第三層網(wǎng)絡(luò)層的隨機(jī)早期檢測(cè)RED
前面我們介紹的都是第四層 TCP 解決網(wǎng)絡(luò)擁塞的策略, 并沒(méi)有和第三層網(wǎng)絡(luò)層結(jié)合起來(lái), 但是其實(shí)他們有著密切的聯(lián)系.
舉個(gè)極端的例子:
- 路由器對(duì)于數(shù)據(jù)一般是采用 FIFO 的方式進(jìn)行轉(zhuǎn)發(fā), 新來(lái)的數(shù)據(jù)儲(chǔ)存到隊(duì)列中, 隊(duì)列滿了就丟棄數(shù)據(jù)
- 路由器一般有很多 TCP 連接, 所以丟棄數(shù)據(jù)的時(shí)候可能會(huì)影響大量的 TCP 連接
- 這些 TCP 群體超時(shí)重傳, 進(jìn)入慢開始, 網(wǎng)絡(luò)負(fù)載重很高突然變得很低, 然后又逐漸增加, 負(fù)載又很高...(稱之為 TCP 的全局同步)
如何能解決這種全局同步的現(xiàn)象呢?
思路就是在可能要網(wǎng)絡(luò)擁塞的時(shí)候就開始隨機(jī)丟包, 讓一部分 TCP 先慢下來(lái), 這就是隨機(jī)早起檢測(cè) RED 的基本思想
比如當(dāng)隊(duì)列長(zhǎng)度達(dá)到一半(最小門限)時(shí)候就開始隨機(jī)丟包, 丟包概率和隨長(zhǎng)度線性遞增, 如下圖所示
具體計(jì)算方法這里就不贅述了
建立連接和斷開連接
TCP 運(yùn)輸連接有 3 個(gè)階段
- 3 次握手建立連接
- 數(shù)據(jù)傳輸
- 4 次揮手?jǐn)嚅_連接
剛剛介紹了第二個(gè)階段數(shù)據(jù)傳輸, 我們來(lái)看看其他兩個(gè)階段
3次握手建立連接
我們首先要知道
(1) SYN 包即使不攜帶數(shù)據(jù)也要占一個(gè)序列號(hào), 比如發(fā)送第一個(gè) SYN 包, 序列號(hào)為 1, 發(fā)送第二個(gè)包, 序列號(hào)為 2
(2) ACK 包返回的是期望的下一個(gè)數(shù)據(jù), 所以 ACK號(hào) = 收到的序列號(hào) + 1
- 客戶端 SYN 包(標(biāo)記 SYN 為1), 選擇一個(gè)初始序列號(hào) seq = x
- 服務(wù)端 SYN/ACK 包(標(biāo)記 SYN 為1, ACK 為1), 選擇一個(gè)初始序列號(hào) seq = y, 確認(rèn)號(hào) ack = x + 1
- 客戶端 ACK 包(標(biāo)記 ACK 為1), seq = x + 1, 確認(rèn)號(hào) ack = y + 1
- 如果客戶端要繼續(xù)發(fā)送數(shù)據(jù), 應(yīng)該從 x + 1 開始發(fā)送, 服務(wù)端應(yīng)該從 y + 1 開始發(fā)送
為什么一定要三次握手呢? 為什么不是兩次或者四次呢?
這主要是為了防止已失效的連接請(qǐng)求報(bào)文段突然又傳送到了B,因而產(chǎn)生錯(cuò)誤
- A 發(fā)送了連接請(qǐng)求1, 連接請(qǐng)求1在網(wǎng)絡(luò)中滯留, A超時(shí)重傳連接請(qǐng)求2, B收到連接請(qǐng)求2建立連接, 傳輸數(shù)據(jù)后斷開連接
- 連接請(qǐng)求1在網(wǎng)絡(luò)中滯留結(jié)束, 傳送到了B, B誤認(rèn)為是A發(fā)出的新的連接請(qǐng)求, 于是向A發(fā)出確認(rèn), 但是A并沒(méi)有發(fā)出連接請(qǐng)求, 所以不會(huì)理睬B的確認(rèn), B就會(huì)一直等待A的確認(rèn), 造成資源浪費(fèi)
4次揮手?jǐn)嚅_連接
我們首先要知道
(1) FIN 包即使不攜帶數(shù)據(jù)也要占一個(gè)序列號(hào), 比如發(fā)送第一個(gè) FIN 包, 序列號(hào)為 1, 發(fā)送第二個(gè)包, 序列號(hào)為 2
- A的應(yīng)用進(jìn)程先向其TCP發(fā)出連接釋放報(bào)文段广辰,并停止再發(fā)送數(shù)據(jù)碟贾,主動(dòng)關(guān)閉TCP連接币喧。A把連接釋放報(bào)文段首部的終止控制位FIN置1,其序號(hào)seq =u袱耽,它等于前面已傳送過(guò)的數(shù)據(jù)的最后一個(gè)字節(jié)的序號(hào)加1杀餐。這時(shí)A進(jìn)入FIN-WAIT-1(終止等待1)狀態(tài),等待B的確認(rèn)朱巨。
- B收到連接釋放報(bào)文段后即發(fā)出確認(rèn)史翘,確認(rèn)號(hào)是ack = u + 1,而這個(gè)報(bào)文段自己的序號(hào)是v冀续,等于B前面已傳送過(guò)的數(shù)據(jù)的最后一個(gè)字節(jié)的序號(hào)加1琼讽。然后B就進(jìn)入CLOSE-WAIT(關(guān)閉等待)狀態(tài)。TCP服務(wù)器進(jìn)程這時(shí)應(yīng)通知高層應(yīng)用進(jìn)程洪唐,因而從A到B這個(gè)方向的連接就釋放了钻蹬,這時(shí)的TCP連接處于半關(guān)閉(half-close)狀態(tài),即A已經(jīng)沒(méi)有數(shù)據(jù)要發(fā)送了凭需,但B若發(fā)送數(shù)據(jù)问欠,A仍要接收。也就是說(shuō)粒蜈,從B到A這個(gè)方向的連接并未關(guān)閉顺献,這個(gè)狀態(tài)可能會(huì)持續(xù)一些時(shí)間。
A收到來(lái)自B的確認(rèn)后枯怖,就進(jìn)入FIN-WAIT-2(終止等待2)狀態(tài)注整,等待B發(fā)出的連接釋放報(bào)文段。
- 若B已經(jīng)沒(méi)有要向A發(fā)送的數(shù)據(jù)度硝,其應(yīng)用進(jìn)程就通知TCP釋放連接肿轨。這時(shí)B發(fā)出的連接釋放報(bào)文段必須使FIN = 1。現(xiàn)假定B的序號(hào)為w(在半關(guān)閉狀態(tài)B可能又發(fā)送了一些數(shù)據(jù))蕊程。B還必須重復(fù)上次已發(fā)送過(guò)的確認(rèn)號(hào)ack = u + 1椒袍。這時(shí)B就進(jìn)入LAST-ACK(最后確認(rèn))狀態(tài),等待A的確認(rèn)存捺。
- A在收到B的連接釋放報(bào)文段后槐沼,必須對(duì)此發(fā)出確認(rèn)。在確認(rèn)報(bào)文段中把ACK置1捌治,確認(rèn)號(hào)ack = w + 1岗钩,而自己的序號(hào)是seq = u + 1(根據(jù)TCP標(biāo)準(zhǔn),前面發(fā)送過(guò)的FIN報(bào)文段要消耗一個(gè)序號(hào))肖油。然后進(jìn)入到TIME-WAIT(時(shí)間等待)狀態(tài)兼吓。請(qǐng)注意,現(xiàn)在TCP連接還沒(méi)有釋放掉森枪。必須經(jīng)過(guò)時(shí)間等待計(jì)時(shí)器(TIME-WAIT timer)設(shè)置的時(shí)間2MSL后视搏,A才進(jìn)入到CLOSED狀態(tài)审孽。
為什么A在TIME-WAIT狀態(tài)必須等待2MSL的時(shí)間呢?這有兩個(gè)理由:
第一浑娜,為了保證A發(fā)送的最后一個(gè)ACK報(bào)文段能夠到達(dá)B佑力。這個(gè)ACK報(bào)文段有可能丟失,因而使處在LAST-ACK狀態(tài)的B收不到對(duì)已發(fā)送的FIN + ACK報(bào)文段的確認(rèn)筋遭。B會(huì)超時(shí)重傳這個(gè)FIN + ACK報(bào)文段打颤,而A就能在2MSL時(shí)間內(nèi)收到這個(gè)重傳的FIN + ACK報(bào)文段。接著A重傳一次確認(rèn)漓滔,重新啟動(dòng)2MSL計(jì)時(shí)器编饺。最后,A和B都正常進(jìn)入到CLOSED狀態(tài)响驴。如果A在TIME-WAIT狀態(tài)不等待一段時(shí)間透且,而是在發(fā)送完ACK報(bào)文段后立即釋放連接,那么就無(wú)法收到B重傳的FIN + ACK報(bào)文段豁鲤,因而也不會(huì)再發(fā)送一次確認(rèn)報(bào)文段秽誊。這樣,B就無(wú)法按照正常步驟進(jìn)入CLOSED狀態(tài)畅形。
第二养距,防止上一節(jié)提到的“已失效的連接請(qǐng)求報(bào)文段”出現(xiàn)在本連接中诉探。A在發(fā)送完最后一個(gè)ACK報(bào)文段后日熬,再經(jīng)過(guò)時(shí)間2MSL,就可以使本連接持續(xù)的時(shí)間內(nèi)所產(chǎn)生的所有報(bào)文段都從網(wǎng)絡(luò)中消失肾胯。這樣就可以使下一個(gè)新的連接中不會(huì)出現(xiàn)這種舊的連接請(qǐng)求報(bào)文段竖席。
reference
計(jì)算機(jī)網(wǎng)絡(luò)-謝希仁: https://weread.qq.com/web/bookDetail/af532c005a007caf51371b1
kcp協(xié)議
TCP是為流量設(shè)計(jì)的(每秒內(nèi)可以傳輸多少KB的數(shù)據(jù)),講究的是充分利用帶寬敬肚。而 KCP是為流速設(shè)計(jì)的(單個(gè)數(shù)據(jù)包從一端發(fā)送到一端需要多少時(shí)間)毕荐,以10%-20%帶寬浪費(fèi)的代價(jià)換取了比 TCP快30%-40%的傳輸速度。
KCP 是基于 UDP 協(xié)議實(shí)現(xiàn)的, 我們看看 UPD 的協(xié)議報(bào)
UDP協(xié)議報(bào)
- 源端口 源端口號(hào)艳馒。在需要對(duì)方回信時(shí)選用憎亚。不需要時(shí)可用全0。
- 目的端口 目的端口號(hào)弄慰。這在終點(diǎn)交付報(bào)文時(shí)必須要使用到第美。
- 長(zhǎng)度 UDP用戶數(shù)據(jù)報(bào)的長(zhǎng)度,其最小值是8(僅有首部)陆爽。
- 檢驗(yàn)和 檢測(cè)UDP用戶數(shù)據(jù)報(bào)在傳輸中是否有錯(cuò)什往。有錯(cuò)就丟棄。
KCP協(xié)議報(bào)
- 連接標(biāo)識(shí) (4 字節(jié)): 這個(gè)連接發(fā)出去的每個(gè)報(bào)文段都會(huì)帶上
conv
, 它也只會(huì)接收conv
與之相等的報(bào)文段. 通信的雙方必須先協(xié)商一對(duì)相同的conv
. KCP 本身不提供任何握手機(jī)制, 協(xié)商conv
交給使用者自行實(shí)現(xiàn), 比如說(shuō)通過(guò)已有的 TCP 連接協(xié)商 - 命令類型 (1字節(jié))
- 分片數(shù)量 (1字節(jié)): 表示隨后還有多少個(gè)報(bào)文屬于同一個(gè)包. (數(shù)據(jù)包的大小可能會(huì)超過(guò)一個(gè) MSS (Maximum Segment Size, 最大報(bào)文段大小). 這個(gè)時(shí)候需要進(jìn)行分片, 分片數(shù)量表示隨后還有多少個(gè)報(bào)文屬于同一個(gè)包.)
- 窗口大小 (2 字節(jié)): 發(fā)送方剩余接收窗口的大小. (類似 TCP 流量控制)
- 時(shí)間戳 (4 字節(jié)): TCP 使用往返時(shí)間計(jì)算 RTT 的, KCP 的時(shí)間需要重外部傳進(jìn)來(lái)
- 序列號(hào) (4 字節(jié)): 類似 TCP 的 seq 序列號(hào)
- 確認(rèn)序列號(hào) (4 字節(jié)): 類似 TCP 的 seq 序列號(hào), 發(fā)送方的接收緩沖區(qū)中最小還未收到的報(bào)文段的編號(hào). 也就是說(shuō), 編號(hào)比它小的報(bào)文段都已全部接收.
- 數(shù)據(jù)長(zhǎng)度 (4 字節(jié)): 數(shù)據(jù)的長(zhǎng)度 (TCP 沒(méi)有數(shù)據(jù)長(zhǎng)度, TCP 是面向流的)
- 數(shù)據(jù) (長(zhǎng)度可變)
kcp 協(xié)議報(bào)的結(jié)構(gòu)體
type segment struct {
// 連接標(biāo)識(shí)
conv uint32
// 命令號(hào)
cmd uint8
// 分片數(shù)量
frg uint8
// 窗口大小
wnd uint16
// 時(shí)間戳
ts uint32
// 序列號(hào)
sn uint32
// 確認(rèn)序列號(hào)
una uint32
// 超時(shí)時(shí)間, 通過(guò)來(lái)回 ts 計(jì)算的 RTT 進(jìn)而計(jì)算出來(lái)的 RTO 和 TCP 的 RTO 類似
rto uint32
// 該報(bào)文傳輸?shù)拇螖?shù)
xmit uint32
// 下次重發(fā)的時(shí)間戳, 初始值為: current + rto
resendts uint32
// ACK 失序次數(shù). 也就是 KCP Readme 中所說(shuō)的 "跳過(guò)" 次數(shù)
fastack uint32
// 是否確認(rèn)
acked uint32 // mark if the seg has acked
// 數(shù)據(jù)
data []byte
}
KCP 實(shí)例
type KCP struct {
// conv 連接標(biāo)識(shí)
// mtu 最大傳輸單元
// mss 最大報(bào)文段大小
// state 狀態(tài), 0 表示連接建立, -1 表示連接斷開. (注意 state 是 unsigned int, -1 實(shí)際上是 0xffffffff)
conv, mtu, mss, state uint32
// snd_una 發(fā)送緩沖區(qū)中最小還未確認(rèn)送達(dá)的報(bào)文段的編號(hào). 也就是說(shuō), 編號(hào)比它小的報(bào)文段都已確認(rèn)送達(dá).
// snd_nxt 下一個(gè)等待發(fā)送的報(bào)文段的編號(hào)
// rcv_nxt 下一個(gè)等待接收的報(bào)文段的編號(hào)
snd_una, snd_nxt, rcv_nxt uint32
// ts_recent 時(shí)間戳
ssthresh uint32
// rx_rttval 用于計(jì)算 rx_rto 的變量
// rx_srtt 用于計(jì)算 rx_rto 的變量
rx_rttvar, rx_srtt int32
// rx_rto 重傳超時(shí)時(shí)間, 通過(guò)來(lái)回 ts 計(jì)算的 RTT 進(jìn)而計(jì)算出來(lái)的 RTO 和 TCP 的 RTO 類似
// rx_minrto 最小重傳超時(shí)時(shí)間
rx_rto, rx_minrto uint32
// snd_wnd 發(fā)送窗口大小
// rcv_wnd 接收窗口大小
// rmt_wnd 遠(yuǎn)端窗口大小
// cwnd 擁塞窗口大小
// probe 擁塞探測(cè)標(biāo)識(shí)
snd_wnd, rcv_wnd, rmt_wnd, cwnd, probe uint32
// interval 間隔時(shí)間, 用于更新 KCP 內(nèi)部的時(shí)間戳
// ts_flush 下次刷新的時(shí)間戳
interval, ts_flush uint32
// nodelay 是否啟用 nodelay 模式
// updated 是否更新了 nodelay 模式
nodelay, updated uint32
// ts_probe 下次探測(cè)的時(shí)間戳
// probe_wait 探測(cè)等待時(shí)間
ts_probe, probe_wait uint32
// dead_link 下次檢測(cè) dead link 的時(shí)間戳
// incr 擁塞窗口大小增量
dead_link, incr uint32
// fastresend 快速重傳模式, ACK 失序 fastresend 次時(shí)觸發(fā)快速重傳
fastresend int32
// nocwnd 沒(méi)有擁塞控制的模式
// stream 流模式
nocwnd, stream int32
// snd_queue 發(fā)送隊(duì)列
snd_queue []segment
// rcv_queue 接收隊(duì)列
rcv_queue []segment
// snd_buf 發(fā)送緩沖區(qū)
snd_buf []segment
// rcv_buf 接收緩沖區(qū)
rcv_buf []segment
// acklist 確認(rèn)列表
acklist []ackItem
// buffer flush 時(shí)候的臨時(shí)緩沖區(qū)
buffer []byte
// reserved 保留字段
reserved int
// output 輸出的回調(diào)函數(shù) func(buf []byte, size int)
output output_callback
}
隊(duì)列和緩沖區(qū)
我們先來(lái)看 snd_queue, rcv_queue, snd_buf 和 rcv_buf 這四個(gè)字段. 它們分別是發(fā)送隊(duì)列, 接收隊(duì)列, 發(fā)送緩沖區(qū)和接收緩沖區(qū). 隊(duì)列和緩沖區(qū)其實(shí)都是循環(huán)雙鏈表, 鏈表節(jié)點(diǎn)的類型都是 struct IKCPSEG.
調(diào)用 ikcp_send 發(fā)送數(shù)據(jù)時(shí)會(huì)先將數(shù)據(jù)加入 snd_queue 中, 然后再伺機(jī)加入 snd_buf. 每次調(diào)用 ikcp_flush 時(shí)都將 snd_buf 中滿足條件的報(bào)文段都發(fā)送出去. 之所以不將報(bào)文直接加入 snd_buf 是為了防止一次發(fā)送過(guò)多的報(bào)文導(dǎo)致?lián)砣? 需要再擁塞算法的控制下伺機(jī)加入 snd_buf 中.
調(diào)用 ikcp_input 收到的數(shù)據(jù)解包后會(huì)先放入 rcv_buf 中, 再在合適的情況下轉(zhuǎn)移到 rcv_queue 中. 調(diào)用 ikcp_recv 接收數(shù)據(jù)時(shí)會(huì)從 rcv_queue 取出數(shù)據(jù)返回給調(diào)用者. 這樣做是因?yàn)閳?bào)文傳輸?shù)倪^(guò)程中會(huì)出現(xiàn)丟包, 失序等情況. 為了保證順序, 需要將收到的報(bào)文先放入 rcv_buf 中, 只有當(dāng) rcv_buf 中的報(bào)文段順序正確才能將其移動(dòng)到 rcv_queue 中供調(diào)用者接收. 如下圖所示, rcv_buf 中節(jié)點(diǎn)為灰色表示可以移動(dòng)到 rcv_queue 中. 只有當(dāng) 2 號(hào)報(bào)文重傳成功后, 才能將 2, 3, 4 號(hào)報(bào)文移動(dòng)到 rcv_queue 中.
總結(jié)如下
- 發(fā)送數(shù)據(jù): 創(chuàng)建報(bào)文實(shí)例后添加到 snd_queue 中, 然后再伺機(jī)添加到 snd_buf 中, 最后調(diào)用 ikcp_flush 發(fā)送出去.
- 接受數(shù)據(jù): 收到數(shù)據(jù)后添加到 rcv_buf 中, 然后再將 順序正確 的報(bào)文伺機(jī)添加到 rcv_queue 中, 最后調(diào)用 ikcp_recv 接收數(shù)據(jù).
技術(shù)特性
TCP是為流量設(shè)計(jì)的(每秒內(nèi)可以傳輸多少KB的數(shù)據(jù))慌闭,講究的是充分利用帶寬别威。而 KCP是為流速設(shè)計(jì)的(單個(gè)數(shù)據(jù)包從一端發(fā)送到一端需要多少時(shí)間)躯舔,以10%-20%帶寬浪費(fèi)的代價(jià)換取了比 TCP快30%-40%的傳輸速度。TCP信道是一條流速很慢省古,但每秒流量很大的大運(yùn)河粥庄,而KCP是水流湍急的小激流。KCP有正常模式和快速模式兩種豺妓,通過(guò)以下策略達(dá)到提高流速的結(jié)果:
RTO翻倍vs不翻倍:
TCP超時(shí)計(jì)算是RTOx2飒赃,這樣連續(xù)丟三次包就變成RTOx8了,十分恐怖科侈,而KCP啟動(dòng)快速模式后不x2载佳,只是x1.5(實(shí)驗(yàn)證明1.5這個(gè)值相對(duì)比較好),提高了傳輸速度臀栈。
選擇性重傳 vs 全部重傳:
TCP丟包時(shí)會(huì)全部重傳從丟的那個(gè)包開始以后的數(shù)據(jù)蔫慧,KCP是選擇性重傳,只重傳真正丟失的數(shù)據(jù)包权薯。
快速重傳:
發(fā)送端發(fā)送了1,2,3,4,5幾個(gè)包姑躲,然后收到遠(yuǎn)端的ACK: 1, 3, 4, 5,當(dāng)收到ACK3時(shí)盟蚣,KCP知道2被跳過(guò)1次黍析,收到ACK4時(shí),知道2被跳過(guò)了2次屎开,此時(shí)可以認(rèn)為2號(hào)丟失阐枣,不用等超時(shí),直接重傳2號(hào)包奄抽,大大改善了丟包時(shí)的傳輸速度蔼两。(TCP 的快速重傳寫死了是 3 次, KCP 可以自己設(shè)置, 一般是是 2 次)
延遲ACK vs 非延遲ACK:
TCP為了充分利用帶寬,延遲發(fā)送ACK(NODELAY都沒(méi)用)逞度,這樣超時(shí)計(jì)算會(huì)算出較大 RTT時(shí)間额划,延長(zhǎng)了丟包時(shí)的判斷過(guò)程。KCP的ACK是否延遲發(fā)送可以調(diào)節(jié)档泽。
UNA vs ACK+UNA:
ARQ模型響應(yīng)有兩種俊戳,UNA(此編號(hào)前所有包已收到,如TCP)和ACK(該編號(hào)包已收到)馆匿,光用UNA將導(dǎo)致全部重傳抑胎,光用ACK則丟失成本太高,以往協(xié)議都是二選其一甜熔,而 KCP協(xié)議中圆恤,除去單獨(dú)的 ACK包外,所有包都有UNA信息。
非退讓流控:
KCP正常模式同TCP一樣使用公平退讓法則盆昙,即發(fā)送窗口大小由:發(fā)送緩存大小羽历、接收端剩余接收緩存大小、丟包退讓及慢啟動(dòng)這四要素決定淡喜。但傳送及時(shí)性要求很高的小數(shù)據(jù)時(shí)秕磷,可選擇通過(guò)配置跳過(guò)后兩步,僅用前兩項(xiàng)來(lái)控制發(fā)送頻率炼团。以犧牲部分公平性及帶寬利用率之代價(jià)澎嚣,換取了開著BT都能流暢傳輸?shù)男Ч?/p>
KCP 最佳實(shí)踐
前向糾錯(cuò)
為了進(jìn)一步提高傳輸速度,下層協(xié)議也許會(huì)使用前向糾錯(cuò)技術(shù)瘟芝。需要注意易桃,前向糾錯(cuò)會(huì)根據(jù)冗余信息解出原始數(shù)據(jù)包。相同的原始數(shù)據(jù)包不要兩次input到KCP锌俱,否則將會(huì)導(dǎo)致 kcp以為對(duì)方重發(fā)了晤郑,這樣會(huì)產(chǎn)生更多的ack占用額外帶寬。
比如下層協(xié)議使用最簡(jiǎn)單的冗余包:?jiǎn)蝹€(gè)數(shù)據(jù)包除了自己外贸宏,還會(huì)重復(fù)存儲(chǔ)一次上一個(gè)數(shù)據(jù)包造寝,以及上上一個(gè)數(shù)據(jù)包的內(nèi)容:
Fn = (Pn, Pn-1, Pn-2)
P0 = (0, X, X)
P1 = (1, 0, X)
P2 = (2, 1, 0)
P3 = (3, 2, 1)
這樣幾個(gè)包發(fā)送出去,接收方對(duì)于單個(gè)原始包都可能被解出3次來(lái)(后面兩個(gè)包任然會(huì)重復(fù)該包內(nèi)容)吭练,那么這里需要記錄一下诫龙,一個(gè)下層數(shù)據(jù)包只會(huì)input給kcp一次,避免過(guò)多重復(fù)ack帶來(lái)的浪費(fèi)鲫咽。
管理大規(guī)模連接
如果需要同時(shí)管理大規(guī)模的 KCP連接(比如大于3000個(gè))屹篓,比如你正在實(shí)現(xiàn)一套類 epoll的機(jī)制短荐,那么為了避免每秒鐘對(duì)每個(gè)連接調(diào)用大量的調(diào)用 ikcp_update玩裙,我們可以使用 ikcp_check 來(lái)大大減少 ikcp_update調(diào)用的次數(shù)盐类。 ikcp_check返回值會(huì)告訴你需要在什么時(shí)間點(diǎn)再次調(diào)用 ikcp_update(如果中途沒(méi)有 ikcp_send, ikcp_input的話晰绎,否則中途調(diào)用了 ikcp_send, ikcp_input的話寓落,需要在下一次interval時(shí)調(diào)用 update)
標(biāo)準(zhǔn)順序是每次調(diào)用了 ikcp_update后,使用 ikcp_check決定下次什么時(shí)間點(diǎn)再次調(diào)用 ikcp_update荞下,而如果中途發(fā)生了 ikcp_send, ikcp_input 的話伶选,在下一輪 interval 立馬調(diào)用 ikcp_update和 ikcp_check。 使用該方法尖昏,原來(lái)在處理2000個(gè) kcp連接且每
個(gè)連接每10ms調(diào)用一次update仰税,改為 check機(jī)制后,cpu從 60%降低到 15%抽诉。
避免緩存積累延遲
不管是 TCP/KCP陨簇,信道能力在那里放著,讓你沒(méi)法無(wú)限制的調(diào)用 send迹淌,請(qǐng)閱讀:“如何避免緩存積累延遲” 這篇 wiki河绽。
協(xié)議棧分層組裝
不要試圖將任何加密或者 FEC相關(guān)代碼實(shí)現(xiàn)到 KCP里面己单,請(qǐng)實(shí)現(xiàn)成不同協(xié)議單元并組裝成你的協(xié)議棧,具體請(qǐng)看:協(xié)議棧分層組裝
如何支持收發(fā)可靠和非可靠數(shù)據(jù)耙饰?
有的產(chǎn)品可能除了需要可靠數(shù)據(jù)纹笼,還需要發(fā)送非可靠數(shù)據(jù),那么 KCP 如何支持這種需求呢苟跪?很簡(jiǎn)單廷痘,你自己實(shí)現(xiàn):
connection.send(channel, pkt, size);
channel == 0 使用 kcp 發(fā)送可靠包,channel == 1 使用 udp 發(fā)送非可靠包件已。
因?yàn)閭鬏斒悄阕约簩?shí)現(xiàn)的笋额,你可以在發(fā)送UDP包的頭部加一個(gè)字節(jié),來(lái)代表這個(gè) channel
篷扩,收到遠(yuǎn)程來(lái)的 UDP以后鳞陨,也可以判斷 channel==0 的話,把剩下的數(shù)據(jù)給 ikcp_input
瞻惋,否則剩下的數(shù)據(jù)為遠(yuǎn)程非可靠包厦滤。
這樣你得到了一個(gè)新的發(fā)送函數(shù),用channel來(lái)區(qū)別你想發(fā)送可靠數(shù)據(jù)還是非可靠數(shù)據(jù)歼狼。再統(tǒng)一封裝一個(gè) connection.recv
函數(shù)掏导,先去 ikcp_recv
那里嘗試收包,收不到的話羽峰,看剛才有沒(méi)有收到 channel=1 的裸UDP包趟咆,有的話返回給上層用戶。
如果你的服務(wù)端是混用 tcp/udp 的話梅屉,你還可以設(shè)計(jì)個(gè) channel=2 使用 TCP 發(fā)送數(shù)據(jù)值纱,針對(duì)一些比較大的,延遲不敏感的東西坯汤。
重設(shè)窗口大小
要解決上面的問(wèn)題首先對(duì)你的使用帶寬有一個(gè)預(yù)計(jì)虐唠,并根據(jù)上面的公式重新設(shè)置發(fā)送窗口和接收窗口大小,你寫后端惰聂,想追求tcp的性能疆偿,也會(huì)需要重新設(shè)置tcp的 sndbuf, rcvbuf 的大小,KCP 默認(rèn)發(fā)送窗口和接收窗口大小都比較小而已(默認(rèn)32個(gè)包)搓幌,你可以朝著 64, 128, 256, 512, 1024 等檔次往上調(diào)杆故,kcptun默認(rèn)發(fā)送窗口 1024,用來(lái)傳高清視頻已經(jīng)足夠溉愁,游戲的話处铛,32-256 應(yīng)該滿足。
不設(shè)置的話,如果默認(rèn) snd_wnd 太小撤蟆,網(wǎng)絡(luò)不是那么順暢篙贸,你越來(lái)越多的數(shù)據(jù)會(huì)滯留在 snd_queue里得不到發(fā)送,你的延遲會(huì)越來(lái)越大枫疆。
設(shè)定了 snd_wnd爵川,遠(yuǎn)端的 rcv_wnd 也需要相應(yīng)擴(kuò)大,并且不小于發(fā)送端的 snd_wnd 大小息楔,否則設(shè)置沒(méi)意義寝贡。
其次對(duì)于成熟的后端業(yè)務(wù),不管用 TCP還是 KCP值依,你都需要實(shí)現(xiàn)相關(guān)緩存控制策略:
緩存控制:傳送文件
你用 tcp傳文件的話圃泡,當(dāng)網(wǎng)絡(luò)沒(méi)能力了,你的 send調(diào)用要不就是阻塞掉愿险,要不就是 EAGAIN颇蜡,然后需要通過(guò) epoll 檢查 EPOLL_OUT事件來(lái)決定下次什么時(shí)候可以繼續(xù)發(fā)送。
KCP 也一樣辆亏,如果 ikcp_waitsnd 超過(guò)閾值风秤,比如2倍 snd_wnd,那么停止調(diào)用 ikcp_send扮叨,ikcp_waitsnd的值降下來(lái)缤弦,當(dāng)然期間要保持 ikcp_update 調(diào)用。
緩存控制:實(shí)時(shí)視頻直播
視頻點(diǎn)播和傳文件一樣彻磁,而視頻直播碍沐,一旦 ikcp_waitsnd 超過(guò)閾值了,除了不再往 kcp 里發(fā)送新的數(shù)據(jù)包衷蜓,你的視頻應(yīng)該進(jìn)入一個(gè) “丟幀” 狀態(tài)累提,直到 ikcp_waitsnd 降低到閾值的 1/2,這樣你的視頻才不會(huì)有積累延遲磁浇。
這和使用 TCP推流時(shí)碰到 EAGAIN 期間斋陪,要主動(dòng)丟幀的邏輯時(shí)一樣的。
同時(shí)扯夭,如果你能做的更好點(diǎn)鳍贾,waitsnd 超過(guò)閾值了,代表一段時(shí)間內(nèi)網(wǎng)絡(luò)傳輸能力下降了交洗,此時(shí)你應(yīng)該動(dòng)態(tài)降低視頻質(zhì)量,減少碼率橡淑,等網(wǎng)絡(luò)恢復(fù)了你再恢復(fù)构拳。
緩存控制:游戲控制數(shù)據(jù)
大部分邏輯嚴(yán)密的 TCP游戲服務(wù)器,都是使用無(wú)阻塞的 tcp鏈接配套個(gè) epoll之類的東西,當(dāng)后端業(yè)務(wù)向用戶發(fā)送數(shù)據(jù)時(shí)會(huì)追加到用戶空間的一塊發(fā)送緩存置森,比如 ring buffer 之類斗埂,當(dāng) epoll 到 EPOLL_OUT 事件時(shí)(其實(shí)也就是tcp發(fā)送緩存有空余了,不會(huì)EAGAIN/EWOULDBLOCK的時(shí)候)凫海,再把 ring buffer 里面暫存的數(shù)據(jù)使用 send 傳遞給系統(tǒng)的 SNDBUF呛凶,直到再次 EAGAIN。
那么 TCP SERVER的后端業(yè)務(wù)持續(xù)向客戶端發(fā)送數(shù)據(jù)行贪,而客戶端又遲遲沒(méi)能力接收怎么辦呢漾稀?此時(shí) epoll 會(huì)長(zhǎng)期不返回 EPOLL_OUT事件,數(shù)據(jù)會(huì)堆積再該用戶的 ring buffer 之中建瘫,如果堆積越來(lái)越多崭捍,ring buffer 會(huì)自增長(zhǎng)的話就會(huì)把 server 的內(nèi)存給耗盡。因此成熟的 tcp 游戲服務(wù)器的做法是:當(dāng)客戶端應(yīng)用層發(fā)送緩存(非tcp的sndbuf)中待發(fā)送數(shù)據(jù)超過(guò)一定閾值啰脚,就斷開 TCP鏈接殷蛇,因?yàn)樵撚脩魶](méi)有接收能力了,無(wú)法持續(xù)接收游戲數(shù)據(jù)橄浓。
使用 KCP 發(fā)送游戲數(shù)據(jù)也一樣粒梦,當(dāng) ikcp_waitsnd 返回值超過(guò)一定限度時(shí),你應(yīng)該斷開遠(yuǎn)端鏈接荸实,因?yàn)樗麄儧](méi)有能力接收了谍倦。
但是需要注意的是,KCP的默認(rèn)窗口都是32泪勒,比tcp的默認(rèn)窗口低很多昼蛀,實(shí)際使用時(shí)應(yīng)提前調(diào)大窗口,但是為了公平性也不要無(wú)止盡放大(不要超過(guò)1024)圆存。
累積緩存: 總結(jié)
緩存積累這個(gè)問(wèn)題叼旋,不管是 TCP還是 KCP你都要處理,因?yàn)門CP默認(rèn)窗口比較大沦辙,因此可能很多人并沒(méi)有處理的意識(shí)夫植。
當(dāng)你碰到緩存延遲時(shí):
- 檢查 snd_wnd, rcv_wnd 的值是否滿足你的要求,根據(jù)上面的公式換算油讯,每秒鐘要發(fā)多少包详民,當(dāng)前 snd_wnd滿足條件么?
- 確認(rèn)打開了 ikcp_nodelay陌兑,讓各項(xiàng)加速特性得以運(yùn)轉(zhuǎn)沈跨,并確認(rèn) nc參數(shù)是否設(shè)置,以關(guān)閉默認(rèn)的類 tcp保守流控方式兔综。
- 確認(rèn) ikcp_update 調(diào)用頻率是否滿足要求(比如10ms一次)饿凛。
如果你還想更激進(jìn):
- 確認(rèn) minrto 是否設(shè)置狞玛,比如設(shè)置成 10ms, nodelay 只是設(shè)置成 30ms,更激進(jìn)可以設(shè)置成 10ms 或者 5ms涧窒。
- 確認(rèn) interval是否設(shè)置心肪,可以更激進(jìn)的設(shè)置成 5ms,讓內(nèi)部始終循環(huán)更快纠吴。
- 每次發(fā)送完數(shù)據(jù)包后硬鞍,手動(dòng)調(diào)用 ikcp_flush
- 降低 mtu 到 470,同樣數(shù)據(jù)雖然會(huì)發(fā)更多的包戴已,但是小包在路由層優(yōu)先級(jí)更高固该。
如果你還想更快,可以在 KCP下層增加前向糾錯(cuò)協(xié)議恭陡。詳細(xì)見:協(xié)議分層蹬音,最佳實(shí)踐。
如何使用
貼一個(gè)快速開始的示例
package main
import (
"crypto/sha1"
"io"
"log"
"testing"
"time"
"github.com/xtaci/kcp-go/v5"
"golang.org/x/crypto/pbkdf2"
)
func TestServer(t *testing.T) {
main()
}
func TestClient(t *testing.T) {
client()
}
func main() {
key := pbkdf2.Key([]byte("demo pass"), []byte("demo salt"), 1024, 32, sha1.New)
block, _ := kcp.NewAESBlockCrypt(key)
if listener, err := kcp.ListenWithOptions("127.0.0.1:12345", block, 10, 3); err == nil {
// spin-up the client
go client()
for {
s, err := listener.AcceptKCP()
if err != nil {
log.Fatal(err)
}
go handleEcho(s)
}
} else {
log.Fatal(err)
}
}
// handleEcho send back everything it received
func handleEcho(conn *kcp.UDPSession) {
buf := make([]byte, 4096)
for {
n, err := conn.Read(buf)
if err != nil {
log.Println(err)
return
}
n, err = conn.Write(buf[:n])
if err != nil {
log.Println(err)
return
}
}
}
func client() {
key := pbkdf2.Key([]byte("demo pass"), []byte("demo salt"), 1024, 32, sha1.New)
block, _ := kcp.NewAESBlockCrypt(key)
// wait for server to become ready
time.Sleep(time.Second)
// dial to the echo server
if sess, err := kcp.DialWithOptions("127.0.0.1:12345", block, 10, 3); err == nil {
for {
data := time.Now().String()
buf := make([]byte, len(data))
log.Println("sent:", data)
if _, err := sess.Write([]byte(data)); err == nil {
// read back the data
if _, err := io.ReadFull(sess, buf); err == nil {
log.Println("recv:", string(buf))
} else {
log.Fatal(err)
}
} else {
log.Fatal(err)
}
time.Sleep(time.Second)
}
} else {
log.Fatal(err)
}
}
reference
kcp Wiki: https://github.com/skywind3000/kcp/wiki