这是即时通讯的 4 种实现方案
source link: https://juejin.cn/post/7057687288154685470
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
可能是年底了的原因,大家工作效率都比较低,一个简单的需求评审会拖拖拉拉开了4个小时,无聊至极。趁这个机会整理一下最近正在思考的问题。服务端如何将数据推送到浏览器。
一般来说,Web
端即时通讯技术因受限于浏览器的设计限制,一直以来实现起来并不容易,这里总结了4种方式。
1. http + ajax
XMLHttpRequest
在和服务端进行数据交互时存在四种状态,很多时候的判断是readyState
为4
时,从response
获取服务端响应结果。其实readyState
等于3
的时候就可以获取到服务端的部分数据了。
可以利用这个属性实现服务端推送。
比如服务使用http
创建服务,每间隔1s
的时候通过write
方法返回一段文本,但是不要调用end
方法。
const http = require('http');
const fs = require('fs');
const app = http.createServer((req, res) => {
// 设置响应头
res.setHeader('Content-type', 'application/json; charset=utf-8');
res.setHeader('Cache-Control', 'max-age=0'); // 没有缓存
let num = 0;
// 地柜返回
const send = () => {
if (num > 20) {
res.end();
return;
}
num++;
const data = Math.random() + '';
res.write(data, 'utf8');
setTimeout(send, 1000);
}
send();
});
app.listen(8081, () => {
console.log('127.0.0.1:8081');
})
复制代码
前端监听XMLHttpRequest
的onreadystatechange
事件,每当服务器返回一段数据都会触发一次onreadystatechange
事件,可以从responseText
中得到当前获取到的全部数据。
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api');
xhr.timeout = 30000;
xhr.responseType = 'text';
xhr.onreadystatechange = function () {
if (this.readyState == 3) { // 分段获取服务端返回的数据
console.log(this.responseText);
}
if (this.readyState == 4) {
if (this.status >= 200 && this.status < 300 || this.status == 304) {
// this.response
} else {
// this.statusText
}
}
}
xhr.send()
复制代码
2. websocket
websocket
具有三个优点,双向通信,自动跨域,性能高。最主要的是可以传输多种格式的数据。WebSocket
协议在2008
年诞生,2011
年成为国际标准。所有浏览器都已经支持了,也是应用很广泛的一种即时通信协议。
websocket
是HTML5
新增的API
,属于浏览器或者前端的内容。后端用的是socket
,socket
协议的历史相当古老基本四十年前就已经存在了。在H5
中websocket
自带一些安全的措施,而原生的socket
就没什么安全性可言了。
客户端浏览器通过实例化Websocket
,传入服务地址创建websocket
链接,message
中会接收到服务端推送的数据,也可通过send
方法向服务端发送数据。
const ws = new Websocket('ws://127.0.0.1:8080/api');
// 原生没有emit,自己封装一个
ws.emit = function(name, ...args) {
ws.send(JSON.stringify({
name,
data: [...args]
}))
}
ws.onopen = function() {
console.log('链接上了');
// ws.send('dadadadadasda'); // 发送数据,只有一个参数一个大字符串
ws.emit('msg', 12, 5, 8);
}; // 已经链接
ws.onmessage = function() {
console.log('接收到消息了')
}; // 收到数据
ws.onclose = function() {
console.log('断开链接了')
}; // 断开了
复制代码
在node
中想要实现socket
可以借助node
原生的net
模块,这是一个相对底层的网络模块,是一个tcp
的库。net
是http
的底层,很多东西都需要自己去实现,比如这里可以使用net.createServer
来创建服务。
websocket
也是给予http
的,先通过http
请求到服务,会携带一个upgrade
为websocket
的请求头,表示希望升级为websocket
,这个时候服务可以返回101
状态码,表示进行服务可以升级。
const http = require('http');
const net = require('net'); // TCP的库,可以理解为原生的Socket
const crypto = require('crypto'); // 借助加密库实现一些安全性
const server = net.createServer(sock=> {
console.log('链接上了');
sock.on('end', () => {
console.log('客户端断开了')
}); // 断开
sock.once('data', (data) => {
console.log('hand shake start...');
// 最先过来的是http头
const str = data.toString();
// 将http头用\r\n切开
let lines = str.split('\r\n');
// 删除第一行和最后一行,因为没啥用
lines = lines.slice(1, lines.length - 2);
// 将所有请求头通过'分号空格'切开
const headers = {};
lines.forEach(line => {
const [key, value ] = line.split(': ');
// 将请求头变成小写
headers[key.toLowerCase()] = val;
})
// http协议转websocket会传入upgrade为websocket
if (headers['upgrade'] != 'websocket') {
console.log('其他协议,暂不支持');
sock.end();
} else if (headers['sec-websocket-version'] != 13) {
console.log('不兼容不是13的版本');
sock.end();
} else {
const key = headers['sec-websocket-key'];
// 13版本的源码是258E,可以百度的到
const mask = '258EAFA5-47DA-95CA-C5AB0DC85B11';
// 需要把key和mask加在一起,然后用sha1加密,再变成base64,还给客户端
// sha1(key + mask) -> base64 -> client;
const hash = crypto.createHash('sha1');
hash.update(key + mask);
const tokey = hash.digest('base64');
// 数据以HTTP发回客户端,因为验证的过程还是http阶段, 状态值为101(正在切换协议,协议升级 Switching Protocols)
sock.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ' + tokey + '\r\n'); // Upgrade: websocket告诉浏览器升级为websocket,冒号要有空格
// 至此,握手已经结束了。因为握手的过程只有一次,所以不要用on处理,用once处理
// 从这里开始,才是真正的数据,以后所有的数据都走这里,所以用on处理
sock.on('data', data => {
// 获取到的数据
// 不过数据是一个buffer的数据包,解析起来比较麻烦。
console.log(data);
})
}
}); // 有数据过来
}).listen(8080);
复制代码
上面介绍的是websocket
的一个实现原理,项目中可以直接使用socket.io
这个库。
前端代码如下:
const sock = io.connect('ws://127.0.0.1:8080/api');
sock.on('connect', () => {
console.log('已链接');
sock.emit('aaa', 12, 5,8);
sock.on('time', (ts) => {
console.loh(ts);
})
});
sock.on('disconnect', () => {
console.log('已断开');
});
复制代码
服务端代码如下:
const http = require('http');
const io = require('socket.io');
// 创建http服务,开启8080端口号
const httpServer = http.createServer().listen(8080);
// socket监听http服务
const wsServer = io.listen(httpServer);
// 当有链接的时候
wsServer.on('connection', sock => {
// 发送
// sock.emit
sock.emit('time', Date.now());
// 接收
sock.on('aaa', (a, b, c) => {
console.loh(a, b, c);
})
})
复制代码
3. SSE
SSE
全称是Server-Sent Events
,指的是网页自动获取来自服务器的更新,也就是自动获取服务端推送至网页的数据,这是一个H5
的属性,除了IE
,其它标准浏览器基本都兼容。
实现方式和第二种有一些像,服务器向客户端声明要发送流信息,然后连续不断地发送过来。这时客户端是不会关闭连接的,会一直等着服务器发过来的新的数据流。比如音视频的媒体流就是这种机制。
SSE
只能服务器向浏览器发送数据,这点和第二种方式很像,能力上都不如websocket
,优点是SSE
使用更加简单,并且基于http
协议,兼容性还可以(当然2022年了,没有啥是兼容性不可以的了)。
H5
端使用EventSource
对象,填入要请求的url
地址就可以了。
var source = new EventSource('/api', {
withCredentials: true
});
source.onopen = function () {
console.log('链接已建立', this.readyState);
}
source.onmessage = function (event) {
console.log('实时获取的数据', event.data);
}
source.onerror = function () {
console.log('发生错误');
}
// 关闭
// source.close();
复制代码
服务器向浏览器发送的 SSE
数据,首先必须设置响应头的Content-type
为text/event-stream
,且编码格式为utf-8
。返回的数据格式必须为data: xxxx\n\n
。除了data
还有event
,id
,以及retry
,可以参考Server-sent_events-mdn。
服务端代码如下:
const http = require('http');
const fs = require('fs');
const app = http.createServer((req, res) => {
res.setHeader('Content-type', 'text/event-stream; charset=utf-8');
res.setHeader('Cache-Control', 'max-age=0'); // 清楚缓存
res.setHeader('Access-Control-Allow-Origin', 'http:127.0.0.1/');
let num = 0;
const send = () => {
if (num > 20) {
res.end();
return;
}
num++;
const data = Math.random() + '';
res.write(`data: ${data}\n\n`, 'utf8');
setTimeout(send, 1000);
}
send();
});
app.listen(8081, () => {
console.log('127.0.0.1:8081');
})
复制代码
4. ajax
ajax
轮询,这个没啥好说的,是个人都想的到,就不介绍了。
没错,这条就是用来凑数的。
Recommend
-
55
前言 本文翻译自Real-Time Communication with Streams Tutorial for iOS 翻译的不对的地方还请多多包涵指正,谢谢~ iOS流式即时通讯教程 从时间初始,人们就已开始梦想着更好地跟遥远的兄弟通讯的方式。从信鸽到无线电波,我们一直在努力将通讯变得更清晰更高效...
-
32
即时通讯安全篇(七):如果这样来理解HTTPS,一篇就够了
-
52
58即时通讯(以下简称微聊,亦或简称IM SDK)作为58集团即时通讯的解决方案,承载了58同城、赶集网、安居客、移动经纪人、招才猫等商业产品线的用户在线沟通能力。目前支持iOS、Android、Web、H5等所有平台。为了满足58集团不断增长的业务...
-
51
本文引用了“蔷薇Nina”的“Nginx 相关介绍(Nginx是什么?能干嘛?)”一文部分内容,感谢作者的无私分享。 1、引言
-
51
程序员 - @imndx - 我们搞了一套开源的 IM,随着时间的推移,开始有人找我们定制开发,故有此疑问,希望大神能解答。另外,如何能规避?如果有人对我们的 IM 感兴趣,可以在这儿围观:http
-
39
2019 年,微信月活账号数量达 11.12 亿,每天发送 450 亿次信息,成功连接 4.1 亿次音视频通讯。据中国互联网协会发布的《中国互联网发展报告 (2019)》,截至 2018 年底,我国网民规模达到 8. 29 亿。从一个线上的即时通讯(IM)软件到逐...
-
25
简要介绍 gim是一个即时通讯服务器,代码全部使用golang完成。主要功能 1.离线消息同步 2.多业务接入 3.单用户多设备同时在线 4.单聊,群聊,以及超大群聊天场景 5.支持服务水平扩展...
-
16
作者: “Lightning 上的即时通讯软件有出现杀手级应用程序的潜力吗?” 当 Lightning Labs 开发者 Joost Jager 首次展示他的...
-
11
laravel实现利用RabbitMQ实现MQTT即时通讯 有时候我们的项目中会用到即时通讯功能,比如电商系统中的客服聊天功能,还有在支付过程中,当用户支付成功后,第三方支付服务会回调我们的回调接口,此时我们需要通知前端支付成功。而 RabbitMQ
-
3
集中化 vs 碎片化 – 谈即时通讯方案 Sun Aug 22, 2021 “海内存知己,天涯若比邻。”信息技术使得后半句真正...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK