Golang中解决Tcp粘包的问题

1、问题分析

在客户端传输的时候,如果我们想要进行消息的连发,或者说一次性发送多个消息包,就必要要解决Tcp粘包的问题。关于Tcp粘包在这里不作过多的讲解,另外本文主要是呈现解决思路,而并非实际的工业生产方案。
请添加图片描述

2、解决办法

在这里为了解决上述粘包的问题,我们需要将原来的数据再进行一层包装,其基本的逻辑概念如下:
请添加图片描述
当然这个具体设计细节需要跟随自己的业务进行改变,在这里我还设计了一个id,主要是方便我后面的一些相关业务。主要解决粘包的还是前面的datalen,读取时会首先读取len,根据这个len来读取后面具体长度的data,进而解决Tcp粘包的问题。

3、代码实现

创建一个项目

3.1 创建封装消息的接口及其实现类

  • 接口StickyBuns/isticky/imessage.go
    package isticky
    
    type IMessage interface {
    	GetMsgID() uint32
    	GetMsgLen() uint32
    	GetData() []byte
    }
    
  • 实现类StickyBuns/sticky/message.go
    package sticky
    type Message struct {
    	Id      uint32 // 消息ID
    	DataLen uint32 // 消息的长度
    	Data    []byte // 消息的内容
    }
    
    func (m *Message) GetMsgID() uint32 {
    	return m.Id
    }
    
    func (m *Message) GetMsgLen() uint32 {
    	return m.DataLen
    }
    
    func (m *Message) GetData() []byte {
    	return m.Data
    }
    

3.2 实现打包、拆包的接口及其实现类⭐️

  • 接口StickyBuns/isticky/idatapack.go
    package isticky
    
    type IDataPack interface {
    	GetHeadLen() uint32
    	Pack(IMessage) ([]byte, error)
    	UnPack([]byte) (IMessage, error)
    }
    
  • 接口实现类StickyBuns/sticky/datapack/go
    package sticky
    
    import (
    	"bytes"
    	"encoding/binary"
    	"fmt"
    	"v1/isticky"
    )
    
    type DataPack struct {
    }
    
    func (d *DataPack) GetHeadLen() uint32 {
    	// 根据自身设计:len 4字节 + id 4字节
    	return 8
    }
    
    // Pack 打包的实现
    func (d *DataPack) Pack(message isticky.IMessage) ([]byte, error) {
    	// 创建一个buffer
    	buffer := bytes.NewBuffer([]byte{})
    
    	// 将DataLen写入
    	if err := binary.Write(buffer, binary.LittleEndian, message.GetMsgLen()); err != nil {
    		fmt.Println("Failed to pack in write DataLen:", err)
    		return nil, err
    	}
    	if err := binary.Write(buffer, binary.LittleEndian, message.GetMsgID()); err != nil {
    		fmt.Println("Failed to pack in write MsgId:", err)
    		return nil, err
    	}
    	if err := binary.Write(buffer, binary.LittleEndian, message.GetData()); err != nil {
    		fmt.Println("Failed to pack in write Data:", err)
    		return nil, err
    	}
    
    	return buffer.Bytes(), nil
    
    }
    
    // UnPack 拆包的实现
    func (d *DataPack) UnPack(binaryData []byte) (isticky.IMessage, error) {
    	// 创建一个二进制的io.Reader
    	dataBuffer := bytes.NewReader(binaryData)
    
    	// 解压Head信息
    	msg := &Message{}
    
    	// 读DataLen
    	if err := binary.Read(dataBuffer, binary.LittleEndian, &msg.DataLen); err != nil {
    		fmt.Println("Failed to unpack in read DataLen:", err)
    		return nil, err
    	}
    	// 读Id
    	if err := binary.Read(dataBuffer, binary.LittleEndian, &msg.Id); err != nil {
    		fmt.Println("Failed to unpack in read id/:", err)
    		return nil, err
    
    	}
    
    	// 注意到这里消息还没有解析
    	return msg, nil
    }
    
    

3.3 测试封包拆包的Server和Client

StickyBuns/sticky/datapack_test.go

package sticky

import (
	"fmt"
	"io"
	"net"
	"testing"
)

func TestDataPack(t *testing.T) {
	/*
		模拟服务器
	*/
	listener, err := net.Listen("tcp", "127.0.0.1:8080")
	if err != nil {
		fmt.Println("Failed to listen at 127.0.0.1:8080:", err)
		return
	}

	go func() {
		for {
			// 进行Accept
			conn, err := listener.Accept()
			if err != nil {
				fmt.Println("Failed to accept:", err)
				continue
			}
			go func(conn net.Conn) {
				dp := DataPack{}
				for {
					// 第一次从conn中读,把包的head(DataLen+Id)读取出来
					headData := make([]byte, dp.GetHeadLen())
					if _, err := io.ReadFull(conn, headData); err != nil {
						fmt.Println("Failed to unpack head:", err)
						return
					}
					// 第二次从conn中读取,之前head已经没有在conn中了,剩下的就是Data和其余消息体
					// headData里面只有头部,将其放入了拆包函数中,解析出DataLen和ID的具体取值
					// 尤其是DataLen他是关键的,因为后面需要该值来确定数据的长度
					msgHead, err := dp.UnPack(headData)
					if err != nil {
						fmt.Println("Failed to unpack:", err)
						return
					}
					msgData := make([]byte, msgHead.GetMsgLen())
					if _, err := io.ReadFull(conn, msgData); err != nil {
						fmt.Println("Failed to unpack data:", err)
					}
					fmt.Printf("---->DataLen=%d\tDataId=%d\tData=%s\n", msgHead.GetMsgLen(), msgHead.GetMsgID(), msgData)
				}
			}(conn)
		}
	}()

	/*
		模拟客户端
	*/
	conn, err := net.Dial("tcp", "127.0.0.1:8080")
	if err != nil {
		fmt.Println("Failed dial to 127.0.0.1:8080:", err)
		return
	}
	// 创建一个打包对象
	dp := DataPack{}
	// 新建一个消息体1
	msg1 := &Message{
		Id:      1,
		DataLen: 5,
		Data:    []byte("hello"),
	}
	sendMsg1, err := dp.Pack(msg1)
	if err != nil {
		fmt.Println("Failed to pack msg1:", err)
		return
	}
	msg2 := &Message{
		Id:      1,
		DataLen: 5,
		Data:    []byte("nihao"),
	}
	sendMsg2, err := dp.Pack(msg2)
	// 将两个消息一起发送
	sendMsg1 = append(sendMsg1, sendMsg2...)
	_, err = conn.Write(sendMsg1)
	if err != nil {
		return
	}

	// 主程序阻塞
	select {}
}

输出结果:

=== RUN   TestDataPack
---->DataLen=5	DataId=1	Data=hello
---->DataLen=5	DataId=1	Data=nihao

即实现了上述的功能。如果你的业务非常的复杂,那么在这里就需要按照自身的业务对消息进行封装,然后分别根据自己对消息的封装然后分别设计自身的封包和拆包的方法。

在 Go ,可以通过 `net.Conn` 接口来写入 TCP 数据包。如果要修改已经发送的数据包,则需要使用底层的 socket API。 下面是一个示例,演示如何使用 Go 的 `syscall` 包来修改已经发送的 TCP 数据包: ```go package main import ( "fmt" "net" "syscall" ) func main() { // 连接到服务器 conn, err := net.Dial("tcp", "localhost:8080") if err != nil { panic(err) } defer conn.Close() // 发送一些数据 message := []byte("hello") _, err = conn.Write(message) if err != nil { panic(err) } // 修改已经发送的数据 fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_TCP) if err != nil { panic(err) } defer syscall.Close(fd) // 构造 IP 数据包头部 ipHeader := []byte{ 0x45, 0x00, 0x00, 0x28, 0x00, 0x00, 0x40, 0x00, 0x40, 0x06, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x01, 0x7f, 0x00, 0x00, 0x01, } // 构造 TCP 数据包头部 tcpHeader := []byte{ 0x00, 0x50, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x02, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, } // 构造 TCP 数据 data := []byte("modified") // 将 IPTCP 头部、数据拼接起来 packet := append(ipHeader, tcpHeader...) packet = append(packet, data...) // 发送数据包 err = syscall.Sendto(fd, packet, 0, &syscall.SockaddrInet4{ Port: 8080, Addr: [4]byte{127, 0, 0, 1}, }) if err != nil { panic(err) } fmt.Println("修改数据成功") } ``` 在上面的示例,我们首先通过 `net.Dial()` 方法连接到服务器,然后发送了一些数据。接着,我们使用 `syscall.Socket()` 创建了一个原始的 TCP 套接字,利用这个套接字可以构造和发送 TCP 数据包。我们构造了 IPTCP 头部,将它们与修改过的数据拼接起来,最后通过 `syscall.Sendto()` 发送数据包。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值