当前位置:首页 > 文章列表 > Golang > Go教程 > Golang通过包长协议处理TCP粘包的问题解决

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

来源:脚本之家 2022-12-28 17:59:55 0浏览 收藏

来到golang学习网的大家,相信都是编程学习爱好者,希望在这里学习Golang相关编程知识。下面本篇文章就来带大家聊聊《Golang通过包长协议处理TCP粘包的问题解决》,介绍一下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 {
            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粘包内容请搜索golang学习网以前的文章或继续浏览下面的相关文章希望大家以后多多支持golang学习网!

终于介绍完啦!小伙伴们,这篇关于《Golang通过包长协议处理TCP粘包的问题解决》的介绍应该让你收获多多了吧!欢迎大家收藏或分享给更多需要学习的朋友吧~golang学习网公众号也会发布Golang相关知识,快来关注吧!

版本声明
本文转载于:脚本之家 如有侵犯,请联系study_golang@163.com删除
Golang轻量级IoC容器安装使用示例Golang轻量级IoC容器安装使用示例
上一篇
Golang轻量级IoC容器安装使用示例
协同开发巧用gitignore中间件避免网络请求携带登录信息
下一篇
协同开发巧用gitignore中间件避免网络请求携带登录信息
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    542次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    511次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    498次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    484次学习
查看更多
AI推荐
  • 千音漫语:智能声音创作助手,AI配音、音视频翻译一站搞定!
    千音漫语
    千音漫语,北京熠声科技倾力打造的智能声音创作助手,提供AI配音、音视频翻译、语音识别、声音克隆等强大功能,助力有声书制作、视频创作、教育培训等领域,官网:https://qianyin123.com
    95次使用
  • MiniWork:智能高效AI工具平台,一站式工作学习效率解决方案
    MiniWork
    MiniWork是一款智能高效的AI工具平台,专为提升工作与学习效率而设计。整合文本处理、图像生成、营销策划及运营管理等多元AI工具,提供精准智能解决方案,让复杂工作简单高效。
    89次使用
  • NoCode (nocode.cn):零代码构建应用、网站、管理系统,降低开发门槛
    NoCode
    NoCode (nocode.cn)是领先的无代码开发平台,通过拖放、AI对话等简单操作,助您快速创建各类应用、网站与管理系统。无需编程知识,轻松实现个人生活、商业经营、企业管理多场景需求,大幅降低开发门槛,高效低成本。
    105次使用
  • 达医智影:阿里巴巴达摩院医疗AI影像早筛平台,CT一扫多筛癌症急慢病
    达医智影
    达医智影,阿里巴巴达摩院医疗AI创新力作。全球率先利用平扫CT实现“一扫多筛”,仅一次CT扫描即可高效识别多种癌症、急症及慢病,为疾病早期发现提供智能、精准的AI影像早筛解决方案。
    98次使用
  • 智慧芽Eureka:更懂技术创新的AI Agent平台,助力研发效率飞跃
    智慧芽Eureka
    智慧芽Eureka,专为技术创新打造的AI Agent平台。深度理解专利、研发、生物医药、材料、科创等复杂场景,通过专家级AI Agent精准执行任务,智能化工作流解放70%生产力,让您专注核心创新。
    96次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码