后端服务器
在这部分,我们继续讨论与用户请求相关的内容。后端服务器是用来处理用户请求的具体逻辑的地方,当前端服务器接收到来自客户端的请求时,通过分析请求的路由,并做简单的校验表明路由是合法的,那么前端服务器就会根据路由策略配置,选择某一后端服务器,发起 rpc 调用。后端服务器的所有调用请求均来自前端服务器的 rpc 调用。
当后端服务器发起 filter-handler 链对前端服务器分派过来的请求进行处理时,如果仅仅需要给用户端响应,那么仅仅通过 rpc 的回调返回具体的响应即可。但是,很多情况下,具体的请求处理逻辑需要给其他用户推送消息。比如,在一个聊天应用中,当有一个用户发起聊天请求时,其聊天的所有内容都需要推送给同一房间的其他用户。当然,消息推送逻辑并不仅仅在后端服务器中使用,前端服务器也可能会有类似的场景。
CoBackendSession 组件和 CoChannel 组件一般是用在后端服务器中的,它们一起来完成给特定的用户推送消息。我们知道,BackendSession 可以看作前端原始 session 在后端服务器的一个代理,CoBackendSession 包装的 BackendSessionService 就是用来创建并管理后端的 BackendSession,并可以通过相应的 bind 以及 push 调用,可以给前端原始的 session 绑定 uid,以及设置一些属性。
CoChannel 包装的 ChannelService 中维护了 Channel 的信息,每一个 Channel 可以看作是一系列绑定用户的 uid 集合,通过 Channel 的相应调用即可向客户端推送消息。以下是对后端服务器来说,相应的类关系图:

- 后端服务器的所有请求都是从前端服务器的 rpc 请求中获得的,也就是说后端服务器的 CoServer 组件的请求是 MsgRemote 派发的;
- 当前端服务器发出 rpc 请求时,会携带用来创建 BackendSession 的信息。在后端服务器中,会创建对应的 session 信息,这个 session 就是 backendSession,对 backendSession 所做的任何更改不会影响原始的前端服务器中的 session。当遇到用户的登录请求时,可能需要给原始的 session 绑定 uid,并且设定一些自定义属性。以聊天为例,后端服务器处理登录请求时,就需要给 session 绑定 uid,并且给其设置属性 room_id 等。这些可以通过使用 BackendSession 的 bind 以及 push 操作。在后端服务器求处理链上的所有 session 参数,其类型均是 BackendSession,对其的直接修改不会直接反映到原始的前端服务器的 session 上;
- 有时候,需要对用户进行分组,以便更好地推送消息。还以聊天为例,一个聊天室的成员应分为一组,当有人说话时,直接将消息推送到这一组即可。pomelo 中的 Channel 就是应用这种场景的,每一个 Channel 中维护一个 uid 列表,当调用 Channel 的 pushMessge 方法时,会给所有的在这个 Channel 中的用户推送消息;
- ChannelService 还提供了 pushMessageByUids 方法,使得推送消息的时候,不用通过 Channel,直接传入一个用户列表即可,这样使得消息推送更加灵活。同时,ChannelService 还提供了 broadcast 方法,可以针对某一类型的前端服务器,给其所维护的所有已经绑定 uid 的 session 广播消息;
- 以上的对 BackendSession 以及 Channel 操作,无论是给 session 绑定 id,还是通过 Channel 发送消息,还是通过 ChannelService 进行广播,实际上都涉及到与客户端的通信,由于后端服务器是无法与客户端进行通信的,这些操作实际上都是对前端服务器的 rpc 调用。因为在前端服务器发起 rpc 调用给后端服务器派发请求的时候,已经携带了前端服务器 id 等信息,在 BackendSession 中会维护此 session 所在的前端服务器的 id,因此,此时后端服务器向前端服务器发起 rpc 调用时,不再需要路由计算,直接使用相应的 frontendId 即可。

- 上面的图中展示了后端服务器中的调用流程,从 MsgRemote 获得请求,然后分派给 CoServer,Server 会发起 Filter-Handler 链对用户请求进行处理,在 Filter-Handler 链中的 session 参数,均为 BackendSession。当调用了 session 的 bind,push,kick 等操作时,CoBackendSession 会向对应的前端服务器发起 rpc 调用,这个 rpc 调用由 SessionRemote 提供服务,完成对应 session 的 bind、push、unbind、kick 等操作
- 如果在 Handler-Filter 链中处理时需要给用户推送或者广播消息,就可以使用 Channel 了。可以通过 Channel 的 pushMessage 给一个 Channel 推送消息,也可以使用 ChannelService 的 pushMessageByUids。这些操作实际上也是对前端服务器的 rpc 调用,为这些操作提供 rpc 服务的是 ChannelRemote。
注意事项
- BackendSession 是前端服务器中的 session 在后端服务器中的代理,当后端服务器需要给前端的原始 session 绑定 uid 或者设置自定义属性时,需要使用调用 bind 和 push,解绑 uid 绑定使用 unbind。如果仅仅调用了 BackendSession 的 set/get,而没有调用 push 的话,那么对 BackendSession 的属性的修改,只在后端服务器的处理链中后面部分有效,而不对其他任何地方的 Session 产生影响。比如,内建的 Filter timeout,在 before filter 中,开启一个定时器,并把定时器 id 作为一个属性 set 到 BackendSession 中,这个定时器 id 属性将会在处理请求链的后面部分可以被访问,因此,在 after filter 中,就通过取得定时器 id 进行了定时器的清理工作。这种对 BackendSession 修改仅仅在后端服务器里有效,不会对前端的原始 session 造成任何影响;
- 对于前端服务器维护的 Session 信息,可以认为,一个客户端连接就对应一个 Session,Session 可以看作与客户端连接一一对应。当用户登录的时候,会使用 uid 绑定对应的 session,也可以理解为这个用户通过哪个 session 进行了登录。在 sessionService 里有选项 singleSession,如果设置为 true 的话,那就表示一个 uid 只允许一个 session 登录,当有新的 session 建立登录的时候,以前的登录会被踢掉。否则,是允许一个 uid 绑定多个 session 的,也就是说一个 uid 允许维持多个连接。这在实际中是很有意义的,比如,用户的客户端可能有多个设备,那么这样的话,多个设备就可以同时在线;
- 关于 Channel,Channel 中维护着一组 uid,每一个 uid 会对应多个 session,每个 session 由 sessionid 以及 serverid 来指定其前端的连接信息,一个 uid 可以加入多个 Channel 中。Channel 是后端服务器本地的,也就是说两个后端服务器 A 和 B 不会共享 Channel 信息,当出现跨服务器访问 Channel 的时候,会出现 Channel 找不到的错误。当确实需要进行共享 Channel 信息时,可以考虑使用 pomelo 提供的 global-channel 插件,那里使用了 redis 来维护 Channel 信息,而不再把 Channel 信息放在服务器本地,后端服务器通过 redis 即可查询 Channel 中的 uid 信息,然后就可以发起调用了。
总结
对客户端请求的处理是 pomelo 较为复杂的部分,它由 pomelo 的多个组件共同完成,前端服务器上的 CoConnector 会加载 connector 并开启请求监听,当有客户端连接的时候,其对应的连接事件会触发,从而会新的连接创建并维护 session,这些操作由 CoSession 完成。当用户请求具体的服务的时候,前端服务器的 CoServer 会完成相应的服务器路由,后端服务器的 Remote 接收到请求后完成请求派发,后端服务器的 CoServer 会启动 Filter-Handler 链对请求进行处理,当处理过程中需要给 session 设置自定义属性以及绑定 uid 时,可以通过 CoBackendSession 来完成,当需要给客户端推送消息的时候,可以使用 CoChannel 提供的功能。当用户的请求通过了 Filter-Handler 链处理后,对应的响应会通过 rpc 调用的回调,再次返回到前端服务器的 rpc 发起者 CoServer,然后 CoConnector 会将后端的响应或者后端推送的消息调度给 CoPushScheduler,由 CoPushScheduler 实现具体的消息发布调度。当可以发布消息的时候,CoPushScheduler 会通过 CoSession 获得到客户端连接的 socket,然后通过 socket 将消息发送出去,完成整个消息处理流程。如果是用户的 notify,将不会发送响应。