原贴: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.最好不要复合保护用户数据,这样可能会出现一些潜在的死锁。
简而言之,服务器的用户数据是一定需要进行保护,但我们在保护的过程中就一定需要万分的小心和谨慎。这篇我就说到这里了,具体的还是需要从实践中来进行学习,下节想和大家讲讲服务器的场景处理部分。先做事去了。呵呵!!有好的想法和建议的和我交流探讨,先谢谢了。