gpt4 book ai didi

6行代码快速解决golang TCP粘包问题

转载 作者:qq735679552 更新时间:2022-09-29 22:32:09 26 4
gpt4 key购买 nike

CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.

这篇CFSDN的博客文章6行代码快速解决golang TCP粘包问题由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.

前言 。

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

下面话不多说了,来一起看看详细的介绍吧.

协议数据包定义 。

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

?
1
2
3
4
5
6
7
8
9
10
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来帮助我们实现网络字节编码.

?
1
2
3
4
5
6
7
8
9
10
11
12
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数据包解析到结构体中,接下来会讲为什么需要添加几个数据无关的长度字段.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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来解决粘包问题.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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数据包 。

完整源码 。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
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粘包问题
  • 断线重连(可以使用心跳实现)(非必须)

总结 。

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我的支持.

原文链接:https://segmentfault.com/a/1190000013493942 。

最后此篇关于6行代码快速解决golang TCP粘包问题的文章就讲到这里了,如果你想了解更多关于6行代码快速解决golang TCP粘包问题的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com