Golang通过包长协议处理TCP粘包的问题解决
tcp粘包产生的原因这里就不说了,因为大家能搜索TCP粘包的处理方法,想必大概对TCP粘包有了一定了解,所以我们直接从处理思路开始讲起
tcp粘包现象代码重现
首先,我们来重现一下TCP粘包,然后再此基础之上解决粘包的问题,这里给出了client和server的示例代码如下
/* 文件名:client.go client客户端的示例代码(未处理粘包问题) 通过无限循环无时间间隔发送数据给server服务器 server将会不间断的出现TCP粘包问题 */ 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) } }
/* 文件名:server.go server服务端的示例代码(未处理粘包问题) 服务端接收到数据后立即打印 此时将会不间断的出现TCP粘包问题 */ 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进行处理
/* 文件名:client.go 使用包长协议,封装TCP包并循环发送给server服务端 */ 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进行处理了,先上代码
/* 文件名:server.go 使用包长协议,处理接收到的封包数据 收到的封包数据,可能存在几种情况: 1、封包总长度不足2字节(这种情况不能完整获取包头),缓存起来与下次获取的数据拼接 2、封包总长度刚好2字节,数据长度信息读出来是0,这种情况可以正常处理并清空缓存 3、封包总长度大于2字节,数据长度信息大于封包数据实际长度,表示数据包不完整,需要等到下一次读取再拼接起来 4、封包总长度大于2字节,数据长度信息等于封包数据实际长度,这种情况(理想情况)可以正常处理并清空缓存 5、封包总长度大于2字节,数据长度信息小于封包实际长度,表示数据包发生TCP粘包了,读取实际数据后,将剩余部分缓存起来等待下次拼接 PS:这里只总结出了这几种情况,其他未发现的情况还需另外处理 */ 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字节,数据长度信息大于封包数据实际长度,表示数据包不完整,需要等到下一次读取再拼接起来
到此这篇关于Golang通过包长协议处理TCP粘包的问题解决的文章就介绍到这了,更多相关Golang TCP粘包内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!