NodeJS 开发多人实时对战游戏服务器 (一)

从一个游戏情怀说起

接触的第一款多人对战游戏是帝国时代,依稀记得那时候上学每周最期待的就是冲到电脑课撸一把罗马复兴,高中开始接触《魔兽争霸3》,一款真正让我迷恋十多年的游戏,怀念那时候的《魔兽争霸十大经典战役》还有到图书馆翻 《大众软件》找各种电子游戏相关的新闻的日子,之后和很多人的经历一样,有了 Dota 有了王者荣耀,打一款MOBA游戏几乎成家常便饭,最近也没忍住撸到王者六十多星 ╮(╯_╰)╭。

帝国时代

魔兽争霸3

阴差阳错成为了一名码农,但不幸的是从来没有机会真正去涉足游戏开发者行业。去年魔兽3重制版出来,没忍住交出了一笔情怀税,算是弥补这么多年对暴雪的亏欠,然后转念一想,码农快十载了难道还任由自己继续堕落下去吗?对战类游戏最大的乐趣就是 “与人斗主宰一切的感觉”,“Triple Kill” “Monster Kill” 缭绕于耳,然而再想想那个真正在虚拟世界主宰一切的其实是制定游戏规则的人,也就是游戏创作者,那种当作者的感觉不是2.5D视角的而是真正的上帝视角,所以去年年中开始决定转行求变,从零起步了解下游戏设计,先从技术入手,啰嗦很多,当然不是为了给自己沉迷网络游戏找借口啦。

一个简单的聊天室

若要问一个能集合多人互动又需要实时同步的简单场景是什么?答案就是聊天室,这也是很多游戏框架的入门demo,不例外,我也是从聊天室开始学习的,很快,写这篇文章的现在我大概花了那么丁点时间快速撸了一个,顺带凭着这么多年积累的前端美感对界面稍微加了点样式,代码地址

聊天室

如果你是一个前端从业者,相信你很快会想到使用 socket.io, 如果你不是,相信你也听过 Websocket。是的,因为简单,我们不用花时间去理解 TCP 的三次握手,拿来即用。为什么聊天室需要Websocket,答案是长连接,在聊天室里,一个房间的任何消息变化都要通过服务端实时广播推送给各个客户端,如下,client1 发送一条消息,其他的 client 都需要收到服务端的消息,而这个的前提是服务端需要知道有多少客户端连接着。

C/S

对比下 http 请求,client1 发送完消息 (Request) 服务端接收后并返回 (Response) 即断开,如此服务端是无法获得其他客户端的连接状态并推送,不过 http 可以使用轮询 (Polling),每个客户端隔一段时间发送一个请求到服务端,如果有发现别的客户端的发送聊天室消息就返回数据,消息延迟跟轮询时间间隔有关, 如此也能做一个聊天室,想想任务也就完成了,但是如果这个聊天室是马化腾发起的呢,目标是做成微信呢?

性能,才是一款优秀的游戏服务器追寻的目标,一条消息服务端广播的数量和客户端数量成正比,n 条消息就是 n * n, 如果再配上轮询,想想王者荣耀 460ms 的延迟是一个玩家能忍受的吗。回到 Websocket 同样会带来性能瓶颈,早期的网络游戏服务器大多是单台服务器单进程架构,所有逻辑都写在一起,同时长连接也需要比短连接带来更多的内存开销,如存储所有客户端Session信息,且内部其实也是通过某种轮询去实现的,这些总总,当我们想去打造一个 “企业级的游戏框架” (这个说法来自 eggjs =。=) 的时候,简单的使用 socket.io, 在面临大量的在线客户端时候,我们可能就到此就止步了,这也是这篇文章的一个背景和初衷,我想聊聊游戏服务器为了性能到底能做什么,可能经验不足,但至少搞下来收获满满。

分布式多进程模型设计

我在 Github 搜了很多游戏框架并对比,最终映入眼帘的就是网易的 pomelo。一是网易的大厂背景,想想当年的梦幻西游,二是它的文档架构的完备性,所以我花了很多时间把它的代码几乎都看完,但是由于它的代码年久失修几乎不维护,同时秉承前端造论圈的坏风气,我重新参考它的代码以更现代化的方式写了一个游戏框架 Regax,并美化了下架构图:

我们回顾上节所讲的性能瓶颈:

  • 单进程单服务器无法承载更多的客户端。
  • 长连接广播带来的开销巨大,特别是游戏场景很频繁需要推送消息。

再看下上边的图到底做了什么:

  • 第一点,所有业务逻辑都以进程服务器粒度拆分,拆分越细越好,提升伸缩性,进程间通过RPC调用,如此可保证进程可跨集群服务器调用,这是分布式架构的基本。
  • 第二点,Socket连接服务器单独拆分,这是最关键的,它只负责连接及广播,不负责任何其他的业务逻辑,保证其性能最大化。

除了解决上节问题再进一步优化:

  • 第三点,协议层更加灵活,不再只是Websocket,由于连接服务器的隔离加纯粹性,服务器可支持多种连接方式共存,如此我们能承载的客户端更多,还可支持灵活切换,如真正的业务场景tcp和udp可根据客户端支持情况自动切换。
  • 第四点,引入网关层,网关层用来控制连接的路由算法,想想农药里的服务器分区策略,再比如地理位置就近原则,分配就近的服务器,进一步提升网络传输效率。
  • 第五点,进程支持权重,权重越高,分配的进程越多机会越大,这也是伸缩性的一种提升。

其他模块就是大众服务器所通用的扩展,如监控及存储等,这里不赘述,真正去理解专研一款优秀的框架设计时候,真的会爱不释手。

一切准备就绪,设计完框架后急需一个业务场景去试炼一番,以此来反哺框架,想想现在能做的太多了,撸一个页游传奇Online渣渣灰绰绰有余,在我所在的支付宝小程序团队也很需要创新场景,框架本身也能给业务带来更多的可能性更多的玩法,最终敲定做了一款简单的多人实时对战贪吃蛇, 可支持和好友一起玩,这时候才是体会开发游戏的乐趣所在。

多人实时对战贪吃蛇

我们参照了王者荣耀的好友匹配+对战的模式设计了下贪吃蛇,如下:

贪吃蛇房间匹配页面

贪吃蛇对战页面

贪吃蛇游戏结束排名

首先按上节的架构,我对服务器做了拆分:

  1. 连接服务器 (ConnectorServer):负责和客户端的Websocket连接及通知,同时校验登陆Token,如果Token不合法直接关闭连接,连接后通过token再去数据库拿用户的昵称等信息。
class ConnectorServer {
    enter({ token }){
        // 1. 校验 Token
        // 2. 通过 Token从数据库获取用户信息, 并创建 Session
        // 3. 监听 Socket关闭
        this.ctx.session.on('disconnect', () => {
            // 4. 如果当前用户在某个房间,发送RPC通知房间服务器踢掉用户
            this.ctx.rpc.room.kickUser(this.ctx.session.uid)
        })
    }
}

 

2. 房间服务器 (RoomServer): 负责房间的创建及加入,并通知房间里所有的用户信息

class RoomServer {
    kickUser() {
        // 1. 踢掉用户
        // 2. 发送 RPC 给 ConnectorServer 广播给客户端房间信息, 这里channel内部封装了rpc
        this.ctx.channel.room.pushMessage('onRoomChange', roomData)
    }
    joinUser() {
        // 1. 加入用户
        // 2. 发送 RPC 给 ConnectorServer 广播给客户端房间信息, 这里channel内部封装了rpc
        this.ctx.channel.room.pushMessage('onRoomChange', roomData)
    }
    startGame() {
        // 1. 发送RPC给 BattleServer 开始游戏
        this.ctx.rpc.battle.start(roomMembers)
    }
}

3. 对战服务器 (BattleServer): 贪吃蛇开始游戏后,会在服务端建立 帧同步 模式,并定时推送消息, 帧同步会再之后介绍:

class BattleServer {
   start() {
     // 模拟帧同步,真正实现会比这个复杂
     setInterval(() => {
        // 按每秒三十帧的频率发送帧数据给所有客户端
        this.ctx.channel.battle.pushMessage('onBattleFrame', currentFrame)
     },1000 / 30)
   },
   syncFrameAction() {
      // 从客户端接收到贪吃蛇的操作动作并插入到当前帧数据里
   }
}

而在客户端:

import { Client } from '@regax/client-websocket'

const client = new Client({ url: 'ws://localhost:8002', reconnect: true })

// 监听服务端断线
client.on('disconnect', () => {})

// 1. 创建 WebSocket 连接
await client.connect()

// 2. 监听房间成员变化,这里会通过服务端广播接收到
client.on('onRoomChange', ( roomData) => {} )

// 3. 监听游戏开始后的帧数据变化
client.on('onBattleFrame', ( frame) => {
  // 每接收到一帧,就驱动贪吃蛇渲染引擎渲染一次
})

// 4. 登陆并校验token
await client.request('connector.enter', { token })
// 5. 加入房间
await client.request('room.joinUser')
// 6. 点击开始游戏
await client.request('room.startGame')
// 7. 操作贪吃蛇时候发送操作行为
await client.request('battle.syncFrameAction', { action })

这样一款多人对战版的贪吃蛇算是基本完成了,但是真正实现的时候遇到不少的坑,如卡顿严重,另外为什么要使用帧同步,帧同步和状态同步的区别在哪,再下一章我会聊一聊这个话题。

最后

如果大家想体验可以到支付宝搜下 `福利贪吃蛇`, 目前集群机器还比较少请轻虐,最后,不忘记招聘,如果你有兴趣,可以私信我, 阿里系能给你的自由度及想象空间挺大。

转自https://zhuanlan.zhihu.com/p/114150098

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在H5中实现多人音频通话并使用.js作为服务器,你可以按照以下步骤进行操作: . 设置Node.js服务器 - 安装Node.js并创建一个新的项目夹。 - 在项目文件夹中行`npm init`命令来初始化项目按照提示填写相关信息。 - 安装需的依赖包,包括和Socket.IO: ``` npm install express.io ``` 2. 创建Node.js服务器代码: - 在项目文件夹中创建一个新的`server.js`文件。 - 导入所需的模块和设置服务器: ```javascript const express = require('express'); const http = require('http'); const socketIO = require('socket.io'); const app = express(); const server = http.createServer(app); const io = socketIO(server); const port = 3000; // 设置服务器端口号 // 设置静态文件目录 app.use(express.static(__dirname + '/public')); // 启动服务器 server.listen(port, () => { console.log(`Server is running on port ${port}`); }); ``` 3. 创建H5页面: - 在项目文件夹中创建一个名为`public`的文件夹。 - 在`public`文件夹中创建一个新的HTML文件(例如:`index.html`)。 - 在HTML文件中添加所需的HTML结构和样式。 4. 实现前端音频通话功能: - 在HTML文件中引入Socket.IO库: ```html <script src="/socket.io/socket.io.js"></script> ``` - 在HTML文件中添加JavaScript代码来处理音频通话逻辑: ```javascript const socket = io(); // 连接到服务器 // 获取用户媒体设备访问权限 navigator.mediaDevices.getUserMedia({ audio: true }) .then(stream => { // 创建本地音频流 const localAudio = new Audio(); localAudio.srcObject = stream; localAudio.play(); // 建立连接并发送本地音频流 socket.emit('join', stream); // 监听其他用户加入房间 socket.on('user joined', remoteStream => { // 创建远程音频流 const remoteAudio = new Audio(); remoteAudio.srcObject = remoteStream; remoteAudio.play(); }); }) .catch(error => { console.error('Error accessing media devices: ', error); }); ``` 5. 在服务器端实现音频通话功能: - 在`server.js`文件中添加以下代码来处理Socket.IO连接和音频流的传输: ```javascript // 监听Socket.IO连接 io.on('connection', socket => { console.log('A user connected'); // 监听用户加入房间事件 socket.on('join', stream => { // 广播其他用户新用户加入房间 socket.broadcast.emit('user joined', stream); }); // 监听Socket.IO断开连接事件 socket.on('disconnect', () => { console.log('A user disconnected'); }); }); ``` 6. 运行服务器: - 在项目文件夹中运行以下命令来启动Node.js服务器: ``` node server.js ``` 7. 在浏览器中访问网页: - 打开多个浏览器窗口或标签,并访问`http://localhost:3000`来查看音频通话效果。 注意:以上代码只是一个基本示例,实际应用中还需要处理更多的错误处理、房间管理、音频控制等功能。你可以根据需求和具体场景进行扩展和修改。同时,也可以参考相关的文档和示例代码来进一步了解和实践WebRTC和Node.js的音频通话功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值