其中 04_XX 部分.
登录是游戏开发中非常重要的一个功能,设计得不好会出大问题。不要认为登录只是简单的账号验证,其中包括的细节与异步非常多。如果同一个账号同时在两个 Socket 上发起登录协议该如何处理,如果有大于 1000 个玩家同时登录怎么办。
游戏一般都会发布到各大平台,除非自己开发自己的 SDK
上面的一层是客户端、中间的一层是第三方平台、底部一层是游戏服务端
每个平台接入的 API 各有不同,但大部分采用了 HTTP。
按照之前的框架设计,验证账号功能,只需要做一个 ThreadObject 即可,在这个类中,处理关于账号验证相关的协议。
//proto_id.proto
enum MsgId{
0;//proto3的枚举,第一个必为0
MI_None = 1;
MI_TestMsg = 1001;//验证账号 C2L表示 Client to Login
C2L_AccountCheck = 1002;//验证返回结果
C2L_AccountCheckRes = 1005;//HTTP请求的返回协议
MI_AccountCheckToHttpRs =
}//C2L_AccountCheck
message AccountCheck{
string account = 1;
string password = 2;
string token = 3;
}
向第三方进行 Token 验证,发起 Http 请求,Http 请求有结果返回时,就定义一个协议包发到 ThreadObject 的消息队列,进行处理
//MI_AccountCheckToHttpRs
message AccountCheckToHttpRs{
int32 return_code = 1;
string account = 2;
}
通知客户端,在返回协议结构中定义了一个枚举,用来标识返回值的类型,以方便客户端界面显示文字。
//C2L_AccountCheckRs
message AccountCheckRs{
enum RetuenCode{
0;
ARC_OK = 1;
ARC_UNKONWN = 2;
ARC_NOT_FOUND_ACCOUNT = 3;
ARC_PASSWORD_WRONG =
...
}int32 return_code = 1;
}
定义完协议后,服务端需要注册 ThreadObject 处理这些协议。
class Account:public ThreadObject{
public:
bool Init() override;
void RegisterMsgFunction() override;
void Update() override;
private:
void HandleAccountCheck(Packet* pPacket);
void HandleAccountCheckToHttpRs(Packet* pPacket);
private:
;
PlayerMgr _playerMgr};
Account 类是 login 进程特有的类,目的是处理客户端发来的账号验证请求,Account 类维护了 PlayerMgr 实例, 其作用是维护所有 Account 类中正在登录的账号对象。
基类 ThreadObject 是运行在线程之上的对象,如果把线程比喻成一条流水线,那么 ThreadObject 类就像运行在其上的包裹。ThreadObject 类 必须实现 3 个虚函数:Init、RegisterMsgFunction 和 Update,分别用于初始化、注册自己感兴趣的协议以及执行更新操作。
将 Account 类放置在线程中
void LoginApp::InitApp(){
("127.0.0.1",2233);
AddListenerToThread...
* pAccount = new Account();
Account->AddObjToThread(pAccount);
_pThreadMgr//ThreadMgr 将Account丢进线程前,会首先调用Account类的Init和RegisterMsgFunction两个函数
}
注册关注的协议
void Account::RegisterMsgFunction(){
(Proto::MsgId::C2L_AccountCheck,std::bind(this,&Account::HandleAccountCheck));
RegisterFunction(Proto::MsgId::MI_AccountCheckToHttpRs,std::bind(this,&Account::HandleAccountCheckToHttpRs));
RegisterFunction}
HandleAccountCheck 处理细节
void Account::HandleAccountCheck(Packet *pPacket)
{
// 从网络层传来的Packet
auto protoCheck = pPacket->ParseToProto<Proto::AccountCheck>();
const auto socket = pPacket->GetSocket();
auto pPlayer = _playerMgr.GetPlayer(protoCheck.account());
if (pPlayer != nullptr)
{
// 如果有相同账号正在登录,就返回客户端消息,同时关闭网络
::AccountCheckRs protoResult;
Proto.set_return_code(Proto::AccountCheckRs::ARC_LOGGING);
protoResultauto pRsPacket = new Packet(Proto::MsgId::C2L_AccountCheckRs, socket);
->SerializeToBuffer(protoResult);
pRsPacket(pRsPacket);
SendPacket// 关闭网络
const auto pPacketDis = new Packet(Proto::MsgId::MI_NetworkDisconnectToNet, socket);
(pPacketDis);
DispatchPacketreturn;
}
// 更新信息
.AddPlayer(socket, protoCheck.account(), protoCheck.password());
_playerMgr// 验证账号(向第三方发送http请求)
*pHttp = new HttpRequestAccount(protoCheck.account(), protoCheck.password());
HttpRequestAccount ::GetInstance()->AddObjToThread(pHttp);
ThreadMgr}
为了异步账号验证,创建一个处理 HTTP 的 ThreadObject HttpRequest
客户端向 Account 发送请求,Account 根据判断创建新的 HttpRequest 对象(是 ThreadObject)加入到 Actor 线程中,同时 HttpRequest 的 Update 可以检查请求状态等,当判断请求返回后可以将消息派发给 ThreadMgr,ThreadMgr 进行广播到 Account。再由 Account 判断并向 Client 进行返回。整体下来几乎所有都是异步过程。
怎样非阻塞的进行 HTTP 请求,请求一个 HTTP 数据时,从请求到返回阶段
enum HttpRequestState{
,//发送数据
HRS_Send,//等待数据
HRS_Process,//完成
HRS_Over,//完成后的非激活状态,等待线程删除,以为ThreadObject不使用后因从ActorThread删除
HRS_NoActive,//请求超时
HRS_Timeout};
真正的核心在于 Update 函数,每一帧都在检查自己的状态,一旦收到数据就要更新自己的状态,还可以处理请求超时判断。
void HttpRequest::Update()
{
switch (State)
{
case HRS_Send:
{
if (ProcessSend())
{
= HRS_Process;
State }
}
break;
case HRS_Process:
{
if (Process())
{
= HRS_Over;
State }
}
break;
case HRS_Over:
{
();
ProcessOver= HRS_NoActive;
State = false;
_active }
break;
//...
}
}
需要特别注意的是,要保证 Update 的每一帧都不能阻塞
让 HttpRequestAccount 继承 HttpRequest 类
class HttpRequestAccount:public HttpRequest{
public:
(std::string account, std::string password);
HttpRequestAccountprotected:
void ProcessMsg(Json::Value value) override;
};
//初始化请求数据
::HttpRequestAccount(std::string account,std::string password):HttpRequest(account){
HttpRequestAccount= password;
_password = CRS_None;
_curlRs = HttpRequestMethod::HRM_Post;
_method = "192.168.0.120/member_login_t.php";
_url .append("account=").append(_account);
_params.append("&password=").append(_password);
_params}
//处理HTTP返回JSON数据
void HttpRequestAccount::ProcessMsg(Json::Value value){
auto code = Proto::AccountCheckRs::ARC_UNKONWN;
int httpCode = value["returncode"].asInt();
if(httpcode == 0){
= Proto::AccountCheckRs::ARC_OK;
code }else if(httpcode == 2){
= Proro::AccountCheckRs::ARC_NOT_FOUND_ACCOUNT;
code }else if(httpcode == 3){
= Proto::AccountCheckRs::ARC_PASSWORD_WRONG;
code }
//广播MI_AccountCheckToHttpRs包
::AccountCheckToHttpRs checkProto;
Proto.set_account(_account);
checkProto.set_return_code(code);
checkProtoauto pCheckPacket = new Packet(Proto:MsgId::MI_AccountCheckToHttpRs, 0);
->SerializeToBuffer(checkProto);
pCheckPacket(pCheckPacket);
DispatchPacket}
然后在 Account 接收到 MI_AccountCheckToHttpRs 包后,进行相应处理。
一个独立的 ThreadObject 的信息是闭塞的,它只处理从消息队列收到的协议,其结果再通过协议发送出去。发送协议的 类并不知道接收协议的类如何处理,也不关心谁去处理。这是低耦合产生的根本原因。Account 类并不知道是谁在处理 HTTP 请求,也不知道谁会将返回结果给它。HttpRequest 类处理完数据之后不知道这些数据谁会需要,只要发送出去就 好了。
现在还是存在一些问题的。举个例子,如果有两张地图,每张地图上有无数个玩家,地图是线程包裹类(ThreadObject),玩家是地图类中的数据,如果玩家发出一个 move 指令,两个地图都注册了要接受 move 指令协议包,但是发出指令的玩家只可能在其中一个地图上,这要如何处理。
需要设计,某个特定的 Socket 发出的协议只能由某个特定的类来处理。
这一问题其实来自于 MessageList,在 ThreadObject 接收到协议包后,判断是否关注相应的 MsgId 后就将协议包丢进了 MessageList 内,并没有做其他判断。
用新的结构 MessageCallBackFunctionInfo 容纳之前 MessageList 中的函数与数据
//抽象
class MessageCallBackFunctionInfo{
public:
virtual bool IsFollowMsgId(Packet* packet) = 0;
virtual void ProcessPacket() = 0;
void AddPacket(Packet* pPacket);
protected:
std::mutex _msgMutex;
std::list<Packet*> _msgList;
};
class MessageList : public IDisposable
{
public:
void AttachCallBackHandler(MessageCallBackFunctionInfo *pCallback)
{
= pCallback;
_pCallBackFuns }
void Disposr() override;
bool IsFollowMsgId(Packet* packet)const;
void ProcessPacket() const;
void AddPacket(Packet* pPacket)const;
static void DispatchPacket(Packet* pPacket);
static void SendPacket(Packet* pPacket);
protected:
*_pCallBackFuns{nullptr};
MessageCallBackFunctionInfo };
//实现MessageCallBackFunctionInfo不会进行过滤的
class MessageCallBackFunction: public MessageCallBackFunctionInfo{
public:
using HandleFunction = std::function<void(Packet*)>;
void RegisterFunction(int msgId, HandleFunction function);
bool IsFollowMsgId(Packet* packet) override;
void ProcessPacket() override;
std::map<int,HandleFunction>& GetCallBackHandler(){return _callbackHandle;}
protected:
std::map<int,HandleFunction> _callbackHandle;
};
//实现MessageCallBackFunctionInfo会进行过滤的
template <class T>
class MessageCallBackFunctionFilterObj : public MessageCallBackFunction
{
public:
using HandleFunctionWithObj = std::function<void(T *, Packet *)>;
using HandleGetObject = std::function<T *(SOCKET)>;
void RegisterFunctionWithObj(int msgId,HandleFunctionWithObj function){
[msgId] = function;
_callbackHandleWithObj}
bool IsFollowMsgId(Packet *packet) override;
void ProcessPacket() override;
{nullptr};
HandleGetObject GetPacketObject
void CopyFrom(MessageCallBackFunction* pInfo);
private:
std::map<int, HandleFunctionWithObj> _callbackHandleWithObj;
};
重点其实在于 MessageCallBackFunctionFilterObj::IsFollowMsgId 其决定着是否将协议包放入 MessageList
template <class T>
bool MessageCallBackFunctionFilterObj<T>::IsFollowMsgId(Packet *packet)
{
// 判断没有过滤机制的Handler是否注册,注册了则返回
if (MessageCallBackFunction::IsFollowMsgId(packet))
{
return true;
}
// 有过滤机制
if (_callbackHandleWithObj.find(packet->GetMsgId()) != _callbackHandleWithObj.end())
{
if (GetPacketObject != nullptr)
{
// 执行过滤机制
*pObj = GetPacketObject(packet->GetSocket());
T if (pObj != nullptr)
{
return true;
}
}
}
return false;
}
在 MessageCallBackFunctionFilterObj 的 ProcessPacket 也要特殊处理,因为有无过滤机制的 Handler 与有过滤机制的。
template<class T>
void MessageCallBackFunctionFilterObj<T>::ProcessPacket()
{
std::list<Packet*> tmpList;
.lock();
_msgMutexstd::copy(_msgList.begin(),_msgList.end(),std::back_inserter(tmpList));
.clear();
_msgList.unlock();
_msgMutex//处理所有包
for(auto packet:tmpList){
//无过滤机制的挂载
const auto handleFilter = _callbackHandle.find(packet->GetMsgId());
if(handleIter != _callbackHandle.end())
{
->second(packet);
handleItercontinue;
}
//有过滤机制的挂载
auto iter = _callbackHandleWithObj.find(packet->GetMsgId());
if(iter!=_callbackHandleWithObj.end())
{
if(GetPacketObject!=nullptr)
{
* pObj = GetPacketObject(packet->GetSocket());
Tif(pObj!=nullptr)
{
->second(pObj,packet);
iter}
}
}
}
.clear();
tmpList}
//将MessageCallBackFunction拷贝到MessageCallBackFunctionFilterObj的基类部分
template<classT>
void MessageCallBackFunctionFilterObj<T>::CopyFrom(MessageCallBackFunction* pInfo){
auto copyFromData = pInfo->GetCallBackHandler();
std::transform(copyFromData.begin(),copyFromData.end(),std::back_inserter(_callbackHandle,[](const std::pair<int,HandleFunction>&p){
return p;
}));
}
ThreadObj 是 MessageList 的派生类,例如有一个 Robot 是一个 ThreadObj
void Robot::RegisterMsgFunction(){
auto pMsgCallBack = new MessageCallBackFunctionFilterObj<Robot>();
->GetPacketObject = [this](SOCKET socket)->Robot*{
pMsgCallBackif(this->GetSocket() == socket)
{
return this;
}
return nullptr;
};
(pMsgCallBack);
AttachCallBackHandler->RegisterFunctionWithObj(Proto::MsgId::C2L_AccountCheckRs,BindFunP2(this, &Robot::HandleAccountCheckRs));
pMsgCallBack}
这样在 Actor Thread 中即时,有许多的 Robot,但对于每个 Robot 对象其只会关注是自己本身的 Proto::MsgId::C2L_AccountCheckRs 协议包,而不会处理其他 Socket 的此协议。
这样一来,每个 ThreadObject 使用 MessageList,可以使用无过滤机制的 MessageCallBackFunction 或者有过滤机制的 MessageCallBackFunctionFilterObj,非常的灵活,面向对象设计。
例如游戏中的 NPC 就可以用状态机来做,Boss 的 AI 就比较复杂,通常一个层次可以打开另一个层次,每一层次的 AI 调用技能都不相同。Boss 可以做出不同的攻击和回血操作,或者在需要时可以改变自己的行走速度。这不是仅用 switch、if else 能解决的,否则代码会又臭又长可维护性极差。
为了让处理变得一目了然,需要做状态机模板,做通用的状态机组件。
对状态机状态节点进行抽象出基本的操作
template <typename enumType, class T>
class StateTemplate
{
public:
()
StateTemplate{
}
void SetParentObj(T *pPobj)
{
= pObj;
_pParentObj }
virtual ~StateTemplate() {}
virtual enumType GetState() = 0; // 得到当前的状态类型
virtual enumType Update() = 0; // 状态机内部的更新
virtual void EnterState() = 0; // 进入本状态
virtual void LeaveState() = 0; // 离开本状态
protected:
*_pParentObj;
T };
template <typename enumType, class T>
class StateTemplate
{
public:
(){}
StateTemplatevoid SetParentObj(T* pObj){
= pObj;
_pParentObj }
public:
virtual ~StateTemplate(){}
virtual enumType GetState() = 0; // 得到当前的状态类型
virtual enumType Update() = 0; // 状态机内部的更新
virtual void EnterState() = 0; // 进入本状态
virtual void LeaveState() = 0; // 离开本状态
};
template <typename enumType, class StateClass, class T>
class StateTemplateMgr
{
public:
virtual ~StateTemplateMgr()
{
if (_pState != nullptr)
{
delete _pState;
}
}
void InitStateTemplateMgr(enumType defaultState)
{
= defaultState;
_defaultState ();
RegisterState}
void ChangeState(enumType stateType)
{
*pNewState = CreateStateObj(stateType);
StateClass if (pNewState == nullptr)
{
return;
}
// 当前处于某节点,则需要先退出
if (_pState != nullptr)
{
->LeaveState();
_pStatedelete _pState;
}
// 进入新状态节点
= pNewState;
_pState ->EnterState();
_pState}
//每帧更换,先执行当前节点的Update根据Update决定状态要不要更换
void UpdateState()
{
if (_pState == nullptr)
{
(_defaultState);
ChangeState}
// 当前状态节点表示需要进行状态改变
= _pState->Update();
enumType curState if (curState != _pState->GetState())
{
(curState);
ChangeState}
}
protected:
// 实现RegisterState 注册节点类型的对象创建的函数到_dynCreateMap
// 使用RegisterStateClass将函数指针加入到map
virtual void RegisterState() = 0;
public:
// 为函数指针起别名 参数为空返回类型为StateClass指针的函数
// 这样的函数指针类型 起别名为 CreateIstancePt
typedef StateClass *(*CreateIstancePt)();
*CreateStateObj(enumType enumValue)
StateClass {
auto iter = _dyCreateMap.find(enumValue);
if (iter == _dynCreateMap.end())
{
return nullptr;
}
= iter->second;
CreateIstancePt np *pState = np();
StateClass ->SetParentObj(static_cast<T *>(this));
pStatereturn pState;
}
void RegisterStateClass(enumType enumValue, CreateIstancePt np)
{
[enumValue] = np;
_dynCreateMap}
protected:
std::map<enumType, CreateIstancePt> _dynCreateMap;
*_pState{nullptr}; // 现在的状态节点
StateClass ; // 默认的状态节点类型
enumType _defaultState};
其实在 ActorThread 不仅可以加入处理 Client 协议包的 ThreadObj,其实还可以做一些模拟 Client 的 ThreadObj,ThreadObj 连接服务器本身,可以向服务端发送请求,并接受服务端的返回,而且这是异步过程,充分体现 Actor Thread Object 的优势。
在游戏制作中有一个很重要的数据结构—状态机
前面如,HttpRequest::Update 中,我们只是使用 switch 来模拟状态机的运行。
void HttpRequest::Update(){
switch(State){
case HRS_Send:...break;
case HRS_Process:...break;
}
}
使用上面的状态机模板应该怎么做呢
对于管理状态的类,有几个必要的处理
例如做一个进行登录测试的 Robot
定义状态枚举
enum RobotStateType{
, //正在连接Login
RobotState_Login_Connecting, //连接成功
RobotState_Login_Connected, //登录成功,即账号验证成功
RobotState_Login_Logined//...
};
写 Robot 状态类,还是抽象类,需要每个具体的 RobotState 继承 RobotState
class RobotState:public StateTemplate<RobotStateType,Robot>
{
public:
() override;
RobotStateType Updatevirtual RobotStateType OnUpdate()
{
return GetState();
}
void EnterState() override{};
virtual void OnEnterState()
{
}
void LeaveState() override;
virtual void OnEnterState()
{
}
void LeaveState() override;
virtual void OnLeaveState()
{
}
};
更详细的 Robot 内容可以看
https://github.com/gaowanlu/GameBookServer/tree/master/04_02_engine_robots/src/tools/robots