golang模拟TCP粘包和拆包

 更新时间:2024年12月31日 08:58:32   作者:赴前尘  
粘包是指在发送多个小的数据包时,接收端会将这些数据包合并成一个数据包接收,拆包是指发送的数据包在传输过程中被分割成多个小包,下面我们来看看go如何模拟TCP粘包和拆包吧

1. 什么是 TCP 粘包与拆包

1.粘包(Sticky Packet)

粘包是指在发送多个小的数据包时,接收端会将这些数据包合并成一个数据包接收。由于 TCP 是面向流的协议,它并不会在每次数据发送时附加边界信息。所以当多个数据包按顺序发送时,接收端可能会一次性接收多个数据包的数据,造成数据被粘在一起。

粘包一般发生在发送端每次写入的数据 < 接收端套接字(Socket)缓冲区的大小。

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

2.拆包(Packet Fragmentation)

拆包是指发送的数据包在传输过程中被分割成多个小包。尽管发送端可能发送了一个完整的消息,但由于 TCP 协议在网络传输时可能会对数据进行分段,接收端可能接收到的是多个小数据包。

拆包一般发生在发送端每次写入的数据 > 接收端套接字(Socket)缓冲区的大小。

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

2. go 模拟TCP粘包

server.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)
	}
}

client.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)
}

执行结果分析

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

3. go模拟TCP拆包

server.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)
	}
}

client.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)
}

执行结果分析

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

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

4.1 自定义协议

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

server.go(接收端)

服务端接收到数据时,首先读取前4个字节来获取消息的长度,然后再根据该长度读取完整的消息体

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)
	}
}

client.go(发送端)

客户端将连接到服务端,并发送多个消息。每个消息的前4字节表示消息的长度,随后是消息体

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 字节)。如果客户端发送的数据长度不足指定长度,则会使用空格填充,确保每个数据包的大小一致

server.go(接收端)

服务端接收到的数据是固定长度的。每次接收 1024 字节的数据,并将其打印出来。如果数据不足 1024 字节,服务端会读取并处理这些数据。

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)
	}
}

client.go(发送端)

客户端会向服务器发送固定长度的消息,如果消息长度不足 1024 字节,则会填充空格

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() 方法按行读取数据来区分每个数据包的边界

server.go(接收端)

服务端会监听端口,并按行读取客户端发送的消息。每个消息的末尾会有一个 \n 来标识消息的结束

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)
	}
}

client.go(发送端)

客户端向服务端发送消息,每条消息末尾都会加上一个 \n,然后发送到服务器

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. 三种方式的优缺点对比

特性固定长度方式特殊字符分隔方式自定义协议方式
实现简单
带宽效率低(需要填充)高(仅传输有效数据)高(仅传输有效数据,且灵活处理)
灵活性
易于调试高(每包大小固定)中(需解析换行符等)低(需要解析协议头和体)
性能开销中等(需要额外解析消息头)
适用场景长度固定的消息消息大小可变但有清晰的分隔符复杂协议、支持多类型消息的场景

以上就是golang模拟TCP粘包和拆包的详细内容,更多关于go TCP粘包和拆包的资料请关注脚本之家其它相关文章!

相关文章

  • go defer避坑指南之拆解延迟语句

    go defer避坑指南之拆解延迟语句

    这篇文章主要为大家详细介绍了go defer避坑指南之如何拆解延迟语句,掌握正确使用方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2023-11-11
  • Go语言实现二维数组的2种遍历方式以及案例详解

    Go语言实现二维数组的2种遍历方式以及案例详解

    这篇文章主要介绍了Go语言实现二维数组的2种遍历方式以及案例详解,图文代码声情并茂,有感兴趣的可以学习下
    2021-03-03
  • GO语言开发环境搭建过程图文详解

    GO语言开发环境搭建过程图文详解

    这篇文章主要介绍了GO语言开发环境搭建过程图文详解,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-01-01
  • Golang中crypto/ecdsa库实现数字签名和验证

    Golang中crypto/ecdsa库实现数字签名和验证

    本文主要介绍了Golang中crypto/ecdsa库实现数字签名和验证,将从ECDSA的基本原理出发,详细解析如何在Go语言中实现数字签名和验证,具有一定的参考价值,感兴趣的可以了解一下
    2024-02-02
  • Go语言流程控制语句

    Go语言流程控制语句

    这篇文章介绍了Go语言流程控制语句的用法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-07-07
  • go下载指定版本的依赖包图文详解

    go下载指定版本的依赖包图文详解

    由于依赖包的每个版本都有一个唯一的目录,所以在多项目场景中需要使用同一个依赖包的多版本时才不会产生冲突,下面这篇文章主要给大家介绍了关于go下载指定版本的依赖包的相关资料,需要的朋友可以参考下
    2023-04-04
  • Golang语言JSON解码函数Unmarshal的使用

    Golang语言JSON解码函数Unmarshal的使用

    本文主要介绍了Golang语言JSON解码函数Unmarshal的使用,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • Go语言 Channel通道详解

    Go语言 Channel通道详解

    Channel是一个通道,可以通过它读取和写入数据,它就像水管一样,网络数据通过Channel 读取和写入,这篇文章主要给大家介绍了关于Go语言 Channel通道的相关资料,需要的朋友可以参考下
    2023-07-07
  • golang协程池设计详解

    golang协程池设计详解

    这篇文章主要介绍了golang协程池设计详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-09-09
  • Go实现跨平台的蓝牙聊天室示例详解

    Go实现跨平台的蓝牙聊天室示例详解

    这篇文章主要为大家介绍了Go实现跨平台的蓝牙聊天室示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-12-12

最新评论