Golang通过包长协议处理TCP粘包的问题解决

tcp粘包产生的原因这里就不说了,因为大家能搜索TCP粘包的处理方法,想必大概对TCP粘包有了一定了解,所以我们直接从处理思路开始讲起

tcp粘包现象代码重现

首先,我们来重现一下TCP粘包,然后再此基础之上解决粘包的问题,这里给出了client和server的示例代码如下

/*
    文件名:client.go
    client客户端的示例代码(未处理粘包问题)
    通过无限循环无时间间隔发送数据给server服务器
    server将会不间断的出现TCP粘包问题
*/
package main
import (
    "fmt"
    "net"
)
func main() {
    conn, err := net.Dial("tcp", ":9000")
    if err != nil {
        return
    }
    defer conn.Close()
    for {
        s := "Hello, Server!"
        n, err := conn.Write([]byte(s))
        if err != nil {
            fmt.Println("Error:", err)
            fmt.Println("Error N:", n)
            return
        }
        // 这里通过限制发送频率和时间间隔来解决TCP粘包
        // 虽然能够实现,但是频率被限制,效率也会被限制
        // time.Sleep(time.Second * 1)
    }
}
/*
    文件名:server.go
    server服务端的示例代码(未处理粘包问题)
    服务端接收到数据后立即打印
    此时将会不间断的出现TCP粘包问题
*/
package main
import (
    "fmt"
    "net"
)
func main() {
    ln, err := net.Listen("tcp", ":9000")
    if err != nil {
        return
    }
    for {
        conn, err := ln.Accept()
        if err != nil {
            continue
        }
        go handleConnection(conn)
    }
}
func handleConnection(conn net.Conn) {
    defer conn.Close()
    tmp := []byte{}
    for {
        buf := make([]byte, 1024)
        n, err := conn.Read(buf)
        if err != nil {
            fmt.Println("Read Error:", err)
            fmt.Println("Read N:", n)
            return
        }
        fmt.Println(string(buf))
    }
}

按顺序启动server.go和client.go,正常情况下每行会输出Hello, World!字样,出现TCP粘包后,将会出现类似Hello, World!Hello之类的字样,后一个包粘到前一个包了

解决TCP粘包有很多种方法,归结起来就是通过自定义通讯协议来解决,例如分隔符协议、MQTT协议、包长协议等等,而我们这里介绍的就是通过包长协议来解决问题的,当然包长协议也有很多种自定义的方法

通过演示的结果,我们可以看出来,后一个包粘到了前一个包,而且后一个包不一定是一个完整的包,也很有可能第一次收到的数据包也不是完整的数据包

tcp粘包问题处理方法

这样我们就有必要校验每次收到的数据包是否是我们期望收到的,比较直观的,客户端和服务端双方协商某种协议,例如包长协议,在客户端发送数据时,先计算一下数据的长度(假设用2字节的uint16表示),然后将计算得到的长度和实际的数据组装成一个包,最后发送给服务端;而服务端接收到数据时,先读取2字节的数据长度信息(可能不足2字节,程序需要针对这种情况设计),然后根据数据长度来读取后边的数据(可能会存在数据过剩、数据刚好、数据不足等情况,程序需要针对这些情况设计)

有了思路之后,我们就需要对发送端和接收端的数据进行处理了,因为发送端较为简单,不需要考虑其他情况,只管封装数据包发送,所以这里我们先对发送端client进行处理

/*
    文件名:client.go
    使用包长协议,封装TCP包并循环发送给server服务端
*/
package main
import (
    "encoding/binary"
    "fmt"
    "net"
)
func main() {
    conn, err := net.Dial("tcp", ":9000")
    if err != nil {
        return
    }
    defer conn.Close()
    for {
        s := "Hello, Server!"
        sbytes := make([]byte, 2+len(s))
        binary.BigEndian.PutUint16(sbytes, uint16(len(s)))
        copy(sbytes[2:], []byte(s))
        n, err := conn.Write(sbytes)
        if err != nil {
            fmt.Println("Error:", err)
            fmt.Println("Error N:", n)
            return
        }
        // time.Sleep(time.Second * 1)
    }
}

按照我们的思路,首先使用len()函数计算出待发送字符串的长度,然后使用make()函数创建一个[]byte切片作为待组装发送的数据包缓存sbyte,长度就是2字节的包头+字符串的长度,接着通过binary.BigEndian.PutUint16()函数来对数据包缓存sbyte进行操作,将字符串的长度信息写入2字节的包头中,紧接着又通过copy()完成封包组装,最后通过conn.Write()将封包发送出去,这样子发送出去的数据大概长成下面的样子

[0][14][H][e][l][l][o][,][ ][S][e][r][v][e][r][!]

其中,封包整体长16bytes,Hello, Server!则长14bytes

好了,至此数据将会循环不简短的发送给服务端,接下来我们就要对服务端server.go进行处理了,先上代码

/*
    文件名:server.go
    使用包长协议,处理接收到的封包数据
    收到的封包数据,可能存在几种情况:
    1、封包总长度不足2字节(这种情况不能完整获取包头),缓存起来与下次获取的数据拼接
    2、封包总长度刚好2字节,数据长度信息读出来是0,这种情况可以正常处理并清空缓存
    3、封包总长度大于2字节,数据长度信息大于封包数据实际长度,表示数据包不完整,需要等到下一次读取再拼接起来
    4、封包总长度大于2字节,数据长度信息等于封包数据实际长度,这种情况(理想情况)可以正常处理并清空缓存
    5、封包总长度大于2字节,数据长度信息小于封包实际长度,表示数据包发生TCP粘包了,读取实际数据后,将剩余部分缓存起来等待下次拼接
    PS:这里只总结出了这几种情况,其他未发现的情况还需另外处理
*/
package main
import (
    "encoding/binary"
    "fmt"
    "net"
)
func main() {
    ln, err := net.Listen("tcp", ":9000")
    if err != nil {
        return
    }
    for {
        conn, err := ln.Accept()
        if err != nil {
            continue
        }
        go handleConnection(conn)
    }
}
func handleConnection(conn net.Conn) {
    defer conn.Close()
    tmp := []byte{}
    for {
        buf := make([]byte, 1024)
        // fmt.Println("len:", len(buf), " cap:", cap(buf))
        n, err := conn.Read(buf)
        if err != nil {
            if e, ok := err.(*net.OpError); ok {
                fmt.Println(e.Source, e.Addr, e.Net, e.Op, e.Err)
                if e.Timeout() {
                    fmt.Println("Timeout Error")
                }
            }
            fmt.Println("Read Error:", err)
            fmt.Println("Read N:", n)
            return
        }
        if n == 0 {
            fmt.Println("Read N:", n)
            return
        }
        tmp = append(tmp, buf[:n]...)
        length := len(tmp)
        if length < 2 {
            continue
        }
        if length >= 2 {
            head := make([]byte, 2)
            copy(head, tmp[:2])
            dataLength := binary.BigEndian.Uint16(head)
            data := make([]byte, dataLength)
            copy(data, tmp[2:dataLength+2])
            fmt.Println(string(data)) // 得到数据
            if uint16(length) == 2+dataLength {
                tmp = []byte{}
            } else if uint16(length) > 2+dataLength {
                tmp = tmp[dataLength+2:]
            }
        }
        // fmt.Println(string(buf))
    }
}

ps:这里的示例代码不能直接用于生产环境,只是提供tcp粘包处理的思路过程,代码还是存在一些问题的,例如server.go服务端还没有对第3种情况进行处理,封包总长度大于2字节,数据长度信息大于封包数据实际长度,表示数据包不完整,需要等到下一次读取再拼接起来

到此这篇关于Golang通过包长协议处理TCP粘包的问题解决的文章就介绍到这了,更多相关Golang TCP粘包内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

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

    前言 什么是TCP粘包问题以及为什么会产生TCP粘包,本文不加讨论.本文使用golang的bufio.Scanner来实现自定义协议解包. 下面话不多说了,来一起看看详细的介绍吧. 协议数据包定义 本文模拟一个日志服务器,该服务器接收客户端传到的数据包并显示出来 type Package struct { Version [2]byte // 协议版本,暂定V1 Length int16 // 数据部分长度 Timestamp int64 // 时间戳 HostnameLength int16

  • Golang TCP粘包拆包问题的解决方法

    什么是粘包问题 最近在使用Golang编写Socket层,发现有时候接收端会一次读到多个数据包的问题.于是通过查阅资料,发现这个就是传说中的TCP粘包问题.下面通过编写代码来重现这个问题: 服务端代码 server/main.go func main() { l, err := net.Listen("tcp", ":4044") if err != nil { panic(err) } fmt.Println("listen to 4044")

  • Golang通过包长协议处理TCP粘包的问题解决

    tcp粘包产生的原因这里就不说了,因为大家能搜索TCP粘包的处理方法,想必大概对TCP粘包有了一定了解,所以我们直接从处理思路开始讲起 tcp粘包现象代码重现 首先,我们来重现一下TCP粘包,然后再此基础之上解决粘包的问题,这里给出了client和server的示例代码如下 /* 文件名:client.go client客户端的示例代码(未处理粘包问题) 通过无限循环无时间间隔发送数据给server服务器 server将会不间断的出现TCP粘包问题 */ package main import

  • 详解python中TCP协议中的粘包问题

    TCP协议中的粘包问题 1.粘包现象 基于TCP实现一个简易远程cmd功能 #服务端 import socket import subprocess sever = socket.socket() sever.bind(('127.0.0.1', 33521)) sever.listen() while True: client, address = sever.accept() while True: try: cmd = client.recv(1024).decode('utf-8') p

  • C#中TCP粘包问题的解决方法

    一.TCP粘包产生的原理 1.TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾.出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成. 2.发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据.若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据.接收方引起的粘包是由于接收方用户进程不及时接收数据,从

  • GO语言如何手动处理TCP粘包详解

    前言 一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据.TCP通讯为何存在粘包呢?主要原因是TCP是以流的方式来处理数据,再加上网络上MTU的往往小于在应用处理的消息数据,所以就会引发一次接收的数据无法满足消息的需要,导致粘包的存在.处理粘包的唯一方法就是制定应用层的数据通讯协议,通过协议来规范现有接收的数据是否满足消息数据的需要.在应用中处理粘包的基础方法主要有两种分别是以4节字描述消息大小或以结束符,实际上也有两者相结合的如HTTP,redis的通讯协议等. 应用场景 大

  • 使用Netty解决TCP粘包和拆包问题过程详解

    前言 上一篇我们介绍了如果使用Netty来开发一个简单的服务端和客户端,接下来我们来讨论如何使用解码器来解决TCP的粘包和拆包问题 TCP为什么会粘包/拆包 我们知道,TCP是以一种流的方式来进行网络转播的,当tcp三次握手简历通信后,客户端服务端之间就建立了一种通讯管道,我们可以想象成自来水管道,流出来的水是连城一片的,是没有分界线的. TCP底层并不了解上层的业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分. 所以对于我们应用层而言.我们直观是发送一个个连续完整TCP数据包的,

  • Netty解决 TCP 粘包拆包的方法

    什么是粘包/拆包 一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据.TCP通讯为何存在粘包呢?主要原因是TCP是以流的方式来处理数据,再加上网络上MTU的往往小于在应用处理的消息数据,所以就会引发一次接收的数据无法满足消息的需要,导致粘包的存在.处理粘包的唯一方法就是制定应用层的数据通讯协议,通过协议来规范现有接收的数据是否满足消息数据的需要. 我们都知道TCP是基于字节流的传输协议. 那么数据在通信层传播其实就像河水一样并没有明显的分界线,而数据具体表示什么意思什么地方有句

  • python3 tcp的粘包现象和解决办法解析

    这篇文章主要介绍了python3 tcp的粘包现象和解决办法解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 服务器端 import socket sk = socket.socket() sk.bind(("127.0.0.1", 6666)) sk.listen() conn, address = sk.accept() def my_send(msg): bs = msg.encode("utf-8") l

  • Netty粘包拆包问题解决方案

    TCP黏包拆包 TCP是一个流协议,就是没有界限的一长串二进制数据.TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题. 怎么解决? • 消息定长度,传输的数据大小固定长度,例如每段的长度固定为100字节,如果不够空位补空格 • 在数据包尾部添加特殊分隔符,比如下划线,中划线等 • 将消息分为消息头和

随机推荐