再谈 Redis Presence

前言

在多处理器和/或机器上扩展服务器时, 需要为 Server 配置 Presence 选项.
Presence 的作用是允许不同进程之间的通信和数据共享, 特别是用在房间匹配上.

NodeJS

Node.js 是 Javascript 在服务端的运行环境,构建在 chrome 的 V8 引擎之上,基于事件驱动、非阻塞 I/O 模型,充分利用操作系统提供的异步 I/O 进行多任务的执行,适合于 I/O 密集型的应用场景,因为异步,程序无需阻塞等待结果返回,而是基于回调通知的机制,原本同步模式等待的时间,则可以用来处理其它任务.
Node.js 单线程只是一个 js主线程,本质上的异步操作还是由线程池完成的,node 将所有的阻塞操作都交给了内部的线程池去实现,本身只负责不断的往返调度,并没有进行真正的 I/O操作,从而实现异步非阻塞 I/O,这便是node单线程和事件驱动的精髓之处了, 不如说对于高并发的应用, 这样设计才更为合适.

Colyseus

看了上面的介绍就可以知道, Colyseus 的 Node 进程是随便开的. 也就是说, 不管这个进程 (即 Server Node) 开在单核 CPU, 多核 CPU, 不同机器之上, 都可以充分利用 CPU 的计算能力, 与其他 Node 共存. 但是, 这些 Server 如何互相联系形成集群呢? 或者说, 如何随用户规模和硬件投入自由缩放呢? Colyseus 使用内存数据库 Redis 进行 Server Nodes 的管理.

Redis

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动分区(Cluster)提供高可用性(high availability)。

  • redis是基于内存的,内存的读写速度非常快;
  • redis是单线程的,省去了很多上下文切换线程的时间;
  • redis使用多路复用技术,可以处理并发的连接。非阻塞 I/O 内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。epoll 中的读、写、关闭、连接都转化成了事件,然后利用 epoll 的多路复用特性,绝不在 I/O上浪费一点时间。

0_1647429461443_2c94231f-f844-4451-8bb7-7af9ec92c345-image.png

Colyseus 使用 Redis 保存连接到它的所有服务器 Nodes, 记录其 ID 与 IP地址 及端口号, 还有每个 Node 有多少种房间, 每种房间都开启了多少个等等, 并以这些数据为依据, 自动执行房间匹配实现负载均衡, 顺便提供多服务器进程间数据共享与订阅/发布等功能. 理论上只要 Redis 的集合里保存得下, 就能使用多少服务器构成集群.
每个 Server 的每个 Room 都可以自由访问 Presence 实例, Redis Presence API 基于 Redis API, 可以在这里找到.

Colyseus Arena

Colyseus Arena 是部署 Colyseus 服务器的最简单解决方案, 让您可以 Get To Fun Faster™

Arena Cloud 是一站式托管解决方案, 可让您专注于多人游戏开发. 它在服务器托管的基础上, 为开发人员提供企业级服务器管理, 基础设施建设, 开发运营和规模扩展等服务. 使用 Arena, 只需在直观的管理仪表板上点击几下, 即可完成 Colyseus 服务器的配置, 管理和更新.
点击这里注册 Arena Cloud 账户.
初级客户免费. 技术支持完善.

Arena Cloud 可以在全球多地使用, 并可应要求提供客户指定区域部署.


RESP.app (RDM) https://download.csdn.net/download/u011720314/82192593
Redis for Linux https://redis.io/download
Redis for Windows https://github.com/tporadowski/redis/releases
Colyseus Arena https://console.colyseus.io/register

请问 Arena Cloud 可以支持中国区部署吗?

目前没有中国区服务器, 有亚洲区的.

我colyseus的后端程序配置了presence。我在本地跑了2个服务A和B,一个服务监听端口2567,一个监听2568.
用户1连接了A服务并创建了一个房间id001,然后用户2连接B服务想加入id001的游戏房间发现加不成功,也查不到这个房间。
----这个是为什么呢?求大佬解答!
colyseus版本是:0.14.23 presence配置也正常,能在A服务存数据,然后在B服务获取数据。

@wuyongquan

用户2 故意连接 服务器A 的 房间 id001, 这种情况下请参考 这里.

@coco 感谢回答!我再想问下,要实现上面的功能一定要使用colyseus/proxy代理吗?同时我也发现colyseus/proxy是v0.10版本才出现的,那是否意味着v0.10版本之前上面提到的需求在colyseus内部实现的?v0.10 是拆开到proxy里实现?

@coco 在群里问了一个colyseus好友,他之前是不用colyseus/proxy模块也能实现上面提到的功能,但他用的是旧版本v0.9

@wuyongquan

你的好友大概是使用 Colyseus 提供的 MatchAPI 自己实现了与 Proxy 类似的功能.
请把好的 Colyseus 群分享给大家吧!

@COCO 您好
感谢您提供中文社区的指导,我按照您的指引 (https://github.com/colyseus/proxy) 设置了动态代理,但我发现动态代理中有3处设置端口的地方,我在前端发起请求的时候应该以哪个端口为准。我发现当我以8083发起的时候,仍然出现:
ServerError: seat reservation expired.的错误,
但如果以9001发起则请求不通:
POST http://127.0.0.1:9001/matchmake/joinOrCreate/game net::ERR_EMPTY_RESPONSE
以下是ecosystem.config.js的内容:

const os = require('os');
module.exports = {
    apps: [
         
        {
            port        : 8083,
            name        : "colyseus",
            script      : "lib/index.js", // your entrypoint file
            watch       : true,           // optional
            instances   : os.cpus().length,
            exec_mode   : 'fork',         // IMPORTANT: do not use cluster mode.
            env: {
                DEBUG: "colyseus:errors",
                NODE_ENV: "production",
            }
        },
        {
            port:9001,
            name        : "colyseus-proxy",
            script      : "./node_modules/@colyseus/proxy/bin/proxy",
            instances   : 1, // scale this up if the proxy becomes the bottleneck
            exec_mode   : 'cluster',
            env: {
                PORT:9001,
                REDIS_URL: "redis://省略...:6379/0"
            }
        }
    ]
}

您好,我修改了./node_modules/@colyseus/proxy/bin/proxy的代码,新增了日志打印,希望能有所参考,具体修改如下如下:
// query pre-existing nodes
discovery_1.getNodeList().
then(function (nodes) {
return nodes.forEach(function (node) { console.log("详情3",node,nodes);return register(node); });
}).
catch(function (err) { return console.error(err); });
发现getNodeList返回的数组中有个别端口为undefined
详情3 { processId: '2NOMTd-kb', address: '172.16.225.246:7001' } [
2|proxy | { processId: '2NOMTd-kb', address: '172.16.225.246:7001' },
2|proxy | { processId: 'N9tmo8HJP', address: '172.16.225.246:undefined' },
2|proxy | { processId: 'ezYAPhlUO', address: '172.16.225.246:7000' },
2|proxy | { processId: 'C7yXZ8SvL', address: '172.16.225.246:7003' },
2|proxy | { processId: 'rPS3NmHCA', address: '172.16.225.246:7001' },
2|proxy | { processId: '7K1i4pnko', address: '172.16.225.246:7002' },
2|proxy | { processId: 'JDwRKgWKY', address: '172.16.225.246:7002' },
2|proxy | { processId: '-LI0NAxC7', address: '172.16.225.246:7002' },
2|proxy | { processId: 'Av8G4ggJB', address: '172.16.225.246:7003' },
2|proxy | { processId: 'TxwZ5Vani', address: '172.16.225.246:undefined' },
2|proxy | { processId: 'x_ZlF1Cjc', address: '172.16.225.246:7001' },
2|proxy | { processId: 'fBRAR1jCx', address: '172.16.225.246:7000' }
2|proxy | ]
这导致了代理失败,因为端口号不能为undefined,具体报错如下:
2|proxy | Using proxy 1 /matchmake/joinOrCreate/game
2|proxy | RangeError [ERR_SOCKET_BAD_PORT]: Port should be >= 0 and < 65536. Received undefined.
2|proxy | at new NodeError (node:internal/errors:372:5)
2|proxy | at validatePort (node:internal/validators:217:11)
2|proxy | at lookupAndConnect (node:net:1035:5)
2|proxy | at Socket.connect (node:net:1011:5)
2|proxy | at Agent.connect [as createConnection] (node:net:203:17)
2|proxy | at Agent.createSocket (node:_http_agent:340:26)
2|proxy | at Agent.addRequest (node:_http_agent:291:10)
2|proxy | at new ClientRequest (node:_http_client:311:16)
2|proxy | at Object.request (node:http:96:10)
我使用mac os系统在本地进行多进程测试,我不知道这是否有影响,还是我必须找一个干净的容器。nodejs版本v16.15.1,其他包版本如下:
"@colyseus/proxy": "^0.12.8",
"@colyseus/redis-driver": "^0.14.22",
"colyseus": "^0.14.0",
"pm2": "^5.2.0",
"tsc": "^2.0.4"

您好, 请问没用proxy的时候一切正常吗?