目錄:
創(chuàng)新互聯(lián) - 西云機(jī)房,四川服務(wù)器租用,成都服務(wù)器租用,四川網(wǎng)通托管,綿陽服務(wù)器托管,德陽服務(wù)器托管,遂寧服務(wù)器托管,綿陽服務(wù)器托管,四川云主機(jī),成都云主機(jī),西南云主機(jī),西云機(jī)房,西南服務(wù)器托管,四川/成都大帶寬,機(jī)柜大帶寬、租用·托管,四川老牌IDC服務(wù)商
以前我也認(rèn)為TCP是相當(dāng)?shù)讓拥臇|西,我永遠(yuǎn)不需要去了解它。雖然差不多是這樣,但是實際生活中,你依然可能遇見和TCP算法相關(guān)的bug,這時候懂一些TCP的知識就至關(guān)重要了。( 本文也可以引申為,系統(tǒng)調(diào)用,操作系統(tǒng)這些都很重要,這個道理適用于很多東西 )
這里推薦一篇小短文, 人人都應(yīng)該懂點TCP
使用TCP協(xié)議通信的雙方必須先建立TCP連接,并在內(nèi)核中為該連接維持一些必要的數(shù)據(jù)結(jié)構(gòu),比如連接的狀態(tài)、讀寫緩沖區(qū)、定時器等。當(dāng)通信結(jié)束時,雙方必須關(guān)閉連接以釋放這些內(nèi)核數(shù)據(jù)。TCP服務(wù)基于流,源源不斷從一端流向另一端,發(fā)送端可以逐字節(jié)寫入,接收端可以逐字節(jié)讀出,無需分段。
需要注意的幾點:
TCP狀態(tài)(11種):
eg.
以上為TCP三次握手的狀態(tài)變遷
以下為TCP四次揮手的狀態(tài)變遷
服務(wù)器通過 listen 系統(tǒng)調(diào)用進(jìn)入 LISTEN 狀態(tài),被動等待客戶端連接,也就是所謂的被動打開。一旦監(jiān)聽到SYN(同步報文段)請求,就將該連接放入內(nèi)核的等待隊列,并向客戶端發(fā)送帶SYN的ACK(確認(rèn)報文段),此時該連接處于 SYN_RECVD 狀態(tài)。如果服務(wù)器收到客戶端返回的ACK,則轉(zhuǎn)到 ESTABLISHED 狀態(tài)。這個狀態(tài)就是連接雙方能進(jìn)行全雙工數(shù)據(jù)傳輸?shù)臓顟B(tài)。
而當(dāng)客戶端主動關(guān)閉連接時,服務(wù)器收到FIN報文,通過返回ACK使連接進(jìn)入 CLOSE_WAIT 狀態(tài)。此狀態(tài)表示——等待服務(wù)器應(yīng)用程序關(guān)閉連接。通常,服務(wù)器檢測到客戶端關(guān)閉連接之后,也會立即給客戶端發(fā)送一個FIN來關(guān)閉連接,使連接轉(zhuǎn)移到 LAST_ACK 狀態(tài),等待客戶端對最后一個FIN結(jié)束報文段的最后一次確認(rèn),一旦確認(rèn)完成,連接就徹底關(guān)閉了。
客戶端通過 connect 系統(tǒng)調(diào)用主動與服務(wù)器建立連接。此系統(tǒng)調(diào)用會首先給服務(wù)器發(fā)一個SYN,使連接進(jìn)入 SYN_SENT 狀態(tài)。
connect 調(diào)用可能因為兩種原因失?。?. 目標(biāo)端口不存在(未被任何進(jìn)程監(jiān)聽)護(hù)著該端口被 TIME_WAIT 狀態(tài)的連接占用( 詳見后文 )。2. 連接超時,在超時時間內(nèi)未收到服務(wù)器的ACK。
如果 connect 調(diào)用失敗,則連接返回初始的 CLOSED 狀態(tài),如果調(diào)用成功,則轉(zhuǎn)到 ESTABLISHED 狀態(tài)。
客戶端執(zhí)行主動關(guān)閉時,它會向服務(wù)器發(fā)送一個FIN,連接進(jìn)入 TIME_WAIT_1 狀態(tài),如果收到服務(wù)器的ACK,進(jìn)入 TIME_WAIT_2 狀態(tài)。此時服務(wù)器處于 CLOSE_WAIT 狀態(tài),這一對狀態(tài)是可能發(fā)生辦關(guān)閉的狀態(tài)(詳見后文)。此時如果服務(wù)器發(fā)送FIN關(guān)閉連接,則客戶端會發(fā)送ACK進(jìn)行確認(rèn)并進(jìn)入 TIME_WAIT 狀態(tài)。
流量控制是為了控制發(fā)送方發(fā)送速率,保證接收方來得及接收。
接收方發(fā)送的確認(rèn)報文中的窗口字段可以用來控制發(fā)送方窗口大小,從而影響發(fā)送方的發(fā)送速率。將窗口字段設(shè)置為 0,則發(fā)送方不能發(fā)送數(shù)據(jù)。
如果網(wǎng)絡(luò)出現(xiàn)擁塞,分組將會丟失,此時發(fā)送方會繼續(xù)重傳,從而導(dǎo)致網(wǎng)絡(luò)擁塞程度更高。因此當(dāng)出現(xiàn)擁塞時,應(yīng)當(dāng)控制發(fā)送方的速率。這一點和流量控制很像,但是出發(fā)點不同。 流量控制是為了讓接收方能來得及接收,而擁塞控制是為了降低整個網(wǎng)絡(luò)的擁塞程度。
TCP 主要通過四種算法來進(jìn)行擁塞控制: 慢開始、擁塞避免、快重傳、快恢復(fù)。
在Linux下有多種實現(xiàn),比如reno算法,vegas算法和cubic算法等。
發(fā)送方需要維護(hù)一個叫做擁塞窗口(cwnd)的狀態(tài)變量,注意擁塞窗口與發(fā)送方窗口的區(qū)別:擁塞窗口只是一個狀態(tài)變量,實際決定發(fā)送方能發(fā)送多少數(shù)據(jù)的是發(fā)送方窗口。
為了便于討論,做如下假設(shè):
發(fā)送的最初執(zhí)行慢開始,令 cwnd=1,發(fā)送方只能發(fā)送 1 個報文段;當(dāng)收到確認(rèn)后,將 cwnd 加倍,因此之后發(fā)送方能夠發(fā)送的報文段數(shù)量為:2、4、8 ...
注意到慢開始每個輪次都將 cwnd 加倍,這樣會讓 cwnd 增長速度非??欤瑥亩沟冒l(fā)送方發(fā)送的速度增長速度過快,網(wǎng)絡(luò)擁塞的可能也就更高。設(shè)置一個慢開始門限 ssthresh,當(dāng) cwnd = ssthresh 時,進(jìn)入擁塞避免,每個輪次只將 cwnd 加 1。
如果出現(xiàn)了超時,則令 ssthresh = cwnd/2,然后重新執(zhí)行慢開始。
在接收方,要求每次接收到報文段都應(yīng)該對最后一個已收到的有序報文段進(jìn)行確認(rèn)。例如已經(jīng)接收到 M1 和 M2,此時收到 M4,應(yīng)當(dāng)發(fā)送對 M2 的確認(rèn)。
在發(fā)送方,如果收到三個重復(fù)確認(rèn),那么可以知道下一個報文段丟失,此時執(zhí)行快重傳,立即重傳下一個報文段。例如收到三個 M2,則 M3 丟失,立即重傳 M3。
在這種情況下,只是丟失個別報文段,而不是網(wǎng)絡(luò)擁塞。因此執(zhí)行快恢復(fù),令 ssthresh = cwnd/2 ,cwnd = ssthresh,注意到此時直接進(jìn)入擁塞避免。
慢開始和快恢復(fù)的快慢指的是 cwnd 的設(shè)定值,而不是 cwnd 的增長速率。慢開始 cwnd 設(shè)定為 1,而快恢復(fù) cwnd 設(shè)定為 ssthresh。
??發(fā)送端的每個TCP報文都必須得到接收方的應(yīng)答,才算傳輸成功。
??TCP為每個TCP報文段都維護(hù)一個重傳定時器。
??發(fā)送端在發(fā)出一個TCP報文段之后就啟動定時器,如果在定時時間類未收到應(yīng)答,它就將重發(fā)該報文段并重置定時器。
??因為TCP報文段最終在網(wǎng)絡(luò)層是以IP數(shù)據(jù)報的形式發(fā)送,而IP數(shù)據(jù)報到達(dá)接收端可能是亂序或者重復(fù)的。TCP協(xié)議會對收到的TCP報文進(jìn)行重排、整理,確保順序正確。
TCP報文段所攜帶的應(yīng)用程序數(shù)據(jù)按照長度分為兩種: 交互數(shù)據(jù) 和 成塊數(shù)據(jù)
對于什么是粘包、拆包問題,我想先舉兩個簡單的應(yīng)用場景:
對于第一種情況,服務(wù)端的處理流程可以是這樣的:當(dāng)客戶端與服務(wù)端的連接建立成功之后,服務(wù)端不斷讀取客戶端發(fā)送過來的數(shù)據(jù),當(dāng)客戶端與服務(wù)端連接斷開之后,服務(wù)端知道已經(jīng)讀完了一條消息,然后進(jìn)行解碼和后續(xù)處理...。對于第二種情況,如果按照上面相同的處理邏輯來處理,那就有問題了,我們來看看 第二種情況 下客戶端發(fā)送的兩條消息遞交到服務(wù)端有可能出現(xiàn)的情況:
第一種情況:
服務(wù)端一共讀到兩個數(shù)據(jù)包,第一個包包含客戶端發(fā)出的第一條消息的完整信息,第二個包包含客戶端發(fā)出的第二條消息,那這種情況比較好處理,服務(wù)器只需要簡單的從網(wǎng)絡(luò)緩沖區(qū)去讀就好了,第一次讀到第一條消息的完整信息,消費完再從網(wǎng)絡(luò)緩沖區(qū)將第二條完整消息讀出來消費。
第二種情況:
服務(wù)端一共就讀到一個數(shù)據(jù)包,這個數(shù)據(jù)包包含客戶端發(fā)出的兩條消息的完整信息,這個時候基于之前邏輯實現(xiàn)的服務(wù)端就蒙了,因為服務(wù)端不知道第一條消息從哪兒結(jié)束和第二條消息從哪兒開始,這種情況其實是發(fā)生了TCP粘包。
第三種情況:
服務(wù)端一共收到了兩個數(shù)據(jù)包,第一個數(shù)據(jù)包只包含了第一條消息的一部分,第一條消息的后半部分和第二條消息都在第二個數(shù)據(jù)包中,或者是第一個數(shù)據(jù)包包含了第一條消息的完整信息和第二條消息的一部分信息,第二個數(shù)據(jù)包包含了第二條消息的剩下部分,這種情況其實是發(fā)送了TCP拆,因為發(fā)生了一條消息被拆分在兩個包里面發(fā)送了,同樣上面的服務(wù)器邏輯對于這種情況是不好處理的。
我們知道tcp是以流動的方式傳輸數(shù)據(jù),傳輸?shù)淖钚挝粸橐粋€報文段(segment)。tcp Header中有個Options標(biāo)識位,常見的標(biāo)識為mss(Maximum Segment Size)指的是,連接層每次傳輸?shù)臄?shù)據(jù)有個最大限制MTU(Maximum Transmission Unit),一般是1500比特,超過這個量要分成多個報文段,mss則是這個最大限制減去TCP的header,光是要傳輸?shù)臄?shù)據(jù)的大小,一般為1460比特。換算成字節(jié),也就是180多字節(jié)。
tcp為提高性能,發(fā)送端會將需要發(fā)送的數(shù)據(jù)發(fā)送到緩沖區(qū),等待緩沖區(qū)滿了之后,再將緩沖中的數(shù)據(jù)發(fā)送到接收方。同理,接收方也有緩沖區(qū)這樣的機(jī)制,來接收數(shù)據(jù)。
發(fā)生TCP粘包、拆包主要是由于下面一些原因:
既然知道了tcp是無界的數(shù)據(jù)流,且協(xié)議本身無法避免粘包,拆包的發(fā)生,那我們只能在應(yīng)用層數(shù)據(jù)協(xié)議上,加以控制。通常在制定傳輸數(shù)據(jù)時,可以使用如下方法:
寫了一個簡單的 golang 版的tcp服務(wù)器實例,僅供參考:
例子
參考和推薦閱讀書目:
注釋:
eg.
TL;DR 在使用 Golang 編寫 TCP/UDP socket 的時候,第一步做的就是地址解析。
該函數(shù)返回的地址包含的信息如下:
TCPAddr 里, IP 既可以是 IPv4 地址,也可以是 IPv6 地址。 Port 就是端口了。 Zone 是 IPv6 本地地址所在的區(qū)域。
從返回結(jié)果看該函數(shù)的參數(shù), network 指 address 的網(wǎng)絡(luò)類型; address 指要解析的地址,會從中解析出我們想要的 IP , Port 和 Zone 。
從源碼中可以看出,參數(shù) network 只能是如下四個值,否則會得到一個錯誤。
解析過程跟 ResolveTCPAddr 的一樣,不過得到的是 *UDPAddr 。
UDPAddr 包含的信息如下:
三次握手:
1. 主動發(fā)起連接請求端(客戶端),發(fā)送 SYN 標(biāo)志位,攜帶數(shù)據(jù)包、包號
2. 被動接收連接請求端(服務(wù)器),接收 SYN,回復(fù) ACK,攜帶應(yīng)答序列號。同時,發(fā)送SYN標(biāo)志位,攜帶數(shù)據(jù)包、包號
3. 主動發(fā)起連接請求端(客戶端),接收SYN 標(biāo)志位,回復(fù) ACK。
被動端(服務(wù)器)接收 ACK —— 標(biāo)志著 三次握手建立完成( Accept()/Dial() 返回 )
四次揮手:
1. 主動請求斷開連接端(客戶端), 發(fā)送 FIN標(biāo)志,攜帶數(shù)據(jù)包
2. 被動接受斷開連接端(服務(wù)器), 發(fā)送 ACK標(biāo)志,攜帶應(yīng)答序列號。 —— 半關(guān)閉完成。
3. 被動接受斷開連接端(服務(wù)器), 發(fā)送 FIN標(biāo)志,攜帶數(shù)據(jù)包
4. 主動請求斷開連接端(客戶端), 發(fā)送 最后一個 ACK標(biāo)志,攜帶應(yīng)答序列號?!?發(fā)送完成,客戶端不會直接退出,等 2MSL時長。
等 2MSL待目的:確保服務(wù)器 收到最后一個ACK
滑動窗口:
通知對端本地存儲數(shù)據(jù)的 緩沖區(qū)容量。—— write 函數(shù)在對端 緩沖區(qū)滿時,有可能阻塞。
TCP狀態(tài)轉(zhuǎn)換:
1. 主動發(fā)起連接請求端:
CLOSED —— 發(fā)送SYN —— SYN_SENT(了解) —— 接收ACK、SYN,回發(fā) ACK —— ESTABLISHED (數(shù)據(jù)通信)
2. 主動關(guān)閉連接請求端:
ESTABLISHED —— 發(fā)送FIN —— FIN_WAIT_1 —— 接收ACK —— FIN_WAIT_2 (半關(guān)閉、主動端)
—— 接收FIN、回復(fù)ACK —— TIME_WAIT (主動端) —— 等 2MSL 時長 —— CLOSED
3. 被動建立連接請求端:
CLOSED —— LISTEN —— 接收SYN、發(fā)送ACK、SYN —— SYN_RCVD —— 接收 ACK —— ESTABLISHED (數(shù)據(jù)通信)
4. 被動斷開連接請求端:
ESTABLISHED —— 接收 FIN、發(fā)送 ACK —— CLOSE_WAIT —— 發(fā)送 FIN —— LAST_ACK —— 接收ACK —— CLOSED
windows下查看TCP狀態(tài)轉(zhuǎn)換:
netstat -an | findstr? 端口號
Linux下查看TCP狀態(tài)轉(zhuǎn)換:
netstat -an | grep? 端口號
TCP和UDP對比:?
TCP: 面向連接的可靠的數(shù)據(jù)包傳遞。 針對不穩(wěn)定的 網(wǎng)絡(luò)層,完全彌補(bǔ)。ACK
UDP:無連接不可靠的報文傳輸。 針對不穩(wěn)定的 網(wǎng)絡(luò)層,完全不彌補(bǔ)。還原網(wǎng)絡(luò)真實狀態(tài)。
優(yōu)點???????????????????????????????????????????????????????????? 缺點
TCP: 可靠、順序、穩(wěn)定 ???????????????????????????????????? 系統(tǒng)資源消耗大,程序?qū)崿F(xiàn)繁復(fù)、速度慢
UDP:系統(tǒng)資源消耗小,程序?qū)崿F(xiàn)簡單、速度快 ???????????????????????? 不可靠、無序、不穩(wěn)定
使用場景:
TCP:大文件、可靠數(shù)據(jù)傳輸。 對數(shù)據(jù)的 穩(wěn)定性、準(zhǔn)確性、一致性要求較高的場合。
UDP:應(yīng)用于對數(shù)據(jù)時效性要求較高的場合。 網(wǎng)絡(luò)直播、電話會議、視頻直播、網(wǎng)絡(luò)游戲。
UDP-CS-Server實現(xiàn)流程:
1.? 創(chuàng)建 udp地址結(jié)構(gòu) ResolveUDPAddr(“協(xié)議”, “IP:port”) —— udpAddr 本質(zhì) struct{IP、port}
2.? 創(chuàng)建用于 數(shù)據(jù)通信的 socket ListenUDP(“協(xié)議”, udpAddr ) —— udpConn (socket)
3.? 從客戶端讀取數(shù)據(jù),獲取對端的地址 udpConn.ReadFromUDP() —— 返回:n,clientAddr, err
4.? 發(fā)送數(shù)據(jù)包給 客戶端 udpConn.WriteToUDP("數(shù)據(jù)", clientAddr)
UDP-CS-Client實現(xiàn)流程:
1.? 創(chuàng)建用于通信的 socket。 net.Dial("udp", "服務(wù)器IP:port") —— udpConn (socket)
2.? 以后流程參見 TCP客戶端實現(xiàn)源碼。
UDPserver默認(rèn)就支持并發(fā)!
------------------------------------
命令行參數(shù): 在main函數(shù)啟動時,向整個程序傳參。 【重點】
語法: go run xxx.go ? argv1 argv2? argv3? argv4 。。。
xxx.exe:? 第 0 個參數(shù)。
argv1 :第 1 個參數(shù)。
argv2 :第 2 個參數(shù)。
argv3 :第 3 個參數(shù)。
argv4 :第 4 個參數(shù)。
使用: list := os.Args? 提取所有命令行參數(shù)。
獲取文件屬性函數(shù):
os.stat(文件訪問絕對路徑) —— fileInfo 接口
fileInfo 包含 兩個接口。
Name() 獲取文件名。 不帶訪問路徑
Size() 獲取文件大小。
網(wǎng)絡(luò)文件傳輸 —— 發(fā)送端(客戶端)
1.? 獲取命令行參數(shù),得到文件名(帶路徑)filePath list := os.Args
2.? 使用 os.stat() 獲取 文件名(不帶路徑)fileName
3.? 創(chuàng)建 用于數(shù)據(jù)傳輸?shù)?socket? net.Dial("tcp", “服務(wù)器IP+port”) —— conn
4.? 發(fā)送文件名(不帶路徑)? 給接收端, conn.write()
5.? 讀取 接收端回發(fā)“ok”,判斷無誤。封裝函數(shù) sendFile(filePath, conn) 發(fā)送文件內(nèi)容
6.? 實現(xiàn) sendFile(filePath,? conn)
1) 只讀打開文件 os.Open(filePath)
for {
2) 從文件中讀數(shù)據(jù)? f.Read(buf)
3) 將讀到的數(shù)據(jù)寫到socket中? conn.write(buf[:n])
4)判斷讀取文件的 結(jié)尾。 io.EOF. 跳出循環(huán)
}
網(wǎng)絡(luò)文件傳輸 —— 接收端(服務(wù)器)
1. 創(chuàng)建用于監(jiān)聽的 socket net.Listen() —— listener
2. 借助listener 創(chuàng)建用于 通信的 socket listener.Accpet()? —— conn
3. 讀取 conn.read() 發(fā)送端的 文件名, 保存至本地。
4. 回發(fā) “ok”應(yīng)答 發(fā)送端。
5. 封裝函數(shù),接收文件內(nèi)容 recvFile(文件路徑)
1) f = os.Create(帶有路徑的文件名)
for {
2)從 socket中讀取發(fā)送端發(fā)送的 文件內(nèi)容 。 conn.read(buf)
3)? 將讀到的數(shù)據(jù) 保存至本地文件 f.Write(buf[:n])
4)? 判斷 讀取conn 結(jié)束, 代表文件傳輸完成。 n == 0? break
}
安裝:
go get -v -u github.com/rocket049/connpool
go get -v -u gitee.com/rocket049/connpool
rocket049/connpool 包是本人用go語言開發(fā)的,提供一個通用的TCP連接池,初始化參數(shù)包括最高連接數(shù)、超時秒數(shù)、連接函數(shù),放回連接池的連接被重新取出時,如果已經(jīng)超時,將會自動重新連接;如果沒有超時,連接將被復(fù)用。
可調(diào)用的函數(shù):
調(diào)用示例: