傳奇源碼分析-服務器端html
LoginGate服務器web
服務器端:算法
1.首先從LoginGate.cpp WinMain分析:sql
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服務器
功能:(1)經過數據庫的直接鏈接驗證用戶名密碼正確;
(2)在接受Gate open user信息後,建立用戶CuserInfo對象保存用戶在CgateInfo,主要保存用戶須要驗證的信息(如ID,密碼,是否驗證經過);
(3)接受Gate轉發的請求server list消息,下發server list列表,前提條件有CuserInfo對象在Gate中(經過client 在gate上的socketid做爲標示判斷)
(3)若是失敗,返回錯誤,若是成功,同時返回server_list.
(4) 數據清理:由client從gate斷開時,清理Gate上的CUserInfo數據
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服務器。
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),發送數據給客戶端。
傳奇文件類型格式探討(一):
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字節大小。兩邊數據對比以後發現一致。知道了圖片格式,咱們能夠寫一個抓圖片格式的程序了。
登陸處理事件:
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廣播新玩家上線(座標)的消息。向該新玩家發送玩家信息(等級,裝備,魔法,攻擊力等)。
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數據包,進行遊戲邏輯處理。
所謂的_TDEFAULTMESSAGE數據封包:就是由_TDEFAULTMESSAGE頭 + 數據內容的一種組合通訊數據包PACKETMSG;
typedef struct tag_TDEFAULTMESSAGE
{
int nRecog;
WORD wIdent;
WORD wParam;
WORD wTag;
WORD wSeries;
} _TDEFAULTMESSAGE, *_LPTDEFAULTMESSAGE;
typedef struct tagPACKETMSG
{
_TDEFAULTMESSAGE stDefMsg;
CHAR szEncodeData[MAX_PATH * 4];
}PACKETMSG, *LPPACKETMSG;
其中,nRecog指的是後面的數據包中的數據(結構)的數目,但不是每一個消息命令都會用到_TDEFAULTMESSAGE以上字段;
WIdent就是消息命令,知道了消息命令和後面的消息內容的個數後,就能夠用指定的方式去讀取nRecog個預約格式(結構)數據出來。
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 移動
遍歷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
1、CWHApp派生CWHWindow和CWHDXGraphicWindow。
2、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怪物位置。
最近對高性能的服務器比較感興趣,讀過了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地圖編輯器這些工具.
DirectX類庫分析(WindHorn):
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鏈接登錄服務器。
Overlapped I/O模型深刻分析
簡述:
Overlapped I/O也稱Asynchronous I/O,異步I/O模型。異步I/O和同步I/O不一樣,同步I/O時,程序被掛起,一直到I/O處理完,程序才能得到控制。異步I/O,調用一個函數告訴OS,進行I/O操做,不等I/O結束就當即返回,繼續程序執行,操做系統完成I/O以後,通知消息給你。Overlapped I/O只是一種模型,它能夠由內核對象(hand),事件內核對象(hEvent), 異步過程調用(apcs) 和完成端口(I/O completion)實現。
Overlapped I/O的設計的目的:
取代多線程功能,(多線程存在同步機制,錯誤處理,在成千上萬個線程I/O中,線程上下文切換是十分消耗CPU資源的)。
Overlapped I/O模型是OS爲你傳遞數據,完成上下文切換,在處理完以後通知你。由程序中的處理,變爲OS的處理。內部也是用線程處理的。
Overlapped數據結構:
typedef struct _OVERLAPPED {
DWORD Internal; 一般被保留,當GetOverlappedResult()傳回False而且GatLastError()並不是傳回ERROR_IO_PENDINO時,該狀態置爲系統定的狀態。
DWORD InternalHigh; 一般被保留,當GetOverlappedResult()傳回False時,爲
被傳輸數據的長度。
DWORD Offset; 指定文件的位置,從該位置傳送數據,文件位置是相對文件開始
處的字節偏移量。調用 ReadFile或WriteFile函數以前調用進
程設置這個成員,讀寫命名管道及通訊設備時調用進程忽略這
個成員;
DWORD OffsetHigh; 指定開始傳送數據的字節偏移量的高位字,讀寫命名管道及通
信設備時調用進程忽略這個成員;
HANDLE hEvent; 標識事件,數據傳送完成時把它設爲信號狀態,調用ReadFile
WriteFile ConnectNamedPipe TransactNamedPipe函數
前,調用進程設置這個成員. 相關函數
CreateEvent ResetEvent GetOverlappedResult
WaitForSingleObject CWinThread GetLastError
} OVERLAPPED, *LPOVERLAPPED;
二個重要功能:
1. 標識每一個正在overlapped 的操做。
2. 程序和系統之間提供了共享區域。參數能夠在區域內雙向傳遞。
OVERLAPPED和數據緩衝區釋放問題:
在請求時,不能釋放,只有在I/O請求完成以後,才能夠釋放。若是發出多個overlapped請求,每一個overlapped讀寫操做,都必須包含文件位置(socket),另外,若是有多個磁盤,I/O執行次序沒法保證。(每一個overlapped都是獨立的請求操做)。
內核對象(hand)實現:
例子:用overlapped模型讀一個磁盤文件內容。
1.把設備句柄看做同步對象,ReadFile將設備句柄設爲無信號。ReadFile 異步I/O字節位置必須在OVERLAPPED結構中指定。
2.完成I/O,設置信息狀態。爲有信號。
3.WaitForSingleObject或WaitForMultipleObject判斷
或者異步設備調用GetOverLappedResult函數。
int main()
{
BOOL rc;
HANDLE hFile;
DWORD numread;
OVERLAPPED overlap;
char buf[READ_SIZE];
char szPath[MAX_PATH];
CheckOsVersion();
GetWindowsDirectory(szPath, sizeof(szPath));
strcat(szPath, "\\WINHLP32.EXE");
hFile = CreateFile( szPath,
GENERIC_READ,
FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL
);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("Could not open %s\n", szPath);
return -1;
}
memset(&overlap, 0, sizeof(overlap));
overlap.Offset = 1500;
rc = ReadFile(
hFile,
buf,
READ_SIZE,
&numread,
&overlap
);
printf("Issued read request\n");
if (rc)
{
printf("Request was returned immediately\n");
}
else
{
if (GetLastError() == ERROR_IO_PENDING)
{
printf("Request queued, waiting...\n");
WaitForSingleObject(hFile, INFINITE);
printf("Request completed.\n");
rc = GetOverlappedResult(
hFile,
&overlap,
&numread,
FALSE
);
printf("Result was %d\n", rc);
}
else
{
printf("Error reading file\n");
}
}
CloseHandle(hFile);
return EXIT_SUCCESS;
}
事件內核對象(hEvent):
內核對象(hand)實現的問題:
不能區分那一個overlapped操做,對同一個文件handle,系統有多個異步操做時(一邊讀文件頭,一邊寫文件尾, 有一個完成,就會有信號,不能區分是那種操做。),爲每一個進行中的overlapped調用GetOverlappedResult是很差的做法。
事件內核對象(hEvent)實現方案:
Overlapped成員hEven標識事件內核對象。CreateEvent,爲每一個請求建立一個事件,初始化每一個請求的hEvent成員(對同一文件多個讀寫請求,每一個操做綁定一個event對象)。調用WaitForMultipleObject來等等其中一個(或所有)完成。
另外Event對象必須是手動重置。使用自動重置(在等待event以前設置,WaitForSingleObject()和 WaitForMultipleObjects()函數永不返回)。
自動重置事件
WaitForSingleObject()和 WaitForMultipleObjects()會等待事件到信號狀態,隨後又自動將其重置爲非信號狀態,這樣保證了等待此事件的線程中只有一個會被喚醒。
手動重置事件
須要用戶調用ResetEvent()纔會重置事件。可能有若干個線程在等待同一事件,這樣當事件變爲信號狀態時,全部等待線程均可以運行了。 SetEvent()函數用來把事件對象設置成信號狀態,ResetEvent()把事件對象重置成非信號狀態,二者均需事件對象句柄做參數。
相關例子以下:
int main()
{
int i;
BOOL rc;
char szPath[MAX_PATH];
CheckOsVersion();
GetWindowsDirectory(szPath, sizeof(szPath));
strcat(szPath, "\\WINHLP32.EXE");
ghFile = CreateFile( szPath,
GENERIC_READ,
FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL
);
if (ghFile == INVALID_HANDLE_VALUE)
{
printf("Could not open %s\n", szPath);
return -1;
}
for (i=0; i<MAX_REQUESTS; i++)
{
QueueRequest(i, i*16384, READ_SIZE);
}
printf("QUEUED!!\n");
MTVERIFY( WaitForMultipleObjects(
MAX_REQUESTS, ghEvents, TRUE, INFINITE
) != WAIT_FAILED );
for (i=0; i<MAX_REQUESTS; i++)
{
DWORD dwNumread;
rc = GetOverlappedResult(
ghFile,
&gOverlapped[i],
&dwNumread,
FALSE
);
printf("Read #%d returned %d. %d bytes were read.\n",
i, rc, dwNumread);
CloseHandle(gOverlapped[i].hEvent);
}
CloseHandle(ghFile);
return EXIT_SUCCESS;
}
int QueueRequest(int nIndex, DWORD dwLocation, DWORD dwAmount)
{
int i;
BOOL rc;
DWORD dwNumread;
DWORD err;
MTVERIFY(
ghEvents[nIndex] = CreateEvent(
NULL, // No security
TRUE, // Manual reset - extremely important!
FALSE, // Initially set Event to non-signaled state
NULL // No name
)
);
gOverlapped[nIndex].hEvent = ghEvents[nIndex];
gOverlapped[nIndex].Offset = dwLocation;
for (i=0; i<MAX_TRY_COUNT; i++)
{
rc = ReadFile(
ghFile,
gBuffers[nIndex],
dwAmount,
&dwNumread,
&gOverlapped[nIndex]
);
if (rc)
{
printf("Read #%d completed immediately.\n", nIndex);
return TRUE;
}
err = GetLastError();
if (err == ERROR_IO_PENDING)
{
// asynchronous i/o is still in progress
printf("Read #%d queued for overlapped I/O.\n", nIndex);
return TRUE;
}
if ( err == ERROR_INVALID_USER_BUFFER ||
err == ERROR_NOT_ENOUGH_QUOTA ||
err == ERROR_NOT_ENOUGH_MEMORY )
{
Sleep(50); // Wait around and try later
continue;
}
break;
}
printf("ReadFile failed.\n");
return -1;
}
異步過程調用(apcs):
事件內核對象(hEvent)的問題:
事件內核對象在使用WaitForMultipleObjects時,只能等待64個對象。須要另建兩個數據組,並gOverlapped[nIndex].hEvent = ghEvents[nIndex]綁定起來。
異步過程調用(apcs)實現方案:
異步過程調用,callback回調函數,在一個Overlapped I/O完成以後,系統調用該回調函數。OS在有信號狀態下(設備句柄),纔會調用回調函數(可能有不少APCS等待處理了),傳給它完成I/O請求的錯誤碼,傳輸字節數和Overlapped結構的地址。
五個函數能夠設置信號狀態:
1. SleepEx
2. WaitForSingleObjectEx
3. WaitForMultipleObjectEx
4. SingalObjectAndWait
5. MsgWaitForMultipleObjectsEx
Main函數調用WaitForSingleObjectEx, APCS被處理,調用回調函數
FileIOCompletionRoutine
VOID WINAPI FileIOCompletionRoutine(
DWORD dwErrorCode, // completion code
DWORD dwNumberOfBytesTransfered, // number of bytes transferred
LPOVERLAPPED lpOverlapped // pointer to structure with I/O information
)
{
int nIndex = (int)(lpOverlapped->hEvent);
printf("Read #%d returned %d. %d bytes were read.\n",
nIndex,
dwErrorCode,
dwNumberOfBytesTransfered);
if (++nCompletionCount == MAX_REQUESTS)
SetEvent(ghEvent); // Cause the wait to terminate
}
int main()
{
int i;
char szPath[MAX_PATH];
CheckOsVersion();
MTVERIFY(
ghEvent = CreateEvent(
NULL, // No security
TRUE, // Manual reset - extremely important!
FALSE, // Initially set Event to non-signaled state
NULL // No name
)
);
GetWindowsDirectory(szPath, sizeof(szPath));
strcat(szPath, "\\WINHLP32.EXE");
ghFile = CreateFile( szPath,
GENERIC_READ,
FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL
);
if (ghFile == INVALID_HANDLE_VALUE)
{
printf("Could not open %s\n", szPath);
return -1;
}
for (i=0; i<MAX_REQUESTS; i++)
{
QueueRequest(i, i*16384, READ_SIZE);
}
printf("QUEUED!!\n");
for (;;)
{
DWORD rc;
rc = WaitForSingleObjectEx(ghEvent, INFINITE, TRUE );
if (rc == WAIT_OBJECT_0)
break;
MTVERIFY(rc == WAIT_IO_COMPLETION);
}
CloseHandle(ghFile);
return EXIT_SUCCESS;
}
int QueueRequest(int nIndex, DWORD dwLocation, DWORD dwAmount)
{
int i;
BOOL rc;
DWORD err;
gOverlapped[nIndex].hEvent = (HANDLE)nIndex;
gOverlapped[nIndex].Offset = dwLocation;
for (i=0; i<MAX_TRY_COUNT; i++)
{
rc = ReadFileEx(
ghFile,
gBuffers[nIndex],
dwAmount,
&gOverlapped[nIndex],
FileIOCompletionRoutine
);
if (rc)
{
printf("Read #%d queued for overlapped I/O.\n", nIndex);
return TRUE;
}
err = GetLastError();
if ( err == ERROR_INVALID_USER_BUFFER ||
err == ERROR_NOT_ENOUGH_QUOTA ||
err == ERROR_NOT_ENOUGH_MEMORY )
{
Sleep(50); // Wait around and try later
continue;
}
break;
}
printf("ReadFileEx failed.\n");
return -1;
}
完成端口(I/O completion):
異步過程調用(apcs)問題:
只有發overlapped請求的線程才能夠提供callback函數(須要一個特定的線程爲一個特定的I/O請求服務)。
完成端口(I/O completion)的優勢:
不會限制handle個數,可處理成千上萬個鏈接。I/O completion port容許一個線程將一個請求暫時保存下來,由另外一個線程爲它作實際服務。
併發模型與線程池:
在典型的併發模型中,服務器爲每個客戶端建立一個線程,若是不少客戶同時請求,則這些線程都是運行的,那麼CPU就要一個個切換,CPU花費了更多的時間在線程切換,線程確沒獲得不少CPU時間。到底應該建立多少個線程比較合適呢,微軟件幫助文檔上講應該是2*CPU個。但理想條件下最好線程不要切換,而又能象線程池同樣,重複利用。I/O完成端口就是使用了線程池。
理解與使用:
第一步:
在咱們使用完成端口以前,要調用CreateIoCompletionPort函數先建立完成端口對象。
定義以下:
HANDLE CreateIoCompletionPort(
HANDLE FileHandle,
HANDLE ExistingCompletionPort,
DWORD CompletionKey,
DWORD NumberOfConcurrentThreads
);
FileHandle:
文件或設備的handle, 若是值爲INVALID_HANDLE_VALUE則產生一個沒有和任何文件handle有關係的port.( 能夠用來和完成端口聯繫的各類句柄,文件,套接字)
ExistingCompletionPort:
NULL時生成一個新port, 不然handle會加到此port上。
CompletionKey:
用戶自定義數值,被交給服務的線程。GetQueuedCompletionStatus函數時咱們能夠徹底獲得咱們在此聯繫函數中的完成鍵(申請的內存塊)。在GetQueuedCompletionStatus
中能夠完封不動的獲得這個內存塊,而且使用它。
NumberOfConcurrentThreads:
參數NumberOfConcurrentThreads用來指定在一個完成端口上能夠併發的線程數量。理想的狀況是,一個處理器上只運行一個線程,這樣能夠避免線程上下文切換的開銷。若是這個參數的值爲0,那就是告訴系統線程數與處理器數相同。咱們能夠用下面的代碼來建立I/O完成端口。
隱藏在之建立完成端口的祕密:
1. 建立一個完成端口
CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, dwNumberOfConcurrentThreads);
2. 設備列表,完成端口把它同一個或多個設備相關聯。
CreateIoCompletionPort(hDevice, hCompPort, dwCompKey, 0) ;
第二步:
根據處理器個數,建立cpu*2個工做線程:
CreateThread(NULL, 0, ServerWorkerThread, CompletionPort,0, &ThreadID))
與此同時,服務器調用WSASocket,bind, listen, WSAAccept,以後,調用
CreateIoCompletionPort((HANDLE) Accept, CompletionPort... )把一個套接字句柄和一個完成端口綁定到一塊兒。完成端口又同一個或多個設備相關聯着,因此以套接字爲基礎,投遞發送和請求,對I/O處理。接着,能夠依賴完成端口,接收有關I/O操做完成狀況的通知。再看程序裏:
WSARecv(Accept, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags,
&(PerIoData->Overlapped), NULL)開始調用,這裏象前面講過的同樣,既然是異步I/O,因此WSASend和WSARecv的調用會當即返回。
系統處理:
當一個設備的異步I/O請求完成以後,系統會檢查該設備是否關聯了一個完成端口,若是是,系統就向該完成端口的I/O完成隊列中加入完成的I/O請求列。
而後咱們須要從這個完成隊列中,取出調用後的結果(須要經過一個Overlapped結構來接收調用的結果)。怎麼知道這個隊列中已經有處理後的結果呢,調用GetQueuedCompletionStatus函數。
工做線程與完成端口:
和異步過程調用不一樣(在一個Overlapped I/O完成以後,系統調用該回調函數。OS在有信號狀態下(設備句柄),纔會調用回調函數(可能有不少APCS等待處理了))
GetQueuedCompletionStatus
在工做線程內調用GetQueuedCompletionStatus函數。
GetQueuedCompletionStatus(
HANDLE CompletionPort,
LPDWORD lpNumberOfBytesTransferred,
LPDWORD lpCompletionKey,
LPOVERLAPPED *lpOverlapped,
DWORD dwMilliseconds
);
CompletionPort:指出了線程要監視哪個完成端口。不少服務應用程序只是使用一個I/O完成端口,全部的I/O請求完成之後的通知都將發給該端口。
lpNumberOfBytesTransferred:傳輸的數據字節數
lpCompletionKey:
完成端口的單句柄數據指針,這個指針將能夠獲得咱們在CreateIoCompletionPort中申請那片內存。
lpOverlapped:
重疊I/O請求結構,這個結構一樣是指向咱們在重疊請求時所申請的內存塊,同時和lpCompletionKey,同樣咱們也能夠利用這個內存塊來存儲咱們要保存的任意數據。
dwMilliseconds:
等待的最長時間(毫秒),若是超時,lpOverlapped被設爲NULL,函數返回False.
GetQueuedCompletionStatus功能及隱藏的祕密:
GetQueuedCompletionStatus使調用線程掛起,直到指定的端口的I/O完成隊列中出現了一項或直到超時。(I/0完成隊列中出現了記錄)調用GetQueuedCompletionStatus時,調用線程的ID(cpu*2個線程,每一個ServerWorkerThread的線程ID)就被放入該等待線程隊列中。
等待線程隊列很簡單,只是保存了這些線程的ID。完成端口會按照後進先出的原則將一個線程隊列的ID放入到釋放線程列表中。
這樣,I/O完成端口內核對象就知道哪些線程正在等待處理完成的I/O請求。當端口的I/O完成隊列出現一項時,完成端口就喚醒(睡眠狀態中變爲可調度狀態)等待線程隊列中的一個線程。線程將獲得完成I/O項中的信息:傳輸的字節數,完成鍵(單句柄數據結構)和Overlapped結構地址,線程是經過GetQueuedCompletionStatus返回這些信息,等待CPU的調度。
GetQueuedCompletionStatus返回可能有多種緣由,若是傳遞無效完成端口句柄,函數返回False,GetLastError返回一個錯誤(ERROR_INVALID_HANDLE),若是超時,返回False, GetLastError返回WAIT_TIMEOUT, i/o完成隊列刪除一項,該表項是一個成功完成的I/O請求,則返回True。
調用GetQueuedCompletionStatus的線程是後進先出的方式喚醒的,好比有4個線程等待,若是有一個I/O,最後一個調用GetQueuedCompletionStatus的線程被喚醒來處理。處理完以後,再調用GetQueuedCompletionStatus進入等待線程隊列中。
深刻分析完成端口線程池調度原理:
假設咱們運行在2CPU的機器上。建立完成端口時指定2個併發,建立了4個工做線程加入線程池中等待完成I/O請求,且完成端口隊列(先入先出)中有3個完成I/O的請求的狀況:
工做線程運行, 建立了4個工做線程,調用GetQueuedCompletionStatus時,該調用線程就進入了睡眠狀態,假設這個時候,I/O完成隊列出現了三項,調用線程的ID就被放入該等待線程隊列中, (如圖):
等待的線程隊列(後進先出) |
進隊列 |
出隊列 |
線 程 A |
線 程 B |
線 程 C |
線 程 D |
I/O完成端口內核對象(第3個參數等級線程隊列),所以知道哪些線程正在等待處理完成的I/O請求。當端口的I/O完成隊列出現一項時,完成端口就喚醒(睡眠狀態中變爲可調度狀態)等待線程隊列中的一個線程(前面講過等待線程隊列是後進先出)。因此線程D將獲得完成I/O項中的信息:傳輸的字節數,完成鍵(單句柄數據結構)和Overlapped結構地址,線程是經過GetQueuedCompletionStatus返回這些信息。
在前面咱們指定了併發線程的數目是2,因此I/O完成端口喚醒2個線程,線程D和線程C,另兩個繼續休眠(線程B,線程A),直到線程D處理完了,發現表項裏還有要處理的,就喚醒同一線程繼續處理。
等待的線程隊列(後進先出) |
進隊列 |
出隊列 |
線 程 A |
線 程 B |
釋放線程隊列 |
線 程 C |
線 程 D |
線程併發量:
併發量限制了與該完成端口相關聯的可運行線程的數目, 它相似閥門的做用。 當與該完成端口相關聯的可運行線程的總數目達到了該併發量,系統就會阻塞任何與該完成端口相關聯的後續線程的執行, 直到與該完成端口相關聯的可運行線程數目降低到小於該併發量爲止。因此解釋了線程池中的運行線程可能會比設置的併發線程多的緣由。
它的做用:
最有效的假想是發生在有完成包在隊列中等待,而沒有等待被知足,由於此時完成端口達到了其併發量的極限。此時,一個正在運行中的線程調用 GetQueuedCompletionStatus時,它就會馬上從隊列中取走該完成包。這樣就不存在着環境的切換,由於該處於運行中的線程就會接二連三地從隊列中取走完成包,而其餘的線程就不能運行了。
注意:若是池中的全部線程都在忙,客戶請求就可能拒絕,因此要適當調整這個參數,得到最佳性能。
線程併發:D線程掛起,加入暫停線程,醒來後又加入釋放線程隊列。
線 程 C |
線 程 B |
線 程 A |
出隊列 |
進隊列 |
等待的線程隊列(後進先出) |
釋放線程隊列 |
暫停線程 |
線 程 D |
線程的安全退出:
PostQueudCompletionStatus函數,咱們能夠用它發送一個自定義的包含了OVERLAPPED成員變量的結構地址,裏面包含一個狀態變量,當狀態變量爲退出標誌時,線程就執行清除動做而後退出。
完成端口使用須要注意的地方:
1.在執行wsasend和wsarecv操做前,請先將overlapped結構體使用memset進行清零。
於敦德 2006-3-16
LiveJournal是99年始於校園中的項目,幾我的出於愛好作了這樣一個應用,以實現如下功能:
LiveJournal採用了大量的開源軟件,甚至它自己也是一個開源軟件。
在上線後,LiveJournal實現了很是快速的增加:
LiveJournal從1臺服務器發展到100臺服務器,這其中經歷了無數的傷痛,但同時也摸索出瞭解決這些問題的方法,經過對LiveJournal的學習,可讓咱們避免LJ曾經犯過的錯誤,而且從一開始就對系統進行良好的設計,以免後期的痛苦。
下面咱們一步一步看LJ發展的腳步。
一臺別人捐助的服務器,LJ最初就跑在上面,就像Google開始時候用的破服務器同樣,值得咱們尊敬。這個階段,LJ的人以驚人的速度熟悉的Unix的操做管理,服務器性能出現過問題,不過還好,能夠經過一些小修小改應付過去。在這個階段裏LJ把CGI升級到了FastCGI。
最終問題出現了,網站愈來愈慢,已經沒法經過優過化來解決的地步,須要更多的服務器,這時LJ開始提供付費服務,多是想經過這些錢來購買新的服務器,以解決當時的困境。
毫無疑問,當時LJ存在巨大的單點問題,全部的東西都在那臺服務器的鐵皮盒子裏裝着。
用付費服務賺來的錢LJ買了兩臺服務器:一臺叫作Kenny的Dell 6U機器用於提供Web服務,一臺叫作Cartman的Dell 6U服務器用於提供數據庫服務。
LJ有了更大的磁盤,更多的計算資源。但同時網絡結構仍是很是簡單,每臺機器兩塊網卡,Cartman經過內網爲Kenny提供MySQL數據庫服務。
暫時解決了負載的問題,新的問題又出現了:
又買了兩臺,Kyle和Stan,此次都是1U的,都用於提供Web服務。目前LJ一共有3臺Web服務器和一臺數據庫服務器。這時須要在3臺Web服務器上進行負載均橫。
LJ把Kenny用於外部的網關,使用mod_backhand進行負載均橫。
而後問題又出現了:
又買了一臺數據庫服務器。在兩臺數據庫服務器上使用了數據庫同步(Mysql支持的Master-Slave模式),寫操做所有針對主數據庫(經過Binlog,主服務器上的寫操做能夠迅速同步到從服務器上),讀操做在兩個數據庫上同時進行(也算是負載均橫的一種吧)。
實現同步時要注意幾個事項:
有錢了,固然要多買些服務器。部署後快了沒多久,又開始慢了。此次有更多的Web服務器,更多的數據庫服務器,存在 IO與CPU爭用。因而採用了BIG-IP做爲負載均衡解決方案。
如今服務器基本上夠了,但性能仍是有問題,緣由出在架構上。
數據庫的架構是最大的問題。因爲增長的數據庫都是以Slave模式添加到應用內,這樣惟一的好處就是將讀操做分佈到了多臺機器,但這樣帶來的後果就是寫操做被大量分發,每臺機器都要執行,服務器越多,浪費就越大,隨着寫操做的增長,用於服務讀操做的資源愈來愈少。
由一臺分佈到兩臺
最終效果
如今咱們發現,咱們並不須要把這些數據在如此多的服務器上都保留一份。服務器上已經作了RAID,數據庫也進行了備份,這麼多的備份徹底是對資源的浪費,屬於冗餘極端過分。那爲何不把數據分佈存儲呢?
問題發現了,開始考慮如何解決。如今要作的就是把不一樣用戶的數據分佈到不一樣的服務器上進行存儲,以實現數據的分佈式存儲,讓每臺機器只爲相對固定的用戶服務,以實現平行的架構和良好的可擴展性。
爲了實現用戶分組,咱們須要爲每個用戶分配一個組標記,用於標記此用戶的數據存放在哪一組數據庫服務器中。每組數據庫由一個master及幾個slave組成,而且slave的數量在2-3臺,以實現系統資源的最合理分配,既保證數據讀操做分佈,又避免數據過分冗餘以及同步操做對系統資源的過分消耗。
由一臺(一組)中心服務器提供用戶分組控制。全部用戶的分組信息都存儲在這臺機器上,全部針對用戶的操做須要先查詢這臺機器獲得用戶的組號,而後再到相應的數據庫組中獲取數據。
這樣的用戶架構與目前LJ的架構已經很相像了。
在具體的實現時須要注意幾個問題:
問題:
對於Master-Slave模式的單點問題,LJ採起了Master-Master模式來解決。所謂Master-Master其實是人工實現的,並非由MySQL直接提供的,實際上也就是兩臺機器同時是Master,也同時是Slave,互相同步。
Master-Master實現時須要注意:
解決方案:
Master-Master模式還有一種用法,這種方法與前一種相比,仍然保持兩臺機器的同步,但只有一臺機器提供服務(讀和寫),在天天晚上的時候進行輪換,或者出現問題的時候進行切換。
如今插播一條廣告,MyISAM VS InnoDB。
使用InnoDB:
使用MyISAM:
去年我寫過一篇文章介紹memcached,它就是由LJ的團隊開發的一款緩存工具,以key-value的方式將數據存儲到分佈的內存中。LJ緩存的數據:
如何創建緩存策略?
想緩存全部的東西?那是不可能的,咱們只須要緩存已經或者可能致使系統瓶頸的地方,最大程度的提交系統運行效率。經過對MySQL的日誌的分析咱們能夠找到緩存的對象。
緩存的缺點?
在數據包級別使用BIG-IP,但BIG-IP並不知道咱們內部的處理機制,沒法判斷由哪臺服務器對這些請求進行處理。反向代理並不能很好的起到做用,不是已經夠快了,就是達不到咱們想要的效果。
因此,LJ又開發了Perlbal。特色:
LJ使用開源的MogileFS做爲分佈式文件存儲系統。MogileFS使用很是簡單,它的主要設計思想是:
到目前爲止就這麼多了,更多文檔能夠在http://www.danga.com/words/找到。Danga.com和LiveJournal.com的同窗們拿這個文檔參加了兩次MySQL Con,兩次OS Con,以及衆多的其它會議,無私的把他們的經驗分享出來,值得咱們學習。在web2.0時代快速開發獲得你們愈來愈多的重視,但良好的設計還是每個應用的基礎,但願web2.0們在成長爲Top500網站的路上,不要由於架構阻礙了網站的發展。