1、TCP粘包问题演示

服务端参考:TCP编程示例 保持不变

客户端如下:

func main() {
    conn, err := net.Dial("tcp", "127.0.0.1:6666")
    if err != nil {
        fmt.Println("dial failed, err", err)
        return
    }
    defer conn.Close()
    for i := 0; i < 20; i++ {
        msg := `Hello, Hello. How are you?`
        conn.Write([]byte(msg))
    }
}

将上面的代码保存后,分别编译。先启动服务端再启动客户端,可以看到服务端输出结果如下:

客户端分20次发送的数据,在服务端并没有成功的输出20次,而是多条数据“粘”到了一起。

2、为什么会出现粘包?

主要原因就是tcp数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发。

“粘包”可发生在发送端也可发生在接收端:

  • 1、由Nagle算法造成的发送端的粘包: Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。

  • 2、接收端接收不及时造成的接收端粘包: TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。

3、解决办法

出现 ”粘包” 的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。

封包:
封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。

包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。

我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。

// 大小端字节序的差别:
//   大端字节序是低地址存高位数据,小端字节序是低地址存低位数据

// Encode 将消息编码
func Encode(message string) ([]byte, error) {
    // 计算消息体的字节长度,转换为 int32 类型(占4个字节)
    length := int32(len(message))
    var pkg = new(bytes.Buffer)

    // 写入消息头(网络传输默认使用的是大端字节序,这里使用大端字节序)
    err := binary.Write(pkg, binary.BigEndian, length)
    if err != nil {
        return nil, err
    }

    // 写入消息实体(网络传输默认使用的是大端字节序,这里使用大端字节序)
    err = binary.Write(pkg, binary.BigEndian, []byte(message))
    if err != nil {
        return nil, err
    }
    return pkg.Bytes(), nil
}

// Decode 将消息解码
func Decode(reader *bufio.Reader) (string, error) {

    // 读取消息的长度,读取前4个字节的数据
    lengthByte, _ := reader.Peek(4)
    lengthBuff := bytes.NewBuffer(lengthByte)

    // 定义长度
    var length int32
    err := binary.Read(lengthBuff, binary.BigEndian, &length)
    if err != nil {
        return "", err
    }

    // Buffered返回缓冲中现有的可读取的字节数。
    if int32(reader.Buffered()) < length+4 {
        return "", err
    }

    // 读取真正的消息数据
    pack := make([]byte, int(4+length))
    _, err = reader.Read(pack)
    if err != nil {
        return "", err
    }
    return string(pack[4:]), nil
}

服务端修改如下:

func main(){
    // 开启一个 tcp 监听
    listen,err := net.Listen("tcp", ":6666")
    if err != nil {
        fmt.Println(err)
        return
    }

    for {
        // 建立连接,等待并将下一个连接返回给侦听器
        conn,err := listen.Accept() 
        if err != nil {
            fmt.Println("accept failed, err:", err)
            continue
        }
        // 启动一个goroutine处理连接
        //go handleConnection(conn)
        go handleConnection2(conn)
    }
}

// 处理连接信息(带解码防粘包的方式)
func handleConnection2(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("read from client failed, err:\n", err)
            break
        }
        fmt.Printf("收到client端发来的数据:%s\n", msg)
    }
}

客户端修改如下:

// 带编码防粘包的方式
func main() {
    conn, err := net.Dial("tcp", "127.0.0.1:6666")
    if err != nil {
        fmt.Println("dial failed, err", err)
        return
    }
    defer conn.Close()
    for i := 0; i < 20; i++ {
        msg := fmt.Sprintf("Hello, Hello. How are you?{%d}", i)
        data,err := proto.Encode(msg)
        if err != nil {
            fmt.Println("encode msg failed, err:", err)
            return
        }

        // 发送数据
        _,err = conn.Write(data)
        if err != nil {
            fmt.Println("发送数据失败,Err:", err.Error())
            return
        }
    }
}

作者:joker.liu  创建时间:2023-04-17 14:40
最后编辑:joker.liu  更新时间:2023-04-23 14:42