go 模拟TCP粘包和拆包,及解决方法

1. 什么是 TCP 粘包与拆包?

  1. 粘包(Sticky Packet)
    粘包是指在发送多个小的数据包时,接收端会将这些数据包合并成一个数据包接收。由于 TCP 是面向流的协议,它并不会在每次数据发送时附加边界信息。所以当多个数据包按顺序发送时,接收端可能会一次性接收多个数据包的数据,造成数据被粘在一起。
    粘包一般发生在发送端每次写入的数据 < 接收端套接字(Socket)缓冲区的大小。

假设发送端发送了两个消息:消息1:"Hello",消息2:"World";由于 TCP 是流协议,接收端可能会接收到如下数据:"HelloWorld"。这种情况就是粘包,接收端就无法准确区分这两个消息。

  1. 拆包(Packet Fragmentation)
    拆包是指发送的数据包在传输过程中被分割成多个小包。尽管发送端可能发送了一个完整的消息,但由于 TCP 协议在网络传输时可能会对数据进行分段,接收端可能接收到的是多个小数据包。
    拆包一般发生在发送端每次写入的数据 > 接收端套接字(Socket)缓冲区的大小。

假设发送端发送了一个大的消息:"Hello, this is a long message.";但是在传输过程中,网络层可能会将该消息拆分成多个小包,接收端可能先收到一部分数据:"Hello, this",然后再收到另外一部分:"is a long message.";这样接收端就会得到多个数据包,且它们并不代表单一的逻辑消息。

2. go 模拟TCP粘包

  1. server.go(接收端)
go 复制代码
package main

import (
	"bufio"
	"fmt"
	"io"
	"net"
)

func handleConnection(conn net.Conn) {
	defer conn.Close()

	// 创建缓冲读取器,读取客户端数据
	reader := bufio.NewReader(conn)
	var buffer [1024]byte

	for {
		// 持续读取数据
		n, err := reader.Read(buffer[:])
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Println("Error reading data:", err)
			break
		}
		recvStr := string(buffer[:n])

		// 打印接收到的数据
		fmt.Println("Received:", recvStr)
	}
}

func main() {
	// 启动服务器,监听 8080 端口
	ln, err := net.Listen("tcp", ":8080")
	if err != nil {
		fmt.Println("Error starting server:", err)
		return
	}
	defer ln.Close()

	fmt.Println("Server started on port 8080...")

	for {
		// 等待客户端连接
		conn, err := ln.Accept()
		if err != nil {
			fmt.Println("Error accepting connection:", err)
			continue
		}

		// 处理连接
		go handleConnection(conn)
	}
}
  1. client.go(发送端)
go 复制代码
package main

import (
	"fmt"
	"net"
	"time"
)

func main() {
	// 连接到服务器
	conn, err := net.Dial("tcp", "localhost:8080")
	if err != nil {
		fmt.Println("Error connecting to server:", err)
		return
	}
	defer conn.Close()

	// 模拟粘包和拆包
	for i := 0; i < 100; i++ {
		// 发送粘包情况:多个小消息一次发送
		message := fmt.Sprintf("Message %d\n", i+1)
		conn.Write([]byte(message))
	}

	// 等待服务器输出接收到的消息
	time.Sleep(2 * time.Second)
}
  1. 执行结果分析

可以看到接收端收到的消息并非都是一条,说明发生了粘包

3. go模拟TCP拆包

  1. server.go(接收端)
go 复制代码
package main

import (
	"bufio"
	"fmt"
	"io"
	"net"
)

func handleConnection(conn net.Conn) {
	defer conn.Close()

	// 创建缓冲读取器,读取客户端数据
	reader := bufio.NewReader(conn)
	var buffer [18]byte

	for {
		// 持续读取数据
		n, err := reader.Read(buffer[:])
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Println("Error reading data:", err)
			break
		}
		recvStr := string(buffer[:n])

		// 打印接收到的数据
		fmt.Println("Received message :", recvStr)
	}
}

func main() {
	// 启动服务器,监听 8080 端口
	ln, err := net.Listen("tcp", ":8080")
	if err != nil {
		fmt.Println("Error starting server:", err)
		return
	}
	defer ln.Close()

	fmt.Println("Server started on port 8080...")

	for {
		// 等待客户端连接
		conn, err := ln.Accept()
		if err != nil {
			fmt.Println("Error accepting connection:", err)
			continue
		}

		// 处理连接
		go handleConnection(conn)
	}
}
  1. client.go(发送端)
go 复制代码
package main

import (
	"fmt"
	"net"
	"strings"
	"time"
)

func main() {
	// 连接到服务器
	conn, err := net.Dial("tcp", "localhost:8080")
	if err != nil {
		fmt.Println("Error connecting to server:", err)
		return
	}
	defer conn.Close()

	// 构造一个超过默认 MTU 的大数据包(32 字节)
	message := strings.Repeat("A", 32)

	// 模拟发送大量数据
	for i := 0; i < 100; i++ {
		fmt.Printf("Sending message : %s\n", message)
		conn.Write([]byte(message))
	}

	// 等待服务器输出
	time.Sleep(2 * time.Second)
}
  1. 执行结果分析

可以看到接收端对接收到的数据进行了拆分,说明发生了拆包

4. 如何解决 TCP 粘包与拆包问题?

4.1 自定义协议

发送端将请求的数据封装为两部分:消息头(发送数据大小)+消息体(发送具体数据);接收端根据消息头的值读取相应长度的消息体数据

  1. server.go(接收端)
    服务端接收到数据时,首先读取前4个字节来获取消息的长度,然后再根据该长度读取完整的消息体
go 复制代码
package main

import (
	"encoding/binary"
	"fmt"
	"io"
	"log"
	"net"
)

// readMessage 函数根据长度字段读取消息
func readMessage(conn net.Conn) (string, error) {
	// 读取4个字节的长度字段
	lenBytes := make([]byte, 4)
	_, err := io.ReadFull(conn, lenBytes)
	if err != nil {
		return "", fmt.Errorf("failed to read length field: %v", err)
	}

	// 解析消息长度
	msgLength := binary.BigEndian.Uint32(lenBytes)

	// 读取消息体
	msgBytes := make([]byte, msgLength)
	_, err = io.ReadFull(conn, msgBytes)
	if err != nil {
		return "", fmt.Errorf("failed to read message body: %v", err)
	}

	return string(msgBytes), nil
}

func handleConnection(conn net.Conn) {
	defer conn.Close()

	// 一直循环接收客户端发来的消息
	for {
		msg, err := readMessage(conn)
		if err != nil {
			log.Printf("Error reading message: %v", err)
			break
		}
		fmt.Println("Received message:", msg)
	}
}

func main() {
	// 启动监听服务
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatalf("Error starting server: %v", err)
	}
	defer listener.Close()

	fmt.Println("Server is listening on port 8080...")

	// 接受客户端连接并处理
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Printf("Error accepting connection: %v", err)
			continue
		}
		// 启动新的 Goroutine 处理客户端请求
		go handleConnection(conn)
	}
}
  1. client.go(发送端)
    客户端将连接到服务端,并发送多个消息。每个消息的前4字节表示消息的长度,随后是消息体
go 复制代码
package main

import (
	"bytes"
	"encoding/binary"
	"log"
	"net"
)

// sendMessage 函数将消息和长度一起发送给服务端
func sendMessage(conn net.Conn, msg string) {
	// 计算消息的长度
	msgLen := uint32(len(msg))
	buf := new(bytes.Buffer)

	// 将消息长度转换为4字节的二进制数据
	binary.Write(buf, binary.BigEndian, msgLen)
	// 将消息体内容添加到缓冲区
	buf.Write([]byte(msg))

	// 发送缓冲区数据到服务端
	conn.Write(buf.Bytes())
}

func main() {
	// 连接到服务端
	conn, err := net.Dial("tcp", "127.0.0.1:8080")
	if err != nil {
		log.Fatalf("Error connecting to server: %v", err)
	}
	defer conn.Close()

	// 发送多个消息
	sendMessage(conn, "Hello, Server!")
	sendMessage(conn, "This is a second message.")
	sendMessage(conn, "Goodbye!")
}
4.2 固定长度数据包

每个消息的长度是固定的(例如 1024 字节)。如果客户端发送的数据长度不足指定长度,则会使用空格填充,确保每个数据包的大小一致

  1. server.go(接收端)
    服务端接收到的数据是固定长度的。每次接收 1024 字节的数据,并将其打印出来。如果数据不足 1024 字节,服务端会读取并处理这些数据。
go 复制代码
package main

import (
	"fmt"
	"io"
	"log"
	"net"
	"strings"
)

// handleConnection 函数处理每个客户端的连接
func handleConnection(conn net.Conn) {
	defer conn.Close()

	// 设定每个消息的固定长度
	const messageLength = 1024
	buf := make([]byte, messageLength)

	for {
		// 每次读取固定长度的消息
		_, err := io.ReadFull(conn, buf)
		if err != nil {
			if err.Error() == "EOF" {
				// 客户端关闭连接
				break
			}
			log.Printf("Error reading message: %v", err)
			break
		}

		// 将读取的字节转换为字符串并打印
		msg := string(buf)
		// 去除空格填充
		fmt.Println("Received message:", strings.TrimSpace(msg))
	}
}

func main() {
	// 启动 TCP 监听
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatalf("Error starting server: %v", err)
	}
	defer listener.Close()

	fmt.Println("Server is listening on port 8080...")

	// 等待客户端连接
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Printf("Error accepting connection: %v", err)
			continue
		}
		// 启动新的 Goroutine 处理每个客户端的连接
		go handleConnection(conn)
	}
}
  1. client.go(发送端)
    客户端会向服务器发送固定长度的消息,如果消息长度不足 1024 字节,则会填充空格
go 复制代码
package main

import (
	"log"
	"net"
	"strings"
)

// sendFixedLengthMessage 函数向服务端发送固定长度的消息
func sendFixedLengthMessage(conn net.Conn, msg string) {
	// 确保消息长度为 1024 字节,不足部分用空格填充
	if len(msg) < 1024 {
		msg = msg + strings.Repeat(" ", 1024-len(msg))
	}

	// 发送消息到服务端
	_, err := conn.Write([]byte(msg))
	if err != nil {
		log.Fatalf("Error sending message: %v", err)
	}
}

func main() {
	// 连接到服务端
	conn, err := net.Dial("tcp", "127.0.0.1:8080")
	if err != nil {
		log.Fatalf("Error connecting to server: %v", err)
	}
	defer conn.Close()

	// 发送固定长度的消息
	sendFixedLengthMessage(conn, "Hello, Server!")
	sendFixedLengthMessage(conn, "This is a second message.")
	sendFixedLengthMessage(conn, "Goodbye!")
}
4.3 特殊字符来标识消息边界

通过在发送端每条消息的末尾加上 \n,然后接收端使用 ReadLine() 方法按行读取数据来区分每个数据包的边界

  1. server.go(接收端)
    服务端会监听端口,并按行读取客户端发送的消息。每个消息的末尾会有一个 \n 来标识消息的结束
go 复制代码
package main

import (
	"bufio"
	"fmt"
	"log"
	"net"
	"strings"
)

func handleConnection(conn net.Conn) {
	defer conn.Close()

	// 创建一个带缓冲的读取器
	reader := bufio.NewReader(conn)

	for {
		// 读取客户端发送的一行数据,直到遇到 '\n' 为止
		line, err := reader.ReadString('\n')
		if err != nil {
			log.Printf("Error reading from client: %v", err)
			break
		}

		// 去掉结尾的换行符
		line = strings.TrimSpace(line)
		fmt.Printf("Received message: %s\n", line)
	}
}

func main() {
	// 启动 TCP 监听
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatalf("Error starting server: %v", err)
	}
	defer listener.Close()

	fmt.Println("Server is listening on port 8080...")

	// 等待客户端连接
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Printf("Error accepting connection: %v", err)
			continue
		}
		// 启动新的 Goroutine 处理每个客户端的连接
		go handleConnection(conn)
	}
}
  1. client.go(发送端)
    客户端向服务端发送消息,每条消息末尾都会加上一个 \n,然后发送到服务器
go 复制代码
package main

import (
	"log"
	"net"
)

func sendMessage(conn net.Conn, message string) {
	// 将消息添加换行符并发送
	message = message + "\n"
	_, err := conn.Write([]byte(message))
	if err != nil {
		log.Fatalf("Error sending message: %v", err)
	}
}

func main() {
	// 连接到服务端
	conn, err := net.Dial("tcp", "127.0.0.1:8080")
	if err != nil {
		log.Fatalf("Error connecting to server: %v", err)
	}
	defer conn.Close()

	// 发送几条消息
	sendMessage(conn, "Hello, Server!")
	sendMessage(conn, "How are you?")
	sendMessage(conn, "Goodbye!")
}

5. 三种方式的优缺点对比

特性 固定长度方式 特殊字符分隔方式 自定义协议方式
实现简单
带宽效率 低(需要填充) 高(仅传输有效数据) 高(仅传输有效数据,且灵活处理)
灵活性
易于调试 高(每包大小固定) 中(需解析换行符等) 低(需要解析协议头和体)
性能开销 中等(需要额外解析消息头)
适用场景 长度固定的消息 消息大小可变但有清晰的分隔符 复杂协议、支持多类型消息的场景
相关推荐
大丈夫立于天地间16 小时前
ISIS协议中的数据库同步
运维·网络·信息与通信
Dream Algorithm16 小时前
路由器的 WAN(广域网)口 和 LAN(局域网)口
网络·智能路由器
IT猿手16 小时前
基于CNN-LSTM的深度Q网络(Deep Q-Network,DQN)求解移动机器人路径规划,MATLAB代码
网络·cnn·lstm
吴盐煮_16 小时前
使用UDP建立连接,会存在什么问题?
网络·网络协议·udp
hyshhhh17 小时前
【算法岗面试题】深度学习中如何防止过拟合?
网络·人工智能·深度学习·神经网络·算法·计算机视觉
Hellc00718 小时前
轮询、WebSocket 和 SSE:实时通信技术全面指南(含C#实现)
网络
xujiangyan_18 小时前
nginx的反向代理和负载均衡
服务器·网络·nginx
GalaxyPokemon18 小时前
Muduo网络库实现 [十] - EventLoopThreadPool模块
linux·服务器·网络·c++
忆源19 小时前
SOME/IP-SD -- 协议英文原文讲解9(ERROR处理)
网络·网络协议·tcp/ip