本文作者:传奇大学

传奇源码分析(硬货)

传奇大学 2018-11-21 82 抢沙发
文章页顶部广告
传奇源码分析(硬货)摘要: DirectX类库分析(WindHorn):1. RegHandler.cpp 注册表访问(读写)。2. CWHApp派生CWHWindow,CWHWindow完成窗口的注册和创建...

DirectX类库分析(WindHorn)

1. RegHandler.cpp 注册表访问(读写)

2. CWHApp派生CWHWindowCWHWindow完成窗口的注册和创建。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

知道了图片格式,我们可以写一个抓图片格式的程序了。

客户端:

传奇的客户端源代码有两个工程,WindHornMir2Ex

先剖析一下WindHorn工程。

1CWHAppCWHWindowCWHDXGraphicWindowWindow程序窗口的创建。
CWHApp派生CWHWindowCWHWindow又派生CWHDXGraphicWindowCWHWindow

中完成窗口的注册和创建。CWHDXGraphicWindow调用CWHWindow完成创建窗口功能,然后再调用CreateDXG()来初始化DirectX

2CWHDefProcess派生出CloginProcessCcharacterProcessCgameProcess三个类。
这三个类是客户端处理的核心类。


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连接登陆服务器。

客户端:

传奇的客户端源代码有两个工程,WindHornMir2Ex

先剖析一下WindHorn工程。

1CWHAppCWHWindowCWHDXGraphicWindowWindow程序窗口的创建。
CWHApp派生CWHWindowCWHWindow又派生CWHDXGraphicWindowCWHWindow

中完成窗口的注册和创建。CWHDXGraphicWindow调用CWHWindow完成创建窗口功能,然后再调用CreateDXG()来初始化DirectX

2CWHDefProcess派生出CloginProcessCcharacterProcessCgameProcess三个类。
这三个类是客户端处理的核心类。


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的格式。分析一下源码吧,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的结构差不多。

登录处理事件:

0WinMain主函数调用g_xLoginProc.Load();加载图片等初始化,设置g_bProcState 的状态。

1CLoginProcess::OnKeyDown-> m_xLogin.OnKeyDown->g_xClientSocket.OnLogin;

WSAAsyncSelect模型ID_SOCKCLIENT_EVENT_MSG,因此,(登录,角色选择,游戏逻辑处理)都回调g_xClientSocket.OnSocketMessage(wParam, lParam)进行处理。

OnSocketMessage函数中:FD_READ事件中:

2g_bProcState判断当前状态,_GAME_PROC时,把GameGate的发送过来的消息压入PacketQ队列中,再进行处理。否则则调用OnMessageReceive(虚方法,根据g_bProcState状态,调用CloginProcess或者是CcharacterProcessOnMessageReceive方法)。

3CloginProcess:调用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;

2g_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服务器。

4CClientSocket::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();

6SetNextProc();执行: g_xGameProc.Load(); g_bProcState = _GAME_PROC;进行游戏状态。

游戏逻辑处理:

1.客户端处理:

CGameProcess::Load() 初始化游戏环境,加载地图等操作,调用ConnectToServerm_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 : 检查是不是NT2000的系统(IOCP

2) InitInstance: 初始化界面,加载WSAStartup

3) MainWndProc窗口回调函数.

2MainWndProc.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)中。

2WM_COMMAND:

IDM_STARTSERVICE: 启动服务(IOCP模型Server响应客户端请求)

IDM_STOPSERVICE: 停止服务(IOCP模型Server)

3WM_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); //IOCPAccept线程

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转发客户端数据 -> WSAAsyncSelectReadloginSrv处理后返回的数据-> ThreadFuncForMsg线程,投递WSASend消息,由Windows处理(IOCP),发送数据给客户端。

LoginSvr服务器

g_gcSock Local 5500端口

1.首先从LoginSvr.cpp WinMain分析:

1) CheckAvailableIOCP : 检查是不是NT2000的系统(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; xUserInfoListg_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_szServerListSelGate服务器列表消息)

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线程接收loginGateGameSvr服务器连接,加入g_xGateList网关列表中,ServerWorkerThread线程从完成端口取得刚完成的WSASend/WSARecv的结果后,进行分析处理两个服务器发送来的消息。服务启动同时,启动ThreadFuncForMsg线程,该线程从g_xMsgQueue(iocp读到的loginGate服务器发来的数据)中取出数据,处理数据。投递I/O,利用IOCP模型,发送到loginGate服务器。

5.接受登录成功后,接收GameSrv服务器发送的消息:

接收GameGate发送的消息:CClientSocket::OnSocketMessageFD_READ事件中,PacketQ.PushQ((BYTE*)pszPacket);把接收到的消息,压入PacketQ队列中。处理PacketQ队列数据是由CGameProcess::Load()时调用OnTimerCGameProcess::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列表,依次调用每个UserInfoOperate来处理命令队列中的所有操作; 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列表,调用OperateOperate遍历m_DelayProcessQ列表,过滤出RM_DOOPENHEALTHRM_STRUCKRM_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对象,设置nVisibleFlag2,设置对象为该商人实体,然后加入m_xVisibleObjectList列表中。

总结:循环列表,找出地图单元格中的所有玩家,把所有玩家(OS_MOVINGOBJECT)加入到m_xVisibleObjectList列表中。

c). 遍历m_xVisibleObjectList列表,(pVisibleObject->nVisibleFlag == 0)则删除该pVisibleObject对象。

d). RunRace调用AddRefMsg 向周围玩家发送SM_TURNSM_HIT

客户端收到消息后相应的处理:

1CGameProcess::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::OnSocketMessageFD_READ事件中,PacketQ.PushQ把接收到的消息,压入PacketQ队列中。处理PacketQ队列数据是由CGameProcess::Load()时调用OnTimerCGameProcess::OnTimer中处理的,处理过程为:

OnTimer -> ProcessPacket -> ProcessPacket处理m_xWaitPacketQueue队列消息(OnSocketMessageRecieve函数中未处理的消息)。

ProcessPacket 函数处理流程:

1 处理本玩家(SM_NOWDEATH, SM_DEATH, SM_CHANGEMAP, SM_STRUCK

a.如果接收到消息是SM_NOWDEATHSM_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_DISAPPEARSM_CLEAROBJECT)消息。

遍历m_xWaitDefaultPacketQueue消息列表

SM_DISAPPEARSM_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置为真。

1m_xMyHero.UpdateMotionState(nLoopTime, bIsMoveTime);处理本玩家消息。

a. UpdatePacketState函数:

遍历m_xPriorPacketQueue队列,如果有SM_NOWDEATHSM_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:

2CheckMappedData函数:遍历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,处理所有NPCUI界面重绘

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派生CWHWindowCWHDXGraphicWindow

二、CWHDefProcess派生出CloginProcessCcharacterProcessCgameProcess

客户端WinMain调用CWHDXGraphicWindow g_xMainWnd;创建一个窗口。

客户端CWHDXGraphicWindow在自己的Create函数中调用了CWHWindowCreate来创建窗口,然后再调用自己的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();如果点中CnoticeBoxm_xNotice.OnButtonDown

if m_xMsgBtn.OnLButtonDown则调用g_xClientSocket.SendNoticeOK()方法,发送还CM_LOGINNOTICEOK消息。

2m_pxSavedTargetActor = NULL;设置为空。CInterface::OnLButtonDown函数会判断

鼠标点击的位置(CmirMsgBox, CscrlBar,CgameBtnGetWindowInMousePos)

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) 返回地图里的物体(草药,物品,金子等)

1memcmp(pMapItem->szName, g_szGoldName 如果是黄金:

m_pMap->RemoveObject从地图中移走该的品。

if (m_pUserInfo->IncGold(pMapItem->nCount))增加用户的金钱(向周转玩家发送RM_ITEMHIDE 消息,隐藏该物体,GoldChanged(),改变玩家的金钱。否则,把黄金返回地图中。

2m_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列表,依次调用每个UserInfoOperate来处理命令队列中的所有操作; 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函数,我们在后面的服务器代码里分析过:它会根据XY坐标,在以自己坐标为中心周围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坐标。

2NPC,怪物:

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 : 检查是不是NT2000的系统(IOCP

2) InitInstance: 初始化界面,加载WSAStartup

3) MainWndProc窗口回调函数.

2MainWndProc.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转发客户端数据 -> WSAAsyncSelectReadDBSrv处理后返回的数据-> ThreadFuncForMsg线程,投递WSASend消息,由Windows处理(IOCP),发送数据给客户端。

RemObject解决自动生成代码

用过DELPHI写过多层框架的,可能能RemObject比较熟悉. RemObjects Service Builder 自动生成代码的同时,也给我们带来困扰. 每个类都要定义在intf文件里, 接口只有一个, 这些显然对我们开发系统来讲, 支持的不够, 我想使用多个接口,也不想没完没了的定义结构.

前一段时间写了一个自动生成元数据的小工具, 可以和界面StringGrid和控件自动绑定,实现数据集的效果,而且在客户端完全放弃了数据集, 使用纯对象, 自己觉得还算不错. 然而在传输的时候, 我又想使用RemObject支持序列化的功能. 这个问题如何解决呢?

后来我想摸索之后发现, 可以通过下面的方法来进行改进.

1. RemObject的工程改为普通工程, 把定义的元数据分离出来, interface下面uses一下.

2. 如果直接传递对象, 接口和代码怎么解决? (因为分离了元数据对象单元,所以编译的时候,不会生成代码. 解决问题的办法在于,RPCServerLibrary. Rodl文件. 正是因为这个文件,所以RemObjects Service Builder 自动生成代码根据其XML文件进行解析,最后生成代码.

   3. 方法定义的地方:

<Operation Name="GetPerson" UID="{B39EB743-BFBD-461B-B7CA-E6099E7C6BAC}">
<Parameters>
<Parameter Name="Result" DataType="Person" Flag="Result">
</Parameter>
</Parameters>
</Operation>

4. 结构体定义的地方:

   <Structs>
<Struct Name="person" UID="{8F16C438-213F-4818-97DC-30446F45D21E}" AutoCreateParams="1">
<Elements>
<Element Name="id" DataType="String">
</Element>
<Element Name="name" DataType="String">
</Element>
<Element Name="age" DataType="Integer">
</Element>
</Elements>
</Struct>
</Structs>

   有了上面的描述, 你应该想到怎么办了吧,对,就是在这里动手,前面我们不是自己做过动态生成元数据代码吗? 现在只要在其中修改这个RODL文件,把元数据和方法加进去, 然后编译的时候,RemObjects Service Builder 就会自动帮我们生成代码了.

   最后,我们把inter文件(自动生成)改一下.删除元数据的声明,建一个新的工程,把所有代码拷进去.呵呵,大功告成. 

 


文章版权及转载声明

作者:传奇大学本文地址:https://www.444.mba/post/790.html发布于 2018-11-21
文章转载或复制请以超链接形式并注明出处传奇大学

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏

分享
您需要 登录账户 后才能发表评论

发表评论

快捷回复:

评论列表 (暂无评论,82人围观)参与讨论

还没有评论,来说两句吧...