如下文章原创版权归csdn wu_yanan2003所有,转载请按如下方式显式标明出处,以示尊重! 作者:wu_yanan2003 Bolg:http://blog.csdn.net/wu_yanan2003 简述 最近对高性能的服务器比较感兴趣,读过了DELPHI的Socker源码WebService及RemObject之后,高性能的服务器感兴趣。 你可能需要的以下知识才能更好的读懂一个商业源码: 1).SOCKET的I/O模型熟悉掌握。 2).面向对象技术的熟悉掌握。 3).Socket的API掌握。 4).多线程技术等。 5).一门熟悉的开发工具掌握,和多种语言的源码阅读能力。 我下的源码 LegendOfMir2_Server:共包含AdminCmd, DBSrv, GameGate, GameSvr,LoginGate, LoginSvr, SelGate七个工程文件。传奇的客户端源代码有两个工程,WindHorn和Mir2Ex。 我分析的, 主要是VC SQL版本的, DELPHI翎风源码不做分析, 另外下载了乐都WIL编辑器和乐都MPA地图编辑器这些工具. 客户端——WindHorn简述(DirectX)和传奇文件格式分析 1. RegHandler.cpp 注册表访问(读写)。 2. CWHApp派生CWHWindow,CWHWindow完成窗口的注册和创建。CWHWindow派生出CWHDXGraphicWindow,CWHDXGraphicWindow调用CWHWindow完成创建窗口功能,然后再调用CreateDXG()来初始化DirectX。 3. WHDefProcess.cpp在构造函数中获得CWHDXGraphicWindow句柄。 Clear函数中调用在后台缓存上进行绘图操作,换页至屏幕。 ShowStatus函数,显示状态信息。 DefMainWndProc函数,调用CWHDXGraphicWindow->MainWndProcDXG消息处理。 4. WHImage.cpp图象处理。加载位图,位图转换。优化处理。 5. WHSurface.cpp 主页面处理。 6. WHWilTexture.cpp 材质渲染。 WILTextureContainer: WIL容器类。m_pNext指向下一个WILTextureContainer,单链表。 7. WHWilImage.cpp 从Data目录中加载Wix文件(内存映射)。 8. WHDXGraphic.cpp 处理DirectX效果。 文件类型格式探讨: Wix文件:索引文件,根据索引查找到相应数据地址(数据文件)。 // WIX 文件头格式 typedef struct tagWIXFILEIMAGEINFO { CHAR szTmp[40]; // 库文件标题 'WEMADE Entertainment inc.' WIL文件头 INT nIndexCount; // 图片数量 INT* pnPosition; // 位置 }WIXIMAGEINFO, *LPWIXIMAGEINFO; 我们下载一个Hedit编辑器打开一个Wil文件,分析一下。我们发现Wix文件中,0x23地址(含该地址)以前的内容是都相同的,即为:#INDX v1.0-WEMADE Entertainment inc. Ofs44 0x2C的地方:存放着0B 00 00 00,高低位转换后为:0xB转换十进制数为11(图片数量)Ofs48 0x30的地方:存放着38 04 00 00,高低位转换后为:0x438 = 1080, 这个就是图象数据的开始位置。 我们用Wil编辑打开对应的Wil文件,发现,果然有11张图片。另外我们发现,在Ofs = 44 -47之间的数据总是38 04 00 00,终于明白,所有的图片起始位置是相同的。 Wil文件: 数据文件。 前面我们说了图象数据的开始位置为0x438 = 1080, 1080中有文件开头的44字节都是相同的。所以,就是说有另外的1036字节是另有用途。1036中有1024是一个256色的调色板。 我们看到图片位置数据为: 20 03 58 02, 转化为十六进制: 0x320, 0x258 刚好就是800*600大小的图片。07 00 D4 FF。图片起始位置为: Ofs 1088: 0x440 图片大小为480000 起始位置:0x440 1088 终止位置:0x7573F 481087 为了验证数据是否正确,我们通过Wil工具,把第一幅图片导出来,然后用Hedit编辑器打开,经过对比,我们发现,数据一致。大小一致。 第二张BMP图片(图片起始位置:0x436 10078) : F0 01 69 01 , 07 00 D4 FF 刚好大小。第二张Wil起始位置:Ofs:481096 0x75748 知道了图片格式,我们可以写一个抓图片格式的程序了。 客户端——全局变量与总体执行流程 传奇的客户端源代码有两个工程,WindHorn和Mir2Ex。 先剖析一下WindHorn工程。 1.CWHApp、CWHWindow和CWHDXGraphicWindow。Window程序窗口的创建。 CWHApp派生CWHWindow,CWHWindow又派生CWHDXGraphicWindow。CWHWindow类 中完成窗口的注册和创建。CWHDXGraphicWindow调用CWHWindow完成创建窗口功能,然后再调用CreateDXG()来初始化DirectX。 2.CWHDefProcess派生出CloginProcess、CcharacterProcess、CgameProcess三个类。 这三个类是客户端处理的核心类。 3. 全局变量: CWHDXGraphicWindow g_xMainWnd; 主窗口类。 CLoginProcess g_xLoginProc; 登录处理。 CCharacterProcess g_xChrSelProc; 角色选择处理。 CgameProcess g_xGameProc; 游戏逻辑处理。 4.代码分析: 1.首先从LoginGate.cpp WinMain分析: g_xMainWnd定义为CWHDXGraphicWindow调用CWHWindow完成创建窗口功能,然后 调用DirectDrawEnumerateEx枚举显示设备,(执行回调函数DXGDriverEnumCallbackEx) 再调用CreateDXG()来初始化DirectX(创建DirectDraw对象, 取得独占和全屏模式, 设置显示模式等)。 g_xSound.InitMirSound创建CSound对象。 g_xSpriteInfo.SetInfo(); 初始化声音,加载Socket库之后,进行CWHDefProcess*指针赋值(事件绑定)。g_bProcState变量反应了当前游戏的状态(登录,角色选择,游戏逻辑处理)。调用Load初始化一些操作(登录,角色选择,游戏逻辑处理)。进行消息循环。 case _LOGIN_PROC: g_xLoginProc.RenderScene(dwDelay); case _CHAR_SEL_PROC: g_xChrSelProc.RenderScene(dwDelay); case _GAME_PROC: g_xGameProc.RenderScene(dwDelay); 根据g_bProcState变量标志,选择显示相应的画面。 2.接收处理网络消息和接收处理窗口消息。 在不同的状态下(登录,角色选择,游戏逻辑处理),接收到的消息(网络,窗口消息)会分派到不同的函数中处理的。这里是用虚函数处理(调用子类方法,由实际的父类完成相应的处理)。 OnMessageReceive主要处理网络消息。DefMainWndProc则处理窗体消息(按键,重绘等),创建窗体类为CWHDXGraphicWindow,回调函数为: MainWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) if ( m_pxDefProcess ) m_pxDefProcess->DefMainWndProc(hWnd, uMsg, wParam, lParam); else return MainWndProcDXG(hWnd, uMsg, wParam, lParam); m_pxDefProcess->DefMainWndProc调用父类的实际处理。 在WM_PAINT事件里: g_xClientSocket.ConnectToServer连接登陆服务器。 客户端——传奇文件类型格式探讨(一) Wix文件:索引文件,根据索引查找到相应数据地址(数据文件)。 // WIX 文件头格式 typedef struct tagWIXFILEIMAGEINFO { CHAR szTmp[40]; // 库文件标题 'WEMADE Entertainment inc.' WIL文件头 INT nIndexCount; // 图片数量 INT* pnPosition; // 位置 }WIXIMAGEINFO, *LPWIXIMAGEINFO; 我们下载一个Hedit编辑器打开一个Wil文件,分析一下。我们发现Wix文件中,0x23地址(含该地址)以前的内容是都相同的,即为:#INDX v1.0-WEMADE Entertainment inc. Ofs44 0x2C的地方:存放着0B 00 00 00,高低位转换后为:0xB转换十进制数为11(图片数量)Ofs48 0x30的地方:存放着38 04 00 00,高低位转换后为:0x438 = 1080, 这个就是图象数据的开始位置。 我们用Wil编辑打开对应的Wil文件,发现,果然有11张图片。另外我们发现,在Ofs = 44 -47之间的数据总是38 04 00 00,终于明白,所有的图片起始位置是相同的。 Wil文件: 数据文件。 前面我们说了图象数据的开始位置为0x438 = 1080, 1080中有文件开头的44字节都是相同的。所以,就是说有另外的1036字节是另有用途。1036中有1024是一个256色的调色板。而Wil里面的图片格式都是256色的位图储存。 我们看到图片位置数据为: 20 03 58 02, 转化为十六进制: 0x320, 0x258 刚好就是800*600大小的图片。07 00 D4 FF为固定值(标识)。图片起始位置为: Ofs 1088: 0x440 图片大小为480000 起始位置:0x440 1088 终止位置:0x7573F 481087 为了验证数据是否正确,我们通过Wil工具,把第一幅图片导出来,然后用Hedit编辑器打开,经过对比,我们发现,数据一致。大小一致。 大家看到图片1的结束位置为0fs 481077,减去1080+1 = 480000刚好800*600大小。 我们用Wil抓图工具打开看一下(确定是800*600大小): 我们导出第二张BMP图片 图片的大小为:496* 361, 我们从Wix中读出第二张图片的索引位置: 根据贴图,我们发现第二张图片的索引位置为: 40 57 07 00,转换为十六进制:0x75740,即为:481088,前面我们讲到第一张图片的结束位置是: 0fs 481077,从Wix中读出来的也刚好为第二张图片的起始位置: (我们分析Wil中的第二张图片,起始位置:0x75740 481088) : F0 01 69 01为图片长宽: 0x1F0, 0x169 为496* 361 。 07 00 D4 FF为固定值(标识)。 我们用工具打开第二张BMP图片,从起始位置,一直选取中至结束,发现刚好选496* 361字节大小。两边数据对比之后发现一致。知道了图片格式,我们可以写一个抓图片格式的程序了。 传奇2和3 文件格式分析比较 贴这个贴子,希望大家少走弯路。网上下载的那个版本应该是从传奇2改的,传奇3的格式。分析一下源码吧,g_xLoginProc.Load(); 之后就加载m_Image.NewLoad(IMAGE_INTERFACE_1, TRUE, TRUE); 继续读Wix文件, ReadFile(hWixFile, &m_stNewWixImgaeInfo, sizeof(NEWWIXIMAGEINFO)-sizeof(INT*), &dwReadLen, NULL); // WIX 文件头格式 (56Byte)(NEW) typedef struct tagNEWWIXFILEIMAGEINFO { CHAR szTitle[20]; // 库文件标题 'WEMADE Entertainment inc.' WIL文件头 INT nIndexCount; // 图片数量 INT* pnPosition; // 位置 }NEWWIXIMAGEINFO, *LPNEWWIXIMAGEINFO; 不看不知道,一看吓一跳,大家看到了吧,这个是新的WIX的定义,不是传奇2的,前面分析过传奇2的图片: 0x23地址(含该地址)以前的内容是都相同的,即为:#INDX v1.0-WEMADE Entertainment inc. Ofs44 0x2C的地方:存放着0B 00 00 00,高低位转换后为:0xB转换十进制数为11(图片数量)Ofs48 0x30的地方:存放着38 04 00 00,高低位转换后为:0x438 = 1080, 这个就是图象数据的开始位置。这里才20个标题长度。 一看就不对。所以如果你下了网上的传奇3的格式,试着读传奇2的图片,是不正确的。具体大家可以调试一下,我调试过了,里面的图片数量根本不对。 汗,居然让人郁闷的是, // WIX 文件头格式 (56Byte) typedef struct tagWIXFILEIMAGEINFO { CHAR szTmp[40]; // 库文件标题 'WEMADE Entertainment inc.' WIL文件头 INT nIndexCount; // 图片数量 INT* pnPosition; // 位置 }WIXIMAGEINFO, *LPWIXIMAGEINFO;我用了这种格式也不对。为什么不对,因为我前面分析过了,0xB转换十进制数为11(图片数量)Ofs48 0x30的地方, 看到没有,图片数量的存放地方。 所以赶快改一下数据结构吧,不知道为什么,难道是我版本有问题,我下了几个资源文件,结果发现问题依然存在。看来不是图片的问题。 另外,下面的工程里的图片,如果要运行,不用改数据结构,请到传奇3客户端官方网站下载。我下载的是1.5版的资源文件。 是传奇2的资源文件。祝大家好运吧! 客户端——传奇文件类型格式探讨(二) // WIX 文件头格式 (NEW) typedef struct tagNEWWIXFILEIMAGEINFO { CHAR szTitle[20]; // 库文件标题 'WEMADE Entertainment inc.' WIL文件头 INT nIndexCount; // 图片数量 INT* pnPosition; // 位置 }NEWWIXIMAGEINFO, *LPNEWWIXIMAGEINFO; 我们下载一个Hedit编辑器打开一个Wil文件,分析一下。我们发现Wix文件中,0x13地址(含该地址)以前的内容是都相同的,即为: ‘ ’20个空格。 图片数量: nIndexCount 18 Ofs 20, 0x14的位置,存放的数据为12 00 00 00,高低位转换后为:0x12十制数为18(图片数量)。Ofs28 0x1C的地方:存放着20 00 00 00,高低位转换后为:0x20 = 32, 这个就是图象数据的开始位置。 我们用Wil编辑打开对应的Wil文件,发现,果然有17张图片(减1)。另外我们发现,在Ofs28 0x1C的地方= 28 -31之间的数据总是20 00 00 00,终于明白,所有的图片起始位置是相同的。 抓图分析,自己就再分析一下吧,和传奇2的结构差不多。 客户端——游戏逻辑处理源分析一 登录处理事件: 0.WinMain主函数调用g_xLoginProc.Load();加载图片等初始化,设置g_bProcState 的状态。 1.CLoginProcess::OnKeyDown-> m_xLogin.OnKeyDown->g_xClientSocket.OnLogin; WSAAsyncSelect模型ID_SOCKCLIENT_EVENT_MSG,因此,(登录,角色选择,游戏逻辑处理)都回调g_xClientSocket.OnSocketMessage(wParam, lParam)进行处理。 OnSocketMessage函数中:FD_READ事件中: 2.g_bProcState判断当前状态,_GAME_PROC时,把GameGate的发送过来的消息压入PacketQ队列中,再进行处理。否则则调用OnMessageReceive(虚方法,根据g_bProcState状态,调用CloginProcess或者是CcharacterProcess的OnMessageReceive方法)。 3.CloginProcess:调用OnSocketMessageRecieve处理返回情况。如果服务器验证失败(SM_ID_NOTFOUND, SM_PASSWD_FAIL)消息,否则收到SM_PASSOK_SELECTSERVER消息(SelGate服务器列表消息)。m_Progress = PRG_SERVER_SELE;进行下一步选择SelGate服务器操作。 4. m_xSelectSrv.OnButtonDown->CselectSrv. OnButtonUp-> g_xClientSocket.OnSelectServer(CM_SELECTSERVER),得到真正的IP地址。调用OnSocketMessageRecieve处理返回的SM_SELECTSERVER_OK消息。并且断开与loginSrv服务器连接。 g_xClientSocket.DisconnectToServer();设置状态为PRG_TO_SELECT_CHR状态。 角色选择处理: 1. WinMain消息循环处理:g_xLoginProc.RenderScene(dwDelay)-> RenderScroll-> SetNextProc调用 g_xClientSocket.m_pxDefProc = g_xMainWnd.m_pxDefProcess = &g_xChrSelProc; g_xChrSelProc.Load(); g_bProcState = _CHAR_SEL_PROC; 2.g_xChrSelProc.Load();连接SelGate服务器(从LoginGate服务器得到IP地址)。 g_xClientSocket.OnQueryChar();查询用户角色信息,发送消息:CM_QUERYCHR,设置状态为_CHAR_SEL_PROC, m_Progress = PRG_CHAR_SELE; 在OnSocketMessageRecieve函数中接收到SelGate服务器发送的消息。 3.点击ChrStart按钮:g_xChrSelProc.OnLButtonDown-> CSelectChr::OnButtonUp-> g_xClientSocket.OnSelChar->发送CM_SELCHR消息到SelGate服务器。 4.CClientSocket::OnSocketMessage->CCharacterProcess::OnMessageReceive (SM_STARTPLAY) 接受到SelGate服务器发送的GameGate服务器IP地址,并断开与SelGate服务器的连接。m_xSelectChr.m_nRenderState = 2; 5. WinMain消息循环处理:g_xLoginProc.RenderScene -> m_xSelectChr.Render(nLoopTime);-> CSelectChr::Render(INT nLoopTime)-> m_nRenderState = m_nRenderState + 10; 为12-> CCharacterProcess::RenderScene执行 m_Progress = PRG_SEL_TO_GAME; m_Progress = PRG_PLAY_GAME; SetNextProc(); 6.SetNextProc();执行: g_xGameProc.Load(); g_bProcState = _GAME_PROC;进行游戏状态。 游戏逻辑处理: 1.客户端处理: CGameProcess::Load() 初始化游戏环境,加载地图等操作,调用ConnectToServer(m_pxDefProc->OnConnectToServer)连接到GameGate游戏网关服务器(DBSrv处理后经SelGate服务器返回的GameGate服务器IP地址)。 CClientSocket->ConnectToServer调用connect时,由GameGate服务器发送GM_OPEN消息到GameSrv服务器。WSAAsyncSelect I/O模型回调函数 g_xClientSocket.OnSocketMessage。然后由m_pxDefProc->OnConnectToServer()调用CGameProcess::OnConnectToServer()函数,调用:g_xClientSocket.SendRunLogin。 2. GameGate服务器ServerWorkerThread处理: GameGate服务器ServerWorkerThread收到消息,ThreadFuncForMsg处理数据,生成MsgHdr结构,并设置 MsgHdr.nCode = 0xAA55AA55; //数据标志 MsgHdr.wIdent = GM_DATA; //数据类型 3. GameSrv服务器ServerWorkerThread线程处理 GameSrv服务器ServerWorkerThread线程处理调用DoClientCertification设置用户信息,及USERMODE_LOGIN的状态。并且调用LoadPlayer(CUserInfo* pUserInfo)函数-> LoadHumanFromDB-> SendRDBSocket发送DB_LOADHUMANRCD请求,返回该玩家的所有数据信息。 4. 客户端登录验证(GameSrv服务器的线程ProcessLogin处理) 用户的验证是由GameSrv服务器的线程ProcessLogin处理。g_xReadyUserInfoList2列表中搜索,判断用户是否已经登录,一旦登录就调用LoadPlayer(这里两个参数): a. 设置玩家游戏状态。m_btCurrentMode状态为USERMODE_PLAYGAME b. 加载物品,个人设置,魔法等。 c. pUserInfo->m_pxPlayerObject->Initialize();初始化用户信息,加载用户坐标,方向,地图。 Initialize执行流程: 1) AddProcess(this, RM_LOGON, 0, 0, 0, 0, NULL);加入登录消息。 2) m_pMap->AddNewObject 地图中单元格(玩家列表)加入该游戏玩家。OS_MOVINGOBJECT玩家状态。 3) AddRefMsg(RM_TURN 向周围玩家群发 RM_TURN消息。以玩家自己为中心,以24*24的区域里,向这个区域所属的块里的所有玩家列表发送消息)广播 AddProcess。 4) RecalcAbilitys 设置玩家的能力属性(攻击力(手,衣服),武器力量等)。 5) 循环处理本游戏玩家的附属物品,把这些物品的力量加到(手,衣服等)的攻击力量里。 6) RM_CHARSTATUSCHANGED消息,通知玩家状态改变消息。 7) AddProcess(this, RM_ABILITY, 0, 0, 0, 0, NULL); 等级 AddProcess(this, RM_SUBABILITY, 0, 0, 0, 0, NULL); AddProcess(this, RM_DAYCHANGING, 0, 0, 0, 0, NULL); 校时 AddProcess(this, RM_SENDUSEITEMS, 0, 0, 0, 0, NULL); 装备 AddProcess(this, RM_SENDMYMAGIC, 0, 0, 0, 0, NULL); 魔法 SysMsg(szMsg, 1) 攻击力 并把用户数据从g_xReadyUserInfoList2列表中删除。 说明: 一旦通过验证,就从验证列表中该玩家,改变玩家状态,LoadPlayer加载用户资源(地图中加入用户信息,向用户24*24区域内的块内玩家发送上线消息GameSrv广播新玩家上线(坐标)的消息。向该新玩家发送玩家信息(等级,装备,魔法,攻击力等)。 服务器端——LoginGate服务器处理 服务器端: 1.首先从LoginGate.cpp WinMain分析: 1) CheckAvailableIOCP : 检查是不是NT,2000的系统(IOCP) 2) InitInstance: 初始化界面,加载WSAStartup 3) MainWndProc窗口回调函数. 2.MainWndProc.CPP中分析回调函数MainWndProc switch (nMsg) { case _IDM_CLIENTSOCK_MSG: case WM_COMMAND: case WM_CLOSE: g_ssock Local 7000 游戏登陆端口 g_csock Remote 5000 发送到logsrv服务器上的套接字 1)_IDM_CLIENTSOCK_MSG 消息:处理与logsrv回调通讯事件。 调用:OnClientSockMsg,该函数是一个回调函数: 当启动服务之后,ConnectToServer函数将(_IDM_CLIENTSOCK_MSG消息 FD_CONNECT|FD_READ|FD_CLOSE)传入WSAAsyncSelect函数。在与hWnd窗口句柄对应的窗口例程中以Windows消息的形式接收网络事件通知。函数OnClientSockMsg,主要完成与logsrv服务器之间的通信(心跳,转发客户端数据包等) switch (WSAGETSELECTEVENT(lParam)) { case FD_CONNECT: case FD_CLOSE: case FD_READ: FD_CONNECT:(重新连接情况) A. CheckSocketError返回正常时: a). ConnectToServer函数首先在服务启动的时候执行一次。回调 FD_CONNECT b).连接logsrv时,开启ThreadFuncForMsg线程,把从客户端发送的数据(g_xMsgQueue, FD_READ事件读到的logSrv服务器发来的数据) 投递I/O,利用IOCP模型,发送到客户端。SleepEx挂起线程。至到一个I/O 完成回调函数被调用。 一个异步过程调用排队到此线程。 ThreadFuncForMsg线程检测(从logSrv收到的g_xMsgQueue数据包-心跳,处理包)。i/o 投递,利用IOCP发送给客户端。 if (nSocket = AnsiStrToVal(pszFirst + 1)) //得到socket WSASend((SOCKET)nSocket, &Buf, 1, &dwSendBytes, 0, NULL, c).终止定时器_ID_TIMER_CONNECTSERVER KillTimer(g_hMainWnd, _ID_TIMER_CONNECTSERVER); d).设置_ID_TIMER_KEEPALIVE定时器 (心跳数据包) SetTimer(g_hMainWnd, _ID_TIMER_KEEPALIVE 调用定时器回调函数OnTimerProc: 定时发关心跳数据包到logsrv服务器。SendExToServer(PACKET_KEEPALIVE); B. 如果socket断开,设置_ID_TIMER_CONNECTSERVER定时器 ConnectToServer尝试重新连接服务器。 _ID_TIMER_CONNECTSERVER, (TIMERPROC)OnTimerProc); FD_CLOSE: 断开与logsrv服务器SOCKET连接,OnCommand(IDM_STOPSERVICE, 0); 回调函数处理IDM_STOPSERVICE。 FD_READ: 接收logsrv服务器发送的数据包(心跳,登陆验证,selCur服务器地址),把数据加入缓冲区(g_xMsgQueue)中。 2)WM_COMMAND: IDM_STARTSERVICE: 启动服务(IOCP模型Server响应客户端请求) IDM_STOPSERVICE: 停止服务(IOCP模型Server) 3)WM_CLOSE: IDM_STOPSERVICE: 停止服务(IOCP模型Server) WSACleanup(); PostQuitMessage(0); //WM_DESTROY消息 IDM_STARTSERVICE: 启动服务(IOCP模型Server响应客户端请求) InitServerSocket:函数: 1) AcceptThread线程: Accept之后生成一个CSessionInfo对象,pNewUserInfo->sock = Accept; 客户端Socket值赋值给结构体。记录客户相关信息。 新的套接字句柄用CreateIoCompletionPort关联到完成端口,然后发出一个异步的WSASend或者WSARecv调用(pNewUserInfo->Recv();接收客户端消息),因为是异步函数,WSASend/WSARecv会马上返回,实际的发送或者接收数据的操作由WINDOWS系统去做。然后把CSessionInfo对象加入g_xSessionList中。向logsrv服务器发送用户Session信息。打包规则‘%0socket/ip$/0’ 在客户accept之后,总投递一个I/O(recv),然后把相应的数据发往logsrv服务器。 2) CreateIOCPWorkerThread函数: 调用CreateIoCompletionPort 并根据处理器数量,创建一个或多个ServerWorkerThread线程。 ServerWorkerThread线程工作原理: 循环调用GetQueuedCompletionStatus()函数来得到IO操作结果。阻塞函数。当WINDOWS系统完成WSASend或者WSArecv的操作,把结果发到完成端口。GetQueuedCompletionStatus()马上返回,并从完成端口取得刚完成的WSASend/WSARecv的结果。然后接着发出WSASend/WSARecv,并继续下一次循环阻塞在GetQueuedCompletionStatus()这里。 a). pSessionInfo为空或者dwBytesTransferred =0 ,在客户端close socket,发相应数据包(异常)到logsrv服务器(X命令-数据包),关闭客户端套按字。 b). while ( pSessionInfo->HasCompletionPacket() ) 如果数据验证正确,就转发数据包(A命令-数据包) logsrv服务器。 c). if (pSessionInfo->Recv() 继续投递I/O操作。 总结: 我们不停地发出异步的WSASend/WSARecv IO操作,具体的IO处理过程由WINDOWS系统完成,WINDOWS系统完成实际的IO处理后,把结果送到完成端口上(如果有多个IO都完成了,那么就在完成端口那里排成一个队列)。我们在另外一个线程里从完成端口不断地取出IO操作结果,然后根据需要再发出WSASend/WSARecv IO操作。 IDM_STOPSERVICE: 停止服务(IOCP模型Server响应客户端请求) Close -> OnCommand(IDM_STOPSERVICE, 0L); ->g_fTerminated = TRUE; 线程退出。 if (g_hAcceptThread != INVALID_HANDLE_VALUE) { TerminateThread(g_hAcceptThread, 0); WaitForSingleObject(g_hAcceptThread, INFINITE); //IOCP的Accept线程 CloseHandle(g_hAcceptThread); g_hAcceptThread = INVALID_HANDLE_VALUE; } if (g_hMsgThread != INVALID_HANDLE_VALUE) { TerminateThread(g_hMsgThread, 0); //窗口例程网络事件回调线程 WaitForSingleObject(g_hMsgThread, INFINITE); CloseHandle(g_hMsgThread); g_hMsgThread = INVALID_HANDLE_VALUE; } ClearSocket(g_ssock); ClearSocket(g_csock); CloseHandle(g_hIOCP); 总结: LoginGate(登录网关服务器),接受客户端连接,并且把用户ID,密码直接发送到LoginSvr服务器中,由LoginSrv服务器验证之后,发送数据包返回给客户端。LoginGate之间是通过定时器,定时发送“心跳”数据。验证服务器存活的。客户端与服务器端的数据在传输中,是进行过加密的。 向loginSrv发送‘%A’+Msg+‘$0’消息: 转发客户端消息。 ‘%X’+Msg+‘$0’消息: 发送用户连接消息,增加到用户列表。 ‘%O’+Msg+‘$0’消息: 发送用户上线消息。 主要流程: 服务启动后,LoginGate启动了AcceptThread,和ServerWorkerThread线程,AcceptThread线程接收客户端连接,并把session信息发送给loginSrv服务器,ServerWorkerThread线程从完成端口取得刚完成的WSASend/WSARecv的结果后,把客户端数据转发给loginSrv服务器。服务启动时,WSAAsyncSelect模型连接到loginSrv服务器中。一旦连接成功,就启动ThreadFuncForMsg线程,该线程从g_xMsgQueue(FD_READ事件读到的loginSrv服务器发来的数据)中取出loginSrv服务器处理过的数据。投递I/O,利用IOCP模型,发送到客户端。 ServerWorkerThread转发客户端数据 -> WSAAsyncSelect的Read读loginSrv处理后返回的数据-> ThreadFuncForMsg线程,投递WSASend消息,由Windows处理(IOCP),发送数据给客户端。 服务器端——LoginSvr服务器分析 g_gcSock Local 5500端口 1.首先从LoginSvr.cpp WinMain分析: 1) CheckAvailableIOCP : 检查是不是NT,2000的系统(IOCP) 2) InitInstance: 初始化界面,加载WSAStartup GetDBManager()->Init( InsertLogMsg, "Mir2_Account", "sa", "prg" ); 数据库管理类,做底层数据库操作。 3) MainWndProc窗口回调函数OnCommand: IDM_STARTSERVICE: 创建LoadAccountRecords线程 a). UPDATE TBL_ACCOUNT重置帐户验证状态。 b). 读服务器列表(TBL_SERVERINFO, selGate服务器),加入g_xGameServerList 遍历xGameServerList列表,把服务器信息加入到一个字符数组g_szServerList中。 c). 启动InitServerThreadForMsg线程。 d). 调用InitServerSocket函数创建两个线程: AcceptThread线程: ServerWorkerThread线程: 调用InitServerSocket函数创建两个线程: 1) AcceptThread线程: Accept之后生成一个CGateInfo对象,CGateInfo->sock = Accept; 客户端Socket值赋值给结构体。记录客户相关信息。新的套接字句柄用CreateIoCompletionPort关联到完成端口,然后发出一个异步的WSASend或者WSARecv调用(pNewUserInfo->Recv();接收客户端消息),因为是异步函数,WSASend/WSARecv会马上返回,实际的发送或者接收数据的操作由WINDOWS系统去做。然后把CGateInfo对象加入g_xGateList中。在客户accept之后,投递一个I/O(recv)。 分析一下g_xGateList发现,每个CGateInfo里有sock; xUserInfoList,g_SendToGateQ,该网关的相关信息依次(网关对应的sock, 用户列列信息,消息队列),可以为多个LoginGate登录网关服务。 2) ServerWorkerThread线程: ServerWorkerThread线程工作原理: 循环调用GetQueuedCompletionStatus()函数来得到IO操作结果。阻塞函数。当WINDOWS系统完成WSASend或者WSArecv的操作,把结果发到完成端口。GetQueuedCompletionStatus()马上返回,并从完成端口取得刚完成的WSASend/WSARecv的结果。然后接着发出WSASend/WSARecv,并继续下一次循环阻塞在GetQueuedCompletionStatus()这里。 a).if (g_fTerminated) 线程结束前:循环遍历g_xGateList,取出pGateInfo关闭套接字,并删除节点。dwBytesTransferred =0 ,关闭该服务器套接字。 b).while ( pGateInfo->HasCompletionPacket() ) 验证消息格式。 case '-': 发送心跳数据包到每个LoginGate服务器。 case 'A': 处理每个LoginGat服务器转发的客户端的消息增加到各自网关(CGateInfo)g_SendToGateQ队列中,然后ThreadFuncForMsg线程进行验证后再发送消息到各个LoginGate服务器。 pGateInfo->ReceiveSendUser(&szTmp[2]); case 'O': 处理每个网关Accept客户端后增加pUserInfo用户信息到各自网关的xUserInfoList列表中。 pGateInfo->ReceiveOpenUser(&szTmp[2]); case 'X': 处理每个网关收到客户端Socket关闭之后发送过来的消息。设置该网关socket相应状态。 pGateInfo->ReceiveCloseUser(&szTmp[2]); case 'S': GameSvr服务器发送的消息,更新TBL_ACCOUNT,验证字段,说明用户已下线,下次登录必须先到LoginSvr服务器再次验证。 pGateInfo->ReceiveServerMsg(&szTmp[2]); case 'M': GameSvr服务器发送的消息,创建一个用户的消息,把用户ID,密码,名字插入TBL_ACCOUNT表中插入成功返回SM_NEWID_SUCCESS,否则SM_NEWID_FAIL,把在信息前加#,信息后加! 不做TBL_ACCOUNTADD表的添加,只增加TBL_ACCOUNT表信息。 ‘A’:是LoginGate 服务器转发客户端消息到g_xMsgQueue队列, 由ThreadFuncForMsg线程处理后,转发到各个loginGate服务器 继续投递I/O操作。 启动InitServerThreadForMsg 创建ThreadFuncForMsg线程。c 收到loginGate服务器发送过来的消息之后,ServerWorkerThread经过数据包分析之后(case 'A'),把客户端的消息,写入g_SendToGateQ队列中,然后在本线程中再进行处理。 遍历g_SendToGateQ队列中数据,验证数据包是否正确(#!字符)根据DefaultMsg.wIdent标志 case CM_IDPASSWORD: 处理登陆业务 遍历xUserInfoList用户列表信息,到数据库表TBL_ACCOUNT中找相应信息,如果失败发送(SM_ID_NOTFOUND, SM_PASSWD_FAIL)消息,否则发送SM_PASSOK_SELECTSERVER+ g_szServerList(SelGate服务器列表消息) SelGate服务器列表消息(对应TBL_SERVERINFO数据库表中数据),供用户选择登录的SelGate服务器。 CM_SELECTSERVER: 选择服务器(SelGate) 遍历xUserInfoList用户列表信息,根据socket,找到用户密钥,消息解密后,遍历g_xGameServerList列表,把用户选择的SelGate服务器转化为IP地址,发送至LoginGate服务器,再转发至客户端。设置该用户SelServer的标志状态。从该网关的xUserInfoList用户列表中删除该用户。 CM_ADDNEWUSER: 新注册用户 判断用户名是否已存在,失败发送SM_NEWID_FAIL消息,成功,写插入表数据,并发送SM_NEWID_SUCCESS消息到 LoginGate服务器,转发至客户端。 IDM_STOPSERVICE: 停止服务(IOCP模型Server响应客户端请求) Close -> OnCommand(IDM_STOPSERVICE, 0L); ->g_fTerminated = TRUE; 三个线程退出。 主要流程: 服务启动后,LoginSvr启动了AcceptThread,和ServerWorkerThread线程,AcceptThread线程接收loginGate,GameSvr服务器连接,加入g_xGateList网关列表中,ServerWorkerThread线程从完成端口取得刚完成的WSASend/WSARecv的结果后,进行分析处理两个服务器发送来的消息。服务启动同时,启动ThreadFuncForMsg线程,该线程从g_xMsgQueue(iocp读到的loginGate服务器发来的数据)中取出数据,处理数据。投递I/O,利用IOCP模型,发送到loginGate服务器。 客户端——游戏逻辑处理源分析二 5.接受登录成功后,接收GameSrv服务器发送的消息: 接收GameGate发送的消息:CClientSocket::OnSocketMessage的FD_READ事件中,PacketQ.PushQ((BYTE*)pszPacket);把接收到的消息,压入PacketQ队列中。处理PacketQ队列数据是由CGameProcess::Load()时调用OnTimer在CGameProcess::OnTimer中处理的, 处理过程为: OnMessageReceive; ProcessPacket(); ProcessDefaultPacket(); OnMessageReceive函数; 1. 判断是否收到心跳数据包,发送'*',发送心跳数据包。 2. 调用OnSocketMessageRecieve函数。这个函数里面详细处理了客户端的游戏执行逻辑。如果是‘+’开头(数据包)则调用OnProcPacketNotEncode处理这种类型数据包。否则得到_TDEFAULTMESSAGE数据包,进行游戏逻辑处理。 OnProcPacketNotEncode说明: 收到GameSrv服务器的相应消息: "GOOD":可以执行动作。 m_bMotionLock为假。 "FAIL":不允许执行动作。人物被拉回移动前位置。 "LNG": "ULNG": "WID": "UWID": "FIR": "UFIR": "PWR": 3. CGameProcess::OnSocketMessageRecieve(char *pszMsg)函数。处理游戏相关的消息。 SM_SENDNOTICE: 服务器提示信息: SM_NEWMAP: 用户登录后,服务器发送的初始化地图消息。 SM_LOGON: 用户登录消息(服务器处理后返回结果)。用户登录成功后,在本地创建游戏对象,并发送消息,请求返回用户物品清单(魔法,等级,物品等)。 SM_MAPDESCRIPTION: 得到服务器发送的地图的描述信息。 SM_ABILITY:服务器发送的本玩家金钱,职业信息。 SM_WINEXP: SM_SUBABILITY : 服务器发送的玩家技能(魔法,杀伤力,速度,毒药,中毒恢复,生命恢复,符咒恢复) SM_ SM_SENDMYMAGIC: 用户魔法列表信息。 SM_MAGIC_LVEXP: 魔法等级列表。 SM_BAGITEMS:用户物品清单 (玩家CM_QUERYBAGITEMS消息) SM_SENDUSEITEMS:用户装备清单 SM_ADDITEM: 拣东西 SM_DELITEM: 丢弃物品。 等等。 4. 部分数据未处理,加入m_xWaitPacketQueue队列中由ProcessPacket处理。 新登录游戏玩家:在OnSocketMessageRecieve函数中依次收到的消息为: 1. GameSrv 服务器ProcessLogin线程返回GameGate服务器后返回的: AddProcess(this, RM_LOGON, 0, 0, 0, 0, NULL);加入登录消息。 SM_NEWMAP, SM_LOGON, SM_USERNAME, SM_MAPDESCRIPTION消息 AddProcess(this, RM_ABILITY, 0, 0, 0, 0, NULL); 等级 SM_ABILITY AddProcess(this, RM_SUBABILITY, 0, 0, 0, 0, NULL); SM_SUBABILITY AddProcess(this, RM_DAYCHANGING, 0, 0, 0, 0, NULL); 校时 SM_DAYCHANGING AddProcess(this, RM_SENDUSEITEMS, 0, 0, 0, 0, NULL); 装备 SM_SENDUSEITEMS AddProcess(this, RM_SENDMYMAGIC, 0, 0, 0, 0, NULL); 魔法 SM_SENDMYMAGIC 客户端收到消息后相应的处理: SM_NEWMAP 接受地图消息 OnSvrMsgNewMap 初始化玩家坐标,m_xMyHero.m_wPosX = ptdm->wParam; m_xMyHero.m_wPosY = ptdm->wTag; 加载地图文件 m_xMap.LoadMapData(szMapName); 设置场景。 m_xLightFog.ChangeLightColor(dwFogColor); SM_LOGON 返回登录消息 OnSvrMsgLogon m_xMyHero.Create初始化玩家信息(头发,武器,加载图片等),设置玩家 地图m_xMyHero.SetMapHandler(&m_xMap),创建用户魔法。加入m_xMagicList列表,pxMagic->CreateMagic, m_xMagicList.AddNode(pxMagic);并向服务器发送CM_QUERYBAGITEMS消息(用户物品清单,血,气,衣服,兵器等)。 SM_USERNAME 获取玩家的游戏角色名字。 SM_MAPDESCRIPTION 地图对应的名字。 SM_BAGITEMS 用户物品清单 (玩家CM_QUERYBAGITEMS消息) SM_CHARSTATUSCHANGED 通知玩家状态改变消息(攻击力,状态)。 SM_ABILITY 玩家金钱,职业 SM_SUBABILITY 玩家技能(魔法,杀伤力,速度,毒药,中毒恢复,生命恢复,符 咒恢复) SM_DAYCHANGING 返回游戏状态。(Day, Fog)让客户端随着服务器的时间,加载不同场景。 SM_SENDUSEITEMS 用户装备清单 SM_SENDMYMAGIC 用户魔法列表信息。 总结: 客户端连接到GameGate游戏网关服务器,并通过GameSrv服务器验证之后,就会收到GameSrv服务器发来的消息。主要是地图消息,登录消息,玩家的装备,技能,魔法,个人设置等等。GameSrv把地图分成若干块,把该玩家加入其中一块,并加入这一块的用户对象列表中,设置其状态为OS_MOVINGOBJECT。客户端加载地图,设置场景,设置自己的玩家状态(此时还没有怪物和其它玩家,所以玩家还需要接收其它游戏玩家和怪物的清单列表)。 客户端——游戏逻辑处理源分析三 6. 接收怪物,商人,其它玩家的消息: ProcessUserHuman:(其它玩家—服务器处理) CPlayerObject->SearchViewRange(); CPlayerObject->Operate(); 遍历UserInfoList列表,依次调用每个UserInfo的Operate来处理命令队列中的所有操作; pUserInfo->Operate()调用m_pxPlayerObject->Operate()调用。根据分发消息(RM_TURN)向客户端发送SM_TURN消息。GameSrv广播新玩家上线(坐标)的消息。向该新玩家发送玩家信息(等级,装备,魔法,攻击力等)。 玩家,移动对象: 1. 遍历m_xVisibleObjectList列表,所有(玩家,商人,怪物)发送调用AddProcess (RM_TURN向周围玩家发送消息)。 地图: 2.遍历m_xVisibleItemList,发送AddProcess(this, RM_ITEMSHOW消息更新地图。 3.遍历m_xVisibleEventList,发送AddProcess(this, RM_SHOWEVENT ProcessMonster线程:(怪物—服务器处理) GameSrv服务器在ProcessMonster线程:创建不同的CMonsterObject对象,并且加入xMonsterObjList列表和pMapCellInfo->m_xpObjectList列表中,然后再调用CMonsterObject::SearchViewRange()更新视线范围内目标,根据g_SearchTable计算出搜索坐标,转换为相应的地图单元格,遍历所有可移动生物,加入m_xVisibleObjectList列表,调用Operate;Operate遍历m_DelayProcessQ列表,过滤出RM_DOOPENHEALTH,RM_STRUCK和RM_MAGSTRUCK三个事件(恢复生命值,攻击,魔法攻击),并处理。 ProcessMerchants线程:(商人--服务器处理) 1). 遍历g_pMerchantInfo结构(根据nNumOfMurchantInfo数量)。得到商人类型相关的地图,创建商人对象,设置不同的编号,坐标,头像及所属地图。在该地图中加入该商人,且在g_xMerchantObjList商人清单中加入该商人。 2). 遍历g_xMerchantObjList, SearchViewRange,对每个商人更新视线范围内目标 a). 遍历m_xVisibleObjectList,设置每个pVisibleObject->nVisibleFlag = 0;设置状态(删除)。 b). 搜索VisibleObjectList列表,(服务器启动时InitializingServer加载 searchTable.tbl),根据坐标,找到相应的地图单元格。然后遍历pMapCellInfo->m_xpObjectList列表,判断如果为OS_MOVINGOBJECT标志,调用UpdateVisibleObject函数,该函数遍历 m_xVisibleObjectList列表,如果找到该商人对象,则pVisibleObject->nVisibleFlag = 1;否则判断pNewVisibleObject对象,设置nVisibleFlag为2,设置对象为该商人实体,然后加入m_xVisibleObjectList列表中。 总结:循环列表,找出地图单元格中的所有玩家,把所有玩家(OS_MOVINGOBJECT)加入到m_xVisibleObjectList列表中。 c). 遍历m_xVisibleObjectList列表,(pVisibleObject->nVisibleFlag == 0)则删除该pVisibleObject对象。 d). RunRace调用AddRefMsg 向周围玩家发送SM_TURN和SM_HIT 客户端收到消息后相应的处理: 1.CGameProcess::OnSocketMessageRecieve加入m_xWaitPacketQueue队列 遍历m_xVisibleObjectList队列中所有移动物体(角色): RM_DISAPPEAR 消失(SM_DISAPPEAR) ProcessDefaultPacket函数 RM_DEATH 死亡(SM_NOWDEATH, SM_DEATH) CHero::OnDeath 其它玩家。 CActor::OnDeath 怪物。 //g_xGameProc.m_xMagicList RM_TURN 移动 SM_TURN消息处理 遍历m_xVisibleItemList队列中所有移动物体(地图): RM_ITEMHIDE 从m_stMapItemList列表中删除该移动对象 RM_ITEMSHOW 遍历m_stMapItemList,如果不存在,则创建一个GROUNDITEM结构,并加入m_stMapItemList列表中。 typedef struct tagGROUNDITEM { INT nRecog; SHORT shTileX; SHORT shTileY; WORD wLooks; CHAR szItemName[40]; }GROUNDITEM, *LPGROUNDITEM; 遍历m_xVisibleEventList队列中所有移动物体(事件): RM_HIDEEVENT RM_SHOWEVENT 2. 部分数据未处理,加入m_xWaitPacketQueue队列中由ProcessPacket处理。 CClientSocket::OnSocketMessage的FD_READ事件中,PacketQ.PushQ把接收到的消息,压入PacketQ队列中。处理PacketQ队列数据是由CGameProcess::Load()时调用OnTimer在CGameProcess::OnTimer中处理的,处理过程为: OnTimer -> ProcessPacket -> ProcessPacket处理m_xWaitPacketQueue队列消息(OnSocketMessageRecieve函数中未处理的消息)。 ProcessPacket 函数处理流程: 1. 处理本玩家(SM_NOWDEATH, SM_DEATH, SM_CHANGEMAP, SM_STRUCK) a.如果接收到消息是SM_NOWDEATH或SM_DEATH 则加入m_xPriorPacketQueue队列。 b. 如果接收到消息是SM_CHANGEMAP则调用LoadMapChanged,设置场景。 c. SM_STRUCK 处理受攻击(本玩家,或者其它的玩家,NPC等)。 2. 其它消息:m_xMyHero.StruckMsgReassign(); m_xMyHero.m_xPacketQueue.PushQ((BYTE*)lpPacketMsg); 判断服务器发送来的消息ID是否相同。m_xMyHero.m_dwIdentity在登录成功的时 候由服务器发送的用户消息获取的。 if ( lpPacketMsg->stDefMsg.nRecog == m_xMyHero.m_dwIdentity ) 如果是服务器端游戏玩家自己发送的消息,则处理自己的消息。否则如果是其它玩家(怪物)发送的消息,遍历m_xActorList列表, 判断该对象是否存在,如果该不存在,则根据stFeature.bGender的类型 _GENDER_MAN: 创建一个CHero对象,加入到m_xActorList列表中。 _GENDER_WOMAN: _GENDER_NPC: 创建一个CNPC对象,加入到m_xActorList列表中。 _GENDER_MON: 创建一个CActor对象,加入到m_xActorList列表中。 然后pxActor->m_xPacketQueue.PushQ 然后把消息压入该对象的xPacketQueue列表中。 总结:ProcessPacket处理 CClientSocket类接受的消息(m_xWaitPacketQueue),判断是否是服务器发送给自己的消息,处理一些发送给自己的重要消息,其它消息处理则加入m_xMyHero.m_xPacketQueue队列中,然后再遍历m_xActorList队列,判断如果服务器端发来的消息里的玩家(NPC,怪物),在m_xActorList队列中找不到,就判断一个加入m_xActorList列表中,并且把该消息压入pxActor->m_xPacketQueue交给该NPC去处理该事件。 而xPacketQueue队列的消息分别由该对象的UpdatePacketState处理,如下: BOOL CActor::UpdatePacketState() ,BOOL CNPC::UpdatePacketState() BOOL CHero::UpdatePacketState()。 ProcessDefaultPacket函数: 处理CGameProcess::OnSocketMessageRecieve 中 SM_CLEAROBJECT消息: 处理(SM_DISAPPEAR,SM_CLEAROBJECT)消息。 遍历m_xWaitDefaultPacketQueue消息列表 SM_DISAPPEAR和SM_CLEAROBJECT: 遍历m_xActorList列表,清除pxActor->m_xPacketQueue队列内所有消息。 m_xActorList.DeleteCurrentNodeEx();从对列中删除该对象。 CHero* pxHero = (CHero*)pxActor; delete((CHero*)pxHero);销毁该玩家。 游戏循环处理: CGameProcess::RenderScene(INT nLoopTime)函数: 主要流程如下: wMoveTime += nLoopTime; 判断wMoveTime>100时,bIsMoveTime置为真。 1.m_xMyHero.UpdateMotionState(nLoopTime, bIsMoveTime);处理本玩家消息。 a. UpdatePacketState函数: 遍历m_xPriorPacketQueue队列,如果有SM_NOWDEATH或SM_DEATH消息,则优先处理。 处理m_xPacketQueue队列中消息。 SM_STRUCK: SM_RUSH SM_BACKSTEP SM_FEATURECHANGED: SM_OPENHEALTH: SM_CLOSEHEALTH: SM_CHANGELIGHT: SM_USERNAME: SM_CHANGENAMECOLOR: SM_CHARSTATUSCHANGE: SM_MAGICFIRE: SM_HEALTHSPELLCHANGED: 2.CheckMappedData函数:遍历m_xActorList列表分别调用 CActor::UpdateMotionState(INT nLoopTime, BOOL bIsMoveTime) CNPC::UpdateMotionState(INT nLoopTime, BOOL bIsMoveTime) CMyHero::UpdateMotionState(INT nLoopTime, BOOL bIsMoveTime) 处理自己消息。 CHero::UpdatePacketState() case SM_SITDOWN: case SM_BUTCH: case SM_FEATURECHANGED: case SM_CHARSTATUSCHANGE: case SM_OPENHEALTH: case SM_CLOSEHEALTH: case SM_CHANGELIGHT: case SM_USERNAME: case SM_CHANGENAMECOLOR: case SM_HEALTHSPELLCHANGED: case SM_RUSH: case SM_BACKSTEP: case SM_NOWDEATH: case SM_DEATH: case SM_WALK: case SM_RUN: case SM_TURN: case SM_STRUCK: case SM_HIT: case SM_FIREHIT: case SM_LONGHIT: case SM_POWERHIT: case SM_WIDEHIT: case SM_MAGICFIRE: case SM_SPELL: CNPC::UpdatePacketState() case SM_OPENHEALTH: case SM_CLOSEHEALTH: case SM_CHANGELIGHT: case SM_USERNAME: case SM_CHANGENAMECOLOR: case SM_HEALTHSPELLCHANGED: case SM_TURN: case SM_HIT: CActor::UpdatePacketState() case SM_DEATH: SetMotionFrame(_MT_MON_DIE, bDir); case SM_WALK: SetMotionFrame(_MT_MON_WALK, bDir); case SM_TURN: SetMotionFrame(_MT_MON_STAND, bDir); case SM_DIGUP: SetMotionFrame(_MT_MON_APPEAR, bDir); case SM_DIGDOWN: SetMotionFrame(_MT_MON_APPEAR, bDir); case SM_FEATURECHANGED: case SM_OPENHEALTH: case SM_CLOSEHEALTH: case SM_CHANGELIGHT: case SM_CHANGENAMECOLOR: case SM_USERNAME: case SM_HEALTHSPELLCHANGED: case SM_BACKSTEP: SetMotionFrame(_MT_MON_WALK, bDir); case SM_STRUCK: SetMotionFrame(_MT_MON_HITTED, m_bCurrDir); case SM_HIT: SetMotionFrame(_MT_MON_ATTACK_A, bDir); case SM_FLYAXE: case SM_LIGHTING: case SM_SKELETON: 收到多个NPC,玩家发送的SM_TURN消息:由下面对象调用处理: CHero::OnTurn CNPC::OnTurn CActor::OnTurn 根据服务器发送的消息,(创建一个虚拟玩家NPC,怪物,在客户端),根据参数,初始化该对象设置(方向,坐标,名字,等级等)。在后面的处理中绘制该对象到UI界面中(移动对象的UI界面处理。) SetMotionFrame(_MT_MON_STAND, bDir); m_bCurrMtn := _MT_MON_STAND m_dwFstFrame , m_dwEndFrame , m_wDelay 第一帧,最后一帧,延迟时间。 3. AutoTargeting 自动搜索目标(NPC,怪物,玩家等) 4. RenderObject补偿对象时间 5. RenderMapTileGrid m_xMagicList,处理玩家魔法后,UI界面的处理。 6. m_xSnow, m_xRain, m_xFlyingTail, m_xSmoke, m_xLightFog设置场景UI界面处理。 7. m_xMyHero.ShowMessage(nLoopTime); 显示用户(UI处理) m_xMyHero.DrawHPBar(); 显示用户HP值。 遍历m_xActorList,处理所有NPC的UI界面重绘 pxHero->ShowMessage(nLoopTime); pxHero->DrawHPBar(); 8. DropItemShow下拉显示。 9. 判断m_pxMouseTargetActor(玩家查看其它玩家,NPC,怪物时) g_xClientSocket.SendQueryName向服务器提交查询信息。 m_pxMouseOldTargetActor = m_pxMouseTargetActor; 保存该对象 m_pxMouseTargetActor->DrawName(); 重绘对象名字(UI界面显示) 下面分析一下用户登录之后的流程: 从前面的分析中可以看到,该用户玩家登录成功之后,得到了服务器发送来的各种消息。处理也比较复杂,同时有一定的优先级处理。并且根据用户登录后的XY坐标,向用户发送来了服务器XY坐标为中心附近单元格中的所有玩家(NPC,怪物)的SM_TURN消息。 客户端根据数据包的标志,创建这些NPC,设置属性,并且把它们加入m_xActorList对列中。最后在UI界面上绘制这些对象。 客户端——游戏逻辑处理源分析四 现在假设玩家开始操作游戏: 传奇的客户端源代码工程WindHorn 一、CWHApp派生CWHWindow和CWHDXGraphicWindow。 二、CWHDefProcess派生出CloginProcess、CcharacterProcess、CgameProcess 客户端WinMain调用CWHDXGraphicWindow g_xMainWnd;创建一个窗口。 客户端CWHDXGraphicWindow在自己的Create函数中调用了CWHWindow的Create来创建窗口,然后再调用自己的CreateDXG()来初始化DirectX。 消息循环: 因此,当客户端鼠标单击的时候,先调用CWHWindow窗口的回调函数WndProc,即: g_pWHApp->MainWndProc g_pWHApp定义为:static CWHApp* g_pWHApp = NULL;在CWHApp 构造函数中赋值为:g_pWHApp = this; g_pWHApp->MainWndProc便调用了CWHApp::MainWndProc,这是一个虚函数,实际上则是调用它的派生类CWHDXGraphicWindow::MainWndProc。 if ( m_pxDefProcess ) return m_pxDefProcess->DefMainWndProc(hWnd, uMsg, wParam, lParam); 根据g_xMainWnd.m_pxDefProcess和全局变量g_bProcState标记当前的处理状态。调用 CLoginProcess->DefMainWndProc CCharacterProcess->DefMainWndProc CGameProcess->DefMainWndProc 当用户进行游戏之后,点击鼠标左键,来处理玩家走动的动作: 客户端执行流程:(玩家走动) CGameProcess::OnLButtonDown(WPARAM wParam, LPARAM lParam)函数:该函数的处理流程: 1. g_xClientSocket.SendNoticeOK();如果点中CnoticeBox则m_xNotice.OnButtonDown if m_xMsgBtn.OnLButtonDown则调用g_xClientSocket.SendNoticeOK()方法,发送还CM_LOGINNOTICEOK消息。 2.m_pxSavedTargetActor = NULL;设置为空。CInterface::OnLButtonDown函数会判断 鼠标点击的位置(CmirMsgBox, CscrlBar,CgameBtn,GetWindowInMousePos) a. g_xClientSocket.SendItemIndex(CM_DROPITEM 丢弃物品) 游戏服务器执行流程m_pxPlayerObject->Operate()调用 m_pUserInfo->UserDropGenItem m_pUserInfo->UserDropItem 删除普通物品。 SM_DROPITEM_SUCCESS 返回删除成功命令 SM_DROPITEM_FAIL 返回删除失败命令 b. 遍历m_stMapItemList列表(存储玩家,怪物,NPC), g_xClientSocket.SendPickUp 发送CM_PICKUP命令。 游戏服务器:m_pxPlayerObject->Operate()调用 PickUp(捡东西)消息处理: m_pMap->GetItem(m_nCurrX, m_nCurrY) 返回地图里的物体(草药,物品,金子等) 1.memcmp(pMapItem->szName, g_szGoldName 如果是黄金: m_pMap->RemoveObject从地图中移走该的品。 if (m_pUserInfo->IncGold(pMapItem->nCount))增加用户的金钱(向周转玩家发送RM_ITEMHIDE 消息,隐藏该物体,GoldChanged(),改变玩家的金钱。否则,把黄金返回地图中。 2.m_pUserInfo->IsEnoughBag() 如果玩家的还可以随身带装备(空间)。m_pMap->RemoveObject从地图中移走该的品。UpdateItemToDB,更新用户信息到数据库。(向周转玩家发送RM_ITEMHIDE 消息,隐藏该物体,SendAddItem(lptItemRcd)向本玩家发送捡到东西的消息。m_pUserInfo->m_lpTItemRcd.AddNewNode并把该物品加入自己的列表中。 c. if m_pxMouseTargetActor g_xClientSocket.SendNPCClick发送CM_CLICKNPC命令。 客户端RenderScene调用m_pxMouseTargetActor = NULL; CheckMappedData(nLoopTime, bIsMoveTime)处理,如果鼠标在某个移动对象的区域内就会设置 m_pxMouseTargetActor为该对象。 如果是NPC: if ( m_pxMouseTargetActor->m_stFeature.bGender == _GENDER_NPC ) g_xClientSocket.SendNPCClick(m_pxMouseTargetActor->m_dwIdentity); CM_CLICKNPC消息: 否则: m_xMyHero.OnLButtonDown d. 否则m_xMyHero.OnLButtonDown 先判断m_xPacketQueue是否有数据,有则先处理。返回。 判断m_pxMap->GetNextTileCanMove 根据坐标,判断地图上该点属性是否可以移动到该位置: 可移动时: 人:SetMotionState(_MT_WALK 骑马:SetMotionState(_MT_HORSEWALK 不可移动时: 人:SetMotionState(_MT_STAND, bDir); 骑马:SetMotionState(_MT_HORSESTAND, bDir); SetMotionState函数: 判断循环遍历目标点的周围八个坐标,如果发现是一扇门,则向服务器发送打开这扇门的命令。g_xClientSocket.SendOpenDoor,否则则发送CM_WALK命令到服务器。 m_bMotionLock = m_bInputLock = TRUE; 设置游戏状态 m_wOldPosX = m_wPosX; 保存玩家X点 m_wOldPosY = m_wPosY; 保存玩家Y点 m_bOldDir = m_bCurrDir; 保存玩家方向 然后调用SetMotionFrame设置m_bCurrMtn = _MT_WALK,方向等游戏状态。 设置m_bMoveSpeed = _SPEED_WALK(移动速度1)。m_pxMap->ScrollMap设置地图的偏移位置(m_shViewOffsetX, m_shViewOffsetY)。然后滚动地图,重绘玩家由CGameProcess::RenderScene CGameProcess::RenderObject->DrawActor重绘。 客户端——游戏逻辑处理源分析五 服务器端响应 游戏服务器执行流程:(玩家走动) GameSrv服务器ProcessUserHuman线程处理玩家消息: 遍历UserInfoList列表,依次调用每个UserInfo的Operate来处理命令队列中的所有操作; pUserInfo->Operate()调用m_pxPlayerObject->Operate()调用。 判断玩家if (!m_fIsDead),如果已死,则发送_MSG_FAIL消息。我们在前面看到过,该消息是被优先处理的。否则则调用WalkTo,并发送_MSG_GOOD消息给客户端。 WalkTo函数的流程: 1) WalkNextPos 根据随机值产生,八个方向的坐标位置。 2) WalkXY怪物走动到一个坐标值中。 CheckDoorEvent根据pMapCellInfo->m_sLightNEvent返回四种状态。 a) 要移动的位置是一扇门 _DOOR_OPEN b) 不是一扇门 _DOOR_NOT c) 是一扇门不可以打开返回 _DOOR_MAPMOVE_BACK或_DOOR_MAPMOVE_FRONT玩家前/后移动 3) 如果_DOOR_OPEN则发送SM_DOOROPEN消息给周围玩家。 4) m_pMap->CanMove如果可以移动,则MoveToMovingObject从当前点移动到另一点。并发送AddRefMsg(RM_WALK)给周围玩家。 AddRefMsg函数,我们在后面的服务器代码里分析过:它会根据X,Y坐标,在以自己坐标为中心周围26*26区域里面,按地图单元格的划分,遍历所有单元格,再遍历所有单元格内的玩家列表,广播发送RM_WALK消息。 客户端执行流程:(反馈服务器端本玩家走动) 1. 服务器如果发送_MSG_FAIL 由客户端CGameProcess::OnProcPacketNotEncode处理。 m_xMyHero.SetOldPosition(); 人: SetMotionFrame(_MT_STAND AdjustMyPostion(); 重绘地图 m_bMotionLock = m_bInputLock = FALSE; 骑马:SetMotionFrame(_MT_HORSESTAND AdjustMyPostion(); 重绘地图 m_bMotionLock = m_bInputLock = FALSE; 2. 服务器如果发送_MSG_GOOD, 由客户端CGameProcess::OnProcPacketNotEncode处理。m_xMyHero.m_bMotionLock = FALSE; 其它客户端执行流程:(反馈服务器端其它玩家) 1.其它玩家: 人: SetMotionFrame(_MT_WALK, bDir); 骑马:SetMotionFrame(_MT_HORSEWALK, bDir); m_bMoveSpeed = _SPEED_WALK; SetMoving(); 设置m_shShiftPixelX, m_shShiftPixelY坐标。 2.NPC,怪物: SetMotionFrame(_MT_MON_WALK, bDir); m_bMoveSpeed = _SPEED_WALK; SetMoving(); 设置m_shShiftPixelX, m_shShiftPixelY坐标。 CGameProcess::RenderObject->DrawActor(m_shShiftPixelX, m_shShiftPixelY)重绘发消息的玩家,NPC怪物位置。 服务器端——SelGate服务器分析 注:客户端从LoginSvr服务器得到SelGate服务器IP之后,连接SelGate服务器,进行角 色创建,删除,选择操作,然后发送数据到DBSrv服务器。 g_ssock Local 7100客户端登陆端口 g_csock Remote 5100发送到DBSrv服务器上的套接字 1.首先从SelGate.cpp WinMain分析: 1) CheckAvailableIOCP : 检查是不是NT,2000的系统(IOCP) 2) InitInstance: 初始化界面,加载WSAStartup 3) MainWndProc窗口回调函数. 2.MainWndProc.CPP中分析回调函数MainWndProc switch (nMsg) { case _IDM_CLIENTSOCK_MSG: case WM_COMMAND: case WM_CLOSE: 1)_IDM_CLIENTSOCK_MSG 消息: 处理与SelGate回调通讯事件。 调用:OnClientSockMsg,该函数是一个回调函数: 当启动服务之后,ConnectToServer函数将(_IDM_CLIENTSOCK_MSG消息 FD_CONNECT|FD_READ|FD_CLOSE)传入WSAAsyncSelect函数。在与hWnd窗口句柄对应的窗口例程中以Windows消息的形式接收网络事件通知。函数OnClientSockMsg,主要完成与DBSrv服务器之间的通信(心跳,转发客户端数据包等) switch (WSAGETSELECTEVENT(lParam)) { case FD_CONNECT: case FD_CLOSE: case FD_READ: FD_CONNECT:(重新连接情况) A. CheckSocketError返回正常时: a). ConnectToServer函数首先在服务启动的时候执行一次。回调 FD_CONNECT b).连接DBSrv时,开启ThreadFuncForMsg线程,把从客户端发送的数据(g_xMsgQueue, FD_READ事件读到的DBSrv服务器发来的数据)投递I/O,利用IOCP模型,发送到客户端。SleepEx挂起线程,至到一个I/O 完成回调函数被调用。一个异步过程调用排队到此线程。 ThreadFuncForMsg线程检测(从DBSrv收到的g_xMsgQueue数据包-心跳,处理包)。i/o 投递,利用IOCP发送给客户端。 if (nSocket = AnsiStrToVal(pszFirst + 1)) //得到socket WSASend((SOCKET)nSocket, &Buf, 1, &dwSendBytes, 0, NULL, NULL); c).终止定时器_ID_TIMER_CONNECTSERVER KillTimer(g_hMainWnd, _ID_TIMER_CONNECTSERVER); d).设置_ID_TIMER_KEEPALIVE定时器 (心跳数据包) SetTimer(g_hMainWnd, _ID_TIMER_KEEPALIVE 调用定时器回调函数OnTimerProc: 定时发关心跳数据包到DBSrv服务器。SendExToServer(PACKET_KEEPALIVE); B. 如果socket断开,设置_ID_TIMER_CONNECTSERVER定时器 ConnectToServer尝试重新连接服务器。 _ID_TIMER_CONNECTSERVER, (TIMERPROC)OnTimerProc); FD_CLOSE: 断开SOCKET连接,OnCommand(IDM_STOPSERVICE, 0); 回调函数处理IDM_STOPSERVICE。 case FD_READ: 接收DBSrv服务器发送的数据包(心跳,登陆验证,selCur服务器地址),把数据加入缓冲区(g_xMsgQueue)中。 WM_COMMAND: IDM_STARTSERVICE: 启动服务(IOCP模型Server响应客户端请求) IDM_STOPSERVICE: 停止服务(IOCP模型Server) WM_CLOSE: IDM_STOPSERVICE: 停止服务(IOCP模型Server) WSACleanup(); PostQuitMessage(0); //WM_DESTROY消息 IDM_STARTSERVICE: 启动服务(IOCP模型Server响应客户端请求) InitServerSocket:函数: 1) AcceptThread线程: Accept之后生成一个CSessionInfo对象,pNewUserInfo->sock = Accept; 客户端Socket值赋值给结构体。记录客户相关信息。 新的套接字句柄用CreateIoCompletionPort关联到完成端口,然后发出一个异步的WSASend或者WSARecv调用(pNewUserInfo->Recv();接收客户端消息),因为是异步函数,WSASend/WSARecv会马上返回,实际的发送或者接收数据的操作由WINDOWS系统去做。然后把CSessionInfo对象加入g_xSessionList中。向DBsrv服务器发送用户Session信息。打包规则‘%0socket/ip$/0’ 在客户accept之后,总投递一个I/O(recv),然后把相应的数据发往DBSrv服务器。 2) CreateIOCPWorkerThread函数: 调用CreateIoCompletionPort 并根据处理器数量,创建一个或多个ServerWorkerThread线程。 ServerWorkerThread线程工作原理: 循环调用GetQueuedCompletionStatus()函数来得到IO操作结果。阻塞函数。当WINDOWS系统完成WSASend或者WSArecv的操作,把结果发到完成端口。GetQueuedCompletionStatus()马上返回,并从完成端口取得刚完成的WSASend/WSARecv的结果。然后接着发出WSASend/WSARecv,并继续下一次循环阻塞在GetQueuedCompletionStatus()这里。 a). pSessionInfo为空或者dwBytesTransferred =0 ,在客户端close socket,发相应数据包(异常)到DBSrv服务器(X命令-数据包),关闭客户端套按字。 b). while ( pSessionInfo->HasCompletionPacket() ) 如果数据验证正确,就转发数据包(A命令-数据包) DBSrv服务器。 c). if (pSessionInfo->Recv() 继续投递I/O操作。 总结: 我们不停地发出异步的WSASend/WSARecv IO操作,具体的IO处理过程由WINDOWS系统完成,WINDOWS系统完成实际的IO处理后,把结果送到完成端口上(如果有多个IO都完成了,那么就在完成端口那里排成一个队列)。我们在另外一个线程里从完成端口不断地取出IO操作结果,然后根据需要再发出WSASend/WSARecv IO操作。 IDM_STOPSERVICE: 停止服务(IOCP模型Server响应客户端请求) Close -> OnCommand(IDM_STOPSERVICE, 0L); ->g_fTerminated = TRUE; 线程退出。 ClearSocket(g_ssock); ClearSocket(g_csock); CloseHandle(g_hIOCP); 总结:SelGate(角色处理服务器),接受客户端连接,并且把用户数据包(角色处理)发送到DBSrv服务器中,由DBSrv服务器处理之后,发送数据包返回给客户端。SelGate之间是通过定时器,定时发送“心跳”数据。验证服务器存活的。客户端与服务器端的数据在传输中,是进行过加密的。 向DBSrv发送 ‘%A’+Msg+‘$0’消息: 转发客户端消息。 ‘%X’+Msg+‘$0’消息: 发送用户连接消息,增加到用户列表。 ‘%O’+Msg+‘$0’消息: 发送用户上线消息。 主要流程: 服务启动后,SelGate启动了AcceptThread,和ServerWorkerThread线程,AcceptThread线程接收客户端连接,并把session信息发送给DBSrv服务器,ServerWorkerThread线程从完成端口取得刚完成的WSASend/WSARecv的结果后,把客户端数据转发给DBSrv服务器。服务启动时,WSAAsyncSelect模型连接到DBSrv服务器中。一旦连接成功,就启动ThreadFuncForMsg线程,该线程从g_xMsgQueue(FD_READ事件读到的DBSrv服务器发来的数据)中取出DBSrv服务器处理过的数据。投递I/O,利用IOCP模型,发送到客户端。 ServerWorkerThread转发客户端数据 -> WSAAsyncSelect的Read读DBSrv处理后返回的数据-> ThreadFuncForMsg线程,投递WSASend消息,由Windows处理(IOCP),发送数据给客户端。
