服务端推送服务端推送是指服务端主动向客户端发送消息(响应)。HTTP协议在应用层的实现中,“请求-响应”是一个往返,它的起点是从客户端开始的,所以在应用层之上无法实现简单的服务端推送功能。目前服务端推送有几种解决方案:长期客户端训练websocket双向连接iframe永久框架长期轮训可以避免短期轮训导致服务器过载,但是服务器返回数据后,client还需要主动发起下一个长轮训请求,等待server响应,所以还是需要建立底层连接和server处理逻辑做相应的处理,不符合逻辑流程简单的服务器推送;websocket连接相对来说是最强大的,但是它对服务器的版本有要求,在能够使用websocket协议的服务器上尽量采用这种方式;iframe永久框架是在页面中嵌入一个专用的iframe页面接收数据,服务器在页面输出相关信息,如,服务器不断向iframe写入类似的脚本标签和数据,实现另一种形式的服务器推送。但是永久框架技术会导致主页面的loading栏一直处于“loading”状态,体验很差。HTML5规范提供了服务器端事件EventSource。浏览器在实现规范的前提下创建EventSource连接后,就可以接收到服务器发送的消息。这些消息需要遵循一定的格式。对于前端开发者来说,只需要在浏览器中监听相应的事件即可。与上述3种实现相比,EventSource流的实现对于客户端开发者来说非常简单,除了基于IE的浏览器(IE、Edge)之外,兼容性都很好;对于服务端,可以兼容老浏览器,不需要升级到其他协议,可以满足简单服务端推送场景的需求。在需要浏览器和服务端强交互的场景下,websocket仍然是最好的选择。浏览器端EventSource规范简析浏览器端需要创建一个EventSource对象,并传入一个服务器端接口URI作为参数。varevtSource=newEventSource('http://localhost:9111/es');其中'http://localhost:9111/es'是服务端吐出数据的接口。目前EventSource在大部分浏览器中都不支持跨域,所以不是跨域的解决方案。默认的EventSource对象通过监听“message”事件从服务器获取消息,“open”事件在http连接建立后触发,“error”事件在通信错误(连接中断,无法从服务器返回数据)向下触发。同时,EventSource规范允许服务端指定自定义事件,客户端可以监听该事件。evtSource.addEventListener('message',function(e){console.log(e.data);});evtSource.addEventListener('error',function(e){console.log(e);})服务器事件流对应的MIME格式为text/event-stream,基于HTTP长连接。HTTP1.1规范默认使用长连接,HTTP1.0的服务器需要特殊设置。服务器返回的数据需要特殊的格式,分为四种消息:event、data、id、retry。其中event指定自定义消息的名称,如event:customMessagen;data指定具体的消息体,可以是对象也可以是String,比如data:JSON.stringify(jsonObj)\n\n,消息体后面有两个换行符n,表示当前消息体已经被已发送,一个换行符表示当前消息还没有结束,浏览器需要等待后面的数据到达后触发事件;id为当前消息的标识,不可设置。一旦设置,就会体现在浏览器端的eventSource对象中(假设服务端返回id:369n),eventSource.lastEventId==369。该字段使用不广泛;retry设置当前http连接失败后的重连间隔。EventSource规范规定http连接失败后客户端默认重连,重连间隔为3s。可以通过设置重试字段来指定重连间隔;每个字段都有一个名称,后跟一个“:”。当有字段没有名字而只有“:”时,这会被服务器解释为“注释”,不会发送给浏览器,如:commission。由于EventSource是基于HTTP连接的,所以在没有数据的时候会出现超时问题。服务器默认的HTTP超时时间为2分钟。在节点端,可以通过response.connection.setTimeou(0)设置为默认超时时间2分钟。所以服务端需要做心跳保活,否则客户端会在连接超时时出现net::ERR_INCOMPLETE_CHUNKED_ENCODING错误。通过阅读相关规范,发现可以使用注释行来防止连接超时,服务端可以周期性的发送消息注释行来保持连接不变。下面提供koa的服务端代码:varfs=require('fs');varpath=require('path');varPassThrough=require('stream').PassThrough;varReadable=require('stream').可读;varkoa=require('koa');varRouter=require('koa-router');varapp=newkoa();varrouter=newRouter();functionRR(){Readable.call(this,arguments);}RR.prototype=newReadable();RR.prototype._read=function(data){}router.get('/',function(ctx,next){ctx.set('content-type','text/html');ctx.body=fs.readFileSync(path.join(process.cwd(),'eventServer.html'));});constsse=(stream,event,data)=>{returnstream.push(`event:${event}\ndata:${JSON.stringify(data)}\n\n`)//returnstream.write(`event:${event}\ndata:${JSON.stringify(data)}\n\n`);}router.get('/es',function(ctx,next){varstream=newRR()//PassThrough();ctx.set({'内容-Type':'text/event-stream','Cache-Control':'no-cache',Connection:'keep-alive'});sse(stream,'test',{a:"仰戈",b:"探戈"});ctx.body=流;setInterval(()=>{sse(stream,'test',{a:"yango",b:Date.now()});},3000);});app.use(router.routes());app.listen(9111,function(){console.log('监听端口9111');});这里需要注意,koa-router的返回值必须是一个Stream(Readable),这是koa的特殊性造成的。如果context.body不是Stream而是string或者Buffer实例,会直接调用nodenative(buffer)中的res.end,结束HTTP响应:koalib/application.js//responsesif(Buffer.isBuffer(body))返回res.end(正文);if('string'==typeofbody)returnres.end(body);if(bodyinstanceofStream)returnbody.pipe(res);因此,服务器端事件流无法正确响应。Stream类型的返回有几种方式,比如通过扩展stream模块的Readable可读流返回或者直接使用PassThrough流返回,或者通过through2模块或者Transform对象返回。归根结底,保证数据可以从流对象通过管道传输到http.ServerResponse对象。附上页面代码querySelector('#info');varse=newEventSource('http://localhost:9111/es');se.addEventListener('test',function(e){infoShow.textContent+=e.data+'\n';});se.addEventListener('error',function(e){console.log(e);})
