6行代码快速解决golang TCP粘包问题
程序员文章站
2022-04-22 08:52:55
前言
什么是tcp粘包问题以及为什么会产生tcp粘包,本文不加讨论。本文使用golang的bufio.scanner来实现自定义协议解包。
下面话不多说了,来一起看看详...
前言
什么是tcp粘包问题以及为什么会产生tcp粘包,本文不加讨论。本文使用golang的bufio.scanner
来实现自定义协议解包。
下面话不多说了,来一起看看详细的介绍吧。
协议数据包定义
本文模拟一个日志服务器,该服务器接收客户端传到的数据包并显示出来
type package struct { version [2]byte // 协议版本,暂定v1 length int16 // 数据部分长度 timestamp int64 // 时间戳 hostnamelength int16 // 主机名长度 hostname []byte // 主机名 taglength int16 // 标签长度 tag []byte // 标签 msg []byte // 日志数据 }
协议定义部分没有什么好讲的,根据具体的业务逻辑定义即可。
数据打包
由于tcp协议是语言无关的协议,所以直接把协议数据包结构体发送到tcp连接中也是不可能的,只能发送字节流数据,所以需要自己实现数据编码。所幸golang提供了binary来帮助我们实现网络字节编码。
func (p *package) pack(writer io.writer) error { var err error 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) return err }
pack方法的输出目标为io.writer,有利于接口扩展,只要实现了该接口即可编码数据写入。binary.bigendian是字节序,本文暂时不讨论,有需要的读者可以自行查找资料研究。
数据解包
解包需要将tcp数据包解析到结构体中,接下来会讲为什么需要添加几个数据无关的长度字段。
func (p *package) unpack(reader io.reader) error { var err error 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) return err }
由于主机名、标签这种数据是不固定长度的,所以需要两个字节来标识数据长度,否则读取的时候只知道一个总的数据长度是无法区分主机名、标签名、日志数据的。
数据包的粘包问题解决
上文只是解决了编码/解码问题,前提是收到的数据包没有产生粘包问题,解决粘包就是要正确分割字节流中的数据。一般有以下做法:
- 定长分隔(每个数据包最大为该长度) 缺点是数据不足时会浪费传输资源
- 特定字符分隔(如rn) 缺点是如果正文中有rn就会导致问题
- 在数据包中添加长度字段(本文采用的)
golang提供了bufio.scanner
来解决粘包问题。
scanner := bufio.newscanner(reader) // reader为实现了io.reader接口的对象,如net.conn scanner.split(func(data []byte, ateof bool) (advance int, token []byte, err error) { if !ateof && data[0] == 'v' { // 由于我们定义的数据包头最开始为两个字节的版本号,所以只有以v开头的数据包才处理 if len(data) > 4 { // 如果收到的数据>4个字节(2字节版本号+2字节数据包长度) length := int16(0) binary.read(bytes.newreader(data[2:4]), binary.bigendian, &length) // 读取数据包第3-4字节(int16)=>数据部分长度 if int(length)+4 <= len(data) { // 如果读取到的数据正文长度+2字节版本号+2字节数据长度不超过读到的数据(实际上就是成功完整的解析出了一个包) return int(length) + 4, data[:int(length)+4], nil } } } return }) // 打印接收到的数据包 for scanner.scan() { scannedpack := new(package) scannedpack.unpack(bytes.newreader(scanner.bytes())) log.println(scannedpack) }
本文的核心就在于scanner.split
方法,该方法用来解析tcp数据包
完整源码
package main import ( "bufio" "bytes" "encoding/binary" "fmt" "io" "log" "os" "time" ) type package struct { version [2]byte // 协议版本 length int16 // 数据部分长度 timestamp int64 // 时间戳 hostnamelength int16 // 主机名长度 hostname []byte // 主机名 taglength int16 // tag长度 tag []byte // tag msg []byte // 数据部分长度 } func (p *package) pack(writer io.writer) error { var err error 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) return err } func (p *package) unpack(reader io.reader) error { var err error 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) return err } func (p *package) string() string { return fmt.sprintf("version:%s length:%d timestamp:%d hostname:%s tag:%s msg:%s", p.version, p.length, p.timestamp, p.hostname, p.tag, p.msg, ) } func main() { hostname, err := os.hostname() if err != 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-02 15: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, ateof bool) (advance int, token []byte, err error) { if !ateof && data[0] == 'v' { if len(data) > 4 { length := int16(0) binary.read(bytes.newreader(data[2:4]), binary.bigendian, &length) if int(length)+4 <= len(data) { return int(length) + 4, data[:int(length)+4], nil } } } return }) for scanner.scan() { scannedpack := new(package) scannedpack.unpack(bytes.newreader(scanner.bytes())) log.println(scannedpack) } if err := scanner.err(); err != nil { log.fatal("无效数据包") } }
写在最后
golang作为一门强大的网络编程语言,实现自定义协议是非常重要的,实际上实现自定义协议也不是很难,以下几个步骤:
- 数据包编码
- 数据包解码
- 处理tcp粘包问题
- 断线重连(可以使用心跳实现)(非必须)
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。