初探NetGame(一):初步架构

网络游戏的程序开发从某种意义上来看,最重要的应该在于游戏服务器端的设计和制作。对于服务器端的制作。将分为以下几个模块进行:

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模型。

posted @ 2010-02-24 10:00  Maxice  阅读(445)  评论(0编辑  收藏  举报