什么是 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
}

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

数据包的粘包问题解决

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

  1. 定长分隔(每个数据包最大为该长度) 缺点是数据不足时会浪费传输资源

  2. 特定字符分隔(如\r\n) 缺点是如果正文中有\r\n就会导致问题

  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 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作为一门强大的网络编程语言,实现自定义协议是非常重要的,实际上实现自定义协议也不是很难,以下几个步骤:

  1. 数据包编码

  2. 数据包解码

  3. 处理TCP粘包问题

  4. 断线重连(可以使用心跳实现)(非必须)

阅读原文

golang 解决 TCP 粘包问题的更多相关文章

  1. python套接字解决tcp粘包问题

    python套接字解决tcp粘包问题 目录 什么是粘包 演示粘包现象 解决粘包 实际应用 什么是粘包 首先只有tcp有粘包现象,udp没有粘包 socket收发消息的原理 发送端可以是一K一K地发送数 ...

  2. Netty使用LineBasedFrameDecoder解决TCP粘包/拆包

    TCP粘包/拆包 TCP是个”流”协议,所谓流,就是没有界限的一串数据.TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TC ...

  3. 深入学习Netty(5)——Netty是如何解决TCP粘包/拆包问题的?

    前言 学习Netty避免不了要去了解TCP粘包/拆包问题,熟悉各个编解码器是如何解决TCP粘包/拆包问题的,同时需要知道TCP粘包/拆包问题是怎么产生的. 在此博文前,可以先学习了解前几篇博文: 深入 ...

  4. netty 解决TCP粘包与拆包问题(二)

    TCP以流的方式进行数据传输,上层应用协议为了对消息的区分,采用了以下几种方法. 1.消息固定长度 2.第一篇讲的回车换行符形式 3.以特殊字符作为消息结束符的形式 4.通过消息头中定义长度字段来标识 ...

  5. netty 解决TCP粘包与拆包问题(一)

    1.什么是TCP粘包与拆包 首先TCP是一个"流"协议,犹如河中水一样连成一片,没有严格的分界线.当我们在发送数据的时候就会出现多发送与少发送问题,也就是TCP粘包与拆包.得不到我 ...

  6. 1. Netty解决Tcp粘包拆包

    一. TCP粘包问题 实际发送的消息, 可能会被TCP拆分成很多数据包发送, 也可能把很多消息组合成一个数据包发送 粘包拆包发生的原因 (1) 应用程序一次写的字节大小超过socket发送缓冲区大小 ...

  7. c#解决TCP“粘包”问题

    一:TCP粘包产生的原理 1,TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾.出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能 ...

  8. 【转】Netty之解决TCP粘包拆包(自定义协议)

    1.什么是粘包/拆包 一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据.TCP通讯为何存在粘包呢?主要原因是TCP是以流的方式来处理数据,再加上网络上MTU的往往小于在应用处理的消 ...

  9. Netty之解决TCP粘包拆包(自定义协议)

    1.什么是粘包/拆包 一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据.TCP通讯为何存在粘包呢?主要原因是TCP是以流的方式来处理数据,再加上网络上MTU的往往小于在应用处理的消 ...

随机推荐

  1. Memcacher win7 安装测试

    1.下载memcache 的windows 稳定版,解压放某个盘下面,比如在H:/wamp/www/php api/memcache: 2.在终端(即cmd 命令界面)下,输入安装命令 :H:/wam ...

  2. 【转】TestNG 与 Junit的比较

    转自 http://www.blogjava.net/fanscial/archive/2005/12/14/23780.html 1.         JDK 5 Annotations (JDK ...

  3. CAP理论中, P(partition tolerance, 分区容错性)的合理解释

    在CAP理论中, 对partition tolerance分区容错性的解释一般指的是分布式网络中部分网络不可用时, 系统依然正常对外提供服务, 而传统的系统设计中往往将这个放在最后一位. 这篇文章对这 ...

  4. 《数字图像处理原理与实践(MATLAB版)》一书之代码Part5

    <数字图像处理原理与实践(MATLAB版)>一书之代码Part5 本文系<数字图像处理原理与实践(MATLAB版)>一书之代码系列的Part5.辑录该书第225至第280页之代 ...

  5. Android Intent之Action应用

    Log.i("txrjsms", "whereDoYouJumpFrom:"+getIntent().getPackage()); 结果是null Log.i( ...

  6. android使用GestureDetector实现手势下滑关闭页面的效果。

    实现类似Android风云直播手机端注册登录页,当手势向下滑动的时候,关闭页面的效果. 使用GestureDetector来实现这个效果,当手势在屏幕上面滑动的时候 ,会掉用onFling方法,所以, ...

  7. CentOS安装Mysql5.6并配置远程连接

    使用的是阿里云提供的CentOS7.3版本,安装的是mysql5.6.40版本.此次安装采用解压压缩包并配置的方式. 1. 卸载自带的Mariadb Centos7将默认数据库mysql替换成了Mar ...

  8. (原)SphereFace及其pytorch代码

    转载请注明出处: http://www.cnblogs.com/darkknightzh/p/8524937.html 论文: SphereFace: Deep Hypersphere Embeddi ...

  9. 开始 App前 需要考虑的几项

    来源:Limboy's HQ http://t.cn/R5sEDMJ 随着工具链的完善,语言的升级以及各种优质教程的涌现,做一个 App 的成本也越来越低了.尽管如此,有些事情最好前期就做起来,避免当 ...

  10. 指尖下的js —— 多触式web前端开发之三:处理复杂手势(转)

    这篇文章着重介绍多触式设备上特有的gesture event(android和iOS对这个事件的封装大同小异).这个事件是对touch event的更高层的封装,和touch一样,它同样包括gestu ...