文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-04-02 19:55

关注

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

在这里插入图片描述

tcp粘包现象代码重现

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


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)
    }
}

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进行处理


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进行处理了,先上代码


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粘包内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯