之所以不提TCP或UDP是因为要不要用UDP自己实现一套TCP是另一个待撕话题,这篇文章不做讨论。因此,我们假设,后续的实现是建立在对底层协议一无所知的前提之上的,这样设计的时候只要适配各种协议,到时候就能按需切换。
socket大家都很熟悉,优点就是各操作系统上抽象统一。
因此,之前的问题可以规约为:如何用socket实现场景同步?
拓扑结构是这样的(之后的所有图片连接箭头的意思表示箭头指向的对于箭头起源的来说是静态的):
场景同步有两个需求:
要做到前者,最理想的情况就是由游戏程序员把控消息流的整套pipeline,换句话说,就是不借助第三方的消息库/连接库。当然,例外是你对某些第三方连接库特别熟悉,比如很多C++服务端库喜欢用的libevent,或者我在本篇文章提供的示例代码所依赖的,mono中的IO模块。
要做到后者,就需要保持场景同步逻辑的简化,也就是说,场景逻辑最好是单线程的,并且跟IO无关。其核心入口就是一个主循环,依次更新场景中的所有entity,刷新状态,并通知client。
正是由于这两个需求的存在,网络库的概念就出现了。网络库由于易于实现,概念简单,而且笼罩着“底层”光环,所以如果除去玩具性质的项目之外,网络库应该是程序员造过最多的轮子之一。
那么,网络库解决了什么问题?抛开多项目代码复用不谈,网络库首先解决的一点就是,将传输层的协议(stream-based的TCP协议或packet-based的UDP协议)转换为应用层的消息协议(通常是packet-based)。对于业务层来说,接收到流和包的处理模型是完全不同的。对于业务逻辑狗来说,包显然是处理起来更直观的。
流转包的方法很多,最简单的可伸缩的non-trivial buffer,ringbuffer,bufferlist,不同的结构适用于不同的需求,有的方便做zero-copy,有的方便做无锁,有的纯粹图个省事。因为如果没有个具体的testcast或者benchmark,谁比谁一定好都说不准。
buffer需要提供的语义也很简单,无非就是add、remove。buffer是只服务于网络库的。
网络库要解决的第二个问题是,为应用层建立IO模型。由于之前提到过的场景服务的rich interaction的特点,poll模型可以避免大量共享状态的存在,理论上应该是最合适场景服务的。所谓poll,就是IO线程准备好数据放在消息队列中,用户线程负责轮询poll,这样,应用层的回调就是由用户线程进入的,保证模型简单。
而至于IO线程是如何准备数据的,平台不同做法不同。linux上最合适的做法是reactor,win最合适的做法就是proactor,一个例外是mono,mono跑在linux平台上的时候虽然IO库是reactor模型,但是在C#层面还是表现为proactor模型。提供统一poll语义的网络库可以隐藏这种平台差异,让应用层看起来就是统一的本线程poll,本线程回调。
网络库要解决的第三个问题是,封装具体的连接细节。cs架构中一方是client一方是server,因此连接细节在两侧是不一样的。而由于socket是全双工的,因此之前所说的IO模型对于任意一侧都是适用的。
连接细节的不同就体现在,client侧,核心需求是发起建立连接,外围需求是重连;server侧,核心需求是接受连接,外围需求是主动断开连接。而两边等到连接建立好,都可以基于这个连接构建同样的IO模型就可以了。
现在,简单介绍一种网络库实现。
具体代码不再在博客里贴了。请参考:Network
引入新的问题如果类比马斯洛需求中的层次,有了网络库,我们只能算是解决了生理需求:可以联网。但是后面还有一系列的复杂问题。
最先碰到的问题就是,玩家数量增加,一个进程扛不住了。那么就需要多个进程,每个进程服务一定数量的玩家。
但是,给定任意两个玩家,他们总有可能有交互的需求。
对于交互需求,比较直观的解决方案是,让两个玩家在各自的进程中跨进程交互。但是这就成了一个分布式一致性问题——两个进程中两个玩家的状态需要保持一致。至于为什么一开始没人这样做,我只能理解为,游戏程序员的计算机科学素养中位程度应该解决不了这么复杂的问题。
因此比较流行的是一种简单一些的方案。场景交互的话,就限定两个玩家必须在同一场景(进程),比如攻击。其他交互的话,就借助第三方的协调者来做,比如公会相关的通常会走一个全局服务器等等。
这样,服务端就由之前的单场景进程变为了多场景进程+协调进程。新的问题出现了:
玩家需要与服务端保持多少条连接?
一种方法是保持O(n)条连接,既不环保,扩展性又差,可以直接pass掉。
那么就只能保持O(1)条连接,如此的话,如何确定玩家正与哪个服务端进程通信?
要解决这个问题,我们只能引入新的抽象。
3.1.2 Gate
定义问题
整理下我们的需求:
要解决这些需求,我们需要引入一种反向代理(reverse proxy)中间件。
反向代理是服务端开发中的一种常见基础设施抽象(infrastructure abstraction),概念很简单,简单说就是内网进程不是借助这种proxy访问外部,而是被动地挂在proxy上,等外部通过这种proxy访问内部。
更具体地说,反向代理就是这样一种server:它接受clients连接,并且会将client的上行包转发给后端具体的服务端进程。
很多年前linux刚支持epoll的时候,流行一个c10k的概念,解决c10k问题的核心就是借助性能不错的反向代理中间件。