golang的socket的粘包拆包问题

接下来我们来学习另一个知识点,粘包和拆包,在上一篇文章中,我们采用了”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 也没办法判断哪一段流属于一个消息。

粘包的主要原因是

  1. 发送方每次写入数据 < 套接字(Socket)缓冲区大小
  2. 接收方读取套接字(Socket)缓冲区数据不够及时,造成积压

拆包的主要原因:

  1. 发送方每次写入数据 > 套接字(Socket)缓冲区大小
  2. 发送的数据大于协议的MTU(Maximum Transmission Unit,最大传输单元),因此必须拆包

对于粘包和拆包的有如下解决方案

  1. 使用特定字符来分割数据包,但是若数据中含有分割字符则会出现Bug
  2. 定长分隔(每个数据包最大为该长度,不足时使用特殊字符填充) ,但是数据不足时会浪费传输资源
  3. 在数据包中添加长度字段,自定义协议实现,golang可以使用bufio.Scanner包来实现自定义协议

只有在直接使用 TCP 协议才存在 “粘包/拆包” 问题,其上层应用层协议比如 HTTP ,已经帮我们处理好了,无需关注这些底层,但是我们自己实现一个自定义协议,就必须考虑这些细节了

如果是UDP的通信呢,需要不需要考虑 ”粘包/拆包” 问题呢??

  1. TCP,Transmission Control Protocol,传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议
  2. UDP,User Datagram Protocol,用户数据包协议,是面向无连接,不可靠的,基于数据报的传输层通信协议

从上面的定义来可以猜一猜.

此条目发表在 好文推荐 分类目录,贴了 标签。将固定链接加入收藏夹。

golang的socket的粘包拆包问题》有 4 条评论

  1. Pingback 引用通告: socket分享目录 | 你好,欢迎来到老张的博客,张素杰

  2. xtgxiso 说:

    粘包

    拆包

  3. xtgxiso 说:

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