TCP的粘包、拆包、解决方案以及Go语言实现

news2024/12/21 15:48:11

什么是粘包,拆包?

  • TCP的粘包和拆包问题往往出现在基于TCP协议的通讯中,比如RPC框架
  • 在使用TCP进行数据传输时,由于TCP是基于字节流的协议,而不是基于消息的协议,可能会出现粘包(多个消息粘在一起)和拆包(一个消息被拆分成多个部分)的问题。这些问题可能会导致数据解析错误或数据不完整。

为什么UDP没有粘包?

  • 由于UDP没有像TCP那样的流控制和拥塞控制机制,它不会对数据进行缓冲或重组。因此,在UDP中,每个数据报都是独立传输的(接收端一次只能接受一条独立的消息),不存在多个消息粘在一起的问题,也就没有粘包的概念。
  • 由于UDP是不可靠的传输协议,它无法保证数据的可靠传输和顺序传输。数据包可能会丢失、重复或乱序到达。在使用UDP时,应该自行处理这些问题,比如使用应答机制、超时重传等手段来保证数据的可靠性和正确性。

粘包拆包发生场景

因为TCP是面向流,没有边界,而操作系统在发送TCP数据时,会通过缓冲区来进行优化,例如缓冲区为1024个字节大小。

  • 如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题。
  • 如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包。

关于粘包和拆包可以参考下图的几种情况:

  • 理想状况:两个数据包逐一分开发送
  • 粘包:两个包一同发送,
  • 拆包:Server接收到不完整的或多出一部分的数据包

在这里插入图片描述

常见的解决方案

  • 固定长度:发送端将每个消息固定为相同的长度,接收端按照固定长度进行拆包。这样可以确保每个消息的长度是一致的,但是对于不同长度的消息可能会浪费一些空间。
  • 分隔符:发送端在每个消息的末尾添加一个特殊的分隔符(比如换行符或特殊字符),接收端根据分隔符进行拆包。这种方法适用于消息中不会出现分隔符的情况。
  • 消息长度前缀:发送端在每个消息前面添加一个固定长度的消息长度字段,接收端先读取消息长度字段,然后根据长度读取相应长度的数据。这种方法可以准确地拆分消息,但需要保证消息长度字段的一致性。

代码实现

固定长度

发送端将每个包都封装成固定的长度,比如20字节大小。如果不足20字节可通过补0或空等进行填充到指定长度;

服务端

package main

import (
	"fmt"
	"log"
	"net"
)

func main() {
	// 监听指定的TCP端口
	listener, err := net.Listen("tcp", "localhost:8080")
	if err != nil {
		log.Fatal(err)
	}
	defer listener.Close()

	fmt.Println("Server started. Listening on localhost:8080...")

	// 接收客户端连接
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Fatal(err)
		}

		// 启动一个并发的goroutine来处理连接
		go handleConnection(conn)
	}
}

// 处理连接
func handleConnection(conn net.Conn) {
	defer conn.Close()

	// 读取固定长度的数据
	fixedLength := 20 // 假设要读取的数据固定长度为20字节
	buffer := make([]byte, fixedLength)

	_, err := conn.Read(buffer)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Received data: %s\n", string(buffer))

	// 可以在这里对接收到的数据进行处理和响应
	// ...

	// 发送响应给客户端
	response := "Hello, Client!"
	_, err = conn.Write([]byte(response))
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Response sent successfully!")
}
 

客户端

package main

import (
    "fmt"
    "log"
    "net"
)

func main() {
    // 建立TCP连接
    conn, err := net.Dial("tcp", "localhost:8080")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    // 发送固定长度的数据
    message := "Hello, Server!"
    fixedLength := 20 // 假设要发送的数据固定长度为20字节

    // 如果消息长度小于固定长度,则使用空字符填充
    if len(message) < fixedLength {
        padding := make([]byte, fixedLength-len(message))
        message += string(padding)
    }

    _, err = conn.Write([]byte(message))
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Data sent successfully!")
}
 

分隔符

发送端在每个包的末尾使用固定的分隔符,例如\n

服务端

package main

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

func main() {
    // 监听指定的TCP端口
    listener, err := net.Listen("tcp", "localhost:8080")
    if err != nil {
        log.Fatal(err)
    }
    defer listener.Close()

    fmt.Println("Server started. Listening on localhost:8080...")

    // 接收客户端连接
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Fatal(err)
        }

        // 启动一个并发的goroutine来处理连接
        go handleConnection(conn)
    }
}

// 处理连接
func handleConnection(conn net.Conn) {
    defer conn.Close()

    reader := bufio.NewReader(conn)

    for {
        // 读取一行数据,以分隔符"\n"作为结束标志
        message, err := reader.ReadString('\n')
        if err != nil {
            log.Println(err)
            break
        }

        // 去除消息中的换行符
        message = strings.TrimRight(message, "\n")

        fmt.Printf("Received message: %s\n", message)

        // 可以在这里对接收到的消息进行处理和响应
        // ...

        // 发送响应给客户端
        response := "Hello, Client!\n"
        _, err = conn.Write([]byte(response))
        if err != nil {
            log.Println(err)
            break
        }
    }

    fmt.Println("Connection closed.")
}
 

客户端

package main

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

func main() {
    // 建立TCP连接
    conn, err := net.Dial("tcp", "localhost:8080")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    reader := bufio.NewReader(os.Stdin)

    for {
        // 读取用户输入的消息
        fmt.Print("Enter message: ")
        message, err := reader.ReadString('\n')
        if err != nil {
            log.Println(err)
            break
        }

        // 发送消息给服务器
        _, err = conn.Write([]byte(message))
        if err != nil {
            log.Println(err)
            break
        }

        // 读取服务器的响应
        response, err := bufio.NewReader(conn).ReadString('\n')
        if err != nil {
            log.Println(err)
            break
        }

        fmt.Printf("Server response: %s", response)
    }

    fmt.Println("Connection closed.")
}
 

消息长度前缀

将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够长度的消息之后才算是读到了一个完整的消息;

代码实现

package main

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

const headerSize = 4 // 头部长度的字节数

func main() {
	// 启动服务器
	go startServer()

	// 连接到服务器
	conn, err := net.Dial("tcp", "localhost:8080")
	if err != nil {
		fmt.Println("连接服务器失败:", err)
		return
	}
	defer conn.Close()

	// 发送消息
	message := "Hello, Server!"
	sendMessage(conn, message)

	// 读取服务器响应
	response, err := readMessage(conn)
	if err != nil {
		fmt.Println("读取消息失败:", err)
		return
	}
	fmt.Println("服务器响应:", response)
}

func startServer() {
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		fmt.Println("启动服务器失败:", err)
		return
	}
	defer listener.Close()

	fmt.Println("服务器已启动,等待连接...")

	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("接受连接失败:", err)
			return
		}
		go handleConnection(conn)
	}
}

func handleConnection(conn net.Conn) {
	fmt.Printf("客户端 %s 已连接\n", conn.RemoteAddr().String())

	defer conn.Close()

	// 读取消息
	message, err := readMessage(conn)
	if err != nil {
		fmt.Println("读取消息失败:", err)
		return
	}
	fmt.Println("收到消息:", message)

	// 发送响应
	response := "Hello, Client!"
	sendMessage(conn, response)
}

func sendMessage(conn net.Conn, message string) error {
	// 计算消息长度
	messageLength := len(message)

	// 将消息长度写入头部
	header := make([]byte, headerSize)
	binary.BigEndian.PutUint32(header, uint32(messageLength))
	if _, err := conn.Write(header); err != nil {
		return fmt.Errorf("写入消息头部失败: %v", err)
	}

	// 写入消息体
	if _, err := conn.Write([]byte(message)); err != nil {
		return fmt.Errorf("写入消息体失败: %v", err)
	}

	return nil
}

func readMessage(conn net.Conn) (string, error) {
	// 读取消息头部
	header := make([]byte, headerSize)
	if _, err := io.ReadFull(conn, header); err != nil {
		return "", fmt.Errorf("读取消息头部失败: %v", err)
	}

	// 解析消息长度
	messageLength := binary.BigEndian.Uint32(header)

	// 读取消息体
	message := make([]byte, messageLength)
	if _, err := io.ReadFull(conn, message); err != nil {
		return "", fmt.Errorf("读取消息体失败: %v", err)
	}

	return string(message), nil
}

  • 这段代码中,我们启动了一个TCP服务器,等待客户端连接。客户端在连接成功后,发送消息给服务器,服务器接收到消息后,返回一个响应。
  • 在发送消息时,我们首先计算消息的长度,并将长度以4字节的大端字节序写入到头部。然后,将消息体写入

总结

  • TCP 不管发送端要发什么,都基于字节流把数据发到接收端。这个字节流里可能包含上一次想要发的数据的部分信息。接收端根据需要在消息里加上识别消息边界的信息。不加就可能出现粘包问题
  • UDP 是基于数据报的传输协议,每个数据报都是独立传输的(接收端一次只能接受一条独立的消息),不会有粘包问题。

参考

  • 硬核图解|tcp为什么会粘包?背后的原因让人暖心

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于matlab创建基于颜色特征的图像检索系统(附源码)

一、前言 此示例说明如何使用自定义的功能包工作流创建基于内容的图像检索 &#xff08;CBIR&#xff09; 系统。 基于内容的图像检索 &#xff08;CBIR&#xff09; 系统用于查找在视觉上与查询图像相似的图像。CBIR系统的应用可以在许多领域找到&#xff0c;例如基于网络的…

记账APP:小哈记账4——记账首页页面的制作(1)

项目介绍&#xff1a; 小哈记账是一款用于记账APP&#xff0c;基于Android Studio开发工具&#xff0c;采用Java语言进行开发&#xff0c;同时使用litepal和阿里云数据库进行数据的增删查改&#xff0c;以图标的形式在App的界面上显示。App可以清晰显示收支情况&#xff0c;并以…

2. MongoDB分片集群架构实战-----MongoDB分片集群和多文档事务详解

分布式技术MongoDB 1. 分片简介2. MongoDB分片集群架构3. 环境搭建3.1 分片集群搭建3.2 使用mtools搭建分片集群 4 使用分片集群5. 分片策略5.1 什么是chunk5.2 分片算法5.3 哈希分片5.4 分片标签5.4 分片键(ShardKey)的选择5.5 分片键(ShardKey)的约束 6. 数据均衡6.1 均衡的方…

【JAVAWEB】JavaScript基础知识

目录 1.认识JavaScript 1.1JavaScript是什么 1.2JavaScript和html,css的关系 1.3JavaScript的组成 2.JavaScript的前置知识 2.1JavaScript的书写形式 行内式 内嵌式 外部式 2.2注释 2.3输入输出 3.JavaScript的语法知识 3.1变量的使用 创建变量 使用变量 3.2动态…

echarts柱状图查找数据

controller层&#xff1a; /*** 查询最近一周每天的行为识别总人数* return*/ApiOperation("查询最近一周每天的行为识别总人数") // RequiresPermissions("zhgd:aialarmdata:selectShu")GetMapping("/selectShu")public List<List> se…

UE5 Motion Warping功能学习

MotionWarping&#xff08;运动扭曲&#xff09;可对角色根运动进行修改&#xff0c;从而让角色根运动动画结束时准确停在某一点&#xff0c;如图&#xff1a; 此外UE5还提供移动步幅、转向的Warping功能&#xff08;防滑步&#xff09;&#xff0c;之前写过一个简单的介绍可…

9.2 IO多路复用select函数

目录 I/O多路复用模型 多路复用的实现方式 select函数 fd_set结构体 I/O多路复用模型 多路复用的实现方式 select函数 int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);poll函数 int poll(struct pollfd *fds, nfds_…

Git 安装 配置初始化

1. Git 下载 1.1 git 官网下载 Git 官网 https://git-scm.com/download/win 根据自己的电脑系统下载对应的版本 1.2 百度网盘下载 百度网盘&#xff1a;https://pan.baidu.com/s/17Thcov7VKfIc_hINcQimrw 提取码&#xff1a;13142. 安装git 2.1 双击安装包 2.2 点击next …

新应用爆发,开启5G的鱼龙之变

鱼龙变化&#xff0c;是中国自古以来就有的吉祥寓意与美好期盼。早在商代出土的玉雕中&#xff0c;就出现了由鱼化为龙的形象。晋代民间的歌谣中&#xff0c;已经有了“东海大鱼化为龙”的说法。在此之后&#xff0c;“鱼跃龙门”成为了考试中第的代名词&#xff0c;也被引申为…

台灯选用什么类型好?分享好用的护眼台灯

建议是选择护眼台灯比较好一点&#xff0c;不管是大人还是孩子使用&#xff0c;都会比普通台灯护眼一些。主要是普通台灯在交流电状态下正常工作的时候&#xff0c;每秒钟会发生闪烁100次左右&#xff0c;这样的频率人眼是看不出来的。但是在台灯下时间一长&#xff0c;极其容易…

sonarqube安装指南

sonarQube安装的指导文章说起来有很多&#xff0c;其他步骤本文不再赘述&#xff0c;可以参考其他文章。 在这里讲一下对JDK版本的事情。 SonarQube Server对系统的JDK、和研发写代码所使用的JDK版本有要求&#xff0c;要求大版本统一&#xff0c;比如大家基本上都在用JDK8&a…

景联文科技高质量教育GPT题库:引领教育行业的技术革命

ChatGPT拉开了大语言题库和生成式AI产业蓬勃发展的序幕。全世界教育科技公司扎堆接入GPT-4&#xff0c;涵盖美国、欧洲、日韩、中东和北非地区等。大语言题库在教育领域中势必将获得更加广阔的应用前景和丰富的应用场景。 杭州景联文科技是AI基础数据行业的头部企业&#xff0c…

实用技巧之拼接

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于拼接的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.什么时候会用到拼接 二.使用什么方式进…

React V6实现父子组件双向绑定传值

功能背景 之前在写vue的时候用到一个很好用的东西&#xff0c;比如控制一个dialog的显示隐藏&#xff0c;那么可以由父组件控制它显示&#xff0c;子组件&#xff08;即这个dialog&#xff09;自己可以关闭自己&#xff0c;那么他们之间只维护一个visible的状态&#xff0c;就需…

预约时间列表 带标签 上午下午 今天明天

/*** 时间列表* $interval 间隔X分钟* */ function timeList($day7,$time108:00,$time222:00,$interval60){$date_list [];//日期列表$today_date strtotime(date(Y-m-d,time()));for($i0;$i<$day;$i){$date_title date(Y-m-d,$today_date($i*86400));$buff array();for…

win7下如何开启远程桌面服务?让别人连接的方法

当某台计算机开启了远程桌面连接功能后就可以在网络的另一端控制这台计算机了&#xff0c;通过远程桌面功能可以实时地操作这台计算机&#xff0c;在上面安装软件&#xff0c;运行程序&#xff0c;所有的一切都好像是直接在该计算机上操作一样。这就是远程桌面的最大功能&#…

基于Python所写的图片批量处理器设计

点击以下链接获取源码资源&#xff1a; https://download.csdn.net/download/qq_64505944/87964231 《图片批量处理器》程序使用说明 在PyCharm中运行《图片批量处理器》即可进入如图1所示的系统主界面。在该界面中&#xff0c;通过顶部的菜单栏可以选择所要进行的操作。 图…

2023上半年软考系统分析师科目一整理-06

2023上半年软考系统分析师科目一整理-06 在数据库设计的需求分析、概念结构设计、逻辑结构设计和物理结构设计的四个阶段中&#xff0c;基本E-R图是( D )。 A.需求分析阶段形成的文档&#xff0c;并作为概念结构设计阶段的设计依据 B.逻辑结构设计阶段形成的文档&#xff0c;…

python获取目标主机的MAC地址

下载插件&#xff1a;WinPcap Download 下载后直接安装&#xff08;虽然已停止更新&#xff0c;但还能正常使用&#xff09; python代码&#xff1a;安装 scapy 包 from scapy.all import Ether, ARP, srpdef get_mac_address(ip):# 创建一个ARP请求数据包arp Ether(dst"…

独家!同比增长超两倍,空气悬架前装市场「爆表」

接近20万套&#xff08;19.56万辆&#xff09;、同比增长超2倍&#xff08;222.88%&#xff09;&#xff0c;这是空气悬架系统在今年1-5月中国乘用车市场交出的答卷。同时&#xff0c;更多的供应商正在进入这个爆发式增长的细分市场。 高工智能汽车研究院监测数据显示&#xf…