Worktile中百万级实时消息推送服务的实现
发表于 11:43|
来源伯乐在线|
作者Worktile研发团队
摘要:相较于手机端的消息推送(一般都是以Socket方式实现),WEB端是基于HTTP协议,很难像TCP一样保持长连接。但随着技术的发展,出现了WebSocket,Comet等新的技术可以达到类似长连接的效果。
在团队协同工具
的使用过程中,你会发现无论是右上角的消息通知,还是在任务面板中拖动任务,还有用户的在线状态,都是实时刷新。Worktile中的推送服务是采用的是基于XMPP协议、Erlang语言实现的Ejabberd,并在其源码基础上,结合我们的业务,对源码作了修改以适配我们自身的需求。另外,基于AMQP协议也可以作为实时消息推送的一种选择,踢踢网就是采用
RabbitMQ+STOMP协议实现的消息推送服务。本文将结合我在Worktile和踢踢网的项目实践,介绍下消息推送服务的具体实现。
实时推送的几种实现方式
相较于手机端的消息推送(一般都是以Socket方式实现),WEB端是基于HTTP协议,很难像TCP一样保持长连接。但随着技术的发展,出现了WebSocket,Comet等新的技术可以达到类似长连接的效果,这些技术大体可分为以下几类:
1)短轮询。页面端通过JS定时异步刷新,这种方式实时效果较差。
2)长轮询。页面端通过JS异步请求服务端,服务端在接收到请求后,如果该次请求没有数据,则挂起这次请求,直到有数据到达或时间片(服务端设定)到,则返回本次请求,客户端接着下一次请求。示例如下:
3)WebSocket。浏览器通过WebSocket协议连接服务端,实现了浏览器和服务器端的全双工通信。需要服务端和浏览器都支持WebSocket协议。
以上几种方式中,方式1实现较简单,但效率和实时效果较差。方式2对服务端实现的要求比较高,尤其是并发量大的情况下,对服务端的压力很大。方式3效率较高,但对较低版本的浏览器不支持,另外服务端也需要有支持WebSocket的实现。Worktile的WEB端实时消息推送,采用的是XMPP扩展协议XEP-0124
),本质是采用方式2长轮询的方式。踢踢网则采用了WebSocket连接RabbitMQ的方式实现,下面我会具体介绍如何用这两种方式实现Server
运行时环境准备
服务端的实现中,无论采用Ejabberd还是RabbitMQ,都是基于Erlang语言开发的,所以必须***Erlang运行时环境。Erlang是一种函数式语言,具有容错、高并发的特点,借助OTP的函数库,很容易构建一个健壮的分布式系统。目前,基于Erlang开发的产品有,数据库方面:Riak(Dynamo实现)、CouchDB,
Webserver方面:Cowboy、Mochiweb, 消息中间件有RabbitMQ等。对于服务端程序员来说,Erlang提供的高并发、容错、热部署等特性是其他语言无法达到的。无论在实时通信还是在游戏程序中,用Erlang可以很容易为每一个上线用户创建一个对应的Process,对一台4核8个G的服务器来说,承载上百万个这样的Process是非常轻松的事。下图是Erlang程序发起Process的一般性示意图:
如图所示,Session Manager(or Gateway)负责为每个用户(UID)创建相对应的Process, 并把这个对应关系(MAP)存放到数据表中。每个Process则对应用户数据,并且他们之间可以相互发送消息。Erlang的优势就是在内存足够的情况下创建上百万个这样的Process,而且它的创建和销毁比J***A的Thread要轻量的多,两者不是一个数量级的。
好了,我们现在开始着手Erlang环境的搭建(实验的系统为Ubuntu&12.04, 4核8个G内存):
1、依赖库***
sudo apt-get install build-essential
sudo apt-get install libncurses5-dev
sudo apt-get install libssl-dev libyaml-dev
sudo apt-get install m4
sudo apt-get install unixodbc unixodbc-dev
sudo apt-get install freeglut3-dev libwxgtk2.8-dev
sudo apt-get install xsltproc
sudo apt-get install fop tk8.5 libxml2-utils
2、官网下载OTP源码包(
), 解压并***:
tar zxvf otpsrcR16B01.tar.gz
cd otpsrcR16B01
make & make install
至此,erlang运行环境就完成了。下面将分别介绍rabbitmq和ejabberd构建实时消息服务。
基于RabbitMQ的实时消息服务
RabbitMQ是在业界广泛应用的消息中间件,也是对AMQP协议实现最好的一种中间件。AMQP协议中定义了Producer、 Consumer、MessageQueue、Exchange、Binding、Virtual
Host等实体,他们的关系如下图所示:
消息发布者(Producer)连接交换器(Exchange), 交换器和消息队列(Message Queue)通过KEY进行Binding,Binding是根据Exchange的类型(分为Fanout、Direct、Topic、Header)分别对消息作不同形式的派发。Message
Queue又分为Durable、Temporary、Auto-Delete三种类型,Durable Queue是持久化队列,不会因为服务ShutDown而消失,Temporary
Queue则服务重启后会消失,Auto-Delete则是在没有Consumer连接时自动删除。另外RabbitMQ有很多第三方插件,可以基于AMQP协议基础之上做出很多扩展的应用。下面我们将介绍WEB
STOMP插件构建基于AMQP之上的STOMP文本协议,通过浏览器WebSocket达到实时的消息传输。系统的结构如图:
如图所示,WEB端我们使用STOMP.JS和SockJS.JS与RabbitMQ的WEB STOMP Plugin通信,手机端可以用STOMPj,
Gozirra(Android)或者Objc-STOMP(IOS)通过STOMP协议与RabbitMQ收发消息。因为我们是实时消息系统通常都是要与已有的用户系统结合,RabbitMQ可以通过第三方插件RabbitMQ-AYTH-Backend-HTTP来适配已有的用户系统,这个插件可以通过HTTP接口完成用户连接时的认证过程。当然,认证方式还有LDAP等其他方式。下面介绍具体步骤:
)下载最新版本的源码包,解压并***:
tar zxf rabbitmq-server-x.x.x.tar.gz
cd rabbitmq-server-x.x.x
make & make install为RabbitMQ***WEB-STOMP插件
cd /path/to/your/rabbitmq
./sbin/rabbitmq-plugins enable rabbitmq_web_stomp
./sbin/rabbitmq-plugins enable rabbitmq_web_stomp_examples
./sbin/rabbitmqctl stop
./sbin/rabbitmqctl start
./sbin/rabbitmqctl status
将会显示下图所示的运行的插件列表
***用户授权插件
cd /path/to/your/rabbitmq/plugins
wget &a href="/community-plugins/v3.3.x/rabbitmq_auth_backend_http-3.3.x-e7ac6289.ez"&/community-plugins/v3.3.x/rabbitmq_auth_backend_http-3.3.x-e7ac6289.ez&/a&
./sbin/rabbitmq-plugins enable rabbitmq_auth_backend_http编辑RabbitMQ.Config文件(默认存放于/ECT/RabbitMQ/下),添加:
{rabbit, [{auth_backends, [rabbit_auth_backend_http]}]},
{rabbitmq_auth_backend_http,
[{user_path, “http://your-server/auth/user”},
{vhost_path, “http://your-server/auth/vhost”},
{resource_path, “http://your-server/auth/resource”}
其中,User_Path是根据用户名密码进行校验,VHOST_Path是校验是否有权限访问VHOST, Resource_Path是校验用户对传入的Exchange、Queue是否有权限。我下面的代码是用Node.js实现的这三个接口的示例:
var express = require('express');
var app = express();
app.get('/auth/user', function(req, res){
var name = req.query.
var pass = req.query.
console.log("name : " + name + ", pass : " + pass);
if(name === 'guest' && pass === "guest"){
console.log("allow");
res.send("allow");
res.send('deny');
app.get('/auth/vhost', function(req, res){
console.log("/auth/vhost");
res.send("allow");
app.get('/auth/resource', function(req, res){
console.log("/auth/resource");
res.send("allow");
app.listen(3000);浏览器端JS实现,示例代码如下:
var ws = new SockJS('http://' + window.location.hostname + ':15674/stomp');
var client = Stomp.over(ws);
// SockJS does not support heart-beat: disable heart-beats
client.heartbeat.outgoing = 0;
client.heartbeat.incoming = 0;
client.debug = pipe('#second');
var print_first = pipe('#first', function(data) {
client.send('/exchange/feed/user_x', {"content-type":"text/plain"}, data);
var on_connect = function(x) {
id = client.subscribe("/exchange/feed/user_x", function(d) {
print_first(d.body);
var on_error = function() {
console.log('error');
client.connect('guest1', 'guest1', on_connect, on_error, '/');
需要说明的时,在这里我们首先要在RabbitMQ实例中创建Feed这个Exchange,我们用STOMP.JS连接成功后,根据当前登陆用户的ID(user_x)绑定到这个Exchange,即Subscribe(“/exchange/feed/user_x”,
…) 这个操作的行为,这样在向RabbitMQ中Feed Exchange发送消息并指定用户ID(user_x)为KEY,页面端就会通过WEB
Socket实时接收到这条消息。
到目前为止,基于RabbitMQ+STOMP实现WEB端消息推送就已经完成,其中很多的细节需要小伙伴们亲自去实践了,这里就不多说了。实践过程中可以参照官方文档:
以上的实现是我本人在踢踢网时采用的方式,下面接着介绍一下现在在Worktile中如何通过Ejabberd实现消息推送。
基于Ejabberd的实时消息推送
与RabbitMQ不同,Ejabberd是XMPP协议的一种实现,与AMQP相比,XMPP广泛应用于即时通信领域。XMPP协议的实现有很多种,比如J***A的OpenFire,但相较其他实现,Ejabberd的并发性能无疑使最优秀的。XMPP协议的前身是Jabber协议,早期的Jabber协议主要包括在线状态(Presence)、好友花名册(Roster)、IQ(Info/Query)几个部分。现在Jabber已经成为RFC的官方标准,如RFC2799,RFC4622,RFC6121,以及XMPP的扩展协议(XEP)。Worktile
Web端的消息提醒功能就是基于XEP-0124、XEP-0206定义的BOSH扩展协议。
由于自身业务的需要,我们对Ejabberd的用户认证和好友列表模块的源码进行修改,通过Redis保存用户的在线状态,而不是Mnesia和MySQL。另外好友这块我们是从已有的数据库中(MongoDB)中获取项目或团队的成员。Web端通过Strophe.JS来连接(HTTP-BIND),Strophe.JS可以以长轮询和WebSocket两种方式来连接,由于Ejabberd还没有好的WebSocket的实现,就采用了BOSH的方式模拟长连接。整个系统的结构如下:
Web端用Strophe.JS通过HTTP-BIND进行连接Nginx代理,Nginx反向代理EjabberdCluster。iOS用XMPP-FramWork连接,
Android可以用Smack直接连Ejabberd服务器集群。这些都是现有的库,无需对Client进行开发。在线状态根据用户UID作为KEY定义了在线、离线、忙等状态存放于Redis中。好友列表从MongoDB的Project表中获取。用户认证直接修改了Ejabberd_Auth_Internal.erl文件,通过MongoDB驱动连接用户库,在线状态等功能是新加了模块,其部分代码如下:
-module(wt_mod_proj).
-behaviour(gen_mod).
-behaviour(gen_server).
-include("ejabberd.hrl").
-include("logger.hrl").
-include("jlib.hrl").
-define(SUPERVISOR, ejabberd_sup).
-define(ONLINE, 1).
-define(OFFLINE, 0).
-define(BUSY, 2).
-define(LE***E, 3).
-export([start_link/2, get_proj_online_users/2]).
%% gen_mod callbacks
-export([start/2, stop/1]).
%% gen_server callbacks
-export([init/1, terminate/2, handle_call/3, handle_cast/2, handle_info/2, code_change/3]).
%% Hook callbacks
-export([user_available/1, unset_presence/3, set_presence/4]).
-export([get_redis/1, remove_online_user/3, append_online_user/3]).
-record(state,{host = &&""&&, server_host, rconn, mconn}).
start_link(Host, Opts) -&
Proc = gen_mod:get_module_proc(Host, ?MODULE),
gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
user_available(New) -&
LUser = New#jid.luser, LServer = New#jid.lserver,
Proc = gen_mod:get_module_proc(LServer, ?MODULE),
gen_server:cast(Proc, {user_available, LUser, LServer}).
append_online_user(Uid, Proj, Host) -&
Proc = gen_mod:get_module_proc(Host, ?MODULE),
gen_server:call(Proc, {append_online_user, Uid, Proj}).
remove_online_user(Uid, Proj, Host) -&
Proc = gen_mod:get_module_proc(Host, ?MODULE),
gen_server:call(Proc, {remove_online_user, Uid, Proj}).
set_presence(User, Server, Resource, Packet) -&
Proc = gen_mod:get_module_proc(Server, ?MODULE),
gen_server:cast(Proc, {set_presence, User, Server, Resource, Packet}).
start(Host, Opts) -&
Proc = gen_mod:get_module_proc(Host, ?MODULE),
ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
transient, 2000, worker, [?MODULE]},
supervisor:start_child(?SUPERVISOR, ChildSpec).
stop(Host) -&
Proc = gen_mod:get_module_proc(Host, ?MODULE),
gen_server:call(Proc, stop),
supervisor:delete_child(?SUPERVISOR, Proc).
init([Host, Opts]) -&
MyHost = gen_mod:get_opt_host(Host, Opts, &&"wtmuc.@HOST@"&&),
RedisHost = gen_mod:get_opt(redis_host, Opts, fun(B) -& B end,?REDIS_HOST),
RedisPort = gen_mod:get_opt(redis_port, Opts, fun(I) when is_integer(I), I&0 -& I end, ?REDIS_PORT),
ejabberd_hooks:add(set_presence_hook, Host, ?MODULE, set_presence, 100),
ejabberd_hooks:add(user_available_hook, Host, ?MODULE, user_available, 50),
ejabberd_hooks:add(sm_remove_connection_hook, Host, ?MODULE, unset_presence, 50),
MongoHost = gen_mod:get_opt(mongo_host, Opts, fun(B) -& binary_to_list(B) end, ?MONGO_HOST),
MongoPort = gen_mod:get_opt(mongo_port, Opts, fun(I) when is_integer(I), I&0 -& I end, ?MONGO_PORT),
{ok, Mongo} = mongo_connection:start_link({MongoHost, MongoPort}),
C = c(RedisHost, RedisPort),
ejabberd_router:register_route(MyHost), {ok, #state{host = Host, server_host = MyHost, rconn = C, mconn = Mongo}}.
terminate(_Reason, #state{host = Host, rconn = C, mconn = Mongo}) -&
ejabberd_hooks:delete(set_presence_hook, Host, ?MODULE, set_presence, 100),
ejabberd_hooks:delete(user_available_hook, Host, ?MODULE, user_available, 50),
ejabberd_hooks:delete(unset_presence_hook, Host, ?MODULE, unset_presence, 50),
eredis:stop(C),
handle_call({append_online_user, Uid, ProjId}, _From, State) -&
C = State#state.rconn,
Key = &&!--?PRE_RPOJ_ONLINE_USERS /binary, ProjId/binary--&&,
Resp = eredis:q(C, ["SADD", Key, Uid]),
{reply, Resp, State};
handle_call({remove_online_user, Uid, ProjId}, _From, State) -&
handle_call({get_proj_online_users, ProjId}, _From, State) -&
handle_cast({set_presence, User, Server, Resource, Packet}, #state{mconn = Mongo} = State) -&
C = State#state.rconn,
Key = &&!--?USER_PRESENCE /binary, User/binary--&&,
Pids = get_user_projs(User, Mongo),
Cmd = get_proj_key(Pids, ["SUNION"]),
case xml:get_subtag_cdata(Packet, &&"show"&&) of
&&"away"&& -&
eredis:q(C, ["SET", Key, ?LE***E]);
&&"offline"&& -&
handle_cast(_Msg, State) -& {noreply, State}.
handle_info({route, From, To, Packet}, #state{host = Host, server_host = MyHost, rconn = RedisConn, mconn = Mongo} = State) -&
case catch do_route(Host, MyHost, From, To, Packet, RedisConn, Mongo) of
{'EXIT', Reason} -&
?ERROR_MSG("~p", [Reason]);
{noreply, State};
handle_info(_Info, State) -& {noreply, State}.
code_change(_OldVsn, State, _Extra) -& {ok, State}.
其中,User\_Available\_HOOK和SM\_Remove\_Connection\_HOOK 就是用户上线和用户断开连接触发的事件,Ejabberd
中正是由于这些HOOK,才能很容易扩展功能。
在用Tsung对Ejabberd进行压力测试,测试机器为4核心8G内存的普通PC,以3台客户机模拟用户登录、设置在线状态、发送一条文本消息、关闭连接操作,在同时在线达到30w时,CPU占用不到3%,内存大概到3个G左右,随着用户数增多,主要内存的损耗较大。由于压力测试比较耗时,再等到有时间的时候,会在做一些更深入的测试。
免费订阅“CSDN云计算(左)和CSDN大数据(右)”微信公众号,实时掌握第一手云中消息,了解最新的大数据进展!
CSDN发布虚拟化、Docker、OpenStack、CloudStack、数据中心等相关云计算资讯, & & 分享Hadoop、Spark、NoSQL/NewSQL、HBase、Impala、内存计算、流计算、机器学习和智能算法等相关大数据观点,提供云计算和大数据技术、平台、实践和产业信息等服务。 & & & &
推荐阅读相关主题:
CSDN官方微信
扫描二维码,向CSDN吐槽
微信号:CSDNnews
相关热门文章& 通常情况下,打开网页或app去查询或者刷新时,客户端向服务器发出请求然后返回数据,客户端与服务端对应的模式是: 客户端请求--服务端响应, 而在有些情况下,服务端会主动推送一些信息到客户端,例如:新闻的订阅,天气的提醒等等,那么在这样的模式下,会有些问题值得思考:
1.应用服务器如何确定每一个应用所在的设备
2.服务端把消息推到哪,客户端又不像服务器有一个固定的地址
服务端主动推送到客户端是怎么一个过程?
结合一个实际问题分析下:
问题提出: 外卖app,&商家在商家后台需要实时的获取到有没有新订单,有的话是几个;这个需求类似与日常中使用QQ或者微信时的新信息提醒一样,只要有新信息就需要提醒
最近工作中遇到一个场景,商家在商家后台需要实时的获取到有没有新订单,有的话是几个;这个需求类似与日常中使用QQ或者微信时的新信息提醒一样,只要有新信息就需要提醒;商家基本在PC上使用,各式浏览器都有:如 IE系列(7.0,8.0,9.0及以上),chrome内核,firefox等;功能所属的部署在Tomcat 6.0上,如果技术需要可以部署到 Tomcat 7.0上;
我们先做做技术调研,这种浏览器与服务器实时通信的方式有哪些方式。
这是我们最自然想到的。 采用常规AJAX轮询的方式,每10s或者30s轮询一次,既可以判断出有有多少个新订单进入,且这种时间间隔对于消息提醒也是可以接受的。这种技术方式实现起来非常简单,目前的机器都是可以机器的,前端浏览器也都支持。
但是这种方式会有非常严重的问题,就是需要不断的向服务器发送消息询问,如果有1w个商家打开了浏览器,采用10s轮询的方式,则服务器则会承担1000 的QPS,这1w个商家可能只有10个有订单通知;这种方式会对服务器造成极大的性能浪费。
还有一个类似的轮询是使用JSONP跨域请求的方式轮询,在实现起来有差别,但基本原理都是相同的,都是客户端不断的向服务器发起请求。
实现简单。
这是通过模拟服务器发起的通信,不是实时通信,不顾及应用的状态改变而盲目检查更新,导致服务器资源的浪费,且会加重网络负载,拖累服务器。
Comet是一种用于Web的推送技术,能使服务器实时地将更新的信息传送到客户端,而无须客户端发出请求,目前有两种实现方式:
长轮询(long polling)
长轮询 (long polling) 是在打开一条连接以后保持,等待服务器推送来数据再关闭,可以采用HTTP长轮询和XHR长轮询两种方式。
HTTP 和JSONP方式的长轮询
把 script 标签附加到页面上以让脚本执行。服务器会挂起连接直到有事件发生,接着把脚本内容发送回浏览器,然后重新打开另一个 script 标签来获取下一个事件,从而实现长轮询的模型。
这种方式是使用比较多的长轮询模式。
客户端打开一个到服务器端的 AJAX 请求然后等待响应;服务器端需要一些特定的功能来允许请求被挂起,只要一有事件发生,服务器端就会在挂起的请求中送回响应并关闭该请求。客户端 JavaScript 响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接;如此循环。
现在浏览器已经支持CROS的跨域方式请求,因此HTTP和JSONP的长轮询方式是慢慢被淘汰的一种技术,建议采用XHR长轮询。
长轮询优缺点
客户端很容易实现良好的错误处理系统和超时管理,实现成本与Ajax轮询的方式类似。
需要服务器端有特殊的功能来临时挂起连接。当客户端发起的连接较多时,服务器端会长期保持多个连接,具有一定的风险。
iframe 是很早就存在的一种 HTML 标记, 通过在 HTML 页面里嵌入一个隐?帧,然后将这个隐?帧的 SRC 属性设为对一个长连接的请求,服务器端就能源源不断地往客户端输入数据。
这种方式每次数据传送不会关闭连接,连接只会在通信出现错误时,或是连接重建时关闭(一些防火墙常被设置为丢弃过长的连接, 服务器端可以设置一个超时时间, 超时后通知客户端重新建立连接,并关闭原来的连接)。
IE、Morzilla Firefox 下端的进度栏都会显示加载没有完成,而且 IE 上方的图标会不停的转动,表示加载正在进行。
Google 的天才们使用一个称为“htmlfile”的 ActiveX 解决了在 IE 中的加载显示问题,并将这种方法用到了 gmail+gtalk 产品中。Alex Russell 在 “What else is burried down in the depth’s of Google’s amazing JavaScript?”文章中介绍了这种方法。Zeitoun 网站提供的 comet-iframe.tar.gz,封装了一个基于 iframe 和 htmlfile 的 JavaScript comet 对象,支持 IE、Mozilla Firefox 浏览器,可以作为参考。
我们常用的网页版的gtalk就是这种实现方式,Google的开发人员使使用一个称为“htmlfile”的 ActiveX 解决了在 IE 中的加载显示问题。
Comet实现框架
CometD&框架是基于 HTTP 的事件驱动通信解决方案,使用了Bayeux通信协议,提供了一个 Java 服务器部件和一个 Java 客户端部件,还有一个基于 jQuery 和 Dojo 的 JavaScript 客户端库。
Bayeux 通信协议主要是基于 HTTP,提供了客户端与服务器之间的响应性双向异步通信。Bayeux 协议基于通道进行通信,通过该通道从客户端到服务器、从服务器到客户端或从客户端到客户端(但是是通过服务器)路由和发送消息。Bayeux 是一种 “发布- 订阅” 协议。
CometD 与三个传输协议绑定在一起:JSON、JSONP 和 WebSocket。他们都依赖于 Jetty Continuations 和 Jetty WebSocket API。在默认情况下,可以在 Jetty 6、Jetty 7、和 Jetty 8 中以及其他所有支持 Servlet 3.0 Specification 的服务中使用 CometD。
服务器和内部构件
Atmosphere框架
Atmosphere提供了一个通用 API,以便使用许多 Web 服务器(包括 Tomcat、Jetty、GlassFish、Weblogic、Grizzly、JBossWeb、JBoss 和 Resin)的 Comet 和 WebSocket 特性。它支持任何支持 Servlet 3.0 Specification 的 Web 服务器。
Atmosphere 提供了一个&客户端库,该库可以使连接设置变得更容易,它能够自动检测可以使用的最佳传输协议(WebSockets 或 CometD)。Atmosphere 的 jQuery 插件的用法与 HTML5 WebSockets API 相似。
Pushlet 使用了观察者模型:客户端发送请求,订阅感兴趣的事件;服务器端为每个客户端分配一个会话 ID 作为标记,事件源会把新产生的事件以多播的方式发送到订阅者的事件队列里。
Pushlet 最后更新于号,之后至今没有再更新。
Cometd 和Atmosphere框架参见示例代码 (/brucefengnju/cometdatoms)。
Comet实现要点
不要在同一客户端同时使用超过两个的 HTTP 长连接
HTTP 1.1 规范中规定,客户端不应该与服务器端建立超过两个的 HTTP 连接, 新的连接会被阻塞,在IE浏览器中严格遵守了这种规定。
服务器端的性能和可扩展性
一般 Web 服务器会为每个连接创建一个线程,如果在大型的商业应用中使用 Comet,服务器端需要维护大量并发的长连接。在这种应用背景下,服务器端需要考虑负载均衡和集群技术;或是在服务器端为长连接作一些改进。
在客户和服务器之间保持“心跳”信息
在浏览器与服务器之间维持一个长连接会为通信带来一些不确定性:因为数据传输是随机的,客户端不知道何时服务器才有数据传送。服务器端需要确保当客户端不再工作时,释放为这个客户端分配的资源,防止内存泄漏。因此需要一种机制使双方知道双方都在正常运行。
服务器端在阻塞读时会设置一个时限,超时后阻塞读调用会返回,同时发给客户端没有新数据到达的心跳信息。此时如果客户端已经关闭,服务器往通道写数据会出现异常,服务器端就会及时释放为这个客户端分配的资源。
如果客户端使用的是基于 AJAX 的长轮询方式;服务器端返回数据、关闭连接后,经过某个时限没有收到客户端的再次请求,会认为客户端不能正常工作,会释放为这个客户端分配、维护的资源。
当服务器处理信息出现异常情况,需要发送错误信息通知客户端,同时释放资源、关闭连接。
WebSocket是HTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket通讯协议于2011年被IETF定为标准RFC 6455,WebSocketAPI被W3C定为标准。在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
浏览器支持
浏览器版本支持
详情查看&Browser compatibility
WebSocket的实现已经有很多种版本,详细可以查看DEMO。
总结下来长轮询不是一个很好的方案,而且对于服务器而言是有风险的;另外支持WebSocket协议的浏览器都比较新,特比是IE需要10以上的版本;而我们的业务是面向于商家端,商家的浏览器版本相对较低,很多对WebSocket都不支持;相对而言Comet的方式比较适合,也有相应的实现框架,实现成本最低;因此最后我们还是决定使用Comet的方式来实现,后面上线运行一段时间之后再来给大家介绍。
转载注明本文地址:
阅读(...) 评论()