什么是TCP粘包问题以及为什么会产生TCP粘包,本文不加讨论。本文使用golang的bufio.Scanner来实现自定义协议解包。

协议数据包定义

本文模拟一个日志服务器,该服务器接收客户端传到的数据包并显示出来

type Package struct {Version        [2]byte // 协议版本,暂定V1Length         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 errorerr = 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 errorerr = 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
}

由于主机名、标签这种数据是不固定长度的,所以需要两个字节来标识数据长度,否则读取的时候只知道一个总的数据长度是无法区分主机名、标签名、日志数据的。

数据包的粘包问题解决

上文只是解决了编码/解码问题,前提是收到的数据包没有产生粘包问题,解决粘包就是要正确分割字节流中的数据。一般有以下做法:

  1. 定长分隔(每个数据包最大为该长度) 缺点是数据不足时会浪费传输资源
  2. 特定字符分隔(如rn) 缺点是如果正文中有rn就会导致问题
  3. 在数据包中添加长度字段(本文采用的)

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 mainimport ("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  // TagMsg            []byte  // 数据部分长度
}func (p *Package) Pack(writer io.Writer) error {var err errorerr = 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 errorerr = 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)// scannerscanner := 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作为一门强大的网络编程语言,实现自定义协议是非常重要的,实际上实现自定义协议也不是很难,以下几个步骤:

  1. 数据包编码
  2. 数据包解码
  3. 处理TCP粘包问题
  4. 断线重连(可以使用心跳实现)(非必须)

本文引用自我自己的博客golang解决TCP粘包问题

6行代码解决golang TCP粘包相关推荐

  1. golang解决TCP粘包问题

    6行代码解决golang TCP粘包 转自:https://studygolang.com/articles/12483 什么是TCP粘包问题以及为什么会产生TCP粘包,本文不加讨论.本文使用gola ...

  2. Netty解决TCP粘包/拆包导致的半包读写问题

    一.TCP粘包/拆包问题说明 TCP是个"流"协议,就是没有界限的一串数据.TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包拆分,所以在业务上认为,一 ...

  3. Netty(二)——TCP粘包/拆包

    转载请注明出处:http://www.cnblogs.com/Joanna-Yan/p/7814644.html 前面讲到:Netty(一)--Netty入门程序 主要内容: TCP粘包/拆包的基础知 ...

  4. TCP——粘包/拆包

    TCP粘包/拆包 TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,它们是连成一片的,其间并没有分界线.TCP底层并不了解上层业务数据的具体含义,它会根 ...

  5. TCP粘包/拆包问题

    目录 TCP粘包/拆包 TCP粘包/拆包问题说明 TCP粘包/拆包发生的原因 粘包问题的解决策略 未考虑TCP粘包导致功能异常案例  TimeServer的改造 TimeClient的改造 利用Lin ...

  6. Netty权威指南(四)TCP粘包/拆包问题

    TCP粘包/拆包问题解决之道 上一章 一.介绍 1.1 TCP粘包/拆包问题说明 1.2 TCP粘包/拆包发生的原因 1.3 粘包问题的解决策略 二.未考虑TCP粘包导致的功能异常案例 2.1 Tim ...

  7. TCP 粘包和拆包及解决方案

    TCP 粘包和拆包基本介绍 1.TCP 是面向连接的,面向流的,提供高可靠性服务.收发两端(客户端和服务器端)都要有一一成对的 socket,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使 ...

  8. golang 解决 TCP 粘包问题

    什么是 TCP 粘包问题以及为什么会产生 TCP 粘包,本文不加讨论.本文使用 golang 的 bufio.Scanner 来实现自定义协议解包. 协议数据包定义 本文模拟一个日志服务器,该服务器接 ...

  9. 【Netty】入门Netty官方例子解析(三)处理一个基于流的传输 TCP粘包和拆包问题分析和解决

    关于 Socket Buffer的一个小警告 基于流的传输比如 TCP/IP, 接收到数据是存在 socket 接收的 buffer 中.不幸的是,基于流的传输并不是一个数据包队列,而是一个字节队列. ...

最新文章

  1. ElasticSearch知识汇总
  2. 小波的秘密10_小波包的数学支撑
  3. 李幸原:看好实时音视频在教育与医疗的前景
  4. java访问远程共享文件
  5. spring的父子容器
  6. python mysql实例_Python 操作MySQL详解及实例
  7. error HLP: Help compilation failed with code 1
  8. 怎样用springboot开发cs_springboot 系列 springboot 初探
  9. PDFCreator(pdf转换器电脑版免费版)官方繁体中文版V4.3.0 | PDF生成器下载 | pdf转换器哪个好用?
  10. html页面小宠物代码大全,宠物店网页设计html代码
  11. Leach协议的实现
  12. 中文标注工具brat的使用
  13. krpano相关笔记
  14. 引用Microsoft.Office.Interop.Excel的解决方法
  15. 面向产品的新一代端到端唤醒框架 wekws 正式发布
  16. 小熊错误_坚守好股票、寻找穿越牛熊十倍股:小熊电器、贝达药业、开立医疗!...
  17. docxtpl 学习笔记
  18. 小程序中使用web-view链接H5网页
  19. iMatrix平台核心功能—Portal 介绍
  20. 【英语学习】【WOTD】prescind 释义/词源/示例

热门文章

  1. 网页css样式中英对照,css中文样式(含中英文对照表).doc
  2. php 怎么打印条形码,php – 如何在垂直标签中垂直打印zpl条形码
  3. thinkphp通过模型查询mysql_thinkPHP视图模型详解,把mysql表关联简单化!
  4. java斗破苍穹游戏阵容,斗破苍穹手游竞技场阵容搭配解析 最强阵容你知道吗
  5. springboot 引入jdbc驱动_Spring Boot:企业常用的 Starter以及实现
  6. python装饰器原理wraps(method)(self)_python装饰器中@wraps作用--修复被装饰后的函数名等属性的改变...
  7. CreateInstall5.7.2 帮助文档翻译
  8. 随机数排列JAVA_随机数生成器,按排序顺序
  9. 全球农业资源利用与粮食安全研讨会 国际农民丰收节贸易会
  10. What are current fashion trends in Sydney?