一、粘包是什么
两个程序能够互相通信是采用了套接字(socket)技术,socket在发送端和接收端都有个缓存机制,发送端在把需要发送的数据先放在缓存上,等数据超过缓存大小时,就会打包发给接收端;接收端接到数据也会先放到缓存,再根据应用程序(recv/read)去读取这些数据,直到读完缓存上的数据。
TCP是一个流协议,TCP只保证把要发送的数据按包序号完整的发送给接收端,接收端读取数据的时候会按应用程序上设置好的大小去读取,假如接收端每次按照300字节大小去读,实际业务需要数据600个字节,那么接收端读取一次是没有把应用需要的数据读取完整,而需读取两次;再假如实际业务需要数据100个字节,接收端按照300字节大小去读取,会把业务上分为两个含义数据一次就读取出来了,这就产生了粘包的问题,所以粘包问题的本质就是数据读取边界错误所致。
二、字节序
字节序是在内存中字节存储顺序,分为大端序(网络字节序)和小端序(主机字节序);
高位字节:在ASCII上开始的字节,如0x00
低位字节:在ASCII上最后的字节,如0x7F
大端序:高位字节存入低地址;
小端序:低位字节存入低地址;
三、解决方案
上面所说的应用数据,是在TCP数据包里面的,应用数据的数据格式由用户自己协定,比如http协议,有头部和包体,头部有设定数据长度,包体则是实际数据 。
那么解决粘包问题思路:
1,设置头部信息,长度固定;
2,使用特定字符作为分隔符,用来分割数据边界问题;
3,添加除头部信息的数据长度。
下面用golang代码结合上面3点思路解决粘包问题:
发送端封装数据 client.go
package main
import (
"encoding/binary"
"fmt"
"net"
)
func main() {
//定义头部信息固定长度为10,分隔符两个字符:0x43, 0x53
headData := []byte{0x43, 0x53, 0, 0, 0, 0, 0, 0, 0, 0}
//实际数据
context := "测试内容"
//实际数据长度用4个字节,小端序方式来储存
binary.LittleEndian.PutUint32(headData[2:6], uint32(len(context)))
//数据包(头部信息+实际数据)
data := make([]byte, 0)
data = append(data, headData...)
data = append(data, context...)
fmt.Println(data) //输出 [67 83 12 0 0 0 0 0 0 0 230 181 139 232 175 149 229 134 133 229 174 185]
//67 83 12 0 0 0 0 0 0 0 为头部信息;230 181 139 232 175 149 229 134 133 229 174 185 为实际数据
conn, err := net.Dial("tcp", "192.168.3.116:9010")
if err != nil {
fmt.Println("connect err=", err)
return
}
_, err = conn.Write(data)
if err != nil {
fmt.Println("send data failed, err= ", err)
return
}
}
接收端拆分数据 server.go
package main
import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"fmt"
"net"
)
func main() {
fmt.Println("服务器开始监听......")
listen, err := net.Listen("tcp", "192.168.3.116:9010")
if err != nil {
fmt.Println("listen err=", err)
return
}
// 延迟关闭
defer listen.Close()
// 循环等待客户端连接
for {
fmt.Println("循环等待客户端连接...")
conn, err := listen.Accept()
if err != nil {
fmt.Println("Accept() err=", err)
}
// 创建goroutine处理客户端连接
go process(conn)
}
}
//处理客户端连接
func process(conn net.Conn) {
defer func() {
fmt.Println("conn.Close")
conn.Close()
}()
reader := bufio.NewReader(conn)
for {
msg, err := Decode(reader)
if err != nil {
fmt.Println("decode failed, err = ", err)
return
}
fmt.Println("收到数据", msg)
}
}
func Decode(reader *bufio.Reader) (string, error) {
var err error
// 读取前10个字节的数据(表示数据包头部的信息)
// peek操作只读数据,但不会移动数据位置!(没有数据则阻塞)
headData, err := reader.Peek(10)
fmt.Println(headData)//[67 83 12 0 0 0 0 0 0 0]
if err != nil {
return "", err
}
//验证头部分割符
if headData[0] != 0x43 || headData[1] != 0x53 {
err = errors.New(fmt.Sprintf("headData[0]: %d,headData[1]: %d", headData[0], headData[1]))
return "", err
}
headLen := len(headData)
//验证头部长度
if headLen < 10 {
err = errors.New(fmt.Sprintf("headData len :%d", headLen))
return "", err
}
//获取实际数据长度byte
lengthByte := headData[2:6]//[12 0 0 0]
lengthBuf := bytes.NewBuffer(lengthByte)
var dataLen int32
//小端序方式转换实际数据长度数字
err = binary.Read(lengthBuf, binary.LittleEndian, &dataLen)
if err != nil {
return "", err
}
//数据包长度(头部信息长度+实际数据长度)
packLen := 10 + dataLen
pack := make([]byte, packLen)
// 判断缓存数据是否满足一个完整数据包
buffLen := int32(reader.Buffered())
if buffLen < packLen {
cacheBuff := make([]byte, 0)
dataBuff := make([]byte, buffLen, buffLen)
for {
n, err := reader.Read(dataBuff)
cacheBuff = append(cacheBuff, dataBuff[0:n]...)
if err == nil {
if int32(len(cacheBuff)) < packLen {
num := packLen - int32(len(cacheBuff))
dataBuff = make([]byte, num, num)
continue
}
} else {
return "", err
}
pack = cacheBuff
break
}
} else {
// 读取整个数据包
_, err = reader.Read(pack)
if err != nil {
return "", err
}
}
// str, _ := DecodeData(pack[10:])
str := pack[10:]
fmt.Println(string(str)//输出:测试内容
return string(str), nil
}