用pomelo做mobile app的server端, 谈谈我的想法【5.29更新】

========5.29=====
目前项目基本运行ok,对pomelo主要有以下修改或者扩展:
【pomelo】-->对pomelo框架库的修改
【project】-->自己项目上层的处理

1,【pomelo】socket.io配置为240秒一次心跳
2,【pomelo】当业务子进程挂掉的时候,自动恢复child process
3,【project】session,uid信息同步保存到mysql中,以便在多connector的时候查询到用户所处的connector,投递notify消息。放弃使用pomelo中的channel服务,调用rpcInvoke直接通过connector发送消息。
4,【project】禁止非认证route,只在connector才能route到api服务器
5,【project】对api服务器添加filter,只用通过认证的session才能调用接口

========old=====
这两天研究了一下pomelo,感觉是一个设计很好的框架. 而且发现基于nodejs socket.io方案可以很好的解决目前android客户端最麻烦的一个问题: 实时在线推送. (因为google的推送服务在国内网络不好用, google自己的推送基本是没指望了). socket.io和connector解决了长连接以及大量用户在线的问题. 当然附加的传输协议压缩也是一个亮点,比原始的http+json节约太多流量了,这点对移动端很重要.

但是这里有个问题还困扰着我,就是原有pomelo里面channel的概念.在push消息的时候,是基于channel的: ChannelService.prototype.pushMessageByUids()等.
但是channel又不是在整个服务器组中全局共享的,而是属于单个backend后台业务服务器. 那么就可能出现本来在同组(类似于qq群)中的用户被分配到了不同backend服务器中.那么push消息就会出问题.

解决的方案就是push是面向connector上的session的.换个角度来设计, 就是backend中完全没有channel. 类似于channel这种群组是存放在mysql中的.每个backend处理的时候,是和mysql这个数据共享者打交道,计算出要发送的对象userIdList, 最后再通过connector上存在的session把它们push出去.

提出这个问题的原因是,如果把pomelo设计为移动终端app的后台server.那么用户就不象游戏那种,单位时间上始终是在某个特定channel中的. 在这种设计中,用户是可以同时存在于多个channel(qq群)中的. 当然mysql不一定是最合适的群组管理者,但我觉得在这种应用中是需要一个全局的用户分组管理服务器的,因为在移动应用中,用户的身份圈子有太多种类了.

发这贴出来,希望能和大家讨论一下这种应用的可行性.

欢迎留言交流:)

标签: pomelo 移动开发
roytan 在 2013-3-31 15:28发布
roytan 在 2013-5-29 18:48重新编辑 分享到 weibo
14 回复
#1 {1} roytan 2013-3-31 16:32 回复

记录一下看pomelo代码的历程:

刚刚看了一下发msg的代码,其实最终是通过SessionService发的.
SessionService.prototype.sendMessage = function(sid, msg)

但是为什么官方api文档上面只暴露这两个kick函数呢? 很奇怪. 我开始只看文档还以为发消息必须得通过channel呢.
SessionService
kick
kickBySessionId

那如果这个sendMessage仅仅需要sid和msg就可以发消息,那问题就简单. sid和用户id绑定的吧,直接for里面循环发到目标用户idlist就行了.

问题: 这个SessionService是不是在所有frontend和backend服务器全局唯一的呢?
查到这句话"Session对象由客户端所连接的frontend服务器维护", session属于单个frontend. 但是SessionService应该是全局的吧,不然何为Service呢, 我猜应该是这样的吧.

SessionService.prototype.create = function(sid, frontendId, socket) {
var session = new Session(sid, frontendId, socket, this);
this.sessions[session.id] = session;

return session;
};
恩. 应该是这样的. 生成session的时候就加到全局sessions里面了.

  • Session service is created by session component and is only
  • <b>available</b> in frontend servers. You can access the service by
  • app.get(&apos;sessionService&apos;) in frontend servers.

这个SessionService只能在frontend调用?
也就是说backend业务处理服务器没法直接调用? 这又怎么弄呢? ............还得继续看代码.......

查看channel的代码,发现最终channel是用这种方式发调用到SessionService发的?
for(var sid in groups) {
app.rpcInvoke(sid, {namespace: namespace, service: service,
method: method, args: [route, msg, groups[sid]]}, rpcCB);
}

shanyechen 2014-7-22 17:59 回复

求解~ 3,【project】session,uid信息同步保存到mysql中,以便在多connector的时候查询到用户所处的connector,投递notify消息。放弃使用pomelo中的channel服务,调用rpcInvoke直接通过connector发送消息

这个怎么做啊~

this.app.rpcInvoke(connector.id, {namespace: '', service: 'connector',
method: 'send', args: [reqId,route, msg,recvs, opts,{isPush:true}]},function(){
next(null, {
code: 300
});
return;
}); 写成这样不保存 但是也没有发送任何消息~? 求教~^_^~

#2 roytan 2013-3-31 16:52 回复

也就是说我还只需要找到这个方法就应该能解决我的问题了.

向全局的sessions中我指定的sessionlist发送消息

但是很奇怪的是,我一直以为channel是属于单个backend服务器的, 而查看这个函数的代码的时候
ChannelService.prototype.pushMessageByUids = function(route, msg, uids, cb)发现该函数中仅仅是会把不在channel中的uids加入channel中,然后发送.
那我是不是新写一个ChannelService.prototype.pushMessageByUidsForce,其中去掉检查uids, 直接基于uids发送就解决问题了:)

这样我就不用关心uids是属于哪个backend的哪个channel的了, 直接数据库中查出来要发送的uids, 然后借用pushMessageByUidsForce就ok了.

最终结果就是,我的mobile app服务器设计里面一个channel都没有,mysql里面的共享用户群组表代替了channel.

OK,写了这么多,希望我的这套理论是正确的:) 空了来试验一下.

#3 {3} edword2012 2013-3-31 17:44 回复

channelService说直白点就是一个把用户简单分成不同组的容器, 消息的发送还是通过session由connectorServer来负责, 这样广播信息比较方便。

你说的mysql里用户群组也类似一个容器, 计算出uids后通过session来发送,其实是一样的, 没本质区别。

但是session service不是全局的, 每个frontend的session service都是独立的, 也就是说如果同一组用户分布在不同的backend server上, 广播消息需要用到rpc, 但这就牵扯到性能问题, rpc通信量过大也会造成消息堵塞, 聊天服务可以这么做, 实时类的MMO就算了

roytan 2013-3-31 21:05 回复

Ok,明白了.

那调用一下gate的dispatcher就应该可以知道具体某个uid到底分配到哪个connector上面了.然后rpc该connector发送消息应该就可以了.

pomelo里面我没看到有gate的代码,反而chat那个demo里面有.也就是说gate都是自己新写的,框架里面默认是没有gate的?

另外,问一个问题,我看到很多函数都需要提供一个route参数,这个是干什么用的? 哪里有例子吗?没怎么看懂这个route差数.

mountain 2013-4-1 08:14 回复

> 但是session service不是全局的, 每个frontend的session service都是独立的, 也就是说如果同一组用户分布在不同的backend server上

如果sessionService不是全局的话,那么像游戏里聊天的世界频道是怎么实现的,这个会同时向所有在线用户发送消息。或者是通过分别向每个channel进行广播实现?

roytan 2013-4-1 09:56 回复

@mountain 你说的很有道理啊,总应该有个全局的什么service可以给任意用户发消息吧,不管在游戏服务器或者移动app服务器上,总会有这个需求的。
感觉这个应该是框架提供的能力,而不应该让backend手动得去查询所有connector然后再发送,虽然效率上看起来差不多。但是从设计的角度,有点像一个缺失的功能点。

#4 {4} roytan 2013-3-31 21:11 回复

另外,感觉目前pomelo里面设计的C/S的通信是不是少了一种模式?
c->s: req, res
c->s: notify
s->c: push
s->c: req, res ???

有没有可能服务器向客户端发request呢, 在某些特定情况下,服务器希望客户端对req有响应包回来的? 这样才是真正的全双工:)

mountain 2013-3-31 21:15 回复

> 有没有可能服务器向客户端发request呢, 在某些特定情况下,服务器希望客户端对req有响应包回来的? 这样才是真正的全双工:)

这种应该通过push和notify的组合就可以实现吧

roytan 2013-3-31 21:32 回复

@mountain
push和notify没有在一个对话的对应关系,不便于组合绑定,我觉得.
比如jsonrpc这样, c/s应该可以双向:

--> {"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}
<-- {"jsonrpc": "2.0", "result": 19, "id": 4}

不管c还是s收到method就回result或者error.

比如目前的notify和push机制, 就不知道这个notify和push对方到底是收到没有,没有后续逻辑可以加上. 就像在游戏服务器中, 比如a->s->b, a发了一个添加好友请求, s给b发push. 但是b怎么回呢?

demon 2013-4-1 11:40 回复

@roytan 因为现在的服务全部是长连接模式,所以在连接正常的情况下,不会出现消息发送但无法收到的情况下。唯一的特例是用户网络断开,而服务端的连接还没有出现超时

关于你提到的s-c:req/res的设计,pomelo中在设计时就排除了这种情况:

  • 客户端是不可信的:由于客户端伪造成本极低,因此对于服务端来说,其前提是假定任何一个客户端都是不可信的。而c-s:req/res需要代码逻辑在客户端运行,这就会造成很严重的安全隐患。
  • 数据操作以服务端为主:由于客户端是不可信的,因此所有的数据修改和有效性判断都在服务端进行的,即使是由客户端产生的数据变化,也需要在服务端进行有效性检查,客户端不能直接修改修改数据
  • 最后一点比较重要的是,安全性问题:比如恶意客户端完全可以直接挂起所有服务端的请求,从而造成服务端的内存泄漏。

对于你提到的那种情况,a->s->b:
如果这种请求不需要b人工确认,那么实际的处理流程就变为两个独立的流程:a<->s , s->b,服务端直接可以确认请求是否合法并返回,同时给b推送一个消息。

如果需要人工确认,实际上因为人不可靠~你不会知道电脑后面的是谁。所以我们完全没必要也不能浪费一个回调的堆栈在b的可能回复上,这种情况应该采用消息模式。需要四个步骤来完成:a<->s. s->b, b->s, s->a。 首先,服务端判断请求是否合法并将结果返回给a(好友请求发送成功/失败!)。之后s会给b发送一条消息。b看到消息,在想回的时候进行回复(可能是一秒钟也可能是一个月)。服务端收到消息,返回给a。中间没有多余的资源浪费和回调造成的资源挂起。

如果我们采用req/res模式,如果b的心情不好,一个月之后回复或者根本不给回复,那么服务端的回调就会一直挂在哪里。。。 。。。 实际上,如果使用req/res,任何服务端发给客户端的消息都可能会产生这个问题!

roytan 2013-4-1 11:50 回复

@demon 嗯,从安全和性能角度上面来讲,确实是有这个问题。我想得太简单了。

#5 {6} mountain 2013-3-31 21:13 回复

route这个是路由功能 是中软的负载均衡,目前只针对connector进行负载均衡。gate不是一种标配
,只不过是在chat的demo里体现了一下。

module.exports.dispatch = function(uid, connectors) {
var index = Math.abs(crc.crc32(uid)) % connectors.length;
return connectors[index];

};
还有你说的

> 然后rpc该connector发送消息应该就可以了.

这种应该是服务之间相互调用才会使用rpc的功能,connector是直接暴露给客户端的,好像目前不支持客户端直接rpc到相应的服务吧?

roytan 2013-3-31 21:17 回复

我的意思是backend业务服务器通过rpc调用connector服务器发送消息给客户端.
服务器组内部的rpc.

roytan 2013-3-31 21:23 回复

能举个例子怎么用route参数吗?
比如我有c1, c2, b1, b2, b3这样5个服务器. 然后b3最终想要通过c1给user-x, c2给user-y发送消息.

这个route是不是"Pomelo 通讯协议"文档里面讲的那个route? 但感觉"Pomelo 通讯协议"讲的是c和s之间的通讯协议啊, pomelo内部服务器也是用这个通信?

mountain 2013-3-31 21:27 回复

@roytan 我不知道你说的

> "Pomelo 通讯协议"文档里面讲的那个route

这个路由规则可以自定义的。具体就是前面那段代码,路由也没有那么小的粒度,只针相应的服务器路由,你说的粒度太细了。应该也可以做到

roytan 2013-3-31 21:42 回复

@mountain
route应该就是这个吧, 大概看明白了:
route字段分析
pomelo中的route是用来确定消息的分发路径,将其交给相应的服务器和服务处理的。route分为两类,由客户端发给服务端消息时使用的route和服务端向客户端广播时使用的route。
前一种route是由服务器自动生成的,其中的字段就代表了对应的方法在服务端的位置。如“area.playerHandeler.attack”则表示在“area”服务器上的“playerHandler”接口中提供的“attack”方法。
后一种route是服务端想客户端推送消息时使用,如“onMove”,“onAttack”等,这些字段是由用户自己定义的。 在一般的web应用等带宽不敏感的环境中,route字段的开销是可以接受的。而在一些移动应用中,带宽~money的情况下,精简route字段就变得有必要了。

demon 2013-4-1 10:58 回复

@roytan 基于route的消息分配要结合路由算法使用的,route可以保证你的请求可以到达正确的服务器类型,但是对于同种服务器则无法区分,一般说是随机到某台服务器的。

如果需要在同种服务器之间区分,则需要设置具体的路由算法。比如lord中针对多场景的路由,就是通过计算session里面对应的areaId路由到正确的服务器上的。设置路由可以参考:
https://github.com/NetEase/lordofpomelo/blob/master/game-server/app.js#L53

app.route('area', routeUtil.area)的第一个参数表示需要路由的服务器类型,第二个参数则表示具体的路由函数。

edword2012 2013-4-1 22:27 回复

@roytan 我觉得严格说路由只有一种, 就是服务器之间的rpc调用才会用到路由。 比如app.rpc.auth.authRemote.auth, 如果在app里设置了针对auth服务器类型的route算法就会调用该算法求出要请求的服务器host, port,然后调用pomelo-rpc模块进行通信。route只针对服务器之间通信, 客户端只能连接到一台frontend服务器, 然后根据route算法或者用户信息(比如在那个area里)等有route算法决定把客户端rpc到那台backend来进行处理,backend处理完在通过froutend发送过来的session通过rpc调用把消息发送到用户连接的froutend服务器上, 再由frontend服务器(connector)发送给客户端, 这是我的理解, 至于服务端发送的onMove, onAttack我觉得这应该是socket.io的通信协议, 对socket.io了解不多, 也不是很确定。因为frontend服务端发送消息给客户端根本不需要路由, 一对一,再怎么路由也是一对一 :)。。

#6 {5} xiecc 2013-4-1 10:15 回复

嗯, 我们计划在0.4版开发基于redis的channel和session, 跟你说的mysql有些类似

roytan 2013-4-1 11:09 回复

能确认一下sessionService到底是全局还是单个frontend服务器的吗? 上面讨论的时候,大家看法有点不一样啊。

xiecc 2013-4-1 15:25 回复

@roytan 目前的SessionService都是在单个frontend服务器上的。
如果需要全局广播消息,可以使用: channelService.broadcast接口,它会对每台connector上绑定的所有Session推送,它还支持filter参数,可以通过函数过滤出想要广播的session。

如果要给给全局的某个频道的用户发消息, 现在的做法是建立一台全局的channel服务器,所有用户都在这台channel服务器, 然后通过这台channel服务器给客户端推消息。

到0.4版我们可以支持将session和channel存到redis, 这样就可以通过全局的channel和session数据推消息了。

mountain 2013-4-1 16:08 回复

@xiecc 能透露下0.4什么时候发布吗

xiecc 2013-4-1 17:18 回复

@mountain 还在整理需求中

MissLee 3-17 16:50 回复

@xiecc 找到答案了

#7 {8} xuanye 2013-4-10 20:51 回复

什么手机应用为啥要做长链呢?聊天的?

roytan 2013-4-12 11:47 回复

嗯。主要是解决现在手机应用都是http+json这种单向短链接的问题。聊天类应用实时性要求较高。

xuanye 2013-4-16 14:01 回复

这样做推送的话还是个问题,特别是待机的时候 ,不知道pomelo是否可以设置心跳包的间隙,频繁的心跳 对电量和流量都是比较大的浪费

roytan 2013-4-16 18:08 回复

@xuanye 我也在查这个问题,不过这个好像是socket.io的问题了。我看pomelo框架没有特别设置socket.io 我想可能得自己改框架,设置socket.io的心跳。
而且,我觉得对于android和ios都是tcp的长连接,是不是没有必要这个心跳了呢?没有细看android和java的代码,不知道它们是长连接还是long pulling?

halfblood 2013-4-16 18:37 回复

@xuanye 长连接是必须的,不过推荐使用mqtt这个协议,该协议本身就很适用于移动端,耗电和流量做了不少优化。心跳可以设计成动态心跳,当网络稳定时候延长心跳间隔,减少耗电和流量。

halfblood 2013-4-16 18:48 回复

@roytan socket.io默认的心跳时间是1分钟,目前android和ios用的是socket长连接,有心跳的,客户端在初次连接到服务端的时候会有个握手请求,该请求负责和服务端进行协商,包括心跳时间,是否采用protobuf和数据字典等,之后客户端按照该协商的结果进行工作。
不过老版本的客户端用的是socket.io,而socket.io本身就已经支持心跳了。

halfblood 2013-4-16 18:49 回复

@xuanye pomelo框架支持应用层设置心跳时间。

roytan 2013-4-16 19:21 回复

@halfblood 应用层哪里设置?我就是找了半天未果,最后直接去改了pomelo底层调用socketio那块。

xuanye 2013-4-19 20:22 回复

@halfblood 好的,有空去看看,还没做到手机那块呢,还在做winform端

#8 roytan 2013-4-16 18:37 回复

我直接把pomelo里面sioconnector.js里面设置
this.wsocket.set('heartbeat timeout', 3600);
this.wsocket.set('heartbeat interval', 3000);
貌似ok。
这样一个小时才心跳一次。不知道对于整体框架有没有影响。

#9 {2} halfblood 2013-4-16 19:48 回复

原生socket的协议,pomelo是支持设置心跳时间的。https://github.com/NetEase/lordofpomelo/blob/master/game-server/app.js 里面app.set('connectorConfig',
{
connector : pomelo.connectors.hybridconnector,
heartbeat : 3,
useDict : true,
useProtobuf : true,
handshake : function(msg, cb){
cb(null, {});
}
});

roytan 2013-4-17 09:48 回复

一直没看明白这个hybridconnector到底是什么东东?
websocket?原生socket?我看到里面既弄了一个tcp server,又绑定了一个websocket。

我测试过这样设置了以后,android和ios的pomelo客户端都连接不上了。。。。

估计得你们的原生socket的android和ios版本发出来以后才可以用这个配置吧?

halfblood 2013-4-17 09:52 回复

@roytan 是的,这个适用于浏览器端的websocket协议,如果你用的是ios和android客户端,那就要等支持原生socket的客户端发布之后才能使用。

#10 {1} Sandrawan 2013-4-19 09:54 回复

有人能回答我 我按照教程搭建了一个game server 和 web server, 为什么点击Test Game Server 没
反应呀!

tengchuan 2013-5-7 10:33 回复

你两个服务器都启动没的?

#11 Sandrawan 2013-4-19 09:55 回复

我用的是windows 2008 server 版

#12 {2} w3hacker 2014-1-22 12:21 回复

终于看完了 没啥有用的

tellyounews 2014-5-6 23:04 回复

嘎嘎嘎嘎啊啊啊啊啊啊啊啊啊啊

w3hacker 2014-5-7 11:22 回复

@tellyounews 走火入魔了?

#13 liusansheng 2014-6-26 16:01 回复

楼主知道webscokit怎么实现吗?

#14 423230557 2015-8-21 23:21 回复

求解 稳定 扩展 性能 如何?

回到顶部