6行代码快速解决golang TCP粘包问题
前言
什么是TCP粘包问题以及为什么会产生TCP粘包,本文不加讨论。本文使用golang的bufio.Scanner来实现自定义协议解包。
下面话不多说了,来一起看看详细的介绍吧。
协议数据包定义
本文模拟一个日志服务器,该服务器接收客户端传到的数据包并显示出来
typePackagestruct{ Version[2]byte//协议版本,暂定V1 Lengthint16//数据部分长度 Timestampint64//时间戳 HostnameLengthint16//主机名长度 Hostname[]byte//主机名 TagLengthint16//标签长度 Tag[]byte//标签 Msg[]byte//日志数据 }
协议定义部分没有什么好讲的,根据具体的业务逻辑定义即可。
数据打包
由于TCP协议是语言无关的协议,所以直接把协议数据包结构体发送到TCP连接中也是不可能的,只能发送字节流数据,所以需要自己实现数据编码。所幸golang提供了binary来帮助我们实现网络字节编码。
func(p*Package)Pack(writerio.Writer)error{ varerrerror err=binary.Write(writer,binary.BigEndian,&p.Version) err=binary.Write(writer,binary.BigEndian,&p.Length) err=binary.Write(writer,binary.BigEndian,&p.Timestamp) err=binary.Write(writer,binary.BigEndian,&p.HostnameLength) err=binary.Write(writer,binary.BigEndian,&p.Hostname) err=binary.Write(writer,binary.BigEndian,&p.TagLength) err=binary.Write(writer,binary.BigEndian,&p.Tag) err=binary.Write(writer,binary.BigEndian,&p.Msg) returnerr }
Pack方法的输出目标为io.Writer,有利于接口扩展,只要实现了该接口即可编码数据写入。binary.BigEndian是字节序,本文暂时不讨论,有需要的读者可以自行查找资料研究。
数据解包
解包需要将TCP数据包解析到结构体中,接下来会讲为什么需要添加几个数据无关的长度字段。
func(p*Package)Unpack(readerio.Reader)error{ varerrerror err=binary.Read(reader,binary.BigEndian,&p.Version) err=binary.Read(reader,binary.BigEndian,&p.Length) err=binary.Read(reader,binary.BigEndian,&p.Timestamp) err=binary.Read(reader,binary.BigEndian,&p.HostnameLength) p.Hostname=make([]byte,p.HostnameLength) err=binary.Read(reader,binary.BigEndian,&p.Hostname) err=binary.Read(reader,binary.BigEndian,&p.TagLength) p.Tag=make([]byte,p.TagLength) err=binary.Read(reader,binary.BigEndian,&p.Tag) p.Msg=make([]byte,p.Length-8-2-p.HostnameLength-2-p.TagLength) err=binary.Read(reader,binary.BigEndian,&p.Msg) returnerr }
由于主机名、标签这种数据是不固定长度的,所以需要两个字节来标识数据长度,否则读取的时候只知道一个总的数据长度是无法区分主机名、标签名、日志数据的。
数据包的粘包问题解决
上文只是解决了编码/解码问题,前提是收到的数据包没有产生粘包问题,解决粘包就是要正确分割字节流中的数据。一般有以下做法:
- 定长分隔(每个数据包最大为该长度)缺点是数据不足时会浪费传输资源
- 特定字符分隔(如rn)缺点是如果正文中有rn就会导致问题
- 在数据包中添加长度字段(本文采用的)
golang提供了bufio.Scanner来解决粘包问题。
scanner:=bufio.NewScanner(reader)//reader为实现了io.Reader接口的对象,如net.Conn scanner.Split(func(data[]byte,atEOFbool)(advanceint,token[]byte,errerror){ if!atEOF&&data[0]=='V'{//由于我们定义的数据包头最开始为两个字节的版本号,所以只有以V开头的数据包才处理 iflen(data)>4{//如果收到的数据>4个字节(2字节版本号+2字节数据包长度) length:=int16(0) binary.Read(bytes.NewReader(data[2:4]),binary.BigEndian,&length)//读取数据包第3-4字节(int16)=>数据部分长度 ifint(length)+4<=len(data){//如果读取到的数据正文长度+2字节版本号+2字节数据长度不超过读到的数据(实际上就是成功完整的解析出了一个包) returnint(length)+4,data[:int(length)+4],nil } } } return }) //打印接收到的数据包 forscanner.Scan(){ scannedPack:=new(Package) scannedPack.Unpack(bytes.NewReader(scanner.Bytes())) log.Println(scannedPack) }
本文的核心就在于scanner.Split方法,该方法用来解析TCP数据包
完整源码
packagemain import( "bufio" "bytes" "encoding/binary" "fmt" "io" "log" "os" "time" ) typePackagestruct{ Version[2]byte//协议版本 Lengthint16//数据部分长度 Timestampint64//时间戳 HostnameLengthint16//主机名长度 Hostname[]byte//主机名 TagLengthint16//Tag长度 Tag[]byte//Tag Msg[]byte//数据部分长度 } func(p*Package)Pack(writerio.Writer)error{ varerrerror err=binary.Write(writer,binary.BigEndian,&p.Version) err=binary.Write(writer,binary.BigEndian,&p.Length) err=binary.Write(writer,binary.BigEndian,&p.Timestamp) err=binary.Write(writer,binary.BigEndian,&p.HostnameLength) err=binary.Write(writer,binary.BigEndian,&p.Hostname) err=binary.Write(writer,binary.BigEndian,&p.TagLength) err=binary.Write(writer,binary.BigEndian,&p.Tag) err=binary.Write(writer,binary.BigEndian,&p.Msg) returnerr } func(p*Package)Unpack(readerio.Reader)error{ varerrerror err=binary.Read(reader,binary.BigEndian,&p.Version) err=binary.Read(reader,binary.BigEndian,&p.Length) err=binary.Read(reader,binary.BigEndian,&p.Timestamp) err=binary.Read(reader,binary.BigEndian,&p.HostnameLength) p.Hostname=make([]byte,p.HostnameLength) err=binary.Read(reader,binary.BigEndian,&p.Hostname) err=binary.Read(reader,binary.BigEndian,&p.TagLength) p.Tag=make([]byte,p.TagLength) err=binary.Read(reader,binary.BigEndian,&p.Tag) p.Msg=make([]byte,p.Length-8-2-p.HostnameLength-2-p.TagLength) err=binary.Read(reader,binary.BigEndian,&p.Msg) returnerr } func(p*Package)String()string{ returnfmt.Sprintf("version:%slength:%dtimestamp:%dhostname:%stag:%smsg:%s", p.Version, p.Length, p.Timestamp, p.Hostname, p.Tag, p.Msg, ) } funcmain(){ hostname,err:=os.Hostname() iferr!=nil{ log.Fatal(err) } pack:=&Package{ Version:[2]byte{'V','1'}, Timestamp:time.Now().Unix(), HostnameLength:int16(len(hostname)), Hostname:[]byte(hostname), TagLength:4, Tag:[]byte("demo"), Msg:[]byte(("现在时间是:"+time.Now().Format("2006-01-0215:04:05"))), } pack.Length=8+2+pack.HostnameLength+2+pack.TagLength+int16(len(pack.Msg)) buf:=new(bytes.Buffer) //写入四次,模拟TCP粘包效果 pack.Pack(buf) pack.Pack(buf) pack.Pack(buf) pack.Pack(buf) //scanner scanner:=bufio.NewScanner(buf) scanner.Split(func(data[]byte,atEOFbool)(advanceint,token[]byte,errerror){ if!atEOF&&data[0]=='V'{ iflen(data)>4{ length:=int16(0) binary.Read(bytes.NewReader(data[2:4]),binary.BigEndian,&length) ifint(length)+4<=len(data){ returnint(length)+4,data[:int(length)+4],nil } } } return }) forscanner.Scan(){ scannedPack:=new(Package) scannedPack.Unpack(bytes.NewReader(scanner.Bytes())) log.Println(scannedPack) } iferr:=scanner.Err();err!=nil{ log.Fatal("无效数据包") } }
写在最后
golang作为一门强大的网络编程语言,实现自定义协议是非常重要的,实际上实现自定义协议也不是很难,以下几个步骤:
- 数据包编码
- 数据包解码
- 处理TCP粘包问题
- 断线重连(可以使用心跳实现)(非必须)
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。