Golang 處理TCP分包、合包——固定包頭+包體方式

TCP分包、合包的場景


TCP通訊是流式的,在發送一個大數據包時,可能會被拆分紅多個數據包進行發送,同時,屢次發送數據包,也可能會被底層合併成一個數據包進行發送。git

  1. 分包:接收一個數據包,須要對數據包進行拆分;
  2. 合包:接收多個數據包,須要對數據包進行合併;

所以,TCP通訊時須要設定通訊協議來正確處理收到的數據,如咱們常見的HTTP、FTP協議等。github

固定包頭+包體方式

在該協議下,一個數據包老是有一個定長的包頭加一個包體構成,其中包頭中會有一個字段說明包體或者整個包的長度。服務器收到數據後就能夠按序解析出包頭 > 包體/包長度 > 包體。服務器

程序默認使用包頭最後一個字節描述包體長度,下面來看看核心代碼實現:app

package tcp_package

import (
    "encoding/binary"
    "fmt"
    "net"
)

type Reader struct {
    Conn         net.Conn
    Buff         []byte      //數據接收緩衝區
    Start        int         //數據讀取開始位置
    End          int         //數據讀取結束位置
    BuffLen      int         //數據接收緩衝區大小
    HeaderLen    int         //包頭長度
    LengthOffset int         //指示包體長度的字段在包頭中的位置(2字節)
    Message      chan string //單次接收的完整數據
}

func NewReader(conn net.Conn, maxBufferSize, headerLen, lengthOffset int) (*Reader, error) {
    if lengthOffset+2 > headerLen {
        return nil, fmt.Errorf("incorrect 'headerLen' or 'lengthOffset'")
    }
    return &Reader{
        Conn:         conn,
        Buff:         make([]byte, maxBufferSize),
        Start:        0,
        End:          0,
        BuffLen:      maxBufferSize,
        HeaderLen:    headerLen,
        LengthOffset: lengthOffset,
        Message:      make(chan string, 10),
    }, nil
}

func (r *Reader) Do() (err error) {
    defer close(r.Message)
    err = r.read()
    if err != nil {
        return fmt.Errorf("read data error:%v", err)
    }
    return
}

//讀取tcp數據流
func (r *Reader) read() error {
    for {
        r.move()
        if r.End == r.BuffLen {
            //緩衝區的寬度容納不了一條消息的長度
            return fmt.Errorf("message is too large:%v", r)
        }
        length, err := r.Conn.Read(r.Buff[r.End:])
        if err != nil {
            return err
        }
        r.End += length
        r.readFromBuff()
    }
}

//前移上一次未處理完的數據
func (r *Reader) move() {
    if r.Start == 0 {
        return
    }
    copy(r.Buff, r.Buff[r.Start:r.End])
    r.End -= r.Start
    r.Start = 0
}

//讀取buff中的單條數據
func (r *Reader) readFromBuff() {
    if r.End-r.Start < r.HeaderLen {
        //包頭的長度不夠,繼續接收
        return
    }
    //讀取包頭數據
    headerData := r.Buff[r.Start:(r.Start + r.HeaderLen)]

    //讀取包體的長度(2字節)
    bodyLen := binary.BigEndian.Uint16(headerData[r.LengthOffset : r.LengthOffset+2])
    if r.End-r.Start-r.HeaderLen < int(bodyLen) {
        //包體的長度不夠,繼續接收
        return
    }

    //讀取包體數據
    bodyData := r.Buff[(r.Start + r.HeaderLen) : r.Start+r.HeaderLen+int(bodyLen)]
    //把完整的數據包用通道傳遞出去
    r.Message <- string(append(headerData, bodyData...))
    //每讀完一次數據 start 後移
    r.Start += r.HeaderLen + int(headerData[r.HeaderLen-1])
    r.readFromBuff()
}

完整項目放在Github上,歡迎給Star~tcp

相關文章
相關標籤/搜索