实时消息推送系统

背景

我主要负责设计和实现消息实时推送系统

这块使用的是基于Tornado内置的异步websocket模块,这里异步指的是websocket内置方法调用是异步的,比如onopen、onmessage、onclose等等

这个系统的目的是把用户在平台上操作的结果推送给用户,比如审核结果、充值结果等等,或者是将一些平台的活动信息、或者是通告以广播的形式推送给平台所有用户。

websocket认证

您知道单台服务器最多只能维持65535(端口号数量)个websocket的长连接,这也是websocket和普通http请求最大的区别,它需要维持住链接有效性,所以我们需要认证系统来规避无效链接,这里使用的是JWT,当客户端发起websocket请求时,会通过url携带一个token,后端异步装饰器进行验证,没有携带token或者token不合法的都会拒绝websocket链接,也就是说消息只会推送给已登录并且在线的用户,平台每天最高有3w个左右的websocket链接。

WebSocket与HTTP的关系

相比HTTP长连接,WebSocket有以下特点:

1)是真正的全双工方式,建立连接后客户端与服务器端是完全平等的,可以互相主动请求。而HTTP长连接基于HTTP,是传统的客户端对服务器发起请求的模式。 2)HTTP长连接中,每次数据交换除了真正的数据部分外,服务器和客户端还要大量交换HTTP header,信息交换效率很低。Websocket协议通过第一个request建立了TCP连接之后,之后交换的数据都不需要发送 HTTP header就能交换数据,这显然和原有的HTTP协议有区别所以它需要对服务器和客户端都进行升级才能实现(主流浏览器都已支持HTML5)。此外还有 multiplexing、不同的URL可以复用同一个WebSocket连接等功能。这些都是HTTP长连接不能做到的。

链接容器

在websocket链接存储方案里,我选择hash(散列表)的形式来存储,key设置为当前登录用户的uid,因为这种数据结构比传统的层级结构更容易定位到具体的用户是否在线,并且可以极其快速的通过key获取到在线用户的链接对象,它所有操作的时间复杂度都是O1,和线性结构比,它有效保证了信息的实时性,但同时,hash也有一定的缺陷,那就是它的系统资源占用要比线性结构高,可以理解为一种用空间换时间的方案。

精准推送和广播

功能上主要是精准推送和广播,精准推送主要通过异步鉴权装饰器返回的用户对象uid,来对websockt链接容器中的用户进行send_message操作,这里主要的问题是消息的状态,也就是已读和未读,我使用redsi中hash来存储历史消息,如果客户端用户的消息弹窗触发了确定事件,或者在消息列表中触发了查阅事件,我们都会将消息状态修改为已读,已读消息不会针对该用户重复推送,当用户触发了websocket的onopen方法,我们会将未读的历史消息针对该用户进行推送。

广播功能并不需要针对用户uid进行推送,但是涉及一个效率问题,如果是3w个链接,遍历hash可以进行批量推送,但会有一定的延时,并非是真实的实时推送,所以采用的是协程异步推送的方式,提高了广播的推送效率。

微服务rpc

后期我将消息推送系统独立了出来,单独维护一个tornado事件循环服务,之后所有需要消息推送服务的系统只要接入这个独立的微服务即可。

当然,要想其他服务调用微服务推送系统的方法,最先想到的就是通过HTTP请求实现。是的,这是很常见的,例如服务B暴露Restful接口,然后让服务A调用它的接口。基于Restful的调用方式因为可读性好(服务B暴露出的是Restful接口,可读性当然好)

然而,基于Restful的远程过程调用有着明显的缺点,主要是效率低、封装调用复杂。当存在大量的服务间调用时,这些缺点变得更为突出。

服务A调用服务B的过程是应用间的内部过程,牺牲可读性提升效率、易用性是可取的。基于这种思路,RPC产生了。

通常,RPC要求在调用方中放置被调用的方法的接口。调用方只要调用了这些接口,就相当于调用了被调用方的实际方法,十分易用。于是,调用方可以像调用内部接口一样调用远程的方法,而不用封装参数名和参数值等操作。

这里我使用的是Jsonrpc,调用方式全部通过json数据传输,并且可以嵌入到Tornado的事件循环中,非常方便。

可能遇到的问题

目前这套系统单机可以维护3w左右的实时推送链接,但在不久的将来,肯定需要进行server端的扩容操作,当架构搭建好之后,只需要加一台服务器即可,nginx修改后台监听逻辑,但是需要注意的是,反向代理策略一定得选择ip-hash的模式,确保客户端和服务端链接绑定。

一台以上服务器怎么链接

使用哈希取模算法

取模算法hash(key)% N,即:对 key 进行 hash 运算后取模,N 是机器的数量;

这样,对 key 进行 hash 后的结果对 3 取模,得到的结果一定是 0、1 或者 2,正好对应服务器node0、node1、node2,存取数据直接找对应的服务器即可,简单粗暴,完全可以解决上述的问题

心跳检测

websocket是前后端交互的长连接,前后端也都可能因为一些情况导致连接失效并且相互之间没有反馈提醒。因此为了保证连接的可持续性和稳定性,websocket心跳重连就应运而生。

在使用原生websocket的时候,如果设备网络断开,不会立刻触发websocket的任何事件,前端也就无法得知当前连接是否已经断开。这个时候如果调用websocket.send方法,浏览器才会发现链接断开了,便会立刻或者一定短时间后(不同浏览器或者浏览器版本可能表现不同)触发onclose函数。

后端websocket服务也可能出现异常,造成连接断开,这时前端也并没有收到断开通知,因此需要前端定时发送心跳消息ping,后端收到ping类型的消息,立马返回pong消息,告知前端连接正常。如果一定时间没收到pong消息,就说明连接不正常,前端便会执行重连。

results matching ""

    No results matching ""