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(接收端)
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(发送端)
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(接收端)
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(发送端)
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个字节来获取消息的长度,然后再根据该长度读取完整的消息体
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字节表示消息的长度,随后是消息体
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 字节,服务端会读取并处理这些数据。
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 字节,则会填充空格
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 来标识消息的结束
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,然后发送到服务器
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. 三种方式的优缺点对比

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/945272.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Day10补代码随想录 理论基础|232.用栈实现队列|225.用队列实现栈|20.有效的括号|1047.删除字符串中的所有相邻重复项

栈和队列理论基础 抽象认识 栈是先进后出(FIFO)&#xff0c;队列是先进先出(LIFO) 队首(先进))队尾(后进)栈顶(后进)栈底(先进) 栈(Stack) 只在一端进行进出操作(只在一端进一端出)像个篮球框&#xff0c;取用篮球从一端进出。 /进栈 int a[1000];//足够大的栈空间 int top-1…

Gemma2 2B 模型的model.safetensors.index.json文件解析

Gemma2 2B 模型的 model.safetensors.index.json 文件解析 在使用 Gemma2 2B 模型或其他大型预训练模型时&#xff0c;model.safetensors.index.json 文件起到了索引的作用&#xff0c;它帮助我们了解模型的结构、参数存储方式以及如何加载模型的具体权重。本博客将深入解析该…

大模型系列——旋转位置编码和长度外推

绝对位置编码 旋转位置编码 论文中有个很直观的图片展示了旋转变换的过程&#xff1a; 对于“我”对应的d维向量&#xff0c; 拆分成d/2组以后&#xff0c;每组对应一个角度&#xff0c;若1对应的向量为(x1,x2)&#xff0c;应用旋转位置编码&#xff0c;相当于这个分量旋转了m…

网络安全威胁2024年中报告

下载地址&#xff1a; 网络安全威胁2024年中报告-奇安信

Momentum Contrast for Unsupervised Visual Representation Learning论文笔记

文章目录 论文地址动量队列对比学习的infoNCE loss为什么需要动量编码器对比学习moco方法中的动量Encoder为什么不能与梯度Encoder完全相同为什么动量编码器和梯度编码器不能完全相同&#xff1f;总结&#xff1a; 我理解&#xff0c;正负样本应该经过同一个encoder&#xff0c…

Unity 使用UGUI制作卷轴开启关闭效果

视频效果 代码 using UnityEngine.UI; using System.Collections; using System.Collections.Generic; using UnityEngine; using DG.Tweening; using DG.Tweening.Core; using DG.Tweening.Plugins.Options;public class JuanZhou : MonoBehaviour {[SerializeField]private …

plsql :用户system通过sysdba连接数据库--报错ora-01031

一、winR cmd通过命令窗口登录sys用户 sql sys/[password]//localhost:1521/[service_name] as sysdba二、输入用户名:sys as sysdba 三、输入密码:自己设的 四、执行grant sysdba to system; 再去PL/SQL连接就可以了

ubuntu 使用samba与windows共享文件[注意权限配置]

在Ubuntu上使用Samba服务与Windows系统共享文件&#xff0c;需要正确配置Samba服务以及相应的权限。以下是详细的步骤&#xff1a; 安装Samba 首先&#xff0c;确保你的Ubuntu系统上安装了Samba服务。 sudo apt update sudo apt install samba配置Samba 安装完成后&#xff0c…

Java - 日志体系_Apache Commons Logging(JCL)日志接口库_适配Log4j2 及 源码分析

文章目录 PreApache CommonsApache Commons ProperLogging &#xff08;Apache Commons Logging &#xff09; JCL 集成Log4j2添加 Maven 依赖配置 Log4j2验证集成 源码分析1. Log4j-jcl 的背景2. log4j-jcl 的工作原理2.1 替换默认的 LogFactoryImpl2.2 LogFactoryImpl 的实现…

仓颉编程语言:编程世界的 “文化瑰宝”

我的个人主页 在当今编程领域百花齐放的时代&#xff0c;各种编程语言争奇斗艳&#xff0c;服务于不同的应用场景和开发者群体。然而&#xff0c;有这样一种编程语言&#xff0c;它承载着独特的文化内涵&#xff0c;宛如编程世界里一颗熠熠生辉的“文化瑰宝”&#xff0c;那就…

Prompt工程--AI开发--可置顶粘贴小工具

PROMPT 1.背景要求&#xff1a;我需要开发一个简单的粘贴小工具&#xff0c;用于方便地粘贴和管理文本内容。该工具需要具备以下功能&#xff1a;粘贴功能&#xff1a;提供一个文本框&#xff0c;用户可以粘贴内容。窗口置顶&#xff1a;支持窗口置顶功能&#xff0c;确保窗口…

利用Abel_Cain软件实现ARP欺骗

ARP协议是“Address Resolution Protocol”&#xff08;地址解析协议&#xff09;的缩写。在局域网中&#xff0c;网络中实际传输的是“帧”&#xff0c;帧里面是有目标主机的MAC地址的。在以太网中&#xff0c;一个主机要和另一个主机进行直接通信&#xff0c;必须要知道目标主…

STM32学习之 按键/光敏电阻 控制 LED/蜂鸣器

STM32学习之 按键/光敏电阻 控制 LED/蜂鸣器 1、按键控制 LED 按键:常见的输入设备&#xff0c;按下导通&#xff0c;松手断开 按键抖动:由子按键内部使用的是机械式弹簧片来进行通断的、所以在按下和松手的瞬间会伴随有一连串的抖动 按键控制LED接线图&#xff1a; 要有工程…

深入解析MySQL索引结构:从数组到B+树的演变与优化

前言&#xff1a; 在数据库查询中&#xff0c;索引是一种关键的性能优化工具。然而&#xff0c;索引的失效可能导致查询效率大幅下降。为了更好地理解索引的工作原理及规避其失效&#xff0c;深入了解索引结构的演变过程尤为重要。 MySQL 的索引数据结构从简单到复杂&#xff0…

window如何将powershell以管理员身份添加到右键菜单?(按住Shift键显示)

window如何将powershell以管理员身份添加到右键菜单&#xff1f; 在 Windows 中&#xff0c;将 PowerShell 以管理员身份添加到右键菜单&#xff0c;可以让你在需要提升权限的情况下快速打开 PowerShell 窗口。以下是详细的步骤&#xff0c;包括手动编辑注册表和使用注册表脚本…

Redis--持久化策略(AOF与RDB)

持久化策略&#xff08;AOF与RDB&#xff09; 持久化Redis如何实现数据不丢失&#xff1f;RDB 快照是如何实现的呢&#xff1f;执行时机RDB原理执行快照时&#xff0c;数据能被修改吗&#xff1f; AOF持久化是怎么实现的&#xff1f;AOF原理三种写回策略AOF重写机制 RDB和AOF合…

uniapp-vue3(下)

关联链接&#xff1a;uniapp-vue3&#xff08;上&#xff09; 文章目录 七、咸虾米壁纸项目实战7.1.咸虾米壁纸项目概述7.2.项目初始化公共目录和设计稿尺寸测量工具7.3.banner海报swiper轮播器7.4.使用swiper的纵向轮播做公告区域7.5.每日推荐滑动scroll-view布局7.6.组件具名…

计算机网络 (16)数字链路层的几个共同问题

一、封装成帧 封装成帧是数据链路层的一个基本问题。数据链路层把网络层交下来的数据构成帧发送到链路上&#xff0c;以及把接收到的帧中的数据取出并上交给网络层。封装成帧就是在一段数据的前后分别添加首部和尾部&#xff0c;构成了一个帧。接收端在收到物理层上交的比特流后…

操作系统论文导读(八):Schedulability analysis of sporadic tasks with multiple criticality specifications——具有多个

Schedulability analysis of sporadic tasks with multiple criticality specifications——具有多个关键性规范的零星任务的可调度性分析 目录 一、论文核心思想 二、基本定义 2.1 关键性指标 2.2 任务及相关参数定义 2.3 几个基础定义 三、可调度性分析 3.1 调度算法分…

「教程」抖音短剧小程序源码开发后上架的教程及好处

上线抖音短剧小程序的步骤 注册账号与准备资料&#xff1a;首先需要在抖音开放平台官网注册一个抖音小程序账号&#xff0c;并完成相关认证&#xff0c;获取小程序开发权限。同时&#xff0c;要准备好短剧相关的素材&#xff0c;如视频、音频、剧本、封面图片等 开发或选择小程…