go語言處理粘包問題

圖片.png

粘包的定義

  • 粘包是指網絡通訊中,發送方發送的多個數據包在接收方的緩衝區黏在一塊兒,多個數據包首尾相連的現象。
  • 例如,基於tcp的套接字實現的客戶端向服務器上傳文件時,內容每每是按照一段一段的字節流發送的,若是不作任何處理,從接收方來看,根本不知道該文件的字節流從何處開始,在何處結束。
  • 所以,所謂粘包問題主要是由於接收方不知道消息之間的界限,不知道一次提取多少字節的數據形成的。

產生的緣由

粘包產生的緣由有發送方和接收方兩方面:
  • 發送方引發粘包的緣由主要是由tcp協議自己形成的。衆所周知,tcp協議是面向鏈接,面向流,提供高可靠性服務的。tcp數據包轉發使用Nagle算法,Nagle算法是爲了提升tcp的傳輸效率,簡單來講,當應用層有一個數據包要發送時,Nagle算法並不會馬上發送,而是繼續收集要發送的消息,直到上一個包獲得確認時,纔會發送數據,此時Nagle算法會將收集的多個的數據包造成一個分組,將這個分組一次性轉發出去。所以,Nagle算法形成了發送方可能存在粘包現象。具體過程以下圖所示:

    圖片.png
    圖片.png
    圖片.png
    圖片.png

  • 接收方引發粘包的緣由主要是因爲接收方對數據包的處理速度遠小於數據包的接收速度致使接收緩衝區的數據積壓而形成的。
  • 然而udp協議不會出現粘包,由於udp是無鏈接,面向消息,提供高效服務的。無鏈接意味着當有數據包要發送時,udp會當即發送,數據包不會積壓;面向消息意味着數據包通常很小,所以接收端處理也不會很耗時,通常不會因爲接收端來不及處理而形成粘包。最重要的時,udp不使用合併優化算法,每一個消息都有單獨的包頭,即便出現很短期內收到多個數據包的狀況,接收方也能根據包頭信息區分數據包之間的邊界。所以,udp不會出現粘包,只可能會出現丟包。

粘包示例(go語言實現)

服務端代碼以下:html

// socket_test/server/main.go

func process(conn net.Conn) {
    defer conn.Close()
    // 使用bufio的讀緩衝區(防止系統緩衝區溢出)
    reader := bufio.NewReader(conn)
    var buf [1024]byte
    for {
        n, err := reader.Read(buf[:])
        if err == io.EOF {
            break
        }
        if err != nil {
            fmt.Println("讀取客戶數據失敗,err:", err)
            break
        }
        recvData := string(buf[:n])
        fmt.Println("收到client發來的數據:", recvData)
    }
}

func main() {
    listen, err := net.Listen("tcp", "127.0.0.1:12345")
    if err != nil {
        fmt.Println("監聽失敗, err:", err)
        return
    }
    defer listen.Close()
    for {
        conn, err := listen.Accept()
        if err != nil {
            fmt.Println("創建會話失敗, err:", err)
            continue
        }
        // 單獨建一個goroutine來維護客戶端的鏈接(不會阻塞主線程)
        go process(conn)
    }
}

客戶端代碼以下:算法

// socket_test/client/main.go

func main() {
    conn, err := net.Dial("tcp", "127.0.0.1:12345")
    if err != nil {
        fmt.Println("鏈接服務器失敗, err", err)
        return
    }
    defer conn.Close()
    for i := 0; i < 20; i++ {
        msg := `Hello, Hello. How are you?`
        conn.Write([]byte(msg))
    }
}

將以上服務器和客戶端的代碼分別編譯並運行,結果以下:後端

收到client發來的數據: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client發來的數據: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client發來的數據: Hello, Hello. How are you?Hello, Hello. How are you?
收到client發來的數據: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client發來的數據: Hello, Hello. How are you?Hello, Hello. How are you?

咱們在客戶端發送了10次數據,但在服務器在輸出了5次,多條數據「粘」到了一塊兒。bash

粘包的解決辦法

基於以前的分析,咱們只要給每一個數據包封裝一個包頭,包頭裏只需包含數據包長度的信息,這樣接收方在收到數據時就能夠先讀取包頭的數據,經過包頭就能夠定位數據包的邊界,粘包問題也就迎刃而解。說白了,就是須要咱們定義一個應用之間通訊的協議,完成對每一個消息的編碼和解碼。代碼以下:服務器

// socket_test/proto/proto.go
package proto

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

func Encode(msg string) ([]byte, error) {
    length := int32(len(msg))
    // 建立一個數據包
    pkg := new(bytes.Buffer)
    // 寫入數據包頭,表示消息體的長度
    err := binary.Write(pkg, binary.LittleEndian, length)
    if err != nil {
        return nil, err
    }
    // 寫入消息體
    err = binary.Write(pkg, binary.LittleEndian, []byte(msg))
    if err != nil {
        return nil, err
    }
    return pkg.Bytes(), nil
}

func Decode(reader *bufio.Reader) (string, error) {
    // 讀取前4個字節的數據(表示數據包長度的信息)
    // peek操做只讀數據,但不會移動讀取位置!!!
    lengthByte, _ := reader.Peek(4)
    // 將前4個字節數據讀入字節緩衝區
    lengthBuf := bytes.NewBuffer(lengthByte)
    var dataLen int32
    // 讀取數據包長度
    err := binary.Read(lengthBuf, binary.LittleEndian, &dataLen)
    if err != nil {
        return "", err
    }
    // 判斷數據包的總長度是否合法
    if int32(reader.Buffered()) < dataLen + 4 {
        return "", err
    }
    pack := make([]byte, 4 + dataLen)
    // 讀取整個數據包
    _, err = reader.Read(pack)
    if err != nil {
        return "", err
    }
    return string(pack[4:]), nil
}

客戶端在發送消息時調用編碼函數對消息進行編碼,代碼以下:
// socket_test/client2/main.go網絡

package main

import (
    "fmt"
    "../proto"
    "net"
)

func main() {
    conn, err := net.Dial("tcp", "127.0.0.1:8888")
    if err != nil {
        fmt.Println("connect err=", err)
        return
    }
    fmt.Println("conn suc=", conn)

    for i := 0; i < 10; i++ {
        data, err := proto.Encode("hello, server!")
        if err != nil {
            fmt.Println("Encode failer, err = ", err)
            return
        }
        _, err = conn.Write(data)
        if err != nil {
            fmt.Println("send data failed, err= ", err)
            return
        }
    }
}

服務器接收消息時調用解碼函數對消息進行解碼,代碼以下:socket

// socket_test/server2/main.go
package main

import (
    "bufio"
    "fmt"
    "../proto"
    "io"
    "net"
)

func process(conn net.Conn) {
    defer conn.Close()
    reader := bufio.NewReader(conn)
    for {
        msg, err := proto.Decode(reader)
        if err == io.EOF {
            return
        }
        if err != nil {
            fmt.Println("decode failed, err = ", err)
            return
        }
        fmt.Println("收到數據", msg)
    }
}

func main() {
    fmt.Println("服務器開始監聽......")
    listen, err := net.Listen("tcp", "127.0.0.1:8888")
    if err != nil {
        fmt.Println("listen err=", err)
        return
    }
    fmt.Printf("listen suc=%v\n", listen)

    // 延遲關閉
    defer  listen.Close()

    // 循環等待客戶端鏈接
    for {
        fmt.Println("循環等待客戶端鏈接...")
        conn, err := listen.Accept()
        if err != nil {
            fmt.Println("Accept() err=", err)
        } else {
            fmt.Printf("Accept() suc=%v, 客戶端ip=%v\n", conn, conn.RemoteAddr().String())
        }
        // 建立goroutine處理客戶端鏈接
        go process(conn)
    }
}

測試結果以下:tcp

收到client發來的數據: hello, server!
收到client發來的數據: hello, server!
收到client發來的數據: hello, server!
收到client發來的數據: hello, server!
收到client發來的數據: hello, server!
收到client發來的數據: hello, server!
收到client發來的數據: hello, server!
收到client發來的數據: hello, server!
收到client發來的數據: hello, server!
收到client發來的數據: hello, server!

能夠看到,此時服務器接收的數據已經沒有了粘包。函數

參考文獻

  1. https://www.liwenzhou.com/pos...
  2. https://www.cnblogs.com/yinbi...
  3. https://www.cnblogs.com/steve...

旅程到此就圓滿結束了~~~

我是lioney,年輕的後端攻城獅一枚,愛鑽研,愛技術,愛分享。
我的筆記,整理不易,感謝閱讀、點贊和收藏。
文章有任何問題歡迎你們指出,也歡迎你們一塊兒交流後端各類問題!
相關文章
相關標籤/搜索