QCon北京「鸿蒙专场」火热来袭!即刻报名,与创新同行~ 了解详情
写点什么

深入浅出 Node.js 游戏服务器开发 -- 分布式聊天服务器搭建

  • 2013-01-23
  • 本文字数:4775 字

    阅读完需:约 16 分钟

在上一篇文章中, 我们介绍了游戏服务器的基本架构、相关框架和 Node.js 开发游戏服务器的优势。本文我们将通过聊天服务器的设计与开发,来更深入地理解 pomelo 开发应用的基本流程、开发思路与相关的概念。本文并不是开发聊天服务器的 tutorial,如果需要 tutorial 和源码可以看文章最后的参考资料。

为什么是聊天服务器?

我们目标是搭建游戏服务器,为什么从聊天开始呢?

聊天可认为是简化的实时游戏,它与游戏服务器有着很多共通之处,如实时性、频道、广播等。由于游戏在场景管理、客户端动画等方面有一定的复杂性,并不适合作为 pomelo 的入门应用。聊天应用通常是 Node.js 入门接触的第一个应用,因此更适合做入门教程。

Pomelo 是游戏服务器框架,本质上也是高实时、可扩展、多进程的应用框架。除了在 library 中有一部分游戏专用的库,其余部分框架完全可用于开发高实时 web 应用。而且与现在有的 Node.js 高实时应用框架如 derby、socketstream、meteor 等比起来有更好的可伸缩性。

对于大多数开发者而言,Node.js 的入门应用都是一个基于 socket.io 开发的普通聊天室, 由于它是基于单进程的 Node.js 开发的, 在可扩展性上打了一定折扣。例如要扩展到类似 irc 那样的多频道聊天室, 频道数量的增多必然会导致单进程的 Node.js 支撑不住。

而基于 pomelo 框架开发的聊天应用天生就是多进程的,可以非常容易地扩展服务器类型和数量。

从单进程到多进程,从 socket.io 到 pomelo

一个基于 socket.io 的原生聊天室应用架构, 以 uberchat 为例。

它的应用架构如下图所示:

服务端由单个 Node.js 进程组成的 chat server 来接收 websocket 请求。

它有以下缺点:

  1. 可扩展性差:只支持单进程的 Node.js, 无法根据 room/channel 分区, 也无法将广播的压力与处理逻辑的压力分开。
  2. 代码量大:基于 socket.io 做了简单封装,服务端就写了约 430 行代码。

用 pomelo 来写这个框架可完全克服以上缺点,并且代码量只要区区 100 多行。

我们要搭建的 pomelo 聊天室具有如下的运行架构:

在这个架构里, 前端服务器也就是 connector 专门负责承载连接, 后端的聊天服务器则是处理具体逻辑的地方。 这样扩展的运行架构具有如下优势: * 负载分离:这种架构将承载连接的逻辑与后端的业务处理逻辑完全分离,这样做是非常必要的, 尤其是广播密集型应用(例如游戏和聊天)。密集的广播与网络通讯会占掉大量的资源,经过分离后业务逻辑的处理能力就不再受广播的影响。

  • 切换简便:因为有了前、后端两层的架构,用户可以任意切换频道或房间都不需要重连前端的 websocket。
  • 扩展性好:用户数的扩展可以通过增加 connector 进程的数量来支撑。频道的扩展可以通过哈希等算法负载均衡到多台聊天服务器上。理论上这个架构可以实现频道和用户的无限扩展。

聊天服务器开发架构

game server 与 web server

聊天服务器项目中分生成了 game-server 目录、web-server 目录与 shared 目录,如下图所示:

这样也将应用天然地隔离成了两个,game server 与 web server。

  • Game server, 即游戏服务器,所有的游戏服务器逻辑都在里实现。客户端通过 websocket(0.3 版会支持 tcp 的 socket)连到 game server。game-server/app.js 是游戏服务器的运行入口。
  • Web server,即 web 服务器, 也可以认为是游戏服务器的一个 web 客户端, 所有客户端的 js 代码,web 端的 html、css 资源都存放在这里,web 服务端的用户登录、认证等功能也在这里实现。pomelo 也提供了其它客户端,包括 ios、android、unity3D 等。
  • Shared 目录,假如客户端是 web,由于服务端和客户端都是 javascript 写的,这时 Node.js 的代码重用优势就体现出来了。shared 目录下可以存放客户端、服务端共用的常量、算法。真正做到一遍代码, 前后端共用。

服务器定义与应用目录

Game server 才是游戏服务器的真正入口,游戏逻辑都在里, 我们简单看一下 game-server 的目录结构,如下图所示:

servers 目录下所有子目录定义了各种类型的服务器,而每个 servers 目录下基本都包含了 handler 和 remote 两个目录。 这是 pomelo 的创新之处,用极简的配置实现游戏服务器的定义,后文会解释 handler 和 remote。

通过 pomelo,游戏开发者可以自由地定义自己的服务器类型,分配和管理进程资源。在 pomelo 中,根据服务器的职责不同,服务器主要分为前端服务器 (frontend) 和后端服务器 (backend) 两大类。其中,前端服务器负责承载客户端的连接和维护 session 信息,所有服务器与客户端的消息都会经过前端服务器;后端服务器负责接收前端服务器分发过来的请求,实现具体的游戏逻辑,并把消息回推给前端服务器,最后发送给客户端。如下图所示:

动态语言的面向对象有个基本概念叫鸭子类型。在 pomelo 中,服务器的抽象也同样可以比喻为鸭子,服务器的对外接口只有两类, 一类是接收客户端的请求, 叫做 handler, 一类是接收 RPC 请求, 叫做 remote, handler 和 remote 的行为决定了服务器长什么样子。 因此开发者只需要定义好 handler 和 remote 两类的行为, 就可以确定这个服务器的类型。 例如 chat 服务器目前的行为只有两类,分别是定义在 handler 目录中的 chatHandler.js,和定义在 remote 目录中的 chatRemote.js。只要定义好这两个类的方法,聊天服务器的对外接口就确定了。

搭建聊天服务器

准备知识

pomelo 的客户端服务器通讯

pomelo 的客户端和服务器之间的通讯可以分为三种:

  • request-response

    pomelo 中最常用的就是 request-response 模式,客户端发送请求,服务器异步响应。客户端的请求发送形式类似 ajax 类似:

复制代码
pomelo.request(url, msg, function(data){});

第一个参数为请求地址,完整的请求地址主要包括三个部分:服务器类型、服务端相应的文件名及对应的方法名。第二个参数是消息体,消息体为 json 格式,第三个参数是回调函数,请求的响应将会把结果置入这个回调函数中返回给客户端。

  • notify

    notify 与 request—response 类似,唯一区别是客户端只负责发送消息到服务器,客户端不接收服务器的消息响应。

复制代码
pomelo.notify(url, msg);
  • push

    push 则是服务器主动向客户端进行消息推送,客户端根据路由信息进行消息区分,转发到后。通常游戏服务器都会发送大量的这类广播。

复制代码
pomelo.on(route, function(data){});

以上是 javascript 的 api, 其它客户端的 API 基本与这个类型。由于 API 与 ajax 极其类似,所有 web 应用的开发者对此都不陌生。

session 介绍

与 web 服务器类似,session 是游戏服务器存放用户会话的抽象。但与 web 不同,游戏服务器的 session 是基于长连接的, 一旦建立就一直保持。这反而比 web 中的 session 更直接,也更简单。 由于长连接的 session 不会 web 应用一样由于连接断开重连带来 session 复制之类的问题,简单地将 session 保存在前端服务器的内存中是明智的选择。

在 pomelo 中 session 也是 key/value 对象,其主要作用是维护当前用户信息,例如:用户的 id,所连接的前端服务器 id 等。session 由前端服务器维护,前端服务器在分发请求给后端服务器时,会复制 session 并连同请求一起发送。任何直接在 session 上的修改,只对本服务器进程生效,并不会影响到用户的全局状态信息。如需修改全局 session 里的状态信息,需要调用前端服务器提供的 RPC 服务。

channel 与广播

广播在游戏中是极其重要的,几乎大部分的消息都需要通过广播推送到客户端,再由客户端播放接收的消息。而 channel 则是服务器端向客户端进行消息广播的通道。 可以把 channel 看成一个用户 id 的容器. 把用户 id 加入到 channel 中成为当中的一个成员,之后向 channel 推送消息,则该 channel 中所有的成员都会收到消息。channel 只适用于服务器进程本地,即在服务器进程 A 创建的 channel 和在服务器进程 B 创建的 channel 是两个不同的 channel,相互不影响。

服务器之间 RPC 通讯

从之前的文章可以了解到,在 pomelo 中,游戏服务器其实是一个多进程相互协作的环境。各个进程之间通信,主要是通过底层统一的 RPC 框架来实现,服务器间的 RPC 调用也实现了零配置。具体 RPC 调用的代码如下:

复制代码
app.rpc.chat.chatRemote.add(session, uid, app.get
('serverId'), function(data){});

其中 app 是 pomelo 的应用对象,app.rpc 表明了是前后台服务器的 Remote rpc 调用,后面的参数分别代表服务器的名称、对应的文件名称及方法名。为了实现这个 rpc 调用,则只需要在对应的 chat/remote/ 中新建文件 chatRemote.js,并实现 add 方法。

聊天室流程概述

下图列出了聊天室进行聊天的完整流程:

通过以上流程, 我们可以看到 pomelo 的基本请求流程和用法。本文不是聊天室的 tutorial,因此下面列出的代码不是完整的,而是用极简的代码来说明 pomelo 的使用流程和 api。

进入聊天室

客户端向前端服务器发起登录请求:

复制代码
pomelo.request('connector.entryHandler.enter',
{user:userInfo}, function(){});

用户进入聊天室后,服务器端首先需要完成用户的 session 注册同时绑定用户离开事件:

复制代码
session.bind(uid);
session.on('closed', onUserLeave.bind(null, app));

另外,服务器端需要通过调用 rpc 方法将用户加入到相应的 channel 中;同时在 rpc 方法中,服务器端需要将该用户的上线消息广播给其他用户,最后服务器端向客户端返回当前 channel 中的用户列表信息。

复制代码
app.rpc.chat.chatRemote.add(session, uid,
serverId,function(){});

发起聊天

客户端向服务端发起聊天请求,请求消息包括聊天内容,发送者和发送目标信息。消息的接收者可以聊天室里所有的用户,也可以是某一特定用户。

服务器端根据客户端的发送的请求,进行不同形式的消息广播。如果发送目标是所有用户,服务器端首先会选择 channel 中的所有用户,然后向 channel 发送消息,最后前端服务器就会将消息分别发送给 channel 中取到的用户;如果发送目标只是某一特定用户,发送过程和之前完全一样,只是服务器端首先从 channel 中选择的只是一个用户,而不是所有用户。

复制代码
if(msg.target == '*')
channel.pushMessage(param);
else
channelService.pushMessageByUids(param,
[{uid:uid, sid:sid}]);

接收聊天消息

客户端接收广播消息,并将消息并显示即可。

复制代码
pomelo.on('onChat', function() {
addMessage(data.from, data.target, data.msg);
$("#chatHistory").show();
});

退出聊天室

用户在退出聊天室时,必须完成一些清理工作。在 session 断开连接时,通过 rpc 调用将用户从 channel 中移除。在用户退出前,还需要将自己下线的消息广播给所有其他用户。

复制代码
app.rpc.chat.chatRemote.kick(session, uid, serverId,
channelName, null);

聊天服务器的可伸缩性与扩展讨论

上一讲已经谈到 pomelo 在提供了一个高可伸缩性的运行架构,对于聊天服务器同样如此。如果想从单频道聊天室扩展到多频道聊天室,增加的代码几乎为零。大部分工作只是在进行服务器和路由的配置。对于服务器配置只需要修改 json 配置文件即可,而对于路由配置只需要增加一个路由算法即可。在 pomelo 中,开发者可以自己配置客户端到服务器的路由规则,这样会使得游戏开发更加灵活。

我们来看一下配置 json 文件对服务器运行架构的影响:

  • 最简服务器与运行架构:

  • 扩展后的服务器与运行架构:

另外,在 0.3 版本的 pomelo 中增加了动态增删服务器的功能,开发者可以在不停服务的情况下根据当前应用运行的负载情况添加新的服务器或者停止闲置的服务器,这样可以让服务器资源得到更充分的利用。

总结

本文通过聊天服务器的搭建过程,分析了 pomelo 开发应用的基本流程,基本架构与相关概念。有了这些知识我们可以轻松地使用 pomelo 搭建高实时应用了。 在后文中我们将分析更复杂的游戏案例,并且会对架构中的一些实现深入剖析。

相关资源:

2013-01-23 23:3138055

评论

发布
暂无评论
发现更多内容

空降负责人如何与团队建立信任?

石云升

极客时间 1月月更 技术领导力实战笔记

精华推荐 | 【深入浅出 RocketMQ原理及实战】「底层源码挖掘系列」透彻剖析贯穿RocketMQ的消费者端的运行核心的流程(上篇)

码界西柚

RocketMQ 消息队列 原理分析

影响产品开发决策的认知偏见

俞凡

认知

如何判断候选人与岗位的匹配程度?

石云升

极客时间 1月月更 技术领导力实战笔记

你知道这个提高 Java 单元测试效率的 IDEA 插件吗

JAVA旭阳

Java

FPGA:逻辑功能的仿真与验证

timerring

FPGA

复习前端:浏览器渲染机制

devpoint

DOM CSSOM 渲染树 重绘 重排

Verilog HDL仿真常用命令

timerring

FPGA

2022年回顾:一个37岁中年程序员的一百场面试

无人之路

自动驾驶 面试 求职 大厂 跳槽

如何用Know Streaming来查询Kafka的消息

石臻臻的杂货铺

Kafk

极客时间运维进阶训练营第12周作业

独钓寒江

简述styled-components性能

devpoint

CSS React 样式组件 前端性能

FPGA:Verilog HDL程序的基本结构

timerring

FPGA

模块六作业

Ryan

架构

模块5 微博高性能计算架构设计

KING

GLM国产大模型训练加速:性能最高提升3倍,显存节省1/3,低成本上手

OneFlow

人工智能 深度学习

设计微博系统中”微博评论“的高性能高可用计算架构

悟空

架构 高可用 高性能 微博评论

国产 ETL工具 ETL产品 数据交换系统

weigeonlyyou

postgresql hadoop elasticsearch Prometheus 时序数据库

消息队列存储数据消息Mysql设计

闲人Eric

架构实战营

我总结了写出高质量代码的12条建议

JAVA旭阳

Java

CleanMyMac X2024版本值不值得买?

茶色酒

CleanMyMac X CleanMyMac X2023

OneFlow v0.9.0正式发布

OneFlow

人工智能 深度学习

企业架构治理指什么,如何做?

涛哥 数字产品和业务架构

架构治理 企业构架

如何妥善且优雅地做好解聘工作?

石云升

极客时间 1月月更 技术领导力实战笔记

复习前端:CSS

devpoint

CSS Flex scss BFC

什么是AirServer?2024版本如何下载安装包

茶色酒

AirServer

CrossOver2023软件Mac电脑版虚拟机安装包

茶色酒

CrossOver2023

会声会影2023功能强大的视频编辑软件

茶色酒

会声会影2023

海外拥有最庞大社区人群的Verasity($VRA),后市值得期待

股市老人

IntelliJ中高效重构的 10 个快捷方式

JAVA旭阳

Java

问题代码定位神器: Git Bisect

俞凡

git

深入浅出Node.js游戏服务器开发--分布式聊天服务器搭建_架构/框架_谢骋超_InfoQ精选文章