如下文章原创版权归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),发送数据给客户端。