欢迎光临散文网 会员登陆 & 注册

BlockOS开发日志(五)多人联机设计

2023-04-16 18:34 作者:Zerous  | 我要投稿

要做中期答辩了,学校要求得有功能实现出来。我本身报的毕设项目其实是它的服务端,所以也不得不先把视角从前端转到后端这块来,实现一个能跑的起来的服务器了。

其实我对游戏服务器这一块一窍不通,所有的思路均来自各种开源项目的捕风捉影和自己的想象,如果有幸有大佬刷到这篇,还希望能指点一二。

对整个通信流程的推理分析

客户机与服务器通信的过程,我认为可以先从现实出发。想象一场特殊的棋局:黑方和白方分别在两个独立的房间里,不得出房间。但是裁判可以出入双方的房间,把话带给对方。那么要怎么去完成这个棋局呢?

嗯...先设立一个具体情境吧。假定我是棋手之一,想要和小明下棋,那么我的第一句话是:“裁判,我要和小明下棋!”那么裁判听到后,就从走廊赶过来:“好嘞!”。那万一裁判没听见或者忙忘了怎么办呢?那当然是再催一下:“搞快点,我要和小明下棋!”,直到裁判过来为止。“要和小明下棋是吧,好。”裁判说着便记下了这个请求。但是裁判也不能就这么走了,如果是认识我是谁还好说,不然跑到小明房间才想起来没问我是谁,白跑一趟不是。“那请问下你是谁?我到时候方便告诉他你要找他下。”裁判问。“叫我小泽就好。”我回答。裁判见状,便回复道:“行,小泽要和小明下棋,我这就去转告小明。”裁判找到小明,问道:“小泽想找你下棋,下不下?”小明答:“好啊。”然后裁判便回来告诉我:“小明同意了,可以开始了。”至此,一场棋局便算是开始了。

等等,那要是小明没来呢?那应该是这样的情况:裁判跑遍了所有房间,一个个问:“你是不是小明?”答曰:“不是。”直到最后一个房间。最后只好回来和我说:“不好意思,小明没来呢。”这样好像有点累哦,应该在房间门口放块黑板。每个人进房间的时候就把自己名字写到黑板上,离开房间的时候就把自己的名字擦掉。这样裁判只要看一眼房间前面的黑版就知道谁在房间了,能省不少力气。

还有还有,那要是我只是想随便找个人下棋呢?那么情境应该就变成了这样。依旧是我先叫裁判过来,告诉他:“裁判我要下棋!”这时候裁判应该有一个登记本,用来记录想下棋的人。如果本子上还没记任何人,那么裁判这时候应该说:“好嘞。我登记一下,等有人想下棋的时候我第一时间通知你。”诶,这时候就多亏了黑板,在进来之前裁判就知道我是谁了,于是裁判就在登记本上写到:“小泽,渴望下棋。”那如果是上面已经登记上了名字呢?裁判就会说:“来得正好,小明在等着下棋呢。”然后瞟一眼黑板上写的字,转头跑到小明的房间:“来了来了,小泽要跟你下棋呢。”接着裁判收到双方的同意后,对局就开始了...吗?!万一这会小明耐不住性子走了呢?这时候裁判又只好回来说:“不好意思,小明刚不耐烦走了,我给你记一下,到时候有人来了通知你。”

最后,从裁判视角来看,还有一种情况。有人进入了房间,但是他不一定是来下棋的,有可能只是误入房间,这里裁判也可以用两种办法处理。一是到房间里去问:“请问你是来下棋的吗,如果是的话麻烦在黑板上写一下称呼。”二是直接在楼外写明规矩:“来下棋者请在房间前的黑板上写好名字,未写名字者将被请出棋馆。”

那么至此建立一场棋局就完成了。那要怎么下棋呢?还是有两种方法。一种是裁判可以自己准备一个棋盘,每当一方下完一步之后,就告诉裁判,然后裁判往自己的棋盘上下好棋子。按照规则处理完成后(如落子,提子等)把棋盘的新变化告诉双方。另一种方法是裁判往两个人的房间来回跑,一方下好后,跑过去另一方那里告诉他,在这里下好了棋子,然后双方都按规则做出自己的判断,更新棋子。可以看出来两种方式各有优劣,前者的好处是不用时时刻刻费心去维护棋盘的状态,但是裁判需要花更多心思;后者的好处是可以少用一个棋盘,裁判也不需要思考可以直接往两边递话,但是如果一方出错了,裁判需要一个个去对问题出在什么地方。后者在判断特殊事件(如判断胜负)的时候也不方便,一个人的棋盘不一定是正确的棋盘,裁判需要综合双方的棋盘判断无误后才可做出决策,前者相对来说就要容易得多,但是相对的,裁判要说更多的话,干更多的事,可能忙不过来。图个方便,还是让裁判多操心一下吧。

下面就来分析怎么把这些流程转译到服务器。在这里服务器的角色对应的是裁判的角色。首先,棋手进入房间。这里的房间和棋手无关,只有裁判负责管理,每个棋手只和自己的房间相关。在这个瞬间,棋馆当局只知道有人进入了一个具体的房间,但是在他把名字写到黑板上之前并不知道他是谁。

这个过程放到计算机网络里最像的应该是TCP的accept过程,服务器知道某一个IP地址的客户端加入了连接,但是也仅此而已。如果服务器看一个TCP连接长时间得不到想要的回应(比如提供用户名),那么就应该从服务器端主动断开连接。

但这仅仅只是TCP,而我的目标应该是建立一个统一的通信接口,不论是TCP还是UDP还是任何别的线程或进程之间的通信方式,只要实现了这个接口,就都应该被服务器所识别,使用到。

提炼一下上面的关键词:连接。也许我需要一个IConnection接口来描述一次连接,或者说故事里的房间。首先它应该有一个HasMessage和一个GetMessage方法。因为在这个棋馆里不止一对棋局需要处理,可能在一个瞬间有很多房间都提出了要求,但是裁判只能同时处理一件事。因此裁判需要把处理这件事的时,其他房间提出的新要求写入待办。以后空闲时就用HasMessage方法查询待办是否还有未处理的要求,如果有,就把这个要求从待办中划掉,去处理。然后还需要一个SendMessage方法。因为裁判需要跑到房间去把消息告诉棋手。最后它还应该有一个Disconnect方法和一个IsDisconnected,用于把误入的路人请出去或者判断棋手是否有事离开了。

嗯,对于一个很多人的棋馆,只有一个裁判还是有点忙不过来,还是请个房间管理员吧,就叫ConnectionManager好了。这样裁判就只要负责把消息告诉管理员,然后管理员按照房间名字去干上面的事情。管理员每做一个来回,还要负责记录房间们提出的新要求,到时候裁判还要通过PollMessage方法把这些要求取出来,然后裁判根据管理员带来的新要求,做出更新,然后把新的任务写进任务清单,然后用ProcessTasks方法告诉管理员去处理。总算轻松一些了,裁判只要坐在裁判室负责维护各个对局的棋盘或者建立棋盘的任务,别的都交给房间管理员去处理。

房间管理员要怎么做呢?房间管理员首先应该要知道各个房间要怎么走,然后要知道谁在哪个房间,因为裁判只知道要把消息按名字派给谁,但是谁在哪个房间,只有管理员知道。所以管理员内部应该维护一个映射表Map<string,IConnection>,用于记录谁在哪个房间。啊,这时候原来需要裁判干的将路人请出房间的事情就可以交给管理员来做,裁判只需要知道来下棋的客人就好了。多一个人还是省了不少心思啊。哦,还少了一步,管理员还要把来真正来下棋的人的消息交给裁判,那么IConnection提供的信息有点少,就把房间进化为User吧,User除了提供IConnection的方法外,还有一个自己的名字,这样管理员接到消息后,顺便把消息来源给贴上,这样裁判就知道这一趟的消息来自于谁了。

而裁判呢,只需要看着各个棋盘,消息清单来了,就依次按消息清单进行处理,如果是建立棋盘相关的消息,就看进行到哪一步了,把要下一步发给谁写入任务清单。如果是对局相关的消息,就把操作加入到棋盘,然后把更新后的棋盘信息和双方也写入任务清单。等下一趟房间管理员来的时候把清单交给他处理就好。

还有吗?

还可以更进一步,比如管理员还可以请更多的下属让他们对这些房间分片区管理。而裁判那边呢,一样也可以请至少两个人,一个专门负责对局建立,一个专门负责棋盘更新,裁判收到消息后,根据消息的种类把任务再派给这两个人,等他们完成后,把任务汇总交给房间管理员处理。对应到计算机上呢,就是开更多线程负责处理这些事,然后由房间管理员线程和裁判线程来进行汇总。但是这中间又有不少协调工作(线程同步)要做。由于时间问题就留待后面优化了。

总结

首先还是再说明一下这方面的资料确实太少了,我只能从现实出发来脑洞实现方式,大概率不是最优解。还请各位指出我的不足之处。

然后就是一些根据这个画出的UML图。

只有连接管理员部分的UML图

一些根据上文写出来的伪码。


BlockOS开发日志(五)多人联机设计的评论 (共 条)

分享到微博请遵守国家法律