什么是粘包
用代码展示粘包现象
server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| package main
import ( "bufio" "fmt" "net" )
func main() { listen, err := net.Listen("tcp", ":8080") if err != nil { fmt.Println("listened failed, err:", err) }
for { conn, err := listen.Accept() if err != nil { fmt.Println("accept failed, err:", err) } go process(conn) } }
func process(conn net.Conn) { defer conn.Close() for { reader := bufio.NewReader(conn) var buf [128]byte n, err := reader.Read(buf[:]) if err != nil { fmt.Println("read from conn err:", err) break } recvStr := string(buf[:n]) fmt.Println("从client收到消息", recvStr) } }
|
client.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package main
import ( "fmt" "net" )
func main() { conn, err := net.Dial("tcp", ":8080") if err != nil { fmt.Println("connect to server err:", err) return } defer conn.Close()
for i := 0; i < 20; i++ { msg := `hello, server` conn.Write([]byte(msg)) } }
|
当client连续向server端连续发送20个数据包的时候,我们看server端打印的内容。
1 2 3 4 5 6
| 从client收到消息 hello, server 从client收到消息 hello, serverhello, serverhello, serverhello, server 从client收到消息 hello, serverhello, server 从client收到消息 hello, serverhello, serverhello, serverhello, serverhello, serverhello, server 从client收到消息 hello, serverhello, serverhello, serverhello, serverhello, serverhello, serverhello, server read from conn err: EOF
|
按照我们预想的,server端应该受到20条消息,每一条消息只包含hello,server
才对。然而server却收到了不到20条消息,而且消息的长短不一。这就是粘包现象。
为什么会出现粘包
主要原因是tcp数据传递的模式是流模式,在保持长连接的时候可以进行多次收和发。
粘包可能发生在发送端也可能发生在接收端:
- 由Nagle算法造成的发送端粘包:Nagle算法是一种改善网络传输效率的算法。简单的来说就是当我们提交一段数据给TCP发送的时候,TCP并不会立即的发送这段数据,而是等一小段时间看看在等待的时间内是否还有其他的数据要发送,若有则一次性把两段数据发送出去。
- 接收端接收不及时造成的粘包:接收端TCP会把收到的数据写入一个缓冲区,然后通知应用层取数据。当应用层由于某些原因不能及时的把数据取走,就会造成TCP缓冲区堆积,存放了几段数据包,造成粘包现象。
如何解决粘包
出现粘包的关键在于接收方不能够确定将要接收的数据包的大小,因此我们需要手动对数据进行封包和拆包操作。
封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两个部分内容了(过滤非法包时封包还会加入包尾)。包头部分的长度是固定的,并且它存储了包体的长度。根据包头的长度固定以及包头中所包含的包体的长度就能够正确的实现拆分出一个完整的数据包。
怎么去封包、解包呢?
我们可以自己定义一种协议规定,比如数据包的前几个字节为包头,里面存储的是发送的数据的长度。
proto/tcp_stick_proto.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| package proto
import ( "bufio" "bytes" "encoding/binary" )
func Encode(message string) ([]byte, error) { length := int32(len(message)) packet := new(bytes.Buffer) err := binary.Write(packet, binary.LittleEndian, length) if err != nil { return nil, err }
err = binary.Write(packet, binary.LittleEndian, []byte(message)) if err != nil { return nil, err } return packet.Bytes(), nil }
func Decode(reader *bufio.Reader) (string, error) { lengthByte, err := reader.Peek(4) if err != nil { return "", err } lengthBuff := bytes.NewBuffer(lengthByte)
var length int32 err = binary.Read(lengthBuff, binary.LittleEndian, &length) if err != nil { return "", err }
if int32(reader.Buffered()) < length+4 { return "", err }
packet := make([]byte, int32(4+length)) _, err = reader.Read(packet) if err != nil { return "", err } return string(packet[4:]), nil }
|
server.go 修改如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| package main
import ( "bufio" "fmt" "github.com/QXQZX/LearnGo/demo/tcp/proto" "net" )
func main() { listen, err := net.Listen("tcp", ":8080") if err != nil { fmt.Println("listened failed, err:", err) }
for { conn, err := listen.Accept() if err != nil { fmt.Println("accept failed, err:", err) } go process(conn) } }
func process(conn net.Conn) { defer conn.Close() reader := bufio.NewReader(conn) for { recvStr, err := proto.Decode(reader) if err != nil { fmt.Println("read from conn err:", err) break } fmt.Println("从client收到消息", recvStr) } }
|
client.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package main
import ( "fmt" "github.com/QXQZX/LearnGo/demo/tcp/proto" "net" "time" )
func main() { conn, err := net.Dial("tcp", ":8080") if err != nil { fmt.Println("connect to server err:", err) return } defer conn.Close()
for i := 0; i < 20; i++ { msg := `hello, server` encode, err := proto.Encode(msg) if err != nil { panic(err) } conn.Write(encode) } time.Sleep(2 * time.Second) }
|
运行,问题解决
为什么UDP不存在粘包问题
UDP不存在粘包问题,是由于UDP发送的时候,没有经过Negal算法优化,不会将多个小包合并一次发送出去。另外,在UDP协议的接收端,采用了链式结构来记录每一个到达的UDP包,这样接收端应用程序一次recv只能从socket接收缓冲区中读出一个数据包。也就是说,发送端send了几次,接收端必须recv几次(无论recv时指定了多大的缓冲区)