28

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

 5 years ago
source link: https://studygolang.com/articles/16300?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Tcp/Udp介绍

Tcp是字节流协议, 数据传输像流水一样没有边界, 那么对等方在一次数据读取后,无法分辨读取是一个消息还是多个,

或者是不足一个, 那么对等方拿到"残缺"消息就不知道如何处理.

Udp是基于消息的传输服务,每个消息就是一个报文,是有边界的,对等方每次接收都是一个完整的消息.

这样就需要我们在应用层,

自己来区分.

粘包是如何出现的?

  • 用户进程write消息, 但内核缓存区不足以容乃这个完整的消息, 一个消息分多次发送出去, 接收的时候就可能一个消息分多次接收
  • Tcp的报文段有大小限制(MSS)
  • IP层最大传输单元(MTU), 会对包进行分片,
  • 其他, Tcp流量控制, 拥塞控制

一般有三种常见的方式

1. 定长消息

发送端和接收端约定消息长度, 缺点: 消息很短时, 效率很低, 浪费带宽

2. 特殊标志作为结束标志

ftp协议就是这种方式, 缺点: 消息内容不能含有这种特殊标志, 会提前终止消息。 redis是如何解决类似的问题的呢, redis自定义

了动态字符串, 里面提到是二进制安全的, 意思就是字符串里面可以含有空字符(assic码为0), 原因就是它记录了这个字符串的长度,

其实也就是下面说的第三种方式

3. 定长的包头 + 变长的包体, 包头中写入包体的长度, 本文主要介绍这种方式:

niYjIb2.png!web

每次都要尽可能的去读数据, 读到之后分析:

先取包头, 在包头里分析出包体的长度, 如果包头都不够, 要继续读数据拼接在已有的数据后面, 继续分析包体的长度, 拿到包体的长度就从包头结束的问题截取包体, 依次递归, 直到对等方关闭

代码

// 读取消息, 可导出的方法
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
        }
    }
}
// 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
}
// 检查应用层缓存区是否包含完整的消息, 不可导出
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
}

演示

go get github.com/weiwenwang/DiyProtocol

cd $GOPATH/github.com/weiwenwang/DiyProtocol/example/server/

go run server.go

EVFVjui.png!web

cd $GOPATH/github.com/weiwenwang/DiyProtocol/example/client/

go run client.go

632mYvB.png!web

详情

源码请移步: github , 本人附上一个demo, 代码注释详细.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK