前情提要

「 话说上回说到!那WebSocket大侠,巧借http之内力,破了敌阵的双工鸳鸯锁,终于突出重围。

然而玄难未了,此时web森林中飞出一只银头红缨枪,划破夜

"莫非!?" websocket大侠喃喃念道,"恐怖如斯,你莫不是就是那个手使单向追魂枪的。。。"

"正是在下!",那人厉声喝道。只见那胸前的纹章铭刻着几个洋文——

读作"EventSource"!」

上一篇文章请看这里:论一个低配版Web实时通信库是如何实现的( WebSocket篇)

引论

simple-socket是我写的一个"低配版"的Web实时通信工具(相对于Socket.io),在参考了相关源码和资料的基础上,实现了前后端实时互通的基本功能,选用了WebSocket ->server-sent-event -> AJAX轮询这三种方式做降级兼容,分为simple-socket-client和simple-socket-server两套代码。

我的上一篇文章讲了如何进行websocket的前后端编码,所以今天来聊一聊event-source这块的

论一个低配版Web实时通信库是如何实现的( WebSocket篇)

github仓库地址

https://github.com/penghuwan/simple-socket

npm命令

npm i simple-socket-serve   (服务端npm包)
npm i simple-socket-client (客户端npm包)

EventSource的前端代码

EventSource的前端API主要有这么四个

  1. 创建es对象:var es = new EventSource(url)

  2. es两端连接事件打开的回调:es.onopen = function () { }

  3. 监听服务端发送事件: es.addEventListener("XXX", function (e) { // e.data }

  4. 监听服务端的message事件es.onmessage = function; 相当于es.addEventListener("message",function);

业务代码如下

(1)前端从服务端接收消息

前端通过监听服务端message事件,接收消息,并解析event和data,然后通过emitter.emit(event, data)触发事件,从而调用socket.on设置的监听回调

function Client() {
this.ws = null
this.es = null; // EventSource对象
init.call(this); // 设置this.type并初始化相关对象例如es或ws
listen.call(this);
// ...
} function listen() {
// 保存this
var self = this;
switch (this.type) {
// 当type为eventsource时,执行以下代码,this.type根据能力检测设置
case 'eventsource':
// 监听触发connect事件,把client对象自身传入当作socket
this.es.onopen = function () {
emitter.emit('connect', self);
};
// 监听服务端传来的message事件
this.es.addEventListener("message", function (e) {
var payload = JSON.parse(e.data);;
var event = payload.event;
var data = payload.data;
emitter.emit(event, data);
}, false);
break;
// ...
}
}

(2)前端发送消息给服务端

由于event-source是单向的,只能从服务端从前端发送消息,而不能从前端发送消息给服务端。这和websocket显著不同

不过别担心,因为我们不是还有AJAX嘛!

对于前端发送消息的情况 我们可以发一个post请求过去,同时借助/eventsource这个路径,告诉服务端这是一个SSE请求

$.ajax({,
type: 'POST',
url: `http://${url}/eventsource`,
data: { event, data },
success: function () {
}
});

EventSource的服务端代码

好像这波就没了吧,OK,我们接下来走下路。

server-sent-event的服务端握手流程

server-sent-event(或event-source),需要借助流(stream)的方式去实现通信。

Stream 是一个抽象接口,Node 中有很多对象实现了这个接口。例如,对http 服务器的request/response 对象就是一个 Stream。

它可以分为四种类型:

  • Readable - 可读操作。

  • Writable - 可写操作。

  • Duplex - 可读可写操作.

  • Transform - 操作被写入数据,然后读出结果。

服务器每次接收的Response是一个Writable,它可以被写入数据,将一个流写入另一个流可以通过调用pipe方法。

所以我们需要创建一个stream的实例,然后通过调用stream.pipe(Response)将流写入响应中,这样就可以被前端es.addEventListener添加的回调给接收到了。

但问题在于 。。。Stream是个抽象接口,Node.js没有给Stream提供构造函数

不过没关系,我们可以这样做:

    • 使用call方法继承stream父函数

    • 使用util.inherits继承stream的原型

    • 重写_read和_write方法(否则会报错)

// 因为我们的流需要写和读,所以使用双工的stream.Duplex构造
function EventStream() {
stream.Duplex.call(this); // 构造函数继承
}
util.inherits(EventStream, stream.Duplex); // 原型继承
// 重写_read和_write方法
EventStream.prototype._read = function () { }
EventStream.prototype._write = function () { }

握手代码逻辑

  1. 创建stream实例,调用pipe方法输送给Response, 同时stream我们保存在socket对象中,在向前端发送数据时候会使用

  2. 将Content-Type字段设置为'text/event-stream',同时Connection设置为'keep-alive'

  3. 将状态码设为200(否则前端onopen方法不会触发)

_handleEShandShake(ctx, socket) {
// 前面定义好的类似stream的类
const eventStream = new EventStream();
// 设置eventStream
socket.setEventStream(eventStream);
// 握手成功后触发onConnection方法,TODO
// 设置符合Event-Source要求的首部
ctx.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
// 将Stream赋给body,Koa底层会判断Stream类型并调用pipe方法流入response
ctx.body = eventStream;
// 设置表示请求成功,否则前端onopen方法不会触发
ctx.status = 200;
// 触发connect方法,传递socket对象
this.emit('connect', socket);
}

Event-Source服务端向前端发送消息。

这里要先说下event-source的报文结构了,由四种字段组成

  • event:事件名,对应前端es.addEventLisener设置的事件名

  • data:数据,为字符串

  • id: 消息标识符,可以缺省

  • retry:表示重新连接的时间间隔

这四个字段两两之间用\n分开,而最后一个字段值需要用\n\n做结尾

例如:`event:message\n data: XXX \n\n`

话不多说,看代码

class Socket extends events.EventEmitter {
constructor(socketId) {
super();
}
// 设置
setEventStream(eventStream) {
this.eventStream = eventStream;
} // 自定义的emit,触发的是前端的on
emit(event, data) {
const dataStr = JSON.stringify({event,data})
if (this.transport === 'eventsource') {
if (!this.eventStream) { throw new Error('eventStream不存在,无法emit') };
// 向stream中写入数据,只要stream尚未关闭
// 数据就会传给前端的onmessage方法或addEventListener('message',fuc)方法
this.eventStream.push(`event:message\ndata:${dataStr}\n\n`);
}
}
}

Event-Source服务端接收前端消息

之前说了,event-source是单向的,所以前端到服务端的传送是通过Ajax请求过来的,所以解析下body,触发事件就OK了

故事到这里就结束了。

有诗为证

江河湖泊浪滔滔,WebSocket多逍遥

EventSource先来却后到,Ajax轮询热血逞英豪!

欲知后事如何,且听下回分解!

知乎专栏

最近也在知乎上写文章,感觉破乎的体验很差!没有博客园好!感觉博客园的各位才个个都是人才,说话又好听!我超喜欢在里面的。

所以说。。。大家好,给大家介绍一下这是我的知乎专栏

https://zhuanlan.zhihu.com/c_135367198

这位路过的大哥你有灵气从键盘喷出,看来是百年一遇的代码奇才,就施舍善心关注一下吧,以解小弟拖家带口之忧,养儿奉母之愁(大雾)

【JavaScript】论一个低配版Web实时通信库是如何实现的之二( EventSource篇)的更多相关文章

  1. 【Node/JavaScript】论一个低配版Web实时通信库是如何实现的( WebSocket篇)

    引论 simple-socket是我写的一个"低配版"的Web实时通信工具(相对于Socket.io),在参考了相关源码和资料的基础上,实现了前后端实时互通的基本功能 选用了Web ...

  2. 【Java】利用注解和反射实现一个"低配版"的依赖注入

    在Spring中,我们可以通过 @Autowired注解的方式为一个方法中注入参数,那么这种方法背后到底发生了什么呢,这篇文章将讲述如何用Java的注解和反射实现一个“低配版”的依赖注入. 下面是我们 ...

  3. 搭建一个低配版的Mock Server

    mock翻译过来是模仿的意思,Server是服务器.粗暴点直译就是模仿服务器. 写在前面 通过阅读本文,你将对Mock的使用有一定的了解,对前后端分离的概念有了更深一步的认识,对Koa的使用有一定的了 ...

  4. 基于canvas和web audio实现低配版MikuTap

    导言 最近发掘了一个特别happy的网页小游戏--MikuTap.打开之后沉迷了一下午,导致开发工作没做完差点就要删库跑路了,还好boss瞥了我一眼就没下文了.于是第二天我就继续沉迷,随着一阵抽搐,这 ...

  5. Jenkins 结合 Docker 为 .NET Core 项目实现低配版的 CI&CD

    随着项目的不断增多,最开始单体项目手动执行 docker build 命令,手动发布项目就不再适用了.一两个项目可能还吃得消,10 多个项目每天让你构建一次还是够呛.即便你的项目少,每次花费在发布上面 ...

  6. 搭建react项目(低配版)

    react项目低配版,可作为react相关测试的基础环境,方便快速进行测试. git clone git@github.com:whosMeya/simple-react-app.git git ch ...

  7. java线程学习第一天__低配版的卖面包机

    package Thread;import javax.xml.bind.ValidationEvent;class snacks{    private int  SaledSnacks=0;   ...

  8. unittest框架(惨不忍睹低配版)

    根据我上个随笔的unittest框架优化得来,虽然对于smtp模块还是有点迷糊,不过还是勉强搭建运行成功了,还是先上代码: #login_test.py import requests class L ...

  9. 分享一个低配VPS下运行的mysql配置文件

    在各种内存CPU核心只有1/2核,内存只有512M/1G的vps下,内存.CPU.硬盘都不是太充裕.因此主要思路是,禁止吃内存大户innodb引擎,默认使用MyISAM.禁止吃硬盘大户log-bin, ...

随机推荐

  1. 高性能javascript-总结(一)

    性能优化 第一章:加载和执行 总结: 将所有<script>标签放到页面底部.这能确保在脚本执行前页面已经渲染了. 合并脚本.<script>标签越少,加载越快,响应速度也更迅 ...

  2. VirtualBox下安装Linux系统

    Linux 开源的类Unix操作系统,拥有图形界面文字界面,旗下发行版不胜其数(Ubuntu.Deepin.CentOS...)在互联网公司中Linux服务器大多是用的CentOS系统,其他Linux ...

  3. 个人永久性免费-Excel催化剂功能第33波-报表形式数据结构转标准数据源

    一般来说,如果有标准的数据源结构,对后续的分析工作将会带来极大的方便.但现实中,许多的原始数据并不预期那样,一个主题的数据已经干净地存放在一个工作表中.今天Excel催化剂再次送上批量化操作,将不规则 ...

  4. Hadoop值Partition分区

    分区操作 为什么要分区? 要求将统计结果按照条件输出到不同文件中(分区).比如:将统计结果按 照手机归属地不同省份输出到不同文件中(分区) 默认 partition 分区 /** 源码中:numRed ...

  5. C#3.0新增功能10 表达式树 06 生成表达式

    连载目录    [已更新最新开发文章,点击查看详细] 到目前为止,你所看到的所有表达式树都是由 C# 编译器创建的. 你所要做的是创建一个 lambda 表达式,将其分配给一个类型为 Expressi ...

  6. 快速java环境变量配置记录

      配置java环境变量就是将java.exe和javac.exe的路径告诉系统,让系统能够找到这两个exe文件,废话不多说,直接开始如何配置环境变量,安装jdk时记住你的安装位置.(配置时必须要的) ...

  7. Linux C 网络编程——多线程的聊天室实现(服务器端)

    服务器端的主要功能: 实现多用户群体聊天功能(此程序最多设定为10人,可进行更改),每个人所发送的消息其他用户均可以收到.用户可以随意的加入或退出(推出以字符串"bye"实现),服 ...

  8. Dubbo源码学习之-Adaptive自适应扩展

    前言 最近三周基本处于9-10-6与9-10-7之间,忙碌的节奏机会丢失了自己.除了之前干施工的那段经历,只看参加软件开发以来,前段时间是最繁忙的了.忙的原因,不是要完成的工作量大,而是各种环境问题, ...

  9. javascript基础学习第三天

    ☞ 命名法: 小驼峰命名法 和 大驼峰命名法(帕斯卡命名法) 变量命名规则:遵循小驼峰命名法 [变量名第一个字母小写后面每一个单词的首字母大写] var userNameAge; 函数命名规则:遵循帕 ...

  10. 基于V2EX API的nodejs组件.

    今天又学习到了新的知(zi)识(shi),来给自己做个笔录,也算在这酷热的天气里给自己写了一篇降温的‘膏药’,话就讲这么多了 ,start off...... 首先 ,依赖选择: /**设置为严格模式 ...