接下来我们来学习另一个知识点,粘包和拆包,在上一篇文章中,我们采用了”reader.ReadString(‘\n’)”来简单处理粘包和拆包问题,现在我们来深入学习一下。
服务端代码示例:
package main /* * 粘包和拆包问题 */ import ( "fmt" "net" "time" ) func DealConn(conn net.Conn) { // 处理完关闭连接 defer conn.Close() fmt.Println("new conn\n") var buf = make([]byte, 5) // 针对当前连接做发送和接受操作 for { n, err := conn.Read(buf) if err != nil { break } if n > 0 { fmt.Println("received msg", n, "bytes:", string(buf[:n])) //模拟处理消息耗时 time.Sleep(time.Second) } } } func main() { // 建立tcp 服务 listen, err := net.Listen("tcp", "0.0.0.0:1215") if err != nil { fmt.Printf("listen failed, err:%v\n", err) return } for { // 等待客户端建立连接 fmt.Printf("accept conn ...... \n") conn, err := listen.Accept() if err != nil { fmt.Printf("accept failed, err:%v\n", err) continue } // 启动一个单独的 goroutine 去处理连接 go DealConn(conn) } }
客户端发送消息代码示例:
package main import ( "net" "time" ) func main() { conn, _ := net.Dial("tcp", "0.0.0.0:1215") defer conn.Close() for i := 0; i < 10; i++ { conn.Write([]byte("4444")) conn.Write([]byte("55555")) conn.Write([]byte("666666")) } time.Sleep(time.Second) }
先运行服务端,然后再运行客户端,可以看到服务端的日志如下:
received msg 5 bytes: 44445 received msg 5 bytes: 55556 received msg 5 bytes: 66666 received msg 5 bytes: 44445 received msg 5 bytes: 55556 received msg 5 bytes: 66666 received msg 5 bytes: 44445 received msg 5 bytes: 55556 received msg 5 bytes: 66666 received msg 5 bytes: 44445 received msg 5 bytes: 55556 received msg 5 bytes: 66666 received msg 5 bytes: 44445 received msg 5 bytes: 55556 received msg 5 bytes: 66666 received msg 5 bytes: 44445 received msg 5 bytes: 55556 received msg 5 bytes: 66666 received msg 5 bytes: 44445 received msg 5 bytes: 55556 received msg 5 bytes: 66666 received msg 5 bytes: 44445 received msg 5 bytes: 55556 received msg 5 bytes: 66666 received msg 5 bytes: 44445 received msg 5 bytes: 55556 received msg 5 bytes: 66666 received msg 5 bytes: 44445 received msg 5 bytes: 55556 received msg 5 bytes: 66666
从我们这个日志,我们看到收到的消息是串了的,那么为什么会出现此问题呢?
这是因为 TCP 是面向连接的传输协议,TCP 传输的数据是以流的形式,而流数据是没有明确的开始结尾边界,所以 TCP 也没办法判断哪一段流属于一个消息。
粘包的主要原因是
- 发送方每次写入数据 < 套接字(Socket)缓冲区大小
- 接收方读取套接字(Socket)缓冲区数据不够及时,造成积压
拆包的主要原因:
- 发送方每次写入数据 > 套接字(Socket)缓冲区大小
- 发送的数据大于协议的MTU(Maximum Transmission Unit,最大传输单元),因此必须拆包
对于粘包和拆包的有如下解决方案
- 使用特定字符来分割数据包,但是若数据中含有分割字符则会出现Bug
- 定长分隔(每个数据包最大为该长度,不足时使用特殊字符填充) ,但是数据不足时会浪费传输资源
- 在数据包中添加长度字段,自定义协议实现,golang可以使用bufio.Scanner包来实现自定义协议
只有在直接使用 TCP 协议才存在 “粘包/拆包” 问题,其上层应用层协议比如 HTTP ,已经帮我们处理好了,无需关注这些底层,但是我们自己实现一个自定义协议,就必须考虑这些细节了
如果是UDP的通信呢,需要不需要考虑 ”粘包/拆包” 问题呢??
- TCP,Transmission Control Protocol,传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议
- UDP,User Datagram Protocol,用户数据包协议,是面向无连接,不可靠的,基于数据报的传输层通信协议
从上面的定义来可以猜一猜.
Pingback 引用通告: socket分享目录 | 你好,欢迎来到老张的博客,张素杰
粘包


拆包
1:4层代理,是最容易实现的,因为不关注内容,纯转发
2:7层代理(Nginx),需要关注内容,相比4层要解析协议,然后进行转发,相对上面而方稍微复杂点
3:以上无状态,分布式部署很简单,有状态的socket服务(redis,mysql,mongodb),不但要有自己的协议,还得考虑存储和算法,更复杂了,尤其需要分布工部署的情况下
更多可参见:https://segmentfault.com/a/1190000039691657