Golang通过包长协议处理TCP粘包的问题解决
tcp粘包产生的原因这里就不说了,因为大家能搜索TCP粘包的处理方法,想必大概对TCP粘包有了一定了解,所以我们直接从处理思路开始讲起
tcp粘包现象代码重现
首先,我们来重现一下TCP粘包,然后再此基础之上解决粘包的问题,这里给出了client和server的示例代码如下
package main
import (
"fmt"
"net"
)
func main() {
conn, err := net.Dial("tcp", ":9000")
if err != nil {
return
}
defer conn.Close()
for {
s := "Hello, Server!"
n, err := conn.Write([]byte(s))
if err != nil {
fmt.Println("Error:", err)
fmt.Println("Error N:", n)
return
}
// 这里通过限制发送频率和时间间隔来解决TCP粘包
// 虽然能够实现,但是频率被限制,效率也会被限制
// time.Sleep(time.Second * 1)
}
}
package main
import (
"fmt"
"net"
)
func main() {
ln, err := net.Listen("tcp", ":9000")
if err != nil {
return
}
for {
conn, err := ln.Accept()
if err != nil {
continue
}
Go handleConnection(conn)
}
}
func handleConnection(conn net.Conn) {
defer conn.Close()
tmp := []byte{}
for {
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
fmt.Println("Read Error:", err)
fmt.Println("Read N:", n)
return
}
fmt.Println(string(buf))
}
}
按顺序启动server.go和client.go,正常情况下每行会输出Hello, World!
字样,出现TCP粘包后,将会出现类似Hello, World!Hello
之类的字样,后一个包粘到前一个包了
解决TCP粘包有很多种方法,归结起来就是通过自定义通讯协议来解决,例如分隔符协议、MQTT协议、包长协议等等,而我们这里介绍的就是通过包长协议来解决问题的,当然包长协议也有很多种自定义的方法
通过演示的结果,我们可以看出来,后一个包粘到了前一个包,而且后一个包不一定是一个完整的包,也很有可能第一次收到的数据包也不是完整的数据包
tcp粘包问题处理方法
这样我们就有必要校验每次收到的数据包是否是我们期望收到的,比较直观的,客户端和服务端双方协商某种协议,例如包长协议,在客户端发送数据时,先计算一下数据的长度(假设用2字节的uint16表示),然后将计算得到的长度和实际的数据组装成一个包,最后发送给服务端;而服务端接收到数据时,先读取2字节的数据长度信息(可能不足2字节,程序需要针对这种情况设计),然后根据数据长度来读取后边的数据(可能会存在数据过剩、数据刚好、数据不足等情况,程序需要针对这些情况设计)
有了思路之后,我们就需要对发送端和接收端的数据进行处理了,因为发送端较为简单,不需要考虑其他情况,只管封装数据包发送,所以这里我们先对发送端client进行处理
package main
import (
"encoding/binary"
"fmt"
"net"
)
func main() {
conn, err := net.Dial("tcp", ":9000")
if err != nil {
return
}
defer conn.Close()
for {
s := "Hello, Server!"
sbytes := make([]byte, 2+len(s))
binary.BigEndian.PutUint16(sbytes, uint16(len(s)))
copy(sbytes[2:], []byte(s))
n, err := conn.Write(sbytes)
if err != nil {
fmt.Println("Error:", err)
fmt.Println("Error N:", n)
return
}
// time.Sleep(time.Second * 1)
}
}
按照我们的思路,首先使用len()
函数计算出待发送字符串的长度,然后使用make()
函数创建一个[]byte切片作为待组装发送的数据包缓存sbyte,长度就是2字节的包头+字符串的长度
,接着通过binary.BigEndian.PutUint16()
函数来对数据包缓存sbyte进行操作,将字符串的长度信息写入2字节的包头中,紧接着又通过copy()
完成封包组装,最后通过conn.Write()
将封包发送出去,这样子发送出去的数据大概长成下面的样子
[0][14][H][e][l][l][o][,][ ][S][e][r][v][e][r][!]
其中,封包整体长16bytes,Hello, Server!
则长14bytes
好了,至此数据将会循环不简短的发送给服务端,接下来我们就要对服务端server.go进行处理了,先上代码
package main
import (
"encoding/binary"
"fmt"
"net"
)
func main() {
ln, err := net.Listen("tcp", ":9000")
if err != nil {
return
}
for {
conn, err := ln.Accept()
if err != nil {
continue
}
go handleConnection(conn)
}
}
func handleConnection(conn net.Conn) {
defer conn.Close()
tmp := []byte{}
for {
buf := make([]byte, 1024)
// fmt.Println("len:", len(buf), " cap:", cap(buf))
n, err := conn.Read(buf)
if err != nil {
if e, ok := err.(*net.OpError); ok {
fmt.Println(e.Source, e.Addr, e.net, e.Op, e.Err)
if e.Timeout() {
fmt.Println("Timeout Error")
}
}
fmt.Println("Read Error:", err)
fmt.Println("Read N:", n)
return
}
if n == 0 {
fmt.Println("Read N:", n)
return
}
tmp = append(tmp, buf[:n]...)
length := len(tmp)
if length < 2 {
continue
}
if length >= 2 {
head := make([]byte, 2)
copy(head, tmp[:2])
dataLength := binary.BigEndian.Uint16(head)
data := make([]byte, dataLength)
copy(data, tmp[2:dataLength+2])
fmt.Println(string(data)) // 得到数据
if uint16(length) == 2+dataLength {
tmp = []byte{}
} else if uint16(length) > 2+dataLength {
tmp = tmp[dataLength+2:]
}
}
// fmt.Println(string(buf))
}
}
ps:这里的示例代码不能直接用于生产环境,只是提供tcp粘包处理的思路过程,代码还是存在一些问题的,例如server.go服务端还没有对第3种情况进行处理,封包总长度大于2字节,数据长度信息大于封包数据实际长度,表示数据包不完整,需要等到下一次读取再拼接起来
相关文章