大佬們都說tcp有黏包的問題脐嫂,tcp卻說:我冤枉晃酒!

相關參考添加鏈接描述
相關參考

什么是tcp

TCP,全稱Transmission Control Protocol澳叉,是一種傳輸控制協(xié)議隙咸,TCP協(xié)議也是計算機網(wǎng)絡中非常復雜的一個協(xié)議

tcp的特點

  • tcp是面向連接的協(xié)議
  • tcp是端到端的鏈接
  • tcp提供可靠的傳輸服務
  • tcp協(xié)議提供雙工通信
  • tcp是面向字節(jié)流的協(xié)議

tcp粘包

tcp有這么多的特點,但是為什么還會出現(xiàn)粘包呢成洗?其實這是對tcp傳輸?shù)囊环N優(yōu)化而引起的一些問題五督。

  • 為什么要優(yōu)化?
    我們前面說了瓶殃, tcp是面向字節(jié)流的協(xié)議充包,而不是消息包的協(xié)議,為什么是面向字節(jié)流遥椿?因為一個tcp連接基矮,它負責傳輸數(shù)據(jù),但是這些數(shù)據(jù)的大小是未知的冠场,可能很大家浇,也可能很小,而且是沒有邊界的慈鸠,它只會將你的數(shù)據(jù)編程字節(jié)流發(fā)到對面去蓝谨,而且保證順序不會亂,而對于字節(jié)流的解析青团,就需要我們自己來搞定了譬巫,那數(shù)據(jù)怎么傳輸呢?方法來了督笆,不管你是什么數(shù)據(jù)芦昔,我都給你轉(zhuǎn)換成二進制。然后由tcp切割為tcp認為合適的長度娃肿。

  • 那么這個長度怎么確定咕缎?
    tcp協(xié)議簡介有興趣的同學可以看一下阮一峰的文章。
    我們知道料扰,從應用層到物理層凭豪,數(shù)據(jù)都是一層一層經(jīng)過打包過的,我們可能一下子沒法知道tcp最大傳輸多少晒杈,但是我們可以反推一下嫂伞,以太網(wǎng)數(shù)據(jù)包(packet)的大小是固定的,最初是1518字節(jié),后來增加到1522字節(jié)帖努。其中撰豺, 1500 字節(jié)是負載(payload),22字節(jié)是頭信息(head)拼余。IP 數(shù)據(jù)包在以太網(wǎng)數(shù)據(jù)包的負載里面污桦,它也有自己的頭信息,最少需要20字節(jié)匙监,所以 IP 數(shù)據(jù)包的負載最多為1480字節(jié)凡橱。TCP 數(shù)據(jù)包在 IP 數(shù)據(jù)包的負載里面。它的頭信息最少也需要20字節(jié)舅柜,因此 TCP 數(shù)據(jù)包的最大負載是 1480 - 20 = 1460 字節(jié)梭纹。由于 IP 和 TCP 協(xié)議往往有額外的頭信息,所以 TCP 負載實際為1400字節(jié)左右致份。

  • 這里插播一個http2的一個改進 相關參考
    在 HTTP/1 中变抽,HTTP 請求和響應都是由「狀態(tài)行、請求 / 響應頭部氮块、消息主體」三部分組成绍载。一般而言,消息主體都會經(jīng)過 gzip 壓縮滔蝉,或者本身傳輸?shù)木褪菈嚎s過后的二進制文件(例如圖片击儡、音頻),但狀態(tài)行和頭部卻沒有經(jīng)過任何壓縮蝠引,直接以純文本傳輸阳谍。而http2里面的一個重大改進,就是壓縮http的協(xié)議的頭信息螃概,怎么實現(xiàn)的頭部壓縮呢矫夯?主要是基于以下幾點:

  • 維護一份相同的靜態(tài)字典(Static Table),包含常見的頭部名稱吊洼,以及特別常見的頭部名稱與值的組合训貌;

  • 維護一份相同的動態(tài)字典(Dynamic Table),可以動態(tài)地添加內(nèi)容冒窍;

  • 支持基于靜態(tài)哈夫曼碼表的哈夫曼編碼(Huffman Coding)
    [圖片上傳失敗...(image-e64940-1677756025803)]

  • 好了递沪,我們前面說了,一個tcp包負載是1400字節(jié)左右综液,那么你發(fā)送2000個字節(jié)款慨,就需要發(fā)送兩個數(shù)據(jù)包,第二個數(shù)據(jù)包可能就是600個字節(jié)谬莹。那么問題來了檩奠,明明一次可以發(fā)送1400字節(jié)约素,但是實際只發(fā)送600個字節(jié),是不是有點浪費網(wǎng)絡之間的IO,怎么辦笆凌?John Nagle(約翰.納格) 提出了一種簡單有效的解決方法。也就是Nagle 算法士葫。相關參考

  • Nagle 算法
    Nagle 算法的基本定義是任一時刻乞而,最多只能有一個未被確認的小段。所謂“小段”慢显,指的是長度小于 MSS 尺寸的數(shù)據(jù)塊爪模,而未被確認則是指沒有收到對方的 ACK 數(shù)據(jù)包。Nagle 算法的規(guī)則(參考 tcp_output.c 文件里 tcp_nagle_check 函數(shù)注釋):
    如果包長度達到 MSS荚藻,則允許發(fā)送屋灌;
    如果該數(shù)據(jù)包含有 FIN,則允許發(fā)送应狱;
    設置了 TCP_NODELAY 選項共郭,則允許發(fā)送;
    未設置 TCP_CORK 選項時疾呻,若所有發(fā)出去的小數(shù)據(jù)包(包長度小于 MSS)均被確認除嘹,則允許發(fā)送;
    上述條件都未滿足岸蜗,但發(fā)送了超時(一般為 200 ms)尉咕,則立即發(fā)送。
    該算法的精妙之處在于它實現(xiàn)了自時鐘(self-clocking)控制:ACK 返回得快璃岳,數(shù)據(jù)傳輸也越快年缎。在相對高延遲的廣域網(wǎng)中,更需要減少微型報的數(shù)目铃慷,該算法使得單位時間內(nèi)發(fā)送的報文段數(shù)據(jù)更少单芜。也就是說,RTT 控制著發(fā)包速率枚冗。

  • 簡單理解 就是如果你普通的數(shù)據(jù)流缓溅,小于傳輸?shù)呢撦d量,我就不傳輸赁温,等到下次有數(shù)據(jù)滿足了我的負載量我再傳輸坛怪,但是我也不能一直等,如果時間超過200ms都么有數(shù)據(jù)流過來股囊,那我就傳輸袜匿。

tcp粘包的演示

服務端

package main

import (
    "bufio"
    "fmt"
    "io"
    "net"
)

func main() {
    network:="tcp"
    address:="127.0.0.1:30000"
    //綁定和監(jiān)聽tpc和端口
    listen, err := net.Listen(network, address)
    if err != nil {
        fmt.Println("listen err")
    }
    //關閉監(jiān)聽
    defer listen.Close()
    for{
        //等待連接
        conn,err:=listen.Accept()
        if err != nil {
            fmt.Println("accept error")
        }
        //從連接里面讀取數(shù)據(jù)
        go process(conn)
    }
}

func process(conn net.Conn){
    defer conn.Close()//關閉連接
    //讀取連接數(shù)據(jù)
    reader:=bufio.NewReader(conn)
    //定義每次接收的長度
    buf:=make([]byte, 7)
    for  {
        //用buf接收連接發(fā)送的內(nèi)容
        read, err := reader.Read(buf)
        //讀完了
        if err == io.EOF {
            break
        }
        if err != nil {
            fmt.Println("read conn err")
        }
        fmt.Printf("the msg i read length is %d \n",read)
        str:=string(buf[:read])
        fmt.Println(str)
    }
}

客戶端代碼

package main

import (
    "fmt"
    "net"
)

func main() {
    network:="tcp"
    address:="127.0.0.1:30000"
    //撥號 請求創(chuàng)建tcp連接
    conn, err := net.Dial(network,address )
    if err != nil {
        fmt.Println("connect err")
    }
    //關閉連接
    defer conn.Close()
    //想tcp寫入數(shù)據(jù)
    conn.Write([]byte("123456789"))
}

  • 我們先后發(fā)送123,1234567稚疹,123456789和123居灯,456并打印出來看看祭务,我們來看截圖里面的內(nèi)容,發(fā)現(xiàn)出現(xiàn)了問題怪嫌,這就是粘包造成的問題义锥,可能會把你的消息分段發(fā)送,也可能會把多段消息合并 岩灭。
    [圖片上傳失敗...(image-9b5fc6-1677756025803)]

tcp粘包的解決

  • tcp是只負責按順序傳輸數(shù)據(jù)拌倍,并沒有邊界的概念,那么我們?nèi)绻胍_定消息邊界噪径,就得發(fā)送一種信號柱恤,或者說一種約定,當接收者接到這種信號找爱,就能知道是消息的開始還是結(jié)尾梗顺,比如我們的http請求有一個content-length
    [圖片上傳失敗...(image-a834cd-1677756025803)]
    那么我們約定消息邊界一般有三種模式
  • 定長消息:協(xié)議提前約定好包的長度為多少,每當接收端接收到固定長度的字節(jié)就確定一個包车摄,就像咱們上面截圖的那個
  • 消息分隔符:利用特殊符號標志著消息的開始或者結(jié)束寺谤,例如 HTTP 協(xié)議中的換行符;
  • 長度前綴:先發(fā)送N個字節(jié)代表包的大辛钒恪(注意大端和小端問題)矗漾,后續(xù)解析也按長度讀取解析。
粘包解決方案相關參考
  • 這里我們使用第三種來實現(xiàn)薄料,即給消息體添加一個長度前綴敞贡。
  • 我們先來寫一個文件,基于長度前綴來編碼和解碼消息
package tcp_code

import (
    "bufio"
    "bytes"
    "encoding/binary"
)

// Encode 將消息編碼后返回byte類型
func Encode(msg string)([]byte,error){
    //1.讀取消息的長度,用int32存放消息長度,這個長度大概能支持4G的數(shù)據(jù)傳輸,如果用int64就代表16777216T
    length:=int32(len(msg))
    //定義一個Buffer結(jié)構(gòu)體用來存儲數(shù)據(jù),Buffer是一個變長緩沖區(qū),可讀可寫
    var pkg =new(bytes.Buffer)
    //把長度以二進制的形式寫入消息頭
    err := binary.Write(pkg, binary.LittleEndian, length)
    if err != nil {
        return nil, err
    }
    //把消息以二進制的形式寫入pkg
    err = binary.Write(pkg, binary.LittleEndian, []byte(msg))
    if err != nil {
        return nil, err
    }
    //將緩沖區(qū)的數(shù)據(jù)返回
    return pkg.Bytes(),nil
}

// Decode 參數(shù)是從連接中獲取的原始消息,用這個方法將消息體解碼
func Decode(reader bufio.Reader)(string,error){
    //1.獲取消息的長度
    //按照約定,讀取前32的長度
    //Peek是返回字節(jié)類型,一個字節(jié)是8個bit,所以是4個字節(jié)即代表32位的長度的數(shù)據(jù)
    lengthByte,_:=reader.Peek(4)
    //轉(zhuǎn)換為buff類型
    lengthBuff:=bytes.NewBuffer(lengthByte)

    //這個長度是指消息體的長度
    var length int32
    //將長度賦值給length
    err := binary.Read(lengthBuff, binary.LittleEndian, &length)
    if err != nil {
        return "", err
    }
    //消息體的長度加上4個字節(jié) 就是完整的消息體了
    totalLen:=length+4
    //查看當前緩存區(qū)中消息的長度,如果消息還沒有傳輸完畢,先不處理
    if int32(reader.Buffered())<totalLen{
        return "", err
    }
    //定義一個切片從緩沖區(qū)獲取數(shù)據(jù)
    pack:=make([]byte,totalLen)
    _, err = reader.Read(pack)
    if err != nil {
        return "", err
    }
    //返回消息 ,注意不要返回前4個byte,前4個byte代表的是消息體的長度
    return string(pack[4:]),nil
}

  • 服務端代碼
package main

import (
    tcp_code "acurd.com/pkg/pkg/tcp-code"
    "bufio"
    "fmt"
    "io"
    "net"
)

func main() {
    network:="tcp"
    address:="127.0.0.1:30000"
    //綁定和監(jiān)聽tpc和端口
    listen, err := net.Listen(network, address)
    if err != nil {
        fmt.Println("listen err")
    }
    //關閉監(jiān)聽
    defer listen.Close()
    for{
        //等待連接
        conn,err:=listen.Accept()
        if err != nil {
            fmt.Println("accept error")
        }
        //從連接里面讀取數(shù)據(jù)
        go process(conn)
    }
}

func process(conn net.Conn){
    defer conn.Close()//關閉連接
    //讀取連接數(shù)據(jù)
    reader:=bufio.NewReader(conn)
    //定義每次接收的長度
    for  {
        //使用decode解碼消息
        msg, err := tcp_code.Decode(reader)
        //讀完了
        if err == io.EOF {
            break
        }
        if err != nil {
            fmt.Println("read conn err")
        }
        fmt.Println(msg)
    }
}
  • 客戶端代碼
package main

import (
    tcp_code "acurd.com/pkg/pkg/tcp-code"
    "bufio"
    "fmt"
    "io"
    "net"
)

func main() {
    network:="tcp"
    address:="127.0.0.1:30000"
    //綁定和監(jiān)聽tpc和端口
    listen, err := net.Listen(network, address)
    if err != nil {
        fmt.Println("listen err")
    }
    //關閉監(jiān)聽
    defer listen.Close()
    for{
        //等待連接
        conn,err:=listen.Accept()
        if err != nil {
            fmt.Println("accept error")
        }
        //從連接里面讀取數(shù)據(jù)
        go process(conn)
    }
}

func process(conn net.Conn){
    defer conn.Close()//關閉連接
    //讀取連接數(shù)據(jù)
    reader:=bufio.NewReader(conn)
    //定義每次接收的長度
    for  {
        //使用decode解碼消息
        msg, err := tcp_code.Decode(reader)
        //讀完了
        if err == io.EOF {
            break
        }
        if err != nil {
            fmt.Println("read conn err")
        }
        fmt.Println(msg)
    }
}
  • 我們看一下效果摄职,發(fā)送了兩個消息誊役,一12345678,一個是abcdefghi


    image.png

總結(jié)

通過上面,我們了解到了原來粘包的問題谷市,并不屬于tcp的鍋蛔垢。tcp是基于數(shù)據(jù)流的傳輸,保證數(shù)據(jù)流的順序迫悠,但是正式由于這種數(shù)據(jù)流的傳輸模式鹏漆,對于tcp來說,自己就像一個傳送帶创泄,傳遞的是一個個的快遞包裹艺玲,源源不斷。具體包裹到是什么鞠抑,到哪里去饭聚,就需要接收端和發(fā)送端通過定制的協(xié)議來編碼和解碼解決。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末搁拙,一起剝皮案震驚了整個濱河市秒梳,隨后出現(xiàn)的幾起案子法绵,更是在濱河造成了極大的恐慌,老刑警劉巖酪碘,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件朋譬,死亡現(xiàn)場離奇詭異,居然都是意外死亡兴垦,警方通過查閱死者的電腦和手機此熬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來滑进,“玉大人,你說我怎么就攤上這事募谎》龉兀” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵数冬,是天一觀的道長节槐。 經(jīng)常有香客問我,道長拐纱,這世上最難降的妖魔是什么铜异? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮秸架,結(jié)果婚禮上揍庄,老公的妹妹穿的比我還像新娘。我一直安慰自己东抹,他們只是感情好蚂子,可當我...
    茶點故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著缭黔,像睡著了一般食茎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上馏谨,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天别渔,我揣著相機與錄音,去河邊找鬼惧互。 笑死哎媚,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的壹哺。 我是一名探鬼主播抄伍,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼管宵!你這毒婦竟也來了截珍?” 一聲冷哼從身側(cè)響起攀甚,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎岗喉,沒想到半個月后秋度,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡钱床,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年荚斯,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片查牌。...
    茶點故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡事期,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出纸颜,到底是詐尸還是另有隱情兽泣,我是刑警寧澤,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布胁孙,位于F島的核電站唠倦,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏涮较。R本人自食惡果不足惜稠鼻,卻給世界環(huán)境...
    茶點故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望狂票。 院中可真熱鬧候齿,春花似錦、人聲如沸闺属。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽屋剑。三九已至润匙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間唉匾,已是汗流浹背孕讳。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留巍膘,地道東北人厂财。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像峡懈,于是被迫代替她去往敵國和親璃饱。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,500評論 2 359

推薦閱讀更多精彩內(nèi)容