Golang通过包长协议处理TCP粘包的问题解决
来源:脚本之家
时间:2022-12-28 17:59:55 358浏览 收藏
来到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相关知识,快来关注吧!
-
488 收藏
-
398 收藏
-
358 收藏
-
311 收藏
-
278 收藏
-
367 收藏
-
419 收藏
-
234 收藏
-
155 收藏
-
457 收藏
-
309 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习
-
- 忧虑的大象
- 这篇技术文章真及时,老哥加油!
- 2023-02-20 21:25:36
-
- 过时的冰淇淋
- 细节满满,mark,感谢作者的这篇技术文章,我会继续支持!
- 2023-01-20 23:18:30
-
- 优美的金鱼
- 真优秀,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,帮助很大,总算是懂了,感谢楼主分享博文!
- 2023-01-05 06:10:09
-
- 心灵美的学姐
- 这篇博文真是及时雨啊,太细致了,太给力了,码起来,关注作者了!希望作者能多写Golang相关的文章。
- 2023-01-03 13:17:03
-
- 故意的唇彩
- 这篇文章内容真及时,好细啊,很有用,收藏了,关注作者大大了!希望作者大大能多写Golang相关的文章。
- 2022-12-29 16:20:12