🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] 参考链接:https://blog.csdn.net/sky1work2?t=1 ## 服务器架构简图如下: ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8xNzcxNDc1NC1mZjA4MTg5ZTlmYWU2ZmMxLmpwZw) ## 连线说明: * **实线**:表示客户端登录流程,①②③④⑤表示登录流程,详细解释见下文。 * **虚线**:表示服务器间的连接,虚线箭头指向监听方。 ## 架构说明: | 英文名称 | 简称 | 中文名称 | 功能简介 | 单个大区需求数量 | 数量说明 | | --- | --- | --- | --- | --- | --- | | **GameClient** | GC | 游戏客户端 | 不解释 | n | 不超服务器承载上限均可 | | **LoginSer** | LS | 游戏登录服务器 | 用于登录验证的服务器 | 1 | 登录入口仅需一个 | | **GateSer** | GS | 游戏网关服务器 | 用于客户端与其场景服和中心服通讯的转接 | n | 根据客户端连接数量调整 | | **BalanceSer** | BS | 游戏负载均衡服务器 | 用于负载均衡,给客户端分配合理的网关服,其上连接多台GS | n | 根据实际需求配置 | | **SceneSer** | SS | 游戏场景服务器 | 用于游戏战斗的服务器 | n | 根据实际需求配置 | | **CentralSer** | CS | 游戏中心服务器 | 用于玩家数据管理,匹配等核心功能服务器 | 1 | 统一管理玩家数据 | | **LogSer** | | 游戏日志服务器 | 用于游戏日志收集 | 1 | 统一日志收集 | | **UserRedis** | | 游戏玩家数据缓存服务器 | 用于缓存玩家数据 | 1 | 统一数据缓存 | | **LogicRedis** | | 游戏逻辑数据缓存服务器 | 用于缓存游戏数据 | 1 | 统一数据缓存 | | **MySql** | | 游戏数据库 | 用于持久化游戏及玩家数据 | 1 | 统一数据存储 | | **Remote** | | 游戏远程控制端 | 用于输入gm命令等远程控制 | n | 根据实际需求配置 | ## 登录流程: * **①②**:客户端发送消息AskLogin到LS,LS根据客户端登录类型生成相应的url,通过消息队列*m\_SDKCallbackQueue*发送给子线程调用url来获取登录验证结果,子线程将url返回结果通过消息队列*m\_DBCallbackQueue*发送给主线程,根据返回结果将BS列表发送给客户端。**ps:这块需要了解boost库和liburl库的基本用法** * **③④**:客户端收到LS返回的BS服务器列表后,选择其中一台BS发送消息 *eMsgToBSFromGC_OneClientLogin* ,BS收到该消息后会向LS发消息 *eMsgToLSFromBC_OneClinetLoginCheck* 来进行身份验证,验证通过后会返回给 *BS一个eMsgToBSFromLS_OneClinetLoginCheckRet* 消息, BS给该玩家生成一个唯一token并将玩家token信息根据负载均衡策略发送 *eMsgToGSFromBS_OneUserLoginToken*消息 给目前负载较小的GS来进行登记,登记成功后GS返回其ip和端口等信息 发送 *eMsgToBSFromGS_OneUserLoginTokenRet* 到BS,进而发送给客户端。 * **⑤**:客户端收到GS的ip和端口等信息后,发送登录消息给GS,GS校验token成功后会将登录信息转发给CS,游戏玩家数据实际是在CS上加载并创建玩家实体。到此,玩家登录完成,后续玩家在CS和SS上的操作消息都会通过GS作为中间桥梁和客户端通讯。 ## 登录服务代码解析 监听端口:49997 供BS服务连接 监听端口:49996 供客户端连接 ## CIocpCtrl:IOCP控制类 多线程循环查询IOCP内部的网络事件,并分派处理.这里多个工作线程仅仅是将所有的网络事件放入循环 内部将创建2 * CPU个工作线程(Worker Item). 多线程调用函数 * 新建客户端连接 poListener->OnAccept(bRet, pstPerIoData); * 接受消息 poSock->OnRecv(dwByteTrabsferred); * 发送消息 poSock->OnSend(dwByteTrabsferred); ``` void OnExecute() { CCPSock* poSock; //代表一个套接字类 CCpListener* poListener; //IOCP监听器 ... //当有客户端请求创建连接时 if(pstPerHandleData->bListen) { poListener->OnAccept(bRet, pstPerIoData); } else { poSock = (CCPSock*)pstPerHandleData->ptr; ··· switch(pstPerIoData->nOp) { case IOCP_RECV: //接受数据 { poSock->DecPostRecv(); if (dwByteTrabsferred > 0) { poSock->OnRecv(dwByteTrabsferred); } else { INFO(_SDT("[%s:%d]CCPSock connID=%d error %d, close it, socket :%d "), MSG_MARK, poSock->GetConnectionID(), ::WSAGetLastError(), poSock->GetSock()); poSock->OnClose(); } } break; case IOCP_SEND: //发送数据 { poSock->DecPostSend(); if (dwByteTrabsferred > 0) { poSock->OnSend(dwByteTrabsferred); } else { INFO(_SDT("[%s:%d]CCPSock connID=%d error %d, close it"), MSG_MARK, poSock->GetConnectionID(), ::WSAGetLastError()); poSock->OnClose(); } } break; case IOCP_CLOSE: { poSock->OnClose(false); } break; default: ; } } ... } ``` ## CCpListener: IOCP监听器 IOCP控制类在有新客户端连接上来时调用监听器的OnAccept函数 ``` void CCpListener::OnAccept(BOOL bSucc, SPerIoData* pstPerIoData) { ... CUCConnection* poConnection = & pConnData->connection; //创建连接对象 ISDSession* poSession = m_poSessionFactory->CreateSession(poConnection); poConnection->SetSession(poSession); ... // // 应该先投递连接事件再关联套接口,否则可能出现第一个Recv事件先于连接事件入队列 // //CEventMgr::Instance()->PushEstablishEvt(poConnection, true, m_dwID); if(false == poSock->AssociateWithIocp()) { poSock->DoClose(); } else { //从内核中拷贝数据到内存中 if(false == poSock->PostRecv()) { poSock->DoClose(); } } } ``` ## CCPSock:套接字类 在CCPSocket::PostRecv();接收数据到内存, 并且接受数据计数+1 ``` bool CCPSocket::PostIRecv() { ... if (0 != WSARecv(m_hSock, &m_stRecvIoData.stWsaBuf, 1, &dwReadLen, &dwFlags, &m_stRecvIoData.stOverlapped, NULL)) { int errNo = WSAGetLastError(); if (errNo != WSA_IO_PENDING) { WARN(_SDT("[%s:%d]post WSARecv failed, errNo=%d, %p "), MSG_MARK, errNo, m_pRecvBuf); return false; } } IncPostRecv(); // 接受消息计数+1 ... } ``` CCPSock::OnRecv(DWORD dwBytes) 添加一个完整的数据包到接收数据事件中 ``` void CCPSock::OnRecv(DWORD dwBytes) { ... CEventMgr::Instance()->PushRecvEvt(m_pConnData, GetConnectionID(), p, nUsed); ... } ``` ## INetSessionMgr:网络会话管理类 主线程定时调用 INetSessionMgr::GetInstance()->Update(); ``` void INetSessionMgr::Update() { mNetModule->Run(); ... } ``` ## IUCNet* mNetModule : 网络模块 获取 NETEVT_RECV 接收数据事件 ``` bool CUCODENetWin::Run(INT32 nCount) { ... switch(stEvent.nType) { case NETEVT_RECV: _ProcRecvEvt(&stEvent.stUn.stRecv); break; ... } CEventMgr::Instance()->ReleaseNetEvt(pstEvent); } ``` ``` void CUCODENetWin::_ProcRecvEvt(SRecvEvt* pstEvent) { ... pConnData->connection.OnRecv(m_pRecvBuf, pstEvent->nLen); ... } ``` ## CClientSession : 客户端会话类 客户端连接类 CClientSession 收到数据回调 ``` void CUCConnection::OnRecv(const char* pData, INT32 nLen) { if(m_nConnStat != CONN_ASSOCIATE) { return; } SDASSERT(m_poSession != NULL); m_poSession->OnRecv(pData, nLen); //回调 } ``` > ISDSession*m_poSession 基类指针指向子类 CClientSession* 回调到 类 INetSession中,因为 CClientSession 是 INetSession子类 ``` void UCAPI INetSession::OnRecv(const char* pBuf, UINT32 dwLen){ ··· bool bRet = pNode->mHandle(pMsgData, n32MsgLen, this, pNetHeader->type); ··· } ``` 在类 CClientSession 中 收到第1消息:请求登录,放入登录队列 ``` bool CClientSession::Msg_Handle_Init(const char* pMsg, int n32MsgLength, INetSession* vthis, int n32MsgID) { // 收到第1消息:请求登录,放入登录队列 boost::shared_ptr<GCToLS::AskLogin> sLogin = ParseProtoMsg<GCToLS::AskLogin>(pMsg, n32MsgLength); if (!sLogin){ //ELOG(LOG_ERROR, "Login Fail With Msg Analysis Error."); SDKAsynHandler::GetInstance().PostToLoginFailQueue(eEC_TBInvalidToken, vthis->GetID()); return 0; } SDKAsynHandler::GetInstance().CheckLogin(*sLogin, vthis->GetID()); vthis->SetInited(true,true); return true; } ``` ``` int SDKAsynHandler::CheckLogin(GCToLS::AskLogin& sAskLogin, int gcnetid){ ... switch(un32platform) { case ePlatform_PC: sSendData = "PCTest"; break; } // 生产消息,往队列 m_SDKCallbackQueue 里加数据 PostMsg(sSendData.c_str(), sSendData.size(), sAskLogin.msgid(), gcnetid, (EUserPlatform)un32platform); } ``` ## 生产消费者模型1 通过 PostMsg()函数生产消息,往队列 m_SDKCallbackQueue 里加数据 ``` void PostMsg(const char* pMsg, int length, int msgid, int gcnetid, EUserPlatform eplat); ``` 在一个线程里,启动一个定时器 newtimer_cb,消费队列 m_SDKCallbackQueue ## 生产消费者模型2 SendToInsertData()函数生产消息 推送到队列 m_DBCallbackQueue ``` SdkConnector::GetInstance().SendToInsertData(sUserData.uin, sTempInfo, gcnetid); ``` 主线程,循环消费 m_DBCallbackQueue ``` SdkConnector::GetInstance().Update(); ``` 登录成功, 给客户端推送BS服务器列表 ``` void SdkConnector::Update(){ ... PostMsgToGC_NotifyServerList(gcnetid); ... } ```