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,年轻的后端攻城狮一枚,爱钻研,爱技术,爱分享。
我的笔记,整理不易,感谢阅读、点赞和收藏。
文章有任何问题欢迎你们指出,也欢迎你们一块儿交流后端各类问题!
相关文章
相关标签/搜索