文章目录

1 项目总体架构2 项目需求2.1 服务器职责2.2 消息的格式和定义

3 基于Tcp连接的通信方式3.1 通道层实现GameChannel类3.1.1 TcpChannel类3.1.2 Tcp工厂类3.1.3 创建主函数,添加Tcp的监听套接字3.1.4 代码测试

3.2 协议层与消息类3.2.1 消息的定义3.2.2 消息类-用户请求对象的创建3.2.3 protoc消息的创建3.2.4 消息对象的构造与解析3.2.5 代码测试-13.2.6 报文里的多条请求3.2.7 Tcp报文粘包的处理3.2.8 数据包代码测试3.2.8.1 完整数据3.2.8.2 数据缺失和错误

3.2.9 协议和通道相互绑定3.2.9.1 循环引用的问题3.2.9.1 相互绑定的实现3.2.9.3 代码测试

3.3 业务层玩家类的创建3.3.1 在Role中绑定协议3.3.2 在协议中绑定一个role3.3.3 在tcp中绑定协议和玩家对象3.3.4 重写协议层获取角色处理对象3.3.5 修改角色Init函数3.3.6 测试代码

1 项目总体架构

2 项目需求

2.1 服务器职责

服务器职责(接收客户端数据,发送数据给客户端)

新客户端连接后,向其发送ID和名称新客户端连接后,向其发送周围玩家的位置新客户端连接后,向周围玩家发送其位置收到客户端的移动信息后,向周围玩家发送其新位置收到客户端的移动信息后,向其发送周围新玩家位置收到客户端的聊天信息后,向所有玩家发送聊天内容客户端断开时,向周围玩家发送其断开的消息

2.2 消息的格式和定义

消息定义

每一条服务器和客户端之前的消息都应该满足以下格式

消息内容的长度(4个字节,低字节在前)| 消息ID(4个字节,低字节在前)| 消息内容 |

消息以及其处理方式已经在客户端实现,本项目要实现的是服务器端的相关处理

详细定义如下

消息ID消息内容发送方向客户端处理服务器处理1玩家ID和玩家姓名S->C记录自己ID和姓名无2聊天内容C->S无广播给所有玩家3新位置C->S无处理玩家位置更新后的信息同步200玩家ID,聊天内容/初始位置/动作(预留)/新位置S->C根据子类型不通而不同无201玩家ID和玩家姓名S->C把该ID的玩家从画面中拿掉无202周围玩家们的位置S->C在画面中显示周围的玩家无

3 基于Tcp连接的通信方式

3.1 通道层实现GameChannel类

GameChannel::GetInputNextStage 函数中直接返回成员变量中的协议对象GameChannel 的析构函数中要一并从kernel中摘掉协议对象,玩家对象并析构之GameChannelFac::CreateTcpDataChannel 函数要一并创建通道对象,协议对象,玩家对象,并将这三者绑定起来,添加到kernel中

3.1.1 TcpChannel类

使用框架提供的Tcp通信类创建GameChannel类继承ZinxTcpData,重写GetInputNextStage函数,将tcp收到的数据交给协议对象解析

每个协议对象只处理本通道的协议数据

GameProtocol* m_proto = NULL;

创建对象啊以后交给m_proto,通过该变量访问通道内的数据

AZinxHandler* GameChannel::GetInputNextStage(BytesMsg& _oInput)

{

return m_proto;

}

3.1.2 Tcp工厂类

创建GameChannelFac类用于创建基于连接的GameChannel对象因为玩家是通过tcp连接,所以tcp通道,协议对象,和玩家对象是一对一对一的绑定关系创建通道的时候,需要创建协议,并且绑定协议对象

ZinxTcpData* GameConnFact::CreateTcpDataChannel(int _fd)

{

/*创建tcp通道对象*/

auto pChannel = new GameChannel(_fd);

/*创建协议对象*/

auto pProtocol = new GameProtocol();

/*绑定协议对象*/

pChannel->m_proto = pProtocol;

/*将协议对象添加到kernel, 注意参数需要为指针*/

ZinxKernel::Zinx_Add_Proto(*pProtocol);

return pChannel;

}

3.1.3 创建主函数,添加Tcp的监听套接字

#include "GameChannel.h"

int main()

{

ZinxKernel::ZinxKernelInit();

/*添加监听通道:需要端口号和连接*/

ZinxKernel::Zinx_Add_Channel(*(new ZinxTCPListen(8899, new GameConnFact())));

ZinxKernel::Zinx_Run();

ZinxKernel::ZinxKernelFini();

}

3.1.4 代码测试

设置标准输入

UserData* GameProtocol::raw2request(std::string _szInput)

{

cout << _szInput << endl;

return nullptr;

}

3.2 协议层与消息类

GameProtocol::GetMsgProcessor 函数即返回绑定的玩家对象GameProtocol::GetMsgSender 函数即返回绑定的通道对象GameProtocol::response2raw 函数要返回消息内容编码后的字节流(将GameMsg 对象中每个消息对象序列化并结合长度消息ID一起粘合起来)GameProtocol::raw2request 函数要将一串tcp数据流转换成游戏消息

3.2.1 消息的定义

//h

enum MSG_TYPE {

MSG_TYPE_LOGIN_ID_NAME = 1,

MSG_TYPE_CHAT_CONTENT = 2,

MSG_TYPE_NEW_POSTION = 3,

MSG_TYPE_BROADCAST = 200,

MSG_TYPE_LOGOFF_ID_NAME = 201,

MSG_TYPE_SRD_POSTION = 202

} enMsgType;

3.2.2 消息类-用户请求对象的创建

一个类一个请求

//h

class GameMsg :

public UserData

{

public:

/*用户的请求信息*/

google::protobuf::Message * pMsg = NULL;

enum MSG_TYPE {

MSG_TYPE_LOGIN_ID_NAME = 1,

MSG_TYPE_CHAT_CONTENT = 2,

MSG_TYPE_NEW_POSTION = 3,

MSG_TYPE_BROADCAST = 200,

MSG_TYPE_LOGOFF_ID_NAME = 201,

MSG_TYPE_SRD_POSTION = 202

} enMsgType;

/*已知消息内容创建消息对象*/

GameMsg(MSG_TYPE _type, google::protobuf::Message * _pMsg);

/*将字节流内容转换成消息结构*/

GameMsg(MSG_TYPE _type, std::string _stream);

/*序列化本消息*/

std::string serialize();

virtual ~GameMsg();

};

一个消息类里应该要放多条请求,每个请求一条消息

class MultiMsg :public UserData {

public:

std::list m_Msgs;

};

3.2.3 protoc消息的创建

protoc msg.proto --cpp_out=./

syntax="proto3";

package pb;

//无关选项,用于客户端

option csharp_namespace="Pb";

message SyncPid{

int32 Pid=1;

string Username=2;

}

message Player{

int32 Pid=1;

Position P=2;

string Username=3;

}

message SyncPlayers{

/*嵌套多个子消息类型Player的消息*/

repeated Player ps=1;

}

message Position{

float X=1;

float Y=2;

float Z=3;

float V=4;

int32 BloodValue=5;

}

message MovePackage{

Position P=1;

int32 ActionData=2;

}

message BroadCast{

int32 Pid=1;

int32 Tp=2;

/*根据Tp不同,Broadcast消息会包含:

聊天内容(Content)或初始位置(P)或新位置P*/

oneof Data{

string Content=3;

Position P=4;

/*ActionData暂时预留*/

int32 ActionData=5;

}

string Username=6;

}

message Talk{

string Content=1;

}

3.2.4 消息对象的构造与解析

GameMsg::GameMsg(MSG_TYPE _type, std::string _stream) :enMsgType(_type)

{

/*通过简单工厂构造具体的消息对象*/

switch (_type)

{

case GameMsg::MSG_TYPE_LOGIN_ID_NAME:

pMsg = new pb::SyncPid();

break;

case GameMsg::MSG_TYPE_CHAT_CONTENT:

pMsg = new pb::Talk();

break;

case GameMsg::MSG_TYPE_NEW_POSTION:

pMsg = new pb::Position();

break;

case GameMsg::MSG_TYPE_BROADCAST:

pMsg = new pb::BroadCast();

break;

case GameMsg::MSG_TYPE_LOGOFF_ID_NAME:

pMsg = new pb::SyncPid();

break;

case GameMsg::MSG_TYPE_SRD_POSTION:

pMsg = new pb::SyncPlayers();

break;

default:

break;

}

/*将参数解析成消息对象内容*/

pMsg->ParseFromString(_stream);

}

std::string GameMsg::serialize()

{

std::string ret;

pMsg->SerializeToString(&ret);

return ret;

}

3.2.5 代码测试-1

3.2.6 报文里的多条请求

//h

class MultiMsg :public UserData {

public:

std::list m_Msgs; //注意此处要加命名空间

};

MultiMsg* pRet = new MultiMsg(); //此时没有用户请求

/*构造一条用户请求*/

GameMsg* pMsg = new GameMsg((GameMsg::MSG_TYPE)id, szLast.substr(8, iLength)); // iLength是正文的长度

pRet->m_Msgs.push_back(pMsg);

//Debug打印每条请求

for (auto single : pRet->m_Msgs)

{

cout << single->pMsg->Utf8DebugString() << endl;

}

3.2.7 Tcp报文粘包的处理

问题:tcp或类似的流式文件无法保证收到的数据按照期望的格式分割。

举例:服务器期望接收2个字节的数据作为一个合理请求。客户端发送了两个请求(四个字节)后,由于网络拥塞,服务器收到了1个字节后,recv返回,1秒钟后,数据到来,再次调用recv会收到3个字节。

常规套路:

设定报文边界,一般使用Tag Length Value的格式recv数据后,若接收缓冲区当前数据长度小于报文内规定长度,则保留当前缓冲区,下次recv数据后重新处理(缓存)若接收缓冲区数据长度大于等于报文内规定长度,则循环生成生成请求并保留后续多余的数据等待下次recv数据后重新处理(滑窗)

UserData* GameProtocol::raw2request(std::string _szInput)

{

MultiMsg* pRet = new MultiMsg(); //此时没有用户请求

szLast.append(_szInput);

while (1)

{

if (szLast.size() < 8)

{

break;

}

/*在前四个字节中读取消息内容长度*/

int iLength = 0;

iLength |= szLast[0] << 0;

iLength |= szLast[1] << 8;

iLength |= szLast[2] << 16;

iLength |= szLast[3] << 24;

/*中四个字节读类型id*/

int id = 0;

id |= szLast[4] << 0;

id |= szLast[5] << 8;

id |= szLast[6] << 16;

id |= szLast[7] << 24;

/*通过读到的长度判断后续报文是否合法*/

if (szLast.size() - 8 < iLength)

{

/*本条报文还没够,啥都不干*/

break;

}

/*构造一条用户请求*/

GameMsg* pMsg = new GameMsg((GameMsg::MSG_TYPE)id, szLast.substr(8, iLength)); // iLength是正文的长度

pRet->m_Msgs.push_back(pMsg);

/*弹出已经处理成功的报文*/

szLast.erase(0, 8 + iLength);

}

//Debug打印每条请求

for (auto single : pRet->m_Msgs)

{

cout << single->pMsg->Utf8DebugString() << endl;

}

return pRet;

}

/*参数来自业务层,待发送的消息

返回值转换后的字节流*/

std::string * GameProtocol::response2raw(UserData & _oUserData)

{

int iLength = 0;

int id = 0;

std::string MsgContent;

GET_REF2DATA(GameMsg, oOutput, _oUserData);

id = oOutput.enMsgType;

MsgContent = oOutput.serialize();

iLength = MsgContent.size();

auto pret = new std::string();

pret->push_back((iLength >> 0) & 0xff);

pret->push_back((iLength >> 8) & 0xff);

pret->push_back((iLength >> 16) & 0xff);

pret->push_back((iLength >> 24) & 0xff);

pret->push_back((id >> 0) & 0xff);

pret->push_back((id >> 8) & 0xff);

pret->push_back((id >> 16) & 0xff);

pret->push_back((id >> 24) & 0xff);

pret->append(MsgContent);

return pret;

}

3.2.8 数据包代码测试

3.2.8.1 完整数据

08000000010000000801120474657374

08 00 00 00 - 前4个字节存储数据消息的长度,变量值是数据消息的长度为8个字节。 01 00 00 00 - 第5-8个字节存储的是用户的ID,变量值表示用户ID是1 08 01 12 04 74 65 73 74 - 末尾8个字节表示数据消息的全部内容

3.2.8.2 数据缺失和错误

收到数据以后,啥都不干

3.2.9 协议和通道相互绑定

3.2.9.1 循环引用的问题

一般地来说,不涉及使用类的成员的时候,尽量避免使用头文件,直接声明一个类即可。

在GameChannel.h中引用了头文件"GameProtocol.h"

#pragma once

#include

#include"GameProtocol.h"

class GameChannel :

public ZinxTcpData

{

public:

GameChannel(int _fd);

virtual ~GameChannel();

GameProtocol * m_proto = NULL;

};

如果在GameProtocol.h中引用GameChannel.h,则会造成循环引用。 处理办法是,直接在前面声明相关的类。

#pragma once

#include

class GameChannel; //避免循环引用

class GameProtocol :

public Iprotocol

{

std::string szLast; //上次未来得及处理的报文

public:

GameChannel* m_channel = NULL;

GameProtocol() ;

virtual ~GameProtocol();

};

3.2.9.1 相互绑定的实现

3.2.9.3 代码测试

pb::SyncPid* pmsg = new pb::SyncPid();

pmsg->set_pid(1);

pmsg->set_username("test");

GameMsg gm(GameMsg::MSG_TYPE_LOGIN_ID_NAME, pmsg);

auto output = gm.serialize();

for (auto byte : output)

{

printf("%02X ", byte);

}

puts("");

char buff[] = { 0x08, 0x01, 0x12, 0x04 ,0x74, 0x65, 0x73, 0x74 };

std::string input(buff, sizeof(buff));

auto ingm = GameMsg(GameMsg::MSG_TYPE_LOGIN_ID_NAME, input);

std::cout << dynamic_cast (ingm.pMsg)->pid() << std::endl;

std::cout << dynamic_cast (ingm.pMsg)->username() << std::endl;

收到数据

07 00 00 00 02 00 00 00 0A 05 68 65 6C 6C 6F

07 00 00 00 - 数据消息的长度是7个字节 02 00 00 00 - 消息ID是2 0A 05 68 65 6C 6C 6F - 转换成string代表"hello"

3.3 业务层玩家类的创建

3.3.1 在Role中绑定协议

class GameProtocol;

class GameRole :

public Irole

{

public:

GameRole() ;

virtual ~GameRole();

// 通过 Irole 继承

virtual bool Init() override;

virtual UserData* ProcMsg(UserData& _poUserData) override;

virtual void Fini() override;

GameProtocol* m_pProto = NULL;

};

3.3.2 在协议中绑定一个role

class GameChannel; //避免循环引用

class GameRole;

class GameProtocol :

public Iprotocol

{

std::string szLast; //上次未来得及处理的报文

public:

GameChannel* m_channel = NULL;

GameRole* m_Role = NULL;

GameProtocol() ;

virtual ~GameProtocol();

// 通过 Iprotocol 继承

virtual UserData* raw2request(std::string _szInput) override;

virtual std::string* response2raw(UserData& _oUserData) override;

virtual Irole* GetMsgProcessor(UserDataMsg& _oUserDataMsg) override;

virtual Ichannel* GetMsgSender(BytesMsg& _oBytes) override;

};

3.3.3 在tcp中绑定协议和玩家对象

ZinxTcpData* GameConnFact::CreateTcpDataChannel(int _fd)

{

/*创建tcp通道对象*/

auto pChannel = new GameChannel(_fd);

/*创建协议对象*/

auto pProtocol = new GameProtocol();

/*创建玩家对象*/

auto pRole = new GameRole();

/*绑定协议对象和通道对象*/

pChannel->m_proto = pProtocol;

pProtocol->m_channel = pChannel;

/*绑定协议对象和玩家对象*/

pProtocol->m_Role = pRole;

pRole->m_pProto = pProtocol;

/*将协议对象添加到kernel, 注意参数需要为指针*/

ZinxKernel::Zinx_Add_Proto(*pProtocol);

/*将玩家对象添加到kernel*/

ZinxKernel::Zinx_Add_Role(*pRole);

return pChannel;

}

3.3.4 重写协议层获取角色处理对象

Irole* GameProtocol::GetMsgProcessor(UserDataMsg& _oUserDataMsg)

{

return m_Role;

}

3.3.5 修改角色Init函数

bool GameRole::Init()

{

return true;

}

3.3.6 测试代码

/*处理游戏相关的用户请求*/

UserData* GameRole::ProcMsg(UserData& _poUserData)

{

/*测试:打印消息内容*/

GET_REF2DATA(MultiMsg, input, _poUserData);

for (auto single : input.m_Msgs)

{

cout << "type is" << single->enMsgType << endl;

cout << single->pMsg->Utf8DebugString() << endl;

}

return nullptr;

08000000010000000801120474657374

参考文章

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: