2

TCP分包、合包的场景


TCP通信是流式的,在发送一个大数据包时,可能会被拆分成多个数据包进行发送,同时,多次发送数据包,也可能会被底层合并成一个数据包进行发送。

  1. 分包:接收一个数据包,需要对数据包进行拆分;
  2. 合包:接收多个数据包,需要对数据包进行合并;

因此,TCP通信时需要设定通信协议来正确处理收到的数据,如我们常见的HTTP、FTP协议等。

固定包头+包体方式

在该协议下,一个数据包总是有一个定长的包头加一个包体构成,其中包头中会有一个字段说明包体或者整个包的长度。服务器收到数据后就可以按序解析出包头 > 包体/包长度 > 包体。

程序默认使用包头最后一个字节描述包体长度,下面来看看核心代码实现:

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~


match
20 声望2 粉丝

不会弹吉他的程序员不是好厨子