};
以上是关于服务器和消息包装类的一些建立和说明。 其实这些方面的知识并不多。 主要是“仁者见仁智者见智”。 网络游戏的制作最重要的就是游戏世界的规划和设计。 同时,这方面也是最难、最难处理的。 我们稍后会与您讨论这个问题。 。
这里继续给大家简单介绍一下游戏服务器中必须要处理的另一项主要技术:
线程池技术
首先向大家简单介绍一下线程池的概念。 首先,让我简单了解一下线程。 线程可以理解为一个函数,是用来执行某个任务或者处理某个特定事务的函数。 例如:
UINT WINAPI FunctionCtrl(void *) //线程处理函数
执行任务或处理特定事项
…………。
返回 EXITFUNCTION_CODE; //退出代码
我们的线程池本身可以理解为很多线程的管理者或者很多线程的协调者。 因为我们的线程池有产生线程的能力,也有取消线程的权利。 这就是一个简单的线程池的概念(我的理解,哈哈!!)接下来我们来详细介绍一下线程池! !
首先我们来介绍一下为什么要使用线程池技术? 大家都知道,我们的游戏服务器需要处理大量的用户请求,同时需要向客户端发送大量的游戏数据来驱动客户端程序的执行,维持游戏的进度。 那么我们的服务器如何处理呢? 其实这里我们充分利用了线程池技术。
那么使用这项技术有哪些好处和优势呢? 以下是对这些内容的简要描述。 如果有不足或者不妥的地方希望大家指正,哈哈! !
大家都知道,我们服务器的整个运行过程中,我们把整个运行时间分成了很多个时间片。 对于这些划分出来的每一个微小的时间片,每个不同的时间片内用户请求处理、需要发送到用户端的游戏数据量也会不同。 处理用户请求并向客户端发送数据的工作是由一系列线程执行的。
鉴于以上,我们可以理性想象服务器运行时的两种情况:
首先是当我们的服务器运行到一定的时间片时,需要处理大量的用户请求,发送大量的数据。 面对如此繁重的工作任务,我们需要大量的工作线程来处理此类任务,以满足我们的需求。 我们的工作需要它。 这意味着我们必须有许多工作线程。
第二种是当我们的服务器运行到一定的时间片时,处理用户请求和发送数据的工作量比较小。 由于任务相对较少,所以我们不需要很多工作线程来处理任务。 也就是说网络游戏制作,我们只需要少量的工作线程就可以满足我们的工作需求。
对于以上两种情况,我们可以解释一下,我们服务器的运行状态在运行过程中是动态变化的,有忙有闲,有时急有时慢。 服务器的行为和性质可以与以下内容进行比较:服务器是一个企业。 当企业业务非常繁忙时,公司的员工数量必须增加以满足业务的需要。 而当公司不景气的时候,业务量就会减少,所以很多员工就会闲置。 那么我们应该做什么呢? 为了不浪费公司资源和员工自己的资源,必须裁员来协调公司的运营。 做这件事的人可能是公司的人力资源部门或其他部门。 现在将其视为人力资源部门。 呵呵。
针对上面的比喻,我们抓几个关键词,列出来与我们的主题对象进行对比,以帮助大家简单了解服务器运行和线程池。
企业:游戏服务器
人力资源部:线程池
工作人员:工作线程
说了这么多废话,我专门提供一下线程池模型ThreadPool.h文件供大家参考:
类 GThreadPoolModel
朋友静态 UINT WINAPI PoolManagerProc(void* pThread); //线程池管理线程
朋友静态 UINT WINAPI WorkerProc (void* pThread); //工作线程
enum SThreadStatus //线程池状态
忙碌的,
普通的,
闲置的
};
enum SReturnvalue //线程返回值
MANAGERPROC_RETURN_value = 10001,
WORKERPROC_RETURN_value = 10002,
…………。
};
民众:
GThreadPoolModel();
虚拟~GThreadPoolModel();
virtual bool StartUp(WORD static_num,WORD max_num)=0; //启动线程
虚拟 bool Stop(void)=0; //停止线程池
虚拟 bool ProcessJob(void *)=0; //提出工作处理要求
受保护:
virtual bool AddNewThread(void)=0; //添加一个新线程
virtual bool DeleteIdleThread(void)=0; //删除空闲线程
静态 UINT WINAPI PoolManagerProc (void* pThread); //线程池管理线程
静态 UINT WINAPI WorkerProc (void* pThread); //工作线程
GThreadPoolModel::SThreadStatus GetThreadPoolStatus( void ); //获取线程池当前工作状态
私人的:
无效初始化();
无效释放();
受保护:
…………………………..
私人的:
};
上面是一个简单的线程池模型类,对于具体的工作处理线程池,可以继承这个模型。 以满足特定需求。 这里我给大家简单介绍一下线程池的处理方法。 如果有什么不对的地方请指正。 同时也欢迎大家与我交流。
这里继续给大家简单介绍一下游戏服务器中必须要处理的另一项主要技术:
内存分配处理技术也可以称为内存池处理技术(这个比较国外,以前流行的更好,哈哈)
首先给大家介绍一下正常情况下对内存的一些基本操作。 简单来说网络游戏制作,内存操作只有三个步骤:申请、使用、销毁。 我们在 C 和 C++ 中处理这些操作的方式略有不同:
在C中,我们一般使用malloc(….)函数来申请,使用free(…)函数来销毁已经申请的内存。
在C++中,我们一般使用new操作符和delete操作符来处理申请和销毁。
大家一定会问,我们平时都是这样处理的! ! 没什么可说的! ! 哈哈,我感觉我还有话要和你说。 我们先来说一些简单的事情吧! !
1. Malloc(…..)和free(….)、new….和delete…必须成对出现,不能混合使用。 如果混在一起,后果不堪设想! ! (没什么,只是内存泄露了,哈哈)
2、我们在使用new...和delete...的时候一定要注意一些细节,否则后果和上面一样! ! 什么细节? 让我们看一个简单的例子:
char *block_memory = NULL;
块内存=新的字符[1024];
删除块内存;
块内存= NULL;
众人沉思了一会。 。 。 。 。 。 。 。 。
大家都错了吗? 这是正确的! !
如果没有什么问题,就需要弥补,但是有问题。 上面实际申请的内存并没有完全释放。 为什么? 因为大家没有注意第一个精确匹配原则。 new中有[],但delete中为什么看不到[]的影子? 这导致了一个很大的错误,1023字节没有被释放。 正确的是:delete[]block_memory;
关于内存的基本操作,我讲的是这两项。 还有其他需要注意的地方,基本上也源于此。
了解了上面之后,我想谈谈服务器内存处理技术。 如果上面的内容还不清楚,那就算了。 呵呵。
众所周知,我们的服务器要频繁的响应客户端消息并向客户端发送消息,同时还要处理服务器后台游戏World的运行。 这样,我们就必须使用大量的内存,并执行大量的内存操作(申请和销毁)。 在这样的操作中,我们也必须保证我们是绝对正确的,否则就会造成内存泄漏,而内存泄漏对于服务器来说是非常可怕的,也可能是我们服务器设计失败的毒药。 而我们如何正确合理的管理服务器内存呢?这就是我们
我们必须建立一套适合我们的内存管理技术。 现在我给大家讲讲我在内存管理方面的一些实践。
首先用图形表示基本原理:
上面的意思是:我们在服务器启动过程中为自己申请一块比较大的内存块,当服务器运行过程中需要使用内存的时候,就去这样一个分配的比较大的内存块去获取。 使用后必须回收。 原理就是这么简单。 而最重要的是我们如何管理这么大的内存块呢?
(好复杂好难,哈哈)
首先3D角色,就内存块操作而言,只有申请(类似于new)和回收(类似于delete)。
其次,我们要知道我们正在使用哪些内存,哪些内存可以申请。
针对以上内容,我简单定义了其中的一些数据结构和类如下,供大家参考。
typedef struct MemoryBlock //内存块结构
无效*缓冲区; //内存块指针
int b_Size; //内存块大小
内存块;
class CMemoryList //List对象类(相当于数组管理类)
民众:
CMemoryList();
虚拟 ~ CMemoryList();
void InitList(int data_size,int data_num);//初始化list数据结构大小和数量
无效AddToList(无效*数据); //添加到列表
无效DeleteItem(int索引); //删除指定索引元素
………………..
私人的:
无效初始化();
无效释放();
私人的:
无效*内存;
int 总大小;
int 总数;
受保护:
};
classes CMemoryPool //内存池处理类
民众:
CMemoryPool();
虚拟 ~ CMemoryPool();
bool InitMemoryPool(int size); //初始化内存池
无效*应用程序内存(int大小); //申请指定大小的内存
void CallBackMemory(void *,int 大小); //回收指定大小的内存
私人的:
无效初始化();
无效释放():
MemoryBlock *UniteMemory(MemoryBlock *block_a, MemoryBlock *block_b); //合并内存
私人的:
内存块内存池_块; //内存池块
CMemoryList *callBackMemory_List; //回收内存列表
CMemoryList *usingMemory_List; //正在使用的内存列表
CMemoryList *spacingMemory_List; //空白内存列表
受保护:
};
以上就是这个内存管理类的一些基本操作和数据定义。 CMemoryList类不是这里的重点,所以暂时不讲。 有时间我会讲一下。 具体内存池处理方法简述如下:
函数InitMemoryPool():初始化并申请一块大内存。
函数ApplicationMemory():申请指定大小。 成功申请内存后,将成功申请的内存及其大小标记到usingMemory_List列表中,同时重新分配spacingMemory_List列表。 以进行适当的管理。
函数CallBackMemory():回收指定大小的内存。 成功回收后,必须修改spacingMemory_List列表。 同时,如果有相邻的内存块,则必须将它们合并为一个大的内存块。 usingMemory_List修改使用列表并删除使用列表中的此项。
以上是一些简单的处理指令。 更详细的说明需要你自己思考和处理。 我不会详细介绍。 呵呵。 如果有什么不足的地方请大家指正,以便大家共同进步。 首先,谢谢你。
最近我主持的项目出现了一些问题,太忙了,所以很长一段时间没有继续写东西和大家讨论制作开发部分。 本节我们要给大家介绍另外一个重要的部分,也是最麻烦的部分:线程同步和数据保护。
我在前面的章节中已经介绍了线程的概念,所以这里不再赘述——“重复再重复”。 有一定线程基础的人都知道,线程一旦创建就像一匹脱缰的野马。 对于这样一匹野马,我们该如何控制和驾驭呢? 简而言之,我们无法控制。 因为我们无从知道CPU什么时候会执行它们,以及执行的顺序是什么?
有人可能会问,如果没有办法控制,那该怎么办呢? 这个问题就是我想在这里给大家解释一下的。 虽然我们无法控制他们的运作,但是我们可以做一些小动作来达到我们自己的意愿。
我们在这里所做的是同步线程。 你应该在《操作系统》中见过同步的概念! 如果你不明白,我简单解释一下:阅读和写作的关系(我阅读时,请不要在书上写任何东西,否则我将无法继续阅读。)
有两种处理类型:用户模式和内核模式。
用户态线程同步有几种类型:原子访问、关键代码段等。
这里主要给大家介绍一下关键代码段的处理(我个人用的比较多,简单实用)。 首先介绍一下它的一些功能,然后提供关键代码段的处理类供大家参考(比较少,所以直接贴出来)
VOID InitializeCriticalSection( //初始化互斥体
LPCRITICAL_SECTION lpCriticalSection // 临界区
);
VOID DeleteCriticalSection( //清除互斥锁
LPCRITICAL_SECTION lpCriticalSection // 临界区
);
VOID EnterCriticalSection( //进入等待
LPCRITICAL_SECTION lpCriticalSection // 临界区
);
VOID LeaveCriticalSection( //释放并离开
LPCRITICAL_SECTION lpCriticalSection // 临界区
);
以上是关键代码段的基础API。 不需要介绍(MSDN)。而我的处理类只是整理了这些函数,这意味着大家可以更好地理解关键代码结束。
。H
class CCriticalSection //共享变量区类
民众:
CCriticalSection();
虚拟 ~CCriticalSection();
无效输入(); //输入互斥量
无效离开(); //留下互斥体以释放资源
私人的:
CRITICAL_SECTION g_CritSect;
};
.cpp
CCriticalSection::CCriticalSection()
初始化CriticalSection(&g_CritSect);
CCriticalSection::~CCriticalSection()
删除CriticalSection(&g_CritSect);
无效CCriticalSection::Enter()
EnterCriticalSection(&g_CritSect);
无效CCriticalSection::Leave()
LeaveCriticalSection(&g_CritSect);
由于篇幅有限,这是关键代码段。 接下来简单介绍一下内核态的同步处理。
哎呀! 这次太可怕了,有很多话要说! 书中的细节我就不多说了。 我只说一下我的日常使用。 首先,内核对象与我们使用的一般对象不同。 我们可以简单地将此类对象理解为特殊对象。 我们的内核式同步就是使用这样一些特殊的对象来处理我们的同步,包括:事件对象、互斥对象、信号量等。关于这些内核对象我只向大家解释两点:
1.内核对象的创建和销毁
2.内核对象的等待处理和等待副作用
第一:基本上,创建内核对象的方式没有太大区别。 例如:要创建一个事件,使用HANDLE CreateEvent(…..),创建一个互斥对象HANDLE CreateMutex(….)。 大家要注意的是,这三个内核对象的创建过程是有一定区别的。 对于事件对象,我们必须明确指出该对象是手动对象还是自动对象,并且此类对象的等待方式完全不同。 下面解释一下区别(哈哈)。 互斥对象比较简单,没什么好说的。 当我们创建信号量的时候,一定要注意我们要定义的最大使用次数和初始化量。 最大数量>初始化数量。 此外,如果我们命名我们的内核对象,我们可以在整个进程中共享它,也可以被其他进程使用,只需OPEN即可。 我就不多说了。
第二:等待内核对象一般我们使用两个API:
DWORD WaitForSingleObject( //等待单个内核对象
HANDLE hHandle, // 对象句柄
DWORD dwMilliseconds // 超时间隔
);
DWORD WaitForMultipleObjects( //等待多个内核对象
DWORD nCount, // 数组中的句柄数
CONST HANDLE *lpHandles, // 对象句柄数组
BOOL fWaitAll, // 等待选项
DWORD dwMilliseconds // 超时间隔
);
有关如何使用它的详细信息,请查看 MSDN。
具体说一下等待副作用,主要是事件对象。 首先,事件对象有两种类型:手动和自动。 手动等待没有副作用(即等待成功后,必须像其他对象一样手动释放)。 自动的则不同,但是事件触发后,返回后自动设置为未触发状态。 由此造成的等待结果也不同。 如果有多个线程等待该事件,如果是手动事件,所有等待线程被激活后都会进入执行状态,而对于自动事件,只有一个线程可以返回继续执行。 因此,我们在使用这些内核对象的时候,一定要充分分析我们的使用目的,然后在创建的时候进行初始化设置。 简单的同步就这样了。 下面我就分析一下我们在游戏服务器处理过程中一般会遇到的数据保护问题:
首先给大家介绍一下服务器端数据保护的重要性。 图示如下:
用户列表
用户删除
用户数据修改
使用数据
加入队列
从上图中,您还应该能够看到我们在游戏服务器中操作用户的频率。 如果我们不处理这种频繁的操作,后果将是悲惨和可怕的。 例如:如果我们在一个线程中删除一个用户,而有一个线程正在使用,那么我们的错误将是不可预测的。 我们会使用错误的数据,这可能会导致服务器崩溃。 此外,当我们有多个线程修改用户数据时,将无法维护我们用户数据的正确性。 任何事情都可能发生。 如何预防此类情况的发生? 我们必须保护服务器数据。 而我们如何正确保护数据,才能维持服务器的稳定运行呢? 说一下实际加工中的一些经验。
1.我们必须充分判断和估计我们服务器中的哪些数据需要保护。 这些都需要设计者和规划者根据自己的经验进行合理的分析。 例如:在线用户信息列表、在线用户数据信息、留言列表等。 。 。 。
2. 正确且非常仔细地保护数据并正确分析要保护的数据。 大家都知道,我们要在很多地方实施我们的保护措施,这意味着我们必须非常小心地写我们的保护。 不正确的保护会导致系统死锁,服务器无法继续(我在处理的时候就遇到过这种情况,看得我头大)。 正确分析要保护的数据,即我们必须估计我们要保护的部分的处理可以相对较快地结束。 否则我们必须想办法解决这个问题:例如:
DATA_STRUCT g_data;
CRITICAL_SECTION g_cs;
EnterCriticalSection(&g_cs);
SendMessage(hWnd,WM_ONEMSG,&g_data,0);
离开关键部分(&g_cs);
上面的处理有一个问题,因为我们不知道SendMessage()什么时候完成。 它可能是 1/1000 毫秒或 1000 年。 那么我们的其他线程就不必生存了。 所以我们必须纠正这种情况。
DATA_STRUCT g_data;
CRITICAL_SECTION g_cs;
EnterCriticalSection(&g_cs);
PostMessage(hWnd,WM_ONEMSG,&g_data,0);
离开关键部分(&g_cs);
或 DATA_STRUCT temp_data;
EnterCriticalSection(&g_cs);
临时数据 = g_cs;
离开关键部分(&g_cs);
SendMessage(hWnd,WM_ONEMSG,& temp_data,0);
3. 最好不要结合对用户数据的保护,因为这可能会导致一些潜在的死锁。
总之,服务器的用户数据必须受到保护,但在保护过程中我们必须极其小心和谨慎。 这篇文章我就停在这里。 具体的还是需要从实践中去学习。 下一节我想和大家聊聊服务端的场景处理部分。 我们先去办事吧。 呵呵! ! 如果您有什么好的想法或者建议数据报告,请与我分享。 先感谢您。