自定义协议/解决tcp粘包问题(golang版本)

weiwenwang · · 1921 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

### Tcp/Udp介绍 Tcp是字节流协议, 数据传输像流水一样没有边界, 那么对等方在一次数据读取后,无法分辨读取是一个消息还是多个, 或者是不足一个, 那么对等方拿到"残缺"消息就不知道如何处理. Udp是基于消息的传输服务,每个消息就是一个报文,是有边界的,对等方每次接收都是一个完整的消息. 这样就需要我们在应用层, 自己来区分. ### 粘包是如何出现的? - 用户进程write消息, 但内核缓存区不足以容乃这个完整的消息, 一个消息分多次发送出去, 接收的时候就可能一个消息分多次接收 - Tcp的报文段有大小限制(MSS) - IP层最大传输单元(MTU), 会对包进行分片, - 其他, Tcp流量控制, 拥塞控制 一般有三种常见的方式 #### 1. 定长消息 发送端和接收端约定消息长度, 缺点: 消息很短时, 效率很低, 浪费带宽 #### 2. 特殊标志作为结束标志 ftp协议就是这种方式, 缺点: 消息内容不能含有这种特殊标志, 会提前终止消息。 redis是如何解决类似的问题的呢, redis自定义 了动态字符串, 里面提到是二进制安全的, 意思就是字符串里面可以含有空字符(assic码为0), 原因就是它记录了这个字符串的长度, 其实也就是下面说的第三种方式 #### 3. 定长的包头 + 变长的包体, 包头中写入包体的长度, 本文主要介绍这种方式: ![tcp字节流](https://i.loli.net/2018/11/15/5bed7f16bfa3c.png) 每次都要尽可能的去读数据, 读到之后分析: 先取包头, 在包头里分析出包体的长度, 如果包头都不够, 要继续读数据拼接在已有的数据后面, 继续分析包体的长度, 拿到包体的长度就从包头结束的问题截取包体, 依次递归, 直到对等方关闭 ### 代码 ```golang // 读取消息, 可导出的方法 func (buffer *Buffer) Read(msg chan string) (error) { for { buffer.grow() // 移动数据 _, err := buffer.readFromReader() // 读数据拼接到定额缓存后面 if err != nil { fmt.Println(err) return err } // 检查定额缓存里面的数据有几个消息(可能不到1个,可能连一个消息头都不够,可能有几个完整消息+一个消息的部分) isBreak := buffer.checkMsg(msg) // 只要读到有完整的消息, isBreak就为true, 跳出去处理 if (isBreak) { return nil } } } ``` ```golang // grow 将有用的字节前移, 不可导出 func (b *Buffer) grow() { if b.start == 0 { return } copy(b.buf, b.buf[b.start:b.end]) b.end -= b.start b.start = 0 } ``` ```golang // 检查应用层缓存区是否包含完整的消息, 不可导出 func (buffer *Buffer) checkMsg(msg chan string) (bool) { var isBreak bool HEADER_LENG := HEAD_SIZE + len(buffer.header) headBuf, err1 := buffer.seek(HEADER_LENG) if err1 != nil { // 一个消息头都不够, 跳出去继续读吧 return false } if (string(headBuf[:len(buffer.header)]) == buffer.header) { // 判断消息头正确性 } else { } contentSize := int(binary.BigEndian.Uint16(headBuf[len(buffer.header):])) if (buffer.len() >= contentSize-HEADER_LENG) { // 一个消息体也是够的 contentBuf := buffer.read(HEADER_LENG, contentSize) // 把消息读出来,把start往后移 msg <- string(contentBuf) // 递归,看剩下的还够一个消息不 isBreak = true buffer.checkMsg(msg) } else { // 一个消息体不够的, 跳出去继续读吧 isBreak = false } return isBreak } ``` ### 演示 ```golang go get github.com/weiwenwang/DiyProtocol cd $GOPATH/github.com/weiwenwang/DiyProtocol/example/server/ go run server.go ``` [![server](https://i.loli.net/2018/11/16/5bee907d09557.png)](https://asciinema.org/a/KLuq19K1Fm0rHkZMy1XZsU3mh) ```golang cd $GOPATH/github.com/weiwenwang/DiyProtocol/example/client/ go run client.go ``` [![client](https://i.loli.net/2018/11/16/5bee907d292ad.png)](https://asciinema.org/a/MFLd5a8t1ULrDnvj1m624DAaS) ### 详情 源码请移步: [github](https://github.com/weiwenwang/DiyProtocol), 本人附上一个demo, 代码注释详细.

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1921 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传