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

这篇具有很好参考价值的文章主要介绍了TCP的粘包、拆包、解决方案以及Go语言实现。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

什么是粘包,拆包?

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

为什么UDP没有粘包?

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

粘包拆包发生场景

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

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

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

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

TCP的粘包、拆包、解决方案以及Go语言实现,Golang,tcp/ip,golang,网络

常见的解决方案

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

代码实现

固定长度

发送端将每个包都封装成固定的长度,比如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.")
}
 

消息长度前缀

将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够长度的消息之后才算是读到了一个完整的消息;文章来源地址https://www.toymoban.com/news/detail-616076.html

代码实现

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为什么会粘包?背后的原因让人暖心

到了这里,关于TCP的粘包、拆包、解决方案以及Go语言实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 「 计算机网络 」TCP的粘包拆包问题

    参考鸣谢 大病初愈,一分钟看懂TCP粘包拆包 雷小帅 TCP 的粘包拆包以及解决方案 一乐说 当我们在进行网络传输时,由于各种原因,数据包的发送和接收可能会出现粘包和拆包的问题。粘包和拆包都是数据分组错误的情况,其中粘包指的是多个数据包被合并成一个,而拆包则

    2024年02月01日
    浏览(27)
  • 网络编程:TCP粘包问题——各层粘包/拆包、Nagle 算法、Go实现长度字段协议解决TCP粘包、使用TCP的应用层协议设计

    1.1 TCP介绍 如上图,TCP具有面向连接、可靠、基于字节流三大特点。 字节流可以理解为一个双向的通道里流淌的数据,这个数据其实就是我们常说的二进制数据,简单来说就是一大堆 01 串。纯裸TCP收发的这些 01 串之间是没有任何边界的,你根本不知道到哪个地方才算一条完

    2024年02月04日
    浏览(28)
  • Netty自定义应用层协议逃不开的粘包和拆包处理

    导致一次发送的数据被分成多个数据包进行传输,或者多次发送的数据被粘成一个数据包进行传输 使用TCP进行数据传输时,TCP是一种有序的字节流,其中是一个一个的数据报文发送到系统的缓冲区中。因此在发送端和接收端之间无法保证数据的分割和边界。这就可能导致数据

    2023年04月23日
    浏览(87)
  • 解决TCP粘包/拆包问题的方法及示例

    TCP粘包和拆包是网络编程中常见的问题,特别是在数据传输的过程中,可能会发生将多个数据包粘在一起或将一个数据包拆成多个数据包的情况,这可能会导致应用程序无法正确解析数据,从而造成数据错误或系统故障。本文将介绍TCP粘包和拆包的原因、解决方案以及两个示

    2024年02月10日
    浏览(36)
  • TCP粘包和拆包问题及其解决方法

    含义: TCP 传输协议是面向流的,没有数据包界限,也就是说消息无边界。客户端向服务端发送数据时,可能将一个完整的报文拆分成多个小报文进行发送,也可能将多个报文合并成一个大的报文进行发送。(TCP协议的底层,并不了解上层业务的具体定义,它会根据TCP缓冲区

    2023年04月21日
    浏览(30)
  • Socket TCP/IP协议数据传输过程中的粘包和分包问题

    一:通过图解法来描述一下分包和粘包,这样客户更清晰直观的了解: 下面对上面的图进行解释: 1.正常情况:如果Socket Client 发送的数据包,在Socket Server端也是一个一个完整接收的,那个就不会出现粘包和分包情况,数据正常读取。 2.粘包情况:Socket Client发送的数据包,

    2024年02月12日
    浏览(33)
  • 粘包/拆包问题一直都存在,只是到TCP就拆不动了。

    OSI open-system-Interconnection TCP/IP 5层协议栈 应用层和操作系统的边界是 系统调用 ,对应到网络编程是socket api TCP/UDP 概况 TCP粘包问题 TCP/IP报头深思 定义了网络框架,以层为单位实现协议,同时控制权逐层传递。 OSI实际并没有落地,TCP/IP 5层协议栈是目前主流的落地实现 。 TC

    2024年02月03日
    浏览(46)
  • C++ Qt TCP协议,处理粘包、拆包问题,加上数据头来处理

    目录 前言: 场景: 原因: 解决: 方案2具体细节: 纯C++服务端处理如下: Qt客户端处理如下:         tcp协议里面,除了心跳检测是关于长连接操作的处理,这个在前一篇已经提到过了,这一篇将会对tcp本身的一个问题,进行处理:那就是做网络通信大概率会遇到的问题

    2024年02月04日
    浏览(43)
  • Unity-TCP-网络聊天功能(一): API、客户端服务器、数据格式、粘包拆包

    TCP是面向连接的。因此需要创建监听器,监听客户端的连接。当连接成功后,会返回一个TcpClient对象。通过TcpClient可以接收和发送数据。 VS创建C# .net控制台应用 项目中创建文件夹Net,Net 下添加TCPServer.cs类,用来创建TCPListener和Accept客户端连接,实例化一个TCPServcer放在Main函数

    2024年02月07日
    浏览(47)
  • go中for range的坑以及解决方案

    相信小伙伴都遇到过以下的循环变量的问题,那是因为循环的val变量是重复使用的,即仅有一份。也就是说,每次循环后赋给val的值就会把前面循环赋给val的值替换掉,所以打印出来的值都是最后一次循环赋给val的值。 使用局部变量/临时变量,即可解决         可以设置

    2024年01月25日
    浏览(29)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包