项目上有个小需求,使用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客户端测试没问题。当然,如果读者需要使用,里面有些变量还需要自己稍微改下。