網絡遊戲服務器開發

原貼:http://blog.csdn.net/surehui/article/details/5427585


當今網絡遊戲在中國大陸已經在大範圍的蔓延,暫且不論這樣的一種趨勢會帶來什麼樣的遊戲產業趨勢。這裏只就網絡遊戲的製作和大家進行交流,同時將自己的製作經驗寫處理,希望爲中國的遊戲業的發展做出一點點的貢獻。。

網絡遊戲的程序開發從某種意義上來看,最重要的應該在於遊戲服務器端的設計和製作。對於服務器端的製作。將分爲以下幾個模塊進行:

1.網絡通信模塊
2.協議模塊
3.線程池模塊
4.內存管理模塊
5.遊戲規則處理模塊
6.後臺遊戲仿真世界模塊。

現在就網絡中的通信模塊處理談一下自己的看法!!

在網絡遊戲客戶端和服務器端進行交互的雙向I/O模型中分別有以下幾種模型:
1. Select模型
2. 事件驅動模型
3. 消息驅動模型
4. 重疊模型
5. 完成端口重疊模型。

  在這樣的幾種模型中,能夠通過硬件性能的提高而提高軟件性能,並且能夠同時處理成千上百個I/O請求的模型。服務器端應該採用的最佳模型是:完成端口模型。然而在衆多的模型之中完成端口的處理是最複雜的,而它的複雜之處就在於多服務器工作線程並行處理客戶端的I/O請求和理解完成端口的請求處理過程。

對於服務器端完成端口的處理過程總結以下一些步驟:

1. 建立服務器端SOCKET套接字描述符,這一點比較簡單。
例如:
SOCKET server_socket;
Server_socket = socket(AF_INET,SOCK_STREAM,0);

2.綁定套接字server_socket。
Const int SERV_TCP_PORT = 5555;
struct sockaddr_in server_address.

memset(&server_address, 0, sizeof(struct sockaddr_in));
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(SERV_TCP_PORT);
//綁定
Bind(serve_socket,( struct sockaddr *)&server_address, sizeof(server_address));

2. 對於建立的服務器套接字描述符偵聽。
Listen(server_socket ,5);

3. 初始化我們的完成端口,開始的時候是產生一個新的完成端口。
HANDLE hCompletionPort;
HCompletionPort = CreateIoCompletionPort(NULL,NULL,NULL,0);

4. 在我們已經產生出來新的完成端口之後,我們就需要進行系統的偵測來得到系統的硬件信息。從而來定出我們的服務器完成端口工作線程的數量。

SYSTEM_INFO system_info;
GetSystemInfo(&system_info);

  在我們知道我們系統的信息之後,我們就需要做這樣的一個決定,那就是我們的服務器系統該有多少個線程進行工作,我一般會選擇當前處理器的2倍來生成我們的工作線程數量(原因考慮線程的阻塞,所以就必須有後備的線程來佔有處理器進行運行,這樣就可以充分的提高處理器的利用率)。

代碼:
WORD threadNum = system_info. DwNumberOfProcessors*2+2;
for(int i=0;I<threadNum;i++)
{
    HANDLE hThread;
    DWORD dwthreadId;
    hThread = _beginthreadex(NULL,ServerWorkThrea,  (LPVOID)hCompletePort,0,&dwthreadId);
    CloseHandle(hThread); 
}
CloseHandle(hThread)在程序代碼中的作用是在工作線程在結束後,能夠自動銷燬對象作用。

6. 產生服務器檢測客戶端連接並且處理線程。
HANDLE hAcceptThread;
DWORD dwThreadId;
hAcceptThread= _beginthreadex(NULL,AcceptWorkThread,NULL, &dwThreadId);
CloseHandle(hAcceptThread);

 

7.連接處理線程的處理,在線程處理之前我們必須定義一些屬於自己的數據結構體來進行網絡I/O交互過程中的數據記錄和保存。

首先我要將如下幾個函數來向大家進行解析:
1.
HANDLE CreateIoCompletionPort (
    HANDLE FileHandle, // handle to file
    HANDLE ExistingCompletionPort, // handle to I/O completion port
    ULONG_PTR CompletionKey, // completion key
    DWORD NumberOfConcurrentThreads // number of threads to execute concurrently
);
參數1:
可以用來和完成端口聯繫的各種句柄,在這其中可以包括如下一些:
套接字,文件等。

參數2:
已經存在的完成端口的句柄,也就是在第三步我們初始化的完成端口的句柄就可以了。

參數3:
這個參數對於我們來說將非常有用途。這就要具體看設計者的想法了, ULONG_PTR對於完成端口而言是一個單句柄數據,同時也是它的完成鍵值。同時我們在進行
這樣的GetQueuedCompletionStatus(….)(以下解釋)函數時我們可以完全得到我們在此聯繫函數中的完成鍵,簡單的說也就是我們在CreateIoCompletionPort(…..)申請的內存塊,在GetQueuedCompletionStatus(……)中可以完封不動的得到這個內存塊,並且使用它。這樣就給我們帶來了一個便利。也就是我們可以定義任意數據結構來存儲我們的信息。在使用的時候只要進行強制轉化就可以了。

參數4:
引用MSDN上的解釋
[in] Maximum number of threads that the operating system allows to concurrently process I/O completion packets for the I/O completion port. If this parameter is zero, the system allows as many concurrently running threads as there are processors in the system.
這個參數我們在使用中只需要將它初始化爲0就可以了。上面的意思我想大家應該也是瞭解的了!嘿嘿!!


我要向大家介紹的第二個函數也就是
2.
BOOL GetQueuedCompletionStatus(
    HANDLE CompletionPort, // handle to completion port
    LPDWORD lpNumberOfBytes, // bytes transferred
    PULONG_PTR lpCompletionKey, // file completion key
    LPOVERLAPPED *lpOverlapped, // buffer
    DWORD dwMilliseconds // optional timeout value
);
參數1:
我們已經在前面產生的完成端口句柄,同時它對於客戶端而言,也是和客戶端SOCKET連接的那個端口。

參數2:
一次完成請求被交換的字節數。(重疊請求以下解釋)

參數3:
完成端口的單句柄數據指針,這個指針將可以得到我們在CreateIoCompletionPort(………)中申請那片內存。
借用MSDN的解釋:
[out] Pointer to a variable that receives the completion key value associated with the file handle whose I/O operation has completed. A completion key is a per-file key that is specified in a call to CreateIoCompletionPort.
所以在使用這個函數的時候只需要將此處填一相應數據結構的空指針就可以了。上面的解釋只有大家自己擺平了。

參數4:
重疊I/O請求結構,這個結構同樣是指向我們在重疊請求時所申請的內存塊,同時和lpCompletionKey,一樣我們也可以利用這個內存塊來存儲我們要保存的任意數據。以便於我們來進行適當的服務器程序開發。
[out] Pointer to a variable that receives the address of the OVERLAPPED structure that was specified when the completed I/O operation was started.(MSDN)

3.
int WSARecv(
    SOCKET s, 
    LPWSABUF lpBuffers, 
    DWORD dwBufferCount, 
    LPDWORD lpNumberOfBytesRecvd, 
    LPDWORD lpFlags, 
    LPWSAOVERLAPPED lpOverlapped, 
    LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine 
);
這個函數也就是我們在進行完成端口請求時所使用的請求接受函數,同樣這個函數可以用ReadFile(………)來代替,但不建議使用這個函數。

參數1:
已經和Listen套接字建立連接的客戶端的套接字。

參數2:
用於接受請求數據的緩衝區。
[in/out] Pointer to an array of WSABUF structures. Each WSABUF structure contains a pointer to a buffer and the length of the buffer.(MSDN)。
參數3:
參數2所指向的WSABUF結構的數量。
[in] Number of WSABUF structures in the lpBuffers array.(MSDN)

參數4:

[out] Pointer to the number of bytes received by this call if the receive operation completes immediately. (MSDN)

參數5:
[in/out] Pointer to flags.(MSDN)
參數6:

這個參數對於我們來說是比較有作用的,當它不爲空的時候我們就是提出我們的重疊請求。同時我們申請的這樣的一塊內存塊可以在完成請求後直接得到,因此我們同樣可以通過它來爲我們保存客戶端和服務器的I/O信息。
參數7:
[in] Pointer to the completion routine called when the receive operation has been completed (ignored for nonoverlapped sockets).(MSDN)
4.
int WSASend(
    SOCKET s, 
    LPWSABUF lpBuffers, 
    DWORD dwBufferCount, 
    LPDWORD lpNumberOfBytesSent, 
    DWORD dwFlags, 
    LPWSAOVERLAPPED lpOverlapped, 
    LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine 
);
參數解釋可以參考上面或者MSDN。在這裏就不再多說了。

下面就關client端用戶連接(connect(……..))請求的處理方式進行

舉例如下:
const int BUFFER_SIZE = 1024;
typedef struct IO_CS_DATA
{
    SOCKET clisnt_s; //客戶端SOCKET
    WSABUF wsaBuf; 
    Char inBuffer[BUFFET_SIZE];
    Char outBuffer[BUFFER_SIZE];
    Int recvLen;
    Int sendLen;
    SYSTEM_TIME start_time;
    SYSTEM_TIME start_time;
}IO_CS_DATA;


UINT WINAPI ServerAcceptThread(LPVOID param)
{
    SOCKET client_s;
    HANDLE hCompltPort = (HANDLE) param;
    struct sockaddr_in client_addr;
    int addr_Len = sizeof(client_addr);
    LPHANDLE_DATA hand_Data = NULL; 
    while(true)
    {
        If((client_s=accept(server_socket,NULL,NULL)) == SOCKET_ERROR)
        {
            printf("Accept() Error: %d",GetLastError());
            return 0;
        }
        hand_Data = (LPHANDLE_DATA)malloc(sizeof(HANDLE_DATA));
        hand_Data->socket = client_s;
        if(CreateIoCompletionPort((HANDLE)client_s,hCompltPort,(DWORD)hand_Data,0)==NULL)
        {
            printf("CreateIoCompletionPort()Error: %d", GetLastError());
        }
        else
        { 
            game_Server->RecvDataRequest(client_s);
        }
    }
    return 0;
}

在這個例子中,我們要闡述的是使用我們已經產生的接受連接線程來完成我們響應Client端的connect請求。關於這個線程我們同樣可以用我們線程池的方式來進行生成多個線程來進行處理,其他具體的函數解釋已經在上面解釋過了,希望不懂的自己琢磨。
關於game_Sever object的定義處理將在下面進行介紹。

class CServerSocket : public CBaseSocket
{
public:
    CServerSocket();
    virtual ~CServerSocket();
    bool StartUpServer(); //啓動服務器
    void StopServer(); //關閉服務器
    //發送或者接受數據(重疊請求)
    bool RecvDataRequest(SOCKET client_s); 
    bool SendDataRequest(SOCKET client_s,char *buf,int b_len);

    void ControlRecvData(SOCKET client_s,char *buf,int b_len);

    void CloseClient(SOCKET client_s); 
private:
    friend UINT WINAPI GameServerThread(LPVOID completionPortID); //遊戲服務器通信工作線程
private:
    void Init();
    void Release();
    bool InitComplePort();
    bool InitServer();
    bool CheckOsVersion();
    bool StartupWorkThread();
    bool StartupAcceptThread();
private:
    enum { SERVER_PORT = 10006};
    UINT cpu_Num; //處理器數量
    CEvent g_ServerStop; //服務器停止事件
    CEvent g_ServerWatch; //服務器監視事件
public:
    HANDLE hCompletionPort; //完成端口句柄
};

在上面的類中,是我們用來處理客戶端用戶請求的服務器端socket模型。
網絡遊戲製作技術(二)—— 消息打包處理部分
   
  
   續上在上面我簡單的說了一下服務器完成端口處理部分,接下來我想大家介紹一下關於如何建立服務器和客戶端的聯繫規則,也就是服務器和客戶端的遊戲協議部分。有不足之處希望大家和我進行交流。

  首先解釋一下這裏協議的概念,協議大家都瞭解是一種通信規則,例如:TCP/IP,UDP等等,這些是我們在網絡通信過程中所處理使用的協議。而我們這裏的協議是我們的遊戲服務器和客戶端的通信規則。簡而言之,也就是客戶端發送到服務器的數據包和服務器發送的數據包雙方解釋規則。下面就通過幾個部分來具體介紹這種協議的建立和處理。

消息頭定義

  如果我們能夠解釋雙方的數據包的意義,我們就必須爲雙方數據包定義一個統一規則的消息頭,我是這麼定義消息頭的。服務器數據包和客戶端數據包分別定義不同的消息頭。以下就是雙方消息頭的簡單定義。

struct ServerMsg_Head //服務器消息頭
{
    WORD s_version; //版本信息
    BYTE s_flages; //消息標誌
    BYTE s_who; //消息驅動者
    BYTE s_sort; //消息類別
    BYTE s_value; //消息值
    WORD s_len; //消息長度
} ;

struct ClientMsg_Head //客戶端消息頭
{
    WORD c_version; //版本信息
    WORD c_flages //消息標誌
    WORD c_sort; //消息類別
    WORD c_value; //消息值
    WORD c_scene; //場景信息
    WORD c_len; //消息長度
};

以上是我個人簡單定義的消息頭,具體的各個參數意義,就是需要規劃設計的人來定了。這些我就不多說了。

  在我們處理完我們的消息頭後,我們就可以將我們的具體遊戲數據進行打包。關於數據打包,我們必須要處理兩件事情:數據打包,數據加密。爲此我就建立相應的class來處理這樣的一些操作。DataCtrl.h處理如下:

class Ppackage類可以拆解爲兩個單獨處理類,打包類和解包類。而此處我就用下面一個類來進行處理。只是給大家開個頭,要設計的更好還是靠大家共同來進行斟酌呀!!


class PPackage //遊戲數據包處理類
{
public:
    PPackage(BYTE msg_type); //設置所打包消息類型
    virtual ~PPackage();
    //消息數據打包部分
    void SetMsgHead(void *); //設置消息頭
    void AddByte(BYTE data); //加入一字節
    void AddWord(WORD data); //加入二字節
    void AddDword(DWORD data); //加入四字節
    void AddPoint(POINT data); //加入八字節
    void AddBuf(char * data ,int data_len); //加入多個字節
    //消息內容獲取
    void FinishPack(); //完成打包
    char *GetPackage(); //獲取數據包
    int GetPacketLen(); //獲取數據包長度


    //消息數據解包部分
    void SetMsgPackage(char *buf,int _Len); //將獲取消息進行錄入
    void *GetMsgHead(); //獲取消息頭數據 
    BYTE GetByte(); //獲取一字節
    WORD GetWord(); //獲取二字節
    DWORD GetDword(); //獲取三字節
    POINT * GetPoint(); //獲取四字節
    char * GetBuf(int buf_len); //獲取多字節
    bool IfFinishGet(); //是否完成解包

private:

    void Init();
    void Release();
    void StartBindPakage(); //開始打包
    void StartUndoPackage(); //開始解包
    bool MessageEncrypt(); //消息加密
    bool MessageUndo(); //消息解密

private:

private:
    BYTE msg_type; / /{1-SERVER_PACKAGE=1,2-CLIENT_PACKAGE=2}
    char * msg_buffer; 
    char * buffer; //後備緩衝區
    int msg_len;
    //消息內容長度
    Server_Msg_Head msg_Head; //消息頭
    int buf_Len;
    int current_pos; //指針的當前位置
protected:
};

  以上就是關於服務器和消息打包類的一些建立和解釋,這些方面知識其實也沒有什麼,主要是“仁者見仁,智者見智”了。而對於網絡遊戲的製作最重要的還是在於Game World的規劃和設計,同時這個方面也是最難和最不好處理的。隨後將和大家進行探討。。 
網絡遊戲製作技術(三)—— 線程池處理部分
   
  
   續上在這裏我將要向大家簡單介紹一下游戲服務器中必須要處理另外一項主要技術:

線程池技術

  開始 我來向大家簡單來介紹一下線程池的概念,先簡單瞭解下線程先,線程可以理解爲一個function , 是一個爲了進行某一項任務或者處理某一項具體事務的函數。例如:

UINT WINAPI FunctionCtrl(void *) //線程處理函數
{
    進行某一項任務或者處理某一項具體事務
    ………….
    return EXITFUNCTION_CODE; //退出碼
}

  而我們的線程池自身可以理解爲是很多線程的一個管理者也可以說是一個很多線程的統籌者。因爲我們的線程池具有生成線程功能也具有撤消線程的權利。這就是簡單的線程池的概念(我的理解,呵呵!!)接下來就來具體介紹線程池了!!

  首先 介紹我們爲什麼要使用線程池技術呢?大家都知道我們的遊戲服務器端要處理大量的用戶請求,,同時需要發送大量的遊戲數據到客戶端,從而來驅動客戶端程序的執行和維持遊戲的進行。那我們的服務器端是如何進行處理的呢?其實在這裏我們就充分用到了線程池技術。
那麼用這種技術有什麼好處和優點呢?以下就來簡述這些,有不足之處和不當之處希望有心人指正,呵呵!!

  大家都瞭解在我們服務器整個運行過程中,我們將整個運行時間分成很多個時間片。而對於這些已經分成的各個微小的時間片而言,在各個不同時間片中要處理的用戶請求和需要發送到用戶端的遊戲數據量也將是不一樣的。而處理用戶的請求和發送數據到客戶端的工作都是由一系列的線程來執行的。


鑑於上面,這樣我們就可以感性的設想下服務器運行中的兩種情況:
  第一種在我們服務器運行到某個時間片需要處理大量的用戶請求和發送大量數據,有這樣繁重的工作任務,我們就需要有很多的工作者線程來處理完成這樣的任務,以此來滿足我們的工作需要。這樣說我們就必須擁有很多工作者線程。

  第二種在我們服務器運行到某個時間片需要處理的用戶請求和發送數據工作量比較小,由於任務比較少,我們用來處理任務的工作者線程也就不需要很多。也就是說我們只要有少量的工作者線程就可以達到我們的工作要求了。

  對於上面的兩種情況,我們可以說明這樣的一個事實,也就是說我們服務器在運行過程中運行狀態是動態改變的,呼忙呼閒,時急時慢的。服務器的這樣的行爲動作和性質可以做一個如下比喻:服務器就是一個企業,在企業業務非常忙的時候,公司的員工數量就必須要增多來滿足業務的需要。而在企業不景氣的時候,接的業務也就比較少,那麼來說就會有很多員工比較閒。那我們該怎麼辦呢?爲了不浪費公司資源和員工自身資源,我們就必須要裁減員工,從而來配合公司的運行。而做這樣工作的可能是公司的人力資源部或者其他部分。現在就認爲是人力資源部了。呵呵。

  對於上面的比喻我們來抓幾個關鍵詞和列舉關鍵詞和我們主題對象進行對照,以此來幫大家來簡單理解服務器運行和線程池。

企業 : 遊戲服務器
人力資源部 : 線程池
職員 : 工作者線程

在說了這麼多的廢話後,就具體的將線程池模型 ThreadPool.h文件提供以供大家參考:

class GThreadPoolModel
{
    friend static UINT WINAPI PoolManagerProc(void* pThread); //線程池管理線程
    friend static UINT WINAPI WorkerProc (void* pThread); //工作者線程
    enum SThreadStatus //線程池狀態
    {
        BUSY,
        NORMAL,
        IDLE
    };
    enum SReturnvalue //線程返回值
    {
        MANAGERPROC_RETURN_value = 10001,
        WORKERPROC_RETURN_value = 10002,
        …………….
    };
public:
    GThreadPoolModel ();
    virtual ~ GThreadPoolModel ();
    virtual bool StartUp(WORD static_num,WORD max_num)=0; //啓動線程馳
    virtual bool Stop(void )=0; //停止線程池
    virtual bool ProcessJob(void *)=0; //提出工作處理要求
protected:
    virtual bool AddNewThread(void )=0; //增加新線程
    virtual bool DeleteIdleThread(void)=0; //刪除空閒線程
    static UINT WINAPI PoolManagerProc (void* pThread); //線程池管理線程
    static UINT WINAPI WorkerProc (void* pThread); //工作者線程
    GThreadPoolModel::SThreadStatus GetThreadPoolStatus( void ); //獲取線程池當前工作狀態
private:
    void Init();
    void Release();
protected:
    ………………………..
private:
};

以上是線程池模型的一個簡單class,而對於具體的工作處理線程池,可以由此模型進行繼承。以此來滿足具體的需要。到這裏就簡單的向大家介紹了線程池的處理方式。有不對之處望指正。同時歡迎大家和我交流。
 
網絡遊戲製作技術(四)—— 服務器內存管理部分
   
  
   續上在這裏我將要向大家簡單介紹一下游戲服務器中必須要處理另外一項主要技術:
  內存分配處理技術也可以稱爲內存池處理技術(這個比較洋氣,前面通俗的好,呵呵)

  開始向大家介紹一般情況下我們對於內存的一些基本操作。簡單而言,內存操作就只有三個步驟:申請、使用、銷燬。而對於這些操作我們在C和C++中的處理方式略有不同:

在C中我們一般用malloc(….)函數來進行申請,而對應銷燬已經申請的內存使用free(…)函數。
在C++我們一般使用new操作符和delete操作符進行處理申請和銷燬。
大家一定要問了,我們一般都是這樣處理的呀!!沒有什麼可以說的哦!!呵呵,我感覺就有還是有一些東東和大家聊的哦。先聊簡單幾條吧!!

1.Malloc(…..)和free(….), new ….和 delete …必須成對出現不可以混雜哦,混雜的話,後果就不可以想了哦!!(也沒有什麼,就是內存被泄漏了,呵呵)

2.在我們使用new …和delete ….一定要注意一些細節,否則後果同上哦!!什麼細節呢?下面看一個簡單的例子:
char *block_memory = NULL;
block_memory = new char[1024];
delete block_memory;
block_memory = NULL;
大家沉思一會。。。。。。。。。
大家看有錯嗎?沒有錯吧!!

  如果說沒有錯的,就要好好補補課了,是有錯的,上面實際申請的內存是沒有完全被釋放的,爲什麼呢?因爲大家沒有注意第一條的完全匹配原則哦,在new 的時候有[ ],我們在delete 怎麼就沒有看見[ ] 的影子呢? 這就造成了大錯有1023個字節沒有被釋放。正確的是 : delete []block_memory;

  關於內存基本操作的我是說這兩條,其他要注意還是有的,基本就源於此了。

  瞭解了上面那些接下來就想大家說說服務器內存處理技術了。上面都沒有弄清楚了,就算了。呵呵。

  大家都知道,我們的服務器要頻繁的響應客戶端的消息同時要將消息發送到客戶端,並且還要處理服務器後臺遊戲World的運行。這樣我們就必須要大量的使用內存,並且要進行大量的內存操作(申請和銷燬)。而在這樣的操作中,我們還必須要保證我們的絕對正確無誤,否則就會造成內存的泄漏,而內存泄漏對於服務器而言是非常可怕的,也可能就是我們服務器設計失敗的毒藥。而我們如何進行服務器內存的正確和合理的管理呢?那就是我們
必須建立一套適合我們自己的內存管理技術。現在就向大家說一說我在內存管理方面的一些做法。
基本原理先用圖形表示一下:

 

  上面的意思是:我們在服務器啓動過程中就爲自己申請一塊比較大的內存塊,而我們在服務器運行過程中需要使用內存我們就到這樣一塊比較大已經申請好的內存塊中去取。而使用完後要進行回收。原理就是這麼簡單。而最重要的是我們如何管理這個大的內存塊呢?
(非常複雜也比較難,呵呵)

首先 就內存塊操作而言就只有申請(類似 new)和回收(類似 delete)。
其次 我們必須要清楚那些內存我們在使用中,那些是可以申請的。

關於上面我簡單將這樣的一些數據結構和class定義在下面供大家參考使用。

typedef struct MemoryBlock //內存塊結構
{
    void *buffer; //內存塊指針
    int b_Size; //內存塊尺寸
} MemoryBlock;

class CMemoryList //列表對象類(相當於數組管理類)
{
public:
    CMemoryList();
    virtual ~ CMemoryList();
    void InitList(int data_size,int data_num);//初始化列表數據結構尺寸和數量
    void AddToList(void *data); //加入列表中
    void DeleteItem(int index); //刪除指定索引元素
    ……………..
private:
    void Init();
    void Release();
private:
    void *memory;
    int total_size;
    int total_num;
protected:
};

classs CMemoryPool //內存池處理類
{
public:
    CMemoryPool(); 
    virtual ~ CMemoryPool();
    bool InitMemoryPool(int size); //初始化內存池
    void * ApplicationMemory(int size); //申請指定size內存
    void CallBackMemory(void *,int size); //回收指定size內存

private:
    void Init();
    void Release():
    MemoryBlock *UniteMemory(MemoryBlock *block_a,MemoryBlock * block_b); //合併內存


private:
    MemoryBlock memoryPool_Block; //內存池塊
    CMemoryList *callBackMemory_List; //回收內存列表
    CMemoryList *usingMemory_List; //使用中內存列表
    CMemoryList *spacingMemory_List; //空白內存列表
protected:
};

以上就是這個內存管理類的一些基本操作和數據定義,class CMemoryList 在這裏不是重點暫且就不說了,有空再聊。而具體的內存池處理方法簡單敘述如下:

函數InitMemoryPool(): 初始化申請一塊超大內存。
函數ApplicationMemory():申請指定尺寸,申請內存成功後,要將成功申請的內存及其尺寸標示到usingMemory_List列表,同時要將spacingMemory_List列表進行重新分配。以便於正確管理。
函數CallBackMemory():回收指定尺寸內存,成功回收後,要修改spacingMemory_List列表,同時如果有相鄰的內存塊就要合併成一個大的內存塊。usingMemory_List修改使用列表,要在使用列表中的這一項刪除。

  以上就是一些簡單處理說明,更加詳細的就需要大家自己琢磨和處理了。我就不細說了。呵呵。不足之處就請大家進行指正,以便讓我們大家都提高。先謝謝了。

 
網絡遊戲製作技術(五)—— 線程同步和服務器數據保護
   
  
  最近因爲自己主持的項目出現些問題,太忙了,所以好久都沒有繼續寫東西和大家進行探討製作開發部分了。在這一節中就要向大家介紹另外一個重要的部分,並且也是最頭疼的部分:線程同步和數據保護。

  關於線程的概念我在前面的章節中已經介紹過了,也就在這裏不累贅—“重複再重複”了。有一定線程基礎的人都知道,線程只要創建後就如同脫繮的野馬,對於這樣的一匹野馬我們怎麼來進行控制和處理呢?簡單的說,我們沒有辦法進行控制。因爲我們更本就沒有辦法知道CPU什麼時候來執行他們,執行他們的次序又是什麼?

  有人要問沒有辦法控制那我們如何是好呢?這個問題也正是我這裏要向大家進行解釋和說明的,雖然我們不能夠控制他們的運行,但我們可以做一些手腳來達到我們自己的意志。

  這裏我們的做手腳也就是對線程進行同步,關於同步的概念大家在《操作系統》中應該都看過吧!不瞭解的話,我簡單說說:讀和寫的關係(我讀書的時候,請你不要在書上亂寫,否則我就沒有辦法繼續閱讀了。)

處理有兩種:用戶方式和內核方式。
用戶方式的線程同步由於有好幾種:原子訪問,關鍵代碼段等。

  在這裏主要向大家介紹關鍵代碼段的處理(我個人用的比較多,簡單實用)。先介紹一下它的一些函數,隨後提供關鍵代碼段的處理類供大家參考(比較小,我就直接貼上來了)

VOID InitializeCriticalSection( //初始化互斥體
    LPCRITICAL_SECTION lpCriticalSection // critical section
);

VOID DeleteCriticalSection( //清除互斥體
    LPCRITICAL_SECTION lpCriticalSection // critical section
);

VOID EnterCriticalSection( //進入等待
    LPCRITICAL_SECTION lpCriticalSection // critical section
);

VOID LeaveCriticalSection( //釋放離開
    LPCRITICAL_SECTION lpCriticalSection // critical section
);

以上就是關於關鍵代碼段的基本API了。介紹就不必了(MSDN)。而我的處理類只是將這幾個函數進行了組織,也就是讓大家能夠更加理解關鍵代碼端

.h
class CCriticalSection //共享變量區類
{
public:
    CCriticalSection();
    virtual ~CCriticalSection();
    void Enter(); //進入互斥體
    void Leave(); //離開互斥體釋放資源
private:
    CRITICAL_SECTION g_CritSect;
};

.cpp
CCriticalSection::CCriticalSection()
{
    InitializeCriticalSection(&g_CritSect);
}

CCriticalSection::~CCriticalSection()
{
    DeleteCriticalSection(&g_CritSect);
}

void CCriticalSection::Enter()
{
    EnterCriticalSection(&g_CritSect);
}

void CCriticalSection::Leave()
{
    LeaveCriticalSection(&g_CritSect);
}

由於篇幅有限關鍵代碼段就說到這裏,接下來向大家簡單介紹一下內核方式下的同步處理。

  哎呀!這下可就慘了,這可是要說好多的哦!書上的羅羅嗦嗦我就不說了,我就說一些我平時的運用吧。首先內核對象和一般的我們使用的對象是不一樣的,這樣的一些對象我們可以簡單理解爲特殊對象。而我們內核方式的同步就是利用這樣的一些特殊對象進行處理我們的同步,其中包括:事件對象,互斥對象,信號量等。對於這些內核對象我只向大家說明兩點:
1.內核對象的創建和銷燬
2.內核對象的等待處理和等待副作用

第一:內核對象的創建方式基本上而言都沒有什麼太大的差別,例如:創建事件就用HANDLE CreateEvent(…..),創建互斥對象 HANDLE CreateMutex(…….)。而大家注意的也是這三個內核對象在創建的過程中是有一定的差異的。對於事件對象我們必須明確指明對象是人工對象還是自動對象,而這種對象的等待處理方式是完全不同的。什麼不同下面說(呵呵)。互斥對象比較簡單沒什麼說的,信號量我們創建必須注意我們要定義的最大使用數量和初始化量。最大數量>初始化量。再有如果我們爲我們的內核對象起名字,我們就可以在整個進程中共用,也可以被其他進程使用,只需要OPEN就可以了。也就不多說了。

第二:內核對象的等待一般情況下我們使用兩個API:
DWORD WaitForSingleObject( //單個內核對象的等待
            HANDLE hHandle, // handle to object
            DWORD dwMilliseconds // time-out interval
);

DWORD WaitForMultipleObjects( //多個內核對象的等待
            DWORD nCount, // number of handles in array
            CONST HANDLE *lpHandles, // object-handle array
            BOOL fWaitAll, // wait option
            DWORD dwMilliseconds // time-out interval
);

具體怎麼用查MSDN了。

  具體我們來說等待副作用,主要說事件對象。首先事件對象是分兩種的:人工的,自動的。人工的等待是沒有什麼副作用的(也就是說等待成功後,要和其他的對象一樣要進行手動釋放)。而自動的就不一樣,但激發事件後,返回後自動設置爲未激發狀態。這樣造成的等待結果也不一樣,如果有多個線程在進行等待事件的話,如果是人工事件,被激活後所有等待線程成執行狀態,而自動事件只能有其中一個線程可以返回繼續執行。所以說在使用這些內核對象的時候,要充分分析我們的使用目的,再來設定我們創建時候的初始化。簡單的同步我就說到這裏了。下面我就將將我們一般情況下處理遊戲服務器處理過程中的數據保護問題分析:

首先向大家說說服務器方面的數據保護的重要性,圖例如下:

用戶列表

    用戶刪除

    用戶數據修改

    使用數據

加入隊列

  對於上面的圖例大家應該也能夠看出在我們的遊戲服務器之中,我們要對於我們用戶的操作是多麼的頻繁。如此頻繁的操作我們如果不進行處理的話,後果將是悲慘和可怕的,舉例:如果我們在一個線程刪除用戶的一瞬間,有線程在使用,那麼我們的錯誤將是不可難以預料的。我們將用到了錯誤的數據,可能會導致服務器崩潰。再者我們多個線程在修改用戶數據我們用戶數據將是沒有辦法保持正確性的。等等情況都可能發生。怎麼樣杜絕這樣的一些情況的發生呢?我們就必須要進行服務器數據的保護。而我們如何正確的保護好數據,才能夠保持服務器的穩定運行呢?下面說一下一些實際處理中的一些經驗之談。

1.我們必須充分的判斷和估計我們服務器中有那些數據要進行數據保護,這些就需要設計者和規劃者要根據自己的經驗進行合理的分析。例如:在線用戶信息列表,在線用戶數據信息,消息列表等。。。。。

2.正確和十分小心的保護數據和正確的分析要保護的數據。大家知道我們要在很多地方實現我們的保護措施,也就是說我們必須非常小心謹慎的來書寫我們的保護,不正確的保護會造成系統死鎖,服務器將無法進行下去(我在處理的過程中就曾經遇到過,頭都大了)。正確的分析要保護的數據,也就是說,我們必須要估計到我們要保護的部分的處理能夠比較快的結束。否則我們必須要想辦法解決這個問題:例如:

DATA_STRUCT g_data;
CRITICAL_SECTION g_cs;

EnterCriticalSection(&g_cs);
SendMessage(hWnd,WM_ONEMSG,&g_data,0);
LeaveCriticalSection(&g_cs);

以上處理就有問題了,因爲我們不知道SendMessage()什麼時候完成,可能是1/1000豪秒,也可能是1000年,那我們其他的線程也就不用活了。所以我們必須改正這種情況。

DATA_STRUCT g_data;
CRITICAL_SECTION g_cs;

EnterCriticalSection(&g_cs);
PostMessage(hWnd,WM_ONEMSG,&g_data,0);
LeaveCriticalSection(&g_cs);

或者 DATA_STRUCT temp_data;

EnterCriticalSection(&g_cs);
temp_data = g_cs;
LeaveCriticalSection(&g_cs);
SendMessage(hWnd,WM_ONEMSG,& temp_data,0);

3.最好不要複合保護用戶數據,這樣可能會出現一些潛在的死鎖。

  簡而言之,服務器的用戶數據是一定需要進行保護,但我們在保護的過程中就一定需要萬分的小心和謹慎。這篇我就說到這裏了,具體的還是需要從實踐中來進行學習,下節想和大家講講服務器的場景處理部分。先做事去了。呵呵!!有好的想法和建議的和我交流探討,先謝謝了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章