代码改变世界

Silverlight 游戏开发记录(2)

2010-12-21 15:01  老咩  阅读(2633)  评论(21编辑  收藏  举报

打人不打脸,不过,我自己打自己的脸总可以了吧。

 

这篇我决定谈论下我的游戏服务端。为什么说我打自己脸,因为很汗颜的说,我的服务端,很大部分借鉴某开源项目。

用c#做游戏服务器,是一种幸福。我做游戏开发之前的工作,除了使用Silverlight做gis客户端外,还需要使用c#来写转发服务器(相当于游戏的网关服务器)。那个时候,项目还处于.net2.0, 使用的还是IAsyncResult。后来在.net 3.5 以后,SocketAsyncEventArgs 的出现大大的方便了 socket的使用。

 

网络通讯从来不是1个简单的问题,看看 tcp/ip 协议那几本大部头,看看 ace 那40多万的代码。为什么说用c#做游戏服务器是一种幸福,因为你可以不去看那几本大部头,.net 基本上把 socket 的使用傻瓜化了。

 

不过,我这个傻瓜,在用傻瓜化 socket 的过程中,还是经历了很多问题。在这里说出来,以供大家参考。

 

最早使用.net 下的socket 时,我纠结于所谓的完成端口问题。使用google搜索,最大的毛病就是老信息,新信息,错误信息,其它语言下的情况交错的冲击你的思想。所以,很长一段时间,我都没有弄清楚什么是完成端口,甚至,当时还学习着自己用C#写完成端口。归根结底,是在没有充分了解多线程的情况下,进行了过度学习。我到后面才明白,开发服务端程序,多线程的理解,有多么的重要。

 

完成端口,是典型的异步机制。实际上,无论是.net 2.0时候的IAsyncResult,还是 后来的SocketAsyncEventArgs。.net 内部使用 socket时,都已经使用了完成端口。

 

完成端口,核心思想是工作线程和i/o读写分离。

 

如果使用过.net 的ThreadPool池,我们会知道,ThreadPool有2个参数。

workerThreads :线程池中辅助线程。
completionPortThreads :线程池中异步 I/O 线程。
 
 completionPortThreads,就是.net 的异步 I/O 线程.
 
.net 中每一个进程都有1个完成端口对象,它内部有个设备列表,用户把某个socket传进完成端口时,它就把这个socket加入到设备列表中监视着。比如在使用 Socket.ReceiveAsync(SocketAsyncEventArgs e) 方法的时候,会将Socket通过ThreadPool.BindHandle 这个方法传进完成端口,随后,会调用 “接收数据 ”这个 I/O 操作。设备驱动程序也完成了接收的时候,IOCP会监测到该设备上IO操作完毕,然后IOCP线程回调I/O 完成事件(通过挂接SocketAsyncEventArgs.Completed)。
 
举个例来说,就好比 医院的检验科(完成端口对象)有1个护士(异步 I/O 线程),她管理着1个处理名单(完成队列),你(socket)过来询问(护士把你加入处理名单),我那个小便检验完了没有?(发起I/O操作),检验完了帮我送到泌尿科(这是后面I/O的完成后的回调事件)。护士催促了内部检验人员(设备驱动)快点检查你的小便(额,不知道你那么急干什么,嘿嘿。),然后护士就继续处理名单。当你的小便检查完毕,护士收到信号(I/O操作完成),护士帮你把检验结果送到泌尿科(回调I/O完成事件)。
 

护士就是完成端口的工作线程,可以发现,检验工作(具体i/o读写过程)并不由她来做,她主要负责将I/O操作和相关对象进行绑定,并且投递这个请求给系统内核,在操作完成后,她还要负责完成后的通知过程(回调处理)。

在微软的建议值中,完成端口的最优工作线程为 cpu核数*2 +2。但是,在.net 中比较难限制,例如在我的2核机器上,ThreadPool池默认就开启了1000个completionPortThreads。如果使用SocketAsyncEventArgs写1个简单的收发程序,在不阻塞的情况下,使用ThreadPool.GetAvailableThreadsNative ,几千连接后,服务端仍然保持999或1000的completionPortThreads。但只要在SocketAsyncEventArgs.Completed 的回调事件中进行Thread.Sleep ,不但连接的增长变得缓慢,completionPortThreads将会明显的被使用。

 

(在接收完成事件中Thread.Sleep(100) 的图示)

 

所以,在游戏服务器的设计中,为了尽可能的高效,我的建议是不要在完成后的回调中就地进行数据的逻辑处理。

 

在这里,我决定给力的打自己的脸。透露我所参考的开源项目,这其实是个老项目,其游戏还顶顶有名,mmo始祖啊。估计很多人都猜到了,是的, RunUO.

 

RunUO是一个 UO的开源服务端程序,完全使用 C# 编写。其内容真是惊天地泣鬼神,为.net 下开发游戏服务器必备神器。地址我就不给了,自己找谷哥去。

 

说到RunUO,还得谈到另一个国内高手的模仿RunUO的项目,以及我当时开发服务器对多线程的怨念。

 

讨论游戏服务器线程,我如果说我觉得单线程才是王道,我相信肯定一堆人出来说多线程的好。实际上,当初我也是这么想的:都什么年代了,还单线程?

那个时候看过云风的1个讲游戏架构的视频,云风就谈到,大话西游就是单线程架构。当时我并没有理解,我认真的学习多线程,了解并行计算等等知识。(c#的多线程有太多可学的地方,例如什么无锁设计。)

 

 在看过RunUO的代码后,我发现RunUO主逻辑处理,没有使用多线程。后来发现了另外1个.net 游戏服务器开源项目,说要商业化,停止更新云云,下来一看,就是RunUO的山寨版,对RunUO进行了部分修改。额,老实说,修改的很漂亮,注释和规范都很强,代码也很见功力,有不少自己的东西,尤其是将主逻辑处理改成了多线程处理。当时我那个高兴啊,以为找到了组织,不过,后来,在开发服务器的过程中,我发现,片面的追求多线程,不针对服务器的功能去设计,是很愚蠢的行为。

http://mmorpg.codeplex.com (国内高手RunUO的山寨版,代码质量很高,我不知道谁开发的,求交往!)

云风有过一句话,服务器,要么做小流量大计算量的工作,要么做大流量小计算量的工作。以前的mmo 游戏,可能会是单一服务器,现在都是多服务器架构。对于游戏服务器组中的某个功能服务器来说,需要判断到底是计算密集型还是I/O密集型。(实际上这也是划分服务器功能的准则)。

 

在这里,我需要对我前面说的单线程做1个解释,我指的其实是场景服务器,主逻辑处理是单线程(还是有其它线程的,比如网络通讯的异步线程,并不是说整个服务器就1个线程)。大部情况下,我们肯定是优先考虑并且重点开发场景服务器。场景服务器,是I/O密集型的服务器,每时每刻,都有很多数据包接收和发送。如果在如此频繁的I/O操作情况下,使用多线程,其性能不升反降(过多的锁),而且,逻辑代码写起来也是极其痛苦的,你需要考虑多线程下各种状况(这个已经不是能力强不强的问题了,非要把简单的复杂化,何必呢,有这功夫你去研究研究算法多好)。

 

那么,游戏服务器就不能用多线程?我只是说,类似场景服务器这种I/O密集型的服务器比较适合单线程,假如,我们的游戏服务器组中还有AI服务器,那么这种计算密集型的服务器,才是体现多线程真正威力的地方。

 

场景服务器使用单线程,有人会说,那游戏才能负载几个人啊?额,随便去下个传奇服务器,操作说明就写到:如果想负载更多玩家,请启动多个场景服务器。

 

场景服务器使用单线程,在我看来还有1个好处,能够方便掌握我们服务器的状况。由于主逻辑线程是单线程,而每1次工作循环的处理时间是200ms(50MZ,这个周期是大话西游的服务器周期),倘若我们发现场景服务器多次工作循环超时,要么是我们的场景服务器优化还不够,某些功能没有分离出去成为其它的功能服务器,要么就是告诉你,你写的场景服务器单个就只能承载这么多玩家,快增加服务器吧。你就可以预估未来的玩家数量,增加相应数量的服务器。(貌似完美世界1个服务器组是8000上限)。

 

服务器的记录就到这里了,只说了我在多线程和网络通讯上的感悟,服务器的线程结构是灵魂,网络是血脉,至于骨骼和肌肉,那都是各位高手擅长的,如果实在和我一样蛋痛,请谷歌 RunUO 和下载上面的 RunUO 山寨版,我相信很快,你就能带领我们让C#就成为中国游戏服务器界开发的主流选择。