go tcp粘包问题处理

 : jank    :   : 256    : 2017-09-13 20:00  go

一、TCP/UDP

1.TCP是个"流"协议,所谓流,就是没有界限的一串数据.就像河里的流水,绵延不断,没有分界。但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包.由于TCP"流"的特性以及网络状况,在进行数据传输时会出现以下几种情况.
假设我们连续调用两次send分别发送两段数据data1和data2,在接收端有以下几种接收情况(当然不止这几种情况,这里只列出了有代表性的情况).
A.先接收到data1,然后接收到data2.
B.先接收到data1的部分数据,然后接收到data1余下的部分以及data2的全部.
C.先接收到了data1的全部数据和data2的部分数据,然后接收到了data2的余下的数据.
D.一次性接收到了data1和data2的全部数据.

对于A这种情况正是我们需要的,而对于B,C,D的情况就是大家经常说的"粘包",就需要我们把接收到的数据进行拆包,拆成一个个独立的数据包.为了拆包就必须在发送端进行封包.

2.UDP是个"数据包"协议,也就是两段数据间是有界限的,在接收端要么接收不到数据要么就是接收一个完整的一段数据,不会少接收也不会多接收。

二、TCP粘包处理

1.封包

封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(以后讲过滤非法包时封包会加入"包尾"内容).包头其实上是个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其他的结构体成员可根据需要自己定义.根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。

golang tcp封包处理如下:

func WriteTCP(operation byte, w []byte, conn *net.TCPConn) (err error) {
var ( //operation 包头信息,w包主体信息,
length = len(string(w))
buf    = make([]byte, length+5)
)
buf[0] = operation  
buf[1] = byte(uint32(length))
buf[2] = byte(uint32(length) >> 8)
buf[3] = byte(uint32(length) >> 16)
buf[4] = byte(uint32(length) >> 24)
copy(buf[5:], w)
log.Debug("len buf: ", len(buf))
if _, err = conn.Write(buf); err != nil {
log.Debug("conn.Write(%s), failed(%s)", string(buf), err)
return
}
return
}

2.解包

 对于拆包目前我最常用的是以下两种方式.
    1.动态缓冲区暂存方式.之所以说缓冲区是动态的是因为当需要缓冲的数据长度超出缓冲区的长度时会增大缓冲区长度.
    大概过程描述如下:
    A,为每一个连接动态分配一个缓冲区,同时把此缓冲区和SOCKET关联,常用的是通过结构体关联.
    B,当接收到数据时首先把此段数据存放在缓冲区中.
    C,判断缓存区中的数据长度是否够一个包头的长度,如不够,则不进行拆包操作.
    D,根据包头数据解析出里面代表包体长度的变量.
    E,判断缓存区中除包头外的数据长度是否够一个包体的长度,如不够,则不进行拆包操作.
    F,取出整个数据包.这里的"取"的意思是不光从缓冲区中拷贝出数据包,而且要把此数据包从缓存区中删除掉.删除的办法就是把此包后面的数据移动到缓冲区的起始地址.

这种方法有两个缺点.1.为每个连接动态分配一个缓冲区增大了内存的使用.2.有三个地方需要拷贝数据,一个地方是把数据存放在缓冲区,一个地方是把完整的数据包从缓冲区取出来,一个地方是把数据包从缓冲区中删除.第二种拆包的方法会解决和完善这些缺点.

前面提到过这种方法的缺点.下面给出一个改进办法, 即采用环形缓冲.但是这种改进方法还是不能解决第一个缺点以及第一个数据拷贝,只能解决第三个地方的数据拷贝(这个地方是拷贝数据最多的地方).第2种拆包方式会解决这两个问题.
环形缓冲实现方案是定义两个指针,分别指向有效数据的头和尾.在存放数据和删除数据时只是进行头尾指针的移动.

2.利用底层的缓冲区来进行拆包
由于TCP也维护了一个缓冲区,所以我们完全可以利用TCP的缓冲区来缓存我们的数据,这样一来就不需要为每一个连接分配一个缓冲区了.另一方面我们知道recv或者wsarecv都有一个参数,用来表示我们要接收多长长度的数据.利用这两个条件我们就可以对第一种方法进行优化.
     对于阻塞SOCKET来说,我们可以利用一个循环来接收包头长度的数据,然后解析出代表包体长度的那个变量,再用一个循环来接收包体长度的数据.

golang tcp解包处理如下:
func handleClient(conn net.Conn) {
    defer conn.Close()
    for {
        var (
            buf [502]byte
        )
        _, err := conn.Read(buf[0:5])
        if err != nil {
            return
        }
        lens := int(uint32(buf[1]) | uint32(buf[2])<<8 | uint32(buf[3])<<16 | uint32(buf[4])<<24)
        fmt.Println("lens:", lens)
        from := conn.RemoteAddr()
        fmt.Println("from: ", from)
        switch buf[0] {
        case proto.HEART_BEAT_PACKET:
            _, err = conn.Read(buf[0:lens])
            if err != nil {
                log.Error("conn.Read() failed(%s)", err)
                continue
            }
            fmt.Println("buf:  ", string(buf[0:lens]))
    
        default:
            log.Error("no data!!")
        }
    }
    return
}



   

备案编号:赣ICP备15011386号

联系方式:qq:1150662577    邮箱:1150662577@qq.com