TCP數(shù)據(jù)邊界(粘包)說到 TCP 數(shù)據(jù)邊界問題,可能很多人還反應(yīng)不過來,這是個(gè)啥東西?不過它還有另外一個(gè)更出名的代名詞,叫做 粘包 。如果以正式的說法來說的話,其實(shí)并不存在所謂的 粘包 問題 ,或者說,這個(gè)東西本身不能算做是一個(gè)問題。這個(gè)我們最后再說,先來看看到底啥是數(shù)據(jù)邊界。 什么是數(shù)據(jù)邊界問題一般來說,我們在 Swoole 中運(yùn)行之前寫過的 TCP 相關(guān)的測試代碼,都沒有什么問題。那是因?yàn)槲覀冎皇亲约簡螜C(jī)測試,沒什么并發(fā)流量,如果一旦踫到高并發(fā),那么就有可能會(huì)出現(xiàn) TCP 數(shù)據(jù)包的邊界問題。 在學(xué)習(xí)網(wǎng)絡(luò)相關(guān)的知識(shí)時(shí),我們知道 TCP 是解決了 UDP 的順序和丟包這兩個(gè)重要的問題,它是會(huì)相互建立連接的,但是連接建立后,TCP 會(huì)以流式的方式進(jìn)行數(shù)據(jù)傳輸。還記得我們使用 Socket 建立連接時(shí)使用的那個(gè)區(qū)分 TCP 和 UDP 的常量嗎?TCP 使用的就是 SOCK_STREAM ,是不是從名字就看出來了,STREAM 代表的就是流。 然而,這種流式數(shù)據(jù)包的傳輸是沒有邊界的。怎么說呢?直接用例子來說話。 服務(wù)端就是我們之前經(jīng)常用的那個(gè) TCP 服務(wù)端,在這里我們直接循環(huán)掛起接收 recv() 方法獲得的數(shù)據(jù),然后在里面打印出來,每次打印后面都加上一段分隔描述的字符串。 客戶端也很簡單,三個(gè)循環(huán),分別使用 send() 發(fā)送數(shù)據(jù),然后第二個(gè)個(gè)循環(huán)之后 sleep() 一下。 到這里,都沒什么問題吧?猜猜我們運(yùn)行客戶端之后,服務(wù)端會(huì)輸出什么? 注意,我們是三個(gè)循環(huán),每個(gè)循環(huán)里至少有一個(gè) send() ,但是注意服務(wù)端,只輸出了兩個(gè)帶 EOF+等號(hào) 的 recv() 信息。 看明白了嗎?send() 和 recv() 并不是匹配的一對,并不是說客戶端發(fā)一次 send() ,服務(wù)端就要接收一次 recv() 。真實(shí)的情況是,如果沒有休息的那一秒的話,我們在這邊一直 send() 到最后,服務(wù)端才會(huì)輸出一條接收到的數(shù)據(jù)。因?yàn)?,send() 發(fā)送的是數(shù)據(jù)流,在未超時(shí)中斷的情況下,它會(huì)持續(xù)發(fā)送,就像高并發(fā)的時(shí)候,數(shù)據(jù)不斷傳送,服務(wù)端最后接到的數(shù)據(jù)可能就是有問題的數(shù)據(jù)。 這就是最直觀的一種數(shù)據(jù)包邊界問題的展示。如果是這種情況,你會(huì)想到什么解決方案呢?我們可以自己定義一個(gè)分隔字符,然后在服務(wù)端截開不同的數(shù)據(jù),就像 HTTP 的請求一樣,HTTP 的描述、頭、Body信息就是以換行符分隔的。HTTP 本身就是一種 TCP 的高層服務(wù),它當(dāng)然也會(huì)有這個(gè)問題,只不過服務(wù)器程序和瀏覽器已經(jīng)幫我們解決了。 上面這個(gè)問題可以再細(xì)分成一個(gè)操作,那就是 分包 操作。另外還有一種情況,比如說一些大文件的傳輸,有可能一次傳輸只是傳來了一部分,這時(shí)候我們需要將之前先接收到的數(shù)據(jù)緩存起來,然后等待后續(xù)的數(shù)據(jù)繼續(xù)發(fā)送過來之后合并在一起成為一個(gè)完整的數(shù)據(jù)包。這種情況叫做 合包 。 接下來,我們看看 Swoole 中處理這種數(shù)據(jù)邊界問題的方式。 解決邊界問題在 Swoole 中,有兩種解決數(shù)據(jù)邊界問題的試試,一個(gè)是通過 EOF 結(jié)束符協(xié)議,另一個(gè)是通過固定包頭+包體協(xié)議的試。我們今天就用 EOF 結(jié)束符協(xié)議來演示一下。 這個(gè) EOF 結(jié)束符,其實(shí)就是我們在上面講到的方法,定義一個(gè)結(jié)束符號(hào),然后讓框架自動(dòng)幫我們完成 分/合包 操作。 在客戶端和服務(wù)端,我們都增加了一個(gè) set() ,然后設(shè)置了一些屬性,open_eof_check 表示開啟 EOF 結(jié)束符檢查,package_eof 表示指定的結(jié)束符號(hào),這里我們使用的是大部分情況下通用的 換行 標(biāo)記,你可以試著換成其它符號(hào)。 然后,客戶端還有一些小變化。我們在第一和第二個(gè)循環(huán)之間加了一個(gè) sleep() ,如果是之前的情況,1-10 和 11-20 是會(huì)因?yàn)闀r(shí)間問題分成兩個(gè) recv() 的,但現(xiàn)在,我們在加了 sleep() 之后,在第二個(gè)循環(huán)結(jié)束的時(shí)候才再發(fā)送了 換行 標(biāo)記??梢韵认胂耄@里輸出的結(jié)果會(huì)是什么樣子的。 第三個(gè)循環(huán)還是老樣子,但是在循環(huán)中,有兩個(gè) send() ,第二個(gè) send() 每次循環(huán)都會(huì)發(fā)送一個(gè) 換行 標(biāo)記。 試試運(yùn)行的結(jié)果吧! 有意思嗎?1-20 還是在一起輸出了,看著和最上面的測試代碼效果一樣呀。不不不,在這段測試代碼中,我們中間 sleep() 了,然后服務(wù)端在第一個(gè)循環(huán)數(shù)據(jù)接收完之后,沒有看到 換行 標(biāo)記,于是等待了 1 秒,接著繼續(xù)接收 11-20 的數(shù)據(jù)之后,看到了 換行 標(biāo)記,才結(jié)束了這次請求接收。這是一個(gè)合包的過程。 第三段循環(huán)中,每次循環(huán)都有一個(gè)結(jié)束標(biāo)記,于是,每一段循環(huán)都會(huì)在服務(wù)端獲得一個(gè) recv() 數(shù)據(jù),就像我們把字符串分割開了一樣。很明顯,這是一個(gè) 分包 過程。 EOF 這種情況比較好理解,另外一種處理 固定包頭 的方式則是另外一種概念。它的特點(diǎn)是一個(gè)數(shù)據(jù)包總是由包頭 + 包體 2 部分組成。包頭由一個(gè)字段指定了包體或整個(gè)包的長度,長度一般是使用 2 字節(jié) /4 字節(jié)整數(shù)來表示。服務(wù)器收到包頭后,可以根據(jù)長度值來精確控制需要再接收多少數(shù)據(jù)就是完整的數(shù)據(jù)包。 關(guān)于固定包頭的處理方式的演示我就不展示了,因?yàn)槲乙矝]怎么接觸過,或者說并沒有太深入的學(xué)習(xí)過這一方面的內(nèi)容,大家有興趣的可以自行查閱相關(guān)資料。 真的有“粘包問題”嗎?其實(shí),粘包,或者說數(shù)據(jù)邊界,本身就是 TCP 協(xié)議在設(shè)計(jì)時(shí)的特點(diǎn)。它本身并不構(gòu)成問題,只是我們在使用的時(shí)候,需要知道多少數(shù)據(jù)應(yīng)該是一個(gè)完整的數(shù)據(jù)包,因此,這是因?yàn)槲覀兊男枨蠖霈F(xiàn)的問題。說得有點(diǎn)繞,換個(gè)角度說,它不是一個(gè)技術(shù)問題,而是一個(gè)需求問題。 好吧,如果還不理解的話,下面參考文檔中第二條鏈接中,知乎的各路大神們早就對這個(gè)問題展開了各種探討,大家可以移步前去觀戰(zhàn)。 總結(jié)要真是細(xì)說起來,這一塊的知識(shí)其實(shí)是網(wǎng)絡(luò)編程相關(guān)的內(nèi)容,但是,如果你要是在簡歷中提到了 Swoole 的話,那么這個(gè)問題的面試出題率還是相當(dāng)高的,畢竟太出名了。希望在今天的學(xué)習(xí)之后,大家能夠更加重視編程四大件的學(xué)習(xí),還記得是哪四個(gè)嗎?操作系統(tǒng)、算法與數(shù)據(jù)結(jié)構(gòu)、計(jì)算機(jī)網(wǎng)絡(luò)、設(shè)計(jì)模式。如果還有一個(gè)的話,可以再把數(shù)據(jù)庫原理加上。要知道,我們之前學(xué)習(xí)的進(jìn)程、線程、協(xié)程全是操作系統(tǒng)這個(gè)學(xué)科中的知識(shí),而TCP、UDP、SOCKET這些又全是計(jì)算機(jī)網(wǎng)絡(luò)中的知識(shí)。學(xué)海無涯呀,各位大佬們好好加油吧。 測試代碼: https://github.com/zhangyue0503/swoole/blob/main/5.%E5%85%B6%E5%AE%83/source/5.2TCP%E6%95%B0%E6%8D%AE%E8%BE%B9%E7%95%8C%EF%BC%88%E7%B2%98%E5%8C%85%E9%97%AE%E9%A2%98%EF%BC%89.php 參考文檔: https://wiki./#/learn?id=tcp數(shù)據(jù)包邊界問題 https://www.zhihu.com/question/20210025 |
|
|