前言
什么是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 }
由于主机名、标签这种数据是不固定长度的,所以需要两个字节来标识数据长度,否则读取的时候只知道一个总的数据长度是无法区分主机名、标签名、日志数据的。
数据包的粘包问题解决
上文只是解决了编码/解码问题,前提是收到的数据包没有产生粘包问题,解决粘包就是要正确分割字节流中的数据。一般有以下做法:
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作为一门强大的网络编程语言,实现自定义协议是非常重要的,实际上实现自定义协议也不是很难,以下几个步骤:
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对小牛知识库的支持。
据我所知,直线的意思是,那个变量运动得到乘以向量inputVec的x部分的绝对值,但我不明白接下来会发生什么。
本文向大家介绍pycharm 快速解决python代码冲突的问题,包括了pycharm 快速解决python代码冲突的问题的使用技巧和注意事项,需要的朋友参考一下 找到冲突的文件(项目中报红的就是冲突文件),如下 :以下是一个标准的冲突表 说明 * : <<<<<<< HEAD 到 =======里面的内容是自己分支commit的内容 =========到 >>>>>>里面的内容是远程下拉的 根据
本文向大家介绍快速解决mysql导出scv文件乱码、蹿行的问题,包括了快速解决mysql导出scv文件乱码、蹿行的问题的使用技巧和注意事项,需要的朋友参考一下 工作原因,常常不能实现完全的线上化(即,所有数据都在线上完成,不需要导入导出),而导出Excel常常比修炼成仙还慢,因此,我们将数据库文件导出到本地使用的时候,常常使用的方法的是导成CSV格式。 而csv格式的也常常出现导出的中文乱码,或者
本文向大家介绍golang网络socket粘包问题的解决方法,包括了golang网络socket粘包问题的解决方法的使用技巧和注意事项,需要的朋友参考一下 本文实例讲述了golang网络socket粘包问题的解决方法。分享给大家供大家参考,具体如下: 看到很多人问这个问题, 今天就写了个例子, 希望能帮助大家 首先说一下什么是粘包:百度上比较通俗的说法是指TCP协议中,发送方发送的若干包数据到接收
本文向大家介绍快速解决PostgreSQL中的Permission denied问题,包括了快速解决PostgreSQL中的Permission denied问题的使用技巧和注意事项,需要的朋友参考一下 想开始学习SQL和Excel那本书,觉得自己亲手去输入才是正道。发现程序后续会用到窗口函数,可是我的mysql没有窗口函数,这本书所提供的数据脚本分别是MS SQL Sever和PostreSQL
本文向大家介绍快速解决Canvas.toDataURL 图片跨域的问题,包括了快速解决Canvas.toDataURL 图片跨域的问题的使用技巧和注意事项,需要的朋友参考一下 如题,在将页面的图片地址进行本地输出时(Html2Canvas.js),因不同源存在跨域问题,会出现toDataURL访问权限问题: 【Redirect at origin 'http://sub1.xx.com' has