游戏开发记录

之前一直想学习开发游戏服务器,经过近9个月的对于c/c++,linux,网络编成的学习,并完成了最开始的半成品flappy bird双人联机游戏服务器,仔细研究了《网络编程与分层协议设计》这本书,对上面的最基本的socket到网络模型,以及一些附带的linux下多线程,多进程,linux内核链表,函数指针等内容进行了详细阅读,并将上面的代码仔细研读并逐行写注释(包括一个linux下ftp客户端程序),最近我的游戏服务器开发工作终于走上了正轨。

为什么想写游戏服务器?事实上我一直更喜欢玩单机游戏,但是之前用windowsAPI和directx做windows窗口游戏的经历让我觉得做界面时间很麻烦的事,而界面方面的就是更新又比较快,而网络编程方面的工作和我的专业通行工程结合的更加紧密,并且感觉技术含量有比较高,所以选择了学习开发游戏服务器。这里我选择了开发一个我目前唯一在玩的一款网络游戏炉石传说服务器。

客户端:

由于我主要想学习服务器方面的知识,所以客户端方面我没有做图形界面,直接使用的linux下的终端进行用户交互操作。I/O模型采用的select模式,使得程序能同时处理标准输入和网络套接字而不会被阻塞。Select的使用方式如下:

1.      声明文件描述符集合fd_set

2.      使用FD_SET,FD_CLR设置文件描述符集合,将需要监视的文件描述符加入到声明的文件描述符集合中。

3.      然后调用select等待文件描述符集合可用

4.      Select返回,则说明文件描述符集合中有文件描述符可用,使用FD_ISSET函数逐一测试文件描述符是否可用,若可用则进行相应的操作。

 

使用select就能实现应用程序对标准输入和网络套接字的同时处理,用户在输入命令

然后根据命令构造需要向服务器发送的消息,的同时,也能接受到网络套接字传输过来的数据。

         然后是网络协议的问题,这里客户端使用的阻塞tcp套接字,(省略500字)。

然后是应用层协议,是这样设计的,首先消息结构体,消息结构体分为消息头部和数据部分,有的消息结构体数据部分是没有的,只有一个消息头。包头结构体有两个成员变量,第一个是一个枚举类型的变量,表示这是什么类型的消息,比如登录消息,寻找游戏消息,使用卡牌消息等,这些在定义枚举类型是都定义好。包头结构体第二个成员变量是一个整型变量,表示数据部分的长度,即数据部分有多少个字节。每种消息都对应自己的消息结构体。在发送端发送消息时,构造这样一个消息结构体,然后发送这个结构体即可。接受端每次接收数据时,先接收消息头部大小字节的数据,然后对头部进行解析,得到该消息的类型和剩余的数据长度,然后在接收剩余数据长度字节的数据,然后根据消息的类型对该消息进行处理。

服务器:

服务器端是一个TCP服务器,为了解决同时处理监听套接字和多个连接套接字的问题,我们需要使用I/O多路复用的模型,这里我们使用的是epoll。

epoll实际上就是select的增强版,epoll的优点:

1.支持一个进程打开大数目的socket描述符,select只支持打开1024个,而epoll所能打开的文件描述符数量很大,与内存相关。

2.select每次使用时都会扫描全部集合,导致i/o效率不随fd数目增加而线性下降,而epoll会直接返回活跃的fd。

3.使用mmap加速内核与用户空间的消息传递(不懂)

实现原理:这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有“活跃”的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个“伪”AIO,因为这时候推动力在os内核。

epoll的使用方式如下:

1.      使用epoll_create()函数创建epoll上下文环境

2.      使用epoll_ctl()函数向epoll上下文中注册文件描述符

3.      然后声明一个struct epoll_event *events的指针,然后创建MAX_EVENTS*sizeof(structepoll_events)大小的内存。

4.      调用epoll_wait(),第二个参数设为上一步声明的events指针,函数返回时返回值就是准备好的文件描述符个数,而这些文件描述符会以event的形式,存在events指针指向的内存空间的前  返回值*sizeof(structepoll_events)大小的空间中。

5.      对events[i],i<返回值进行遍历处理。

 

服务器的主要工作流程:

首先有一个文件描述符结构体,成员变量首先是一个int型的文件描述符,一个函数指针,一个linux内核散列表的节点。这个结构体就是用于储存每个套接字文件描述符的结构体。Int型变量用于储存socket值,通过这个结构体的引入,将所有套接字的处理统一为对这个函数指针的调用,从而可以通过遍历链表的形式进行处理,这个结构体是用哈希表的形式存储的,第三个变量用于这个结构体的存储。

服务器首先创建一个监听套接字,进行一些常规的设置地址,绑定等操作之后,调用一个init函数对监听套接字进行初始化,主要进行两个工作,一个是将该套接字在epoll上下文中注册,第二个就是创建文件描述符结构体,包括创建内存,套接字赋值,函数指针赋值,这里函数指针指向的应该是处理监听套接字的函数,当监听套接字可读时,我们需要调用accept函数创建连接套接字,这里的函数指针指向的就是创建连接并初始化连接套接字文件描述符的函数,最后使用linux内核的哈希表的函数将这个结构体加入哈希表中。

然后先说一下创建连接套接字函数,和初始化监听套接字的操作差不多,只不过先要调用accept获得连接套接字,然后也是一样的在epoll里注册,然后是初始化,这里的函数指针就是指向的对玩家连接进行消息处理的函数。

这里说一下有个问题还没有修改,就是我上面所说的文件描述符结构体在程序中实际上用的就是玩家结构体,所以实际上监听套接字也拥有一个玩家结构体,由于是我已开始写的就是一个最简陋的像上面所说的程序,之后加功能时修改的结构体,一直没注意,今天写blog才注意到这个问题。

 

然后接着之前的主程序说,创建完监听套接字和初始化之后便开始,并将epoll的那些准备工作做好之后就开始调用epoll_wait,然后对返回的events结构体数组进行遍历,调用其回调函数进行处理文件描述符。这里肯定首先是监听套接字可用,然后创建连接套接字,然后对监听套接字和多个连接套接字同时监听,对监听套接字执行创建连接操作,对连接套接字执行消息处理操作。

 

还有就是关于linux内核链表的问题,这里的套接字结构体是使用linux内核哈希表储存的,哈希表在完美散列的情况下查找,插入,删除复杂度都是o(1),从而通过epoll返回的文件描述符就能马上找到对应的结构体,进行处理。使用linux内核链表是有一个坑坑了我好久,因为我的epoll构架参考的《基于linux平台-分层网络协议》这本书,上面的代码也是有问题的。链表的遍历操作是通过一个宏实现的,这里有一个问题就是我们处理套接字结构体时,有可能处理的是退出消息,有可能是需要进行删除这个链表节点的操作,但是使用普通的遍历链表宏在遍历链表时删除节点会发生段错误,只有使用另一个加了一个记录当前位置的参数的宏,才能在遍历链表时删除节点,但是那本书上的程序就犯了这个错误,深坑啊。

 

然后是消息的处理,基本就是通过对消息头类型值的判断,来调用不同的处理消息的函数,并执行相关的操作。最后就是写游戏逻辑,游戏逻辑也没有什么好说的,只要说一下游戏相关的数据结构和创建游戏和结束游戏功能。

游戏结构体里实际上是没有存什么数据的,只有两个指向玩家结构体的指针,而玩家结构体中也有一个指向游戏结构体的指针,所以玩家结构体和游戏结构体可以互相访问的,通过一个结构体的不完成声明就可以实现了。可以这样写让我确实觉得c语言中指针的强大之处,有了指针让我觉得很自由,没有约束的感觉。当然指针的存在也使程序有了发生段错误和内存泄露的可能。玩家结构体中游戏个running_game结构体指针,游戏的相关数据都存在这个结构体中,当游戏创建时,就会创建内存在这个指针上储存游戏信息。

然后就说到了游戏创建的功能,游戏创建的功能是这样做的,首先我这里创建了一个线程来完成这个功能,一般情况下,多线程使用消息列队进行通信,这里也可以做一个寻找游戏的等待列队,然后多线程两边要通过一个互斥量加锁,保证不对这段共享内存同时访问,然后这里还要使用一个条件变量,使两个线程能够以无竞争的方式等待特定条件的发生,我们这里要实现的是让哈希列队大于1的时候才将创建游戏线程唤醒,所以在创建游戏线程开始工作时,先获取互斥量,然后再将互斥量和条件变量传入条件变量等待函数,这时此函数会将该线程放到条件变量的等待列表上,然后对互斥量进行解锁,并进入休眠状态,当条件变量满足时,即hash列队大于1时,主线程会调用一个发送信号的函数,这时创建游戏线程重新启动,然后对互斥量加锁,开始工作。因为我要完成使等级相近过的玩家匹配到一起的功能,所以我将这里的列队换成了更具玩家rank等级进行hash的哈希表,表中的每条链也是符合列队的规则先进先出的,所以完成这个功能是就是创建游戏线程依次从等级高到等级低的链表上取节点,两两匹配游戏,优先同一级别的匹配在一起,直到这个链表的最后一个节点,如果没有节点了就与比他底一级别的列队上的节点匹配游戏。然后这里的(条件变量)设置的是当hash表中的元素大于1时唤醒创建游戏线程。

最后就是结束游戏功能,这里主要做的就是释放内存工作,这个工作一定要做,有几个malloc就要对应几个free,并且要一层一层的释放,上面说的在创建游戏的时候在running_game游戏结构体中会有玩家的牌库链表,手牌链表等,所以在结束游戏时,要首先把这些链表释放掉,然后在释放running_game。C语言指针的强大也带来了一些问题,这里确实能体会到一些人说,c++最大的优点就是有一个确定性析构,这样至少确定了这样一种规则,在销毁对象时要记得进行析构的操作。

 

接下来可以做的一些功能和改进,首先游戏里最重要的定时功能还没有做,读配置功能可以尽快完成,界面占时不准备做了,然后是玩家可以尝试用红黑树储存一下,然后通信可以加上验证完整性和加密功能,游戏逻辑上其他有意思的功能也可以实现以下,最后就是做测试,做一个客户端机器人测试游戏的处理速度,和内存消耗。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值