百万在线:大型游戏服务端开发
上QQ阅读APP看书,第一时间看更新

1.4 用分布式扩能

对于中大型商业游戏来说,往往出现全服爆满的现象(如图1-12),1000多人的承载量远远不够。根据游戏厂商的新闻稿可知,2012年《梦幻西游》最高同时在线玩家达到了270多万人;2016年《王者荣耀》的同时在线玩家超过了300万人。既然单个程序的承载量有限,最直接的办法就是开启多个程序来提高承载量。

图1-12 玩家爆满的游戏画面示意图

1.4.1 多个程序协同工作

图1-13展示了一种由多个程序共同协作的服务端模型,图中程序A和程序B分别处理客户端消息,程序C作为中转站,负责程序A和程序B之间的通信。每个程序均独立运行,可以将其部署在不同的物理机上,形成天然的分布式系统。

图1-13 多进程服务端示意图

说明:为统一术语,本书中“服务端”代表整个游戏服务端系统;“程序”“进程”或“节点”代表一个操作系统进程;“物理机”代表服务器,涵盖了实体服务器和云服务器。

尽管单个程序还是最多承载1000余人,但是只须开启1000个程序,并将其布置在数百台物理机上,理论上就可以支撑100万玩家,总承载量得以提高。

1.4.2 三个层次的交互

在分布式结构中,数据的交互被分成了三个层次,如表1-2所示。这就要求开发者能对游戏业务功能做出合理的切分。在游戏中,有些功能是强交互的,有些功能是弱交互的。以MMORPG为例,同一个场景的角色交互很强,每走一步都要让对方知道,可以在同一个程序中处理同一个场景逻辑;不同场景的角色交互较弱,只有聊天、好友、公会这些功能需要交互,可以将同一个服务器的玩家都放在同一台物理机上处理;不同服务器的玩家交互很少,可以放到不同的物理机上。

表1-2 不同交互场景的区别

1.4.3 搭个简单的分布式服务端

理论归理论,实践出真知。实现1.2.4节的“走路”程序是场景服务器的一项主要功能,尽管一个场景只能支撑数十人,只要多开几个场景就能够支持更多玩家。本节将实现图1-14所示的分布式程序,系统中有两个“走路”程序,分别代表兽人村落和森林两个游戏场景,客户端直接连接角色所在的场景,玩家只能看到所在场景的角色,不同场景角色可以全服聊天。该程序可分成三个步骤实现。

图1-14 简单的分布式系统

第一步,编写聊天服务器。聊天服务器其实是转发服务器,它管理着场景服务器发来的连接(见代码1-5中的scenes),只要收到场景服务器的消息,它就会广播给所有的场景服务器。聊天服务器会监听8010端口,等待场景服务器连接。

代码1-5 聊天服务器(Node.js)

(资源:Chapter1/3_chat_server.js)


var net = require('net');

var scenes = new Map();

var server = net.createServer(function(socket){
    scenes.set(socket, true) //新连接

    socket.on('data', function(data) { //收到数据
        for (let s of scenes.keys()) {
            s.write(data);
        }
    });
});

server.listen(8010);

第二步,让场景服务器(“走路”程序)连接聊天服务器。场景服务器即是服务端又是客户端,对于玩家来说,它是服务端,对于聊天服务器来说,它又是客户端。在“走路”程序的基础上,让场景服务器连接聊天服务器(见代码1-6中的net.connect),当场景服务器收到聊天服务器发来的数据时,就会把它原封不动地广播给客户端。

代码1-6 场景服务器的部分代码,用于连接聊天服务器(Node.js)

(资源:Chapter1/3_walk_server.js)


var net = require('net');
//"走路"程序略 server.listen(8001);

var chatSocket = net.connect({port: 8010}, function() {});
chatSocket.on('data', function(data){
    for (let s of roles.keys()) {
        s.write(data);
    }
});

第三步,给场景服务器添加聊天功能(见代码1-7)。假设客户端除了发送“left”“right”等指令外,还会发送聊天文字,那么在收到聊天消息后它会把消息原样发给聊天服务器。整个消息流程是:①场景服务器将聊天消息发送给聊天服务器;②聊天服务器把消息广播给所有场景服务器;③各个场景服务器分别将聊天消息广播给场景中的所有玩家。

代码1-7 场景服务器处理聊天消息的部分代码(Node.js)

(资源:Chapter1/3_walk_server.js)


    //接收到数据
    socket.on('data', function(data){
        ……
        //更新位置
        if(cmd == "left\r\n") role.x--;
        ……
        else { 
            chatSocket.write(data);
            return;
        };
        …… 
    });

现在可以进行测试了,先运行聊天服务器,再依次运行两个场景服务器(假设监听的端口分别为8001和8002)。如图1-15所示,客户端A和B连接第一个场景服务器,客户端C连接第二个场景服务器,服务器中的小方块代表各个程序,方块中的数字代表该程序的监听端口。当客户端A走动时,因为A、B同在一个场景中,所以它们会收到移动消息,而客户端C不在同一场景中,因此它不会收到;若客户端A发送聊天信息“战神公会招人”,三个客户端都能收到。

图1-15 测试分布式服务端

1.4.4 一致性问题

分布式程序要处理很多异常情况。如果程序部署在不同物理机上,连接不太稳定,需要处理好断线重连、断线期间的消息重发,以及断线后进程间状态不一致的问题。图1-16展示的是因网络不畅通导致的异常情形,假如客户端A的玩家向客户端B的玩家购买道具,消息需要通过程序C中转,因程序A和程序C之间的网络连接出现异常,出现了客户端B的玩家被扣除了道具,客户端A的玩家却没得到道具的情况。程序A与程序C的网络连接异常,游戏功能受到了影响,就算一段时间后重新连接上,两个进程的状态也可能会不一致。

图1-16 分布式程序的异常情形

一致性问题是分布式系统的一大难题,在游戏业务中,开发者一般会把一致性问题抛给具体业务去处理。对于图1-16所示的异常情况,需要给每个交易赋予唯一编号。程序C除了转发消息,还需要记录程序A对每个交易的执行状态,如果转发失败,程序C要在稍后重发交易消息,直到程序A成功执行。而程序A也需要记录每个交易的状态,如果某个交易已经成功执行,则不再响应程序C发来的消息,避免重复添加道具。

另外,管理数百台物理机、成百上千个程序也不容易,第一,物理机多了,某一台出故障的可能性很大;第二,开启或关闭全部程序要花费很长时间。