go byte切片和C struct互转

本文介绍如何使用Go语言重写一个C++ TCP服务器,处理基于二进制协议的消息。文章详细展示了如何定义Go结构体来匹配原始C++的二进制协议,并实现了TCP粘包的解包和打包过程,包括使用`binary.Read`和`binary.Write`处理字节序。此外,还提供了一个简单的客户端测试案例表明代码的正确性。
摘要由CSDN通过智能技术生成

项目上有个小需求,使用go重写一个服务器,替换原来的c++写的服务器。

由于原来c++的服务器和外部采用的协议是tcp 二进制协议,没有用protobuf,所以要稍微多做点工作。

原二进制协议大致是如下这样:

#pragma pack(1)
//基本的报文头
typedef struct tagMsgHead
{
	unsigned short     nCmd;
	unsigned short     nResvered;
	long               nSize;     //数据包的总的长度
}MSGHEAD,*PBASEMSGHEAD;

typedef struct tagPVCSLoginRequest{
	int n;
}PVCSLoginRequest, *PPVCSLoginRequest;

typedef struct tagPVCSLoginResponse{
	int error_code;
}PVCSLoginResponse, *PPVCSLoginResponse;
#pragma pack()

一个请求的数据包就是MSGHEAD+PVCSLoginRequest。

注意,上面的协议采用1字节对齐的方式存储。

由于目前我还没看到go里面可以设置类似的1字节对齐方式,所以翻译的协议全采用byte数组,如下:

type tagPVCSBaseMsgHeader struct {
	cmd      [2]byte
	reserved [2]byte
	size     [4]byte
}

type tagPVCSLoginRequest struct {
	n [4]byte
}

type tagPVCSLoginResponse struct {
	errorCode [4]byte
}

采用byte数组的一个好处是,我不需要再关心C++/C里的类型在go的编译器下是不是同样长度,也避免了go里字节对齐的规则。

每个服务器,都涉及到解包的问题,也就是处理tcp粘包。这就涉及到把收到的数据进行解析,为了解析方便,可以把数据转到相应的结构体类型。不过既然知道协议了,硬解析也是可以的。

byte切片转struct主要用的binary.Read和binary.Write,这里牵涉到一个字节序,也就是大端/小端的问题,可以参考我另一篇文章:大端/小端

一般我们的机器采用的都是小端序,所以Read一般用binary.LittleEndian解码方式。

整个解包和发包代码如下:

package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"net"
	"strconv"
	"unsafe"
)

func startListen() {
	src := "0.0.0.0:" + strconv.Itoa(config.serverPort)
	listener, _ := net.Listen("tcp", src)
	fmt.Println("listening on: ", src)

	defer listener.Close()

	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("some connection err: %s\n", err)
			continue
		}

		fmt.Println("get one connection, remote:", conn.RemoteAddr().String())

		remoteClient := &RemoteClient{}
		//go
		go remoteClient.handleConnection(conn)
	}
}

type RemoteClient struct {
	readerChan chan []byte
	connection net.Conn
	isLogined  bool
}

func (client *RemoteClient) handleConnection(conn net.Conn) {

	defer conn.Close()

	client.isLogined = false
	client.connection = conn
	client.readerChan = make(chan []byte, config.maxPacketLen)
	receiveBuffer := make([]byte, config.maxPacketLen)

	var curReceivedLen int32
	var curPacketLen int32
	curReceivedLen = 0
	curPacketLen = 0

	msgHeader := tagPVCSBaseMsgHeader{}

	go client.handleClientData()

	for {
		n, err := client.connection.Read(receiveBuffer[curReceivedLen:])
		if err != nil {
			curReceivedLen = 0
			continue
		}
		curReceivedLen += int32(n)
		if curReceivedLen < int32(unsafe.Sizeof(msgHeader)) {
			continue
		}
		//fmt.Println("receiveBuf:", receiveBuffer)
		if curPacketLen == 0 {
			binary.Read(bytes.NewBuffer(receiveBuffer[4:8]), binary.LittleEndian, &curPacketLen)
			fmt.Println("curPacketLen:", curPacketLen)
			//deal the illegal packet
			if curPacketLen > int32(config.maxPacketLen) {
				return
			}

			if curPacketLen <= curReceivedLen {
				client.readerChan <- receiveBuffer[0:curPacketLen]
				if curPacketLen < curReceivedLen {
					copy(receiveBuffer[0:], receiveBuffer[curPacketLen:]) //copy the left data
				}
				curReceivedLen -= curPacketLen
				curPacketLen = 0
			} else {
				continue
			}
		} else {
			if curPacketLen > curReceivedLen {
				continue
			} else {
				client.readerChan <- receiveBuffer[0:curPacketLen]
				if curPacketLen < curReceivedLen {
					copy(receiveBuffer[0:], receiveBuffer[curPacketLen:]) //copy the left data
				}
				curReceivedLen -= curPacketLen
				curPacketLen = 0
			}
		}
	}
}

func (client *RemoteClient) handleClientData() {
	for {
		select {
		case data := <-client.readerChan:
			if data == nil {
				goto _exit
			} else {
				var cmd int16
				binary.Read(bytes.NewBuffer(data[0:2]), binary.LittleEndian, &cmd)
				fmt.Println("cmd:", cmd)
				switch cmd {
				case int16(pvcsCmdLoginRequest):
					client.handleLoginRequest(data[8:])
				case int16(pvcsCmdKeepAlive):
					client.handleKeepAlive(data[8:])
				default:
					fmt.Println("get invalid cmd:", cmd)
				}
			}
		}
	}
_exit:
}

func (client *RemoteClient) handleLoginRequest(data []byte) {
	client.isLogined = true

	responseBuffer := make([]byte, config.maxPacketLen)
	var responseMsgHeader tagPVCSBaseMsgHeader
	int16ToByteSlice(responseMsgHeader.cmd[0:2], pvcsCmdLoginResponse)

	var loginResponseInfo tagPVCSLoginResponse
	int32ToByteSlice(loginResponseInfo.errorCode[0:4], int32(10))

	packetLen := int32(unsafe.Sizeof(responseMsgHeader) + unsafe.Sizeof(loginResponseInfo))
	int32ToByteSlice(responseMsgHeader.size[0:4], packetLen)

	newBuf := new(bytes.Buffer)
	binary.Write(newBuf, binary.LittleEndian, responseMsgHeader)
	fmt.Println("newBuf1:", newBuf.Bytes())
	copy(responseBuffer[0:unsafe.Sizeof(responseMsgHeader)], newBuf.Bytes())

	newBuf2 := new(bytes.Buffer)
	binary.Write(newBuf2, binary.LittleEndian, loginResponseInfo)
	fmt.Println("newBuf2:", newBuf2.Bytes())
	copy(responseBuffer[unsafe.Sizeof(responseMsgHeader):], newBuf2.Bytes())
	client.connection.Write(responseBuffer[0:packetLen])
}

func (client *RemoteClient) handleKeepAlive(data []byte) {

}

以上代码,我写了个C++的tcp socket客户端测试没问题。当然,如果读者需要使用,里面有些变量还需要自己稍微改下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值