3

这是即时通讯的 4 种实现方案

 2 years ago
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.
neoserver,ios ssh client

可能是年底了的原因,大家工作效率都比较低,一个简单的需求评审会拖拖拉拉开了4个小时,无聊至极。趁这个机会整理一下最近正在思考的问题。服务端如何将数据推送到浏览器。

一般来说,Web端即时通讯技术因受限于浏览器的设计限制,一直以来实现起来并不容易,这里总结了4种方式。

1. http + ajax

XMLHttpRequest在和服务端进行数据交互时存在四种状态,很多时候的判断是readyState4时,从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');
})
复制代码

前端监听XMLHttpRequestonreadystatechange事件,每当服务器返回一段数据都会触发一次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年成为国际标准。所有浏览器都已经支持了,也是应用很广泛的一种即时通信协议。

websocketHTML5新增的API,属于浏览器或者前端的内容。后端用的是socketsocket协议的历史相当古老基本四十年前就已经存在了。在H5websocket自带一些安全的措施,而原生的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的库。nethttp的底层,很多东西都需要自己去实现,比如这里可以使用net.createServer来创建服务。

websocket也是给予http的,先通过http请求到服务,会携带一个upgradewebsocket的请求头,表示希望升级为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-typetext/event-stream,且编码格式为utf-8。返回的数据格式必须为data: xxxx\n\n。除了data还有eventid,以及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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK