转自:https://studygolang.com/articles/12483

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

协议数据包定义

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

type Package struct {
Version []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---p.HostnameLength--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[] == 'V' { // 由于我们定义的数据包头最开始为两个字节的版本号,所以只有以V开头的数据包才处理
if len(data) > { // 如果收到的数据>4个字节(2字节版本号+2字节数据包长度)
length := int16()
binary.Read(bytes.NewReader(data[:]), binary.BigEndian, &length) // 读取数据包第3-4字节(int16)=>数据部分长度
if int(length)+ <= len(data) { // 如果读取到的数据正文长度+2字节版本号+2字节数据长度不超过读到的数据(实际上就是成功完整的解析出了一个包)
return int(length) + , data[:int(length)+], 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 []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---p.HostnameLength--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: []byte{'V', ''},
Timestamp: time.Now().Unix(),
HostnameLength: int16(len(hostname)),
Hostname: []byte(hostname),
TagLength: ,
Tag: []byte("demo"),
Msg: []byte(("现在时间是:" + time.Now().Format("2006-01-02 15:04:05"))),
}
pack.Length = + + pack.HostnameLength + + 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[] == 'V' {
if len(data) > {
length := int16()
binary.Read(bytes.NewReader(data[:]), binary.BigEndian, &length)
if int(length)+ <= len(data) {
return int(length) + , data[:int(length)+], 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粘包处理(转)

    在用golang开发人工客服系统的时候碰到了粘包问题,那么什么是粘包呢?例如我们和客户端约定数据交互格式是一个json格式的字符串: {"Id":1,"Name" ...

  2. 【GoLang】golang TCP 粘包处理 示例

    参考资料: http://www.01happy.com/golang-tcp-socket-adhere/

  3. TCP粘包/拆包问题

    无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑TCP底层的粘包/拆包机制. TCP粘包/拆包 TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河 ...

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

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

  5. TCP粘包/拆包 ByteBuf和channel 如果没有Netty? 传统的多线程服务器,这个也是Apache处理请求的模式

    通俗地讲,Netty 能做什么? - 知乎 https://www.zhihu.com/question/24322387 谢邀.netty是一套在java NIO的基础上封装的便于用户开发网络应用程 ...

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

    无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑TCP底层的粘包/拆包机制. TCP粘包/拆包 TCP是个“流”协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片 ...

  7. TCP粘"包"问题浅析及解决方案Golang代码实现

    一.粘"包"问题简介 在socket网络编程中,都是端到端通信,客户端端口+客户端IP+服务端端口+服务端IP+传输协议就组成一个可以唯一可以明确的标识一条连接.在TCP的sock ...

  8. golang 解决 TCP 粘包问题

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

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

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

随机推荐

  1. xencenter如何安装Centos7虚拟机系统

    xencenter的ip地址192.168.245.134(win10系统) 首先我们在win10系统安装好xencenter(这个软件可以直接在xenserver启动后,通过访问xenserver的 ...

  2. Android USB gadget configfs学习笔记总结

    1.一个config_item 是通过显式用户空间mkdir操作创建的,通过rmdir销毁.属性(文件)在mkdir之后出现,可以通过read和write读取或修改属性文件.与sysfs一样,read ...

  3. sqler sql 转rest api 源码解析(二) resp 协议

    resp 协议主要是方便使用redis 客户端进行连接,resp 主要是依赖 tidwall/redcon golang redis 协议包 resp 服务说明 server_resp.go 文件,干 ...

  4. NSNull floatValue intValue 找不到指定方法解决方式

    最近遇到一个问题:         因为后台人员对于接口数据没有做空值处理.导致client接收到的有些数据为空(NSNull),而针对此类数据恰好client的存储结构为int和float类型.类型 ...

  5. [转]Java NIO通俗易懂简明教程

    Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API.本系列教程将有助于你学习和理解Java NIO. Java NIO提供了与 ...

  6. mysql之 OPTIMIZE TABLE整理碎片

    来看看手册中关于 OPTIMIZE 的描述: OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ... 如果您已经删除 ...

  7. GoJS 在 vue 项目中的使用

    GoJS 在 html vue 项目中的使用,github地址:https://github.com/cag2050/gojs_demo GoJS 在 vue-cli 2.x 项目中的使用,githu ...

  8. php+js实现重定向跳转并post传参

    页面重定向跳转并post传参 $mdata=json_encode($mdata);//如果是字符串无需使用json echo " <form style='display:none; ...

  9. Scrapy 设置请求头

    爬虫的过程有些网站设置反盗链,需要我们在请求头中添加下,修改settings.py文件中添加 DEFAULT_REQUEST_HEADERS = { 'Accept': 'text/html,appl ...

  10. 配置中心Server端

    为什么需要统一配置中心 1.不方便维护.一个功能被多个人开发,如果其中一个人修改了配置文件,另外一个人测试之前的功能,准备使用之前的配置. 2.配置内容安全与权限.线上的配置是不会对开发公开,特别是数 ...