当涉及到实现实时通信的Web应用程序时,两种常见的技术选择是服务器发送事件(Server-Sent Events,SSE)和WebSocket,本文将详细讲讲这两种技术,并比较它们的异同点。
服务器发送事件SSE(Server-Sent Events)是一种基于HTTP的单向通信机制,用于实现服务器主动向客户端推送数据的技术,也被称为“事件流”(Event Stream)。它基于HTTP协议,利用其长链接的特性,在客户端与服务器之间建立一条持久化连接,并通过这条连接实现服务器向客户端实时数据推送。
它的工作原理如下:
SSE的特点和适用场景:
当下火热的ChatGPT实现对话消息的流式返回就是基于服务器发送事件SSE技术来实现的。
EventSource
对象是 HTML5 新增的一个客户端 API,用于通过服务器推送实时更新的数据和通知。在使用 EventSource
对象时,可以通过以下方法进行配置和操作:
1.EventSource() 构造函数
EventSource的构造函数接收一个 URL 参数,通过该 URL 可以建立起与服务器的连接,并开始接收服务器发送的数据【服务器和客户端建立持久性连接的关键】。
const eventSource = new EventSource(url, options);
2.EventSource.onmessage 事件
onmessage监听服务器发送的数据,当接收到数据时,就触发该事件,可以用EventSource的实例对象的监听事件函数来代替使用。
如下:
const sse = new EventSource('http://localhost:3000/api/sse' )
// 第一个参数对应后端nodejs自定义的事件名,默认事件名是message
sse.addEventListener('message', (e) => {
console.log(e.data)
})
3. EventSource.onopen 事件
onopen 事件表示 EventSource 对象已经和服务器建立了连接,并开始接收来自服务器的数据。当 EventSource 对象建立连接时,触发该事件。
4.EventSource.close() 方法
close() 方法用于关闭 EventSource 对象与服务器的连接,停止接收服务器发送的数据。
5.EventSource.readyState 属性
readyState 属性表示当前 EventSource 对象的状态,它是一个只读属性,它的值有以下几个:
if (eventSource.readyState === EventSource.CONNECTING) {
console.log('正在连接服务器...');
} else if (eventSource.readyState === EventSource.OPEN) {
console.log('已经连接上服务器!');
} else if (eventSource.readyState === EventSource.CLOSED) {
console.log('连接已经关闭。');
}
下面我们通过代码来体会一下SSE技术,以下是一段文本,我们基于SSE技术实现:node后端读取文本,前端流式展示文本内容。
谁让你读了这么多书,又知道了双水村以外还有个大世界……
如果从小你就在这个天地里日出而作,日落而息,那你现在就会和众乡亲抱同一理想:
经过几年的辛劳,像大哥一样娶个满意的媳妇,生个胖儿子,加上你的体魄,会成为一名出色的庄稼人。
不幸的是,你知道的太多了,思考的太多了,因此才有了这种不能为周围人所理解的苦恼。
——《平凡的世界》
node后端index.js
const express = require('express')
const fs = require('fs')
const app = express()
app.get('/api/sse', (req, res) => {
// 设置请求的客户端的响应标头,参数1:必选,三位数的http状态码,参数2:标头对象
res.writeHead(200, {
'Content-Type': 'text/event-stream', // SSE(事件流)核心代码,表示使用SSE
'Access-Control-Allow-Origin': '*' // 解决跨域,* 这种方式不安全,仅用于测试
})
const data = fs.readFileSync('./index.txt', 'utf8') // 异步utf8格式读取文件,得到的是字符串
const arr = data.split('') // 将字符串分割成数组
let current = 0
// mock SSE 数据
let timer = setInterval(() => { // 定时器实现持久化返回数据
if (current >= arr.length) {
clearInterval(timer)
return
} else {
// 返回自定义事件名 默认是message
res.write(`id:${current}\n`)
res.write(`event:lol\n`) // 定义发送事件名
// 向请求的客户端发送响应内容 我的理解就是可以让客户端执行一段js代码
res.write(`data:${arr[current++]}\n\n`)
}
}, 300);
})
app.listen(3000, () => {
console.log('server is running');
})
使用node ./index.js
运行node服务。
以上代码在node后端实现了localhost:3000的服务器的/api/sse接口通过Content-Type:text/event-stream
头部标识SSE连接,并将文本内容以300ms返回一个字符的速度发送给客户端。
值得注意的是:
res.writeHead()
方法实现:设置请求的客户端的响应标头,和res.writeHead()
实现相同功能,但wirteHead()
可以一次设置多个响应标头。
res.write()
方法实现:向请求的客户端发送响应内容。在res.end()
之前可以多次被执行调用,传入模板字符串时,我的理解是可以让客户端执行一段js代码。
注意看上述代码中的res.write
片段:
// 返回自定义事件名 默认是message
res.write(`id:${current}\n`)
res.write(`event:lol\n`)
// 向请求的客户端发送响应内容 我的理解就是可以让客户端执行一段js代码
res.write(`data:${arr[current++]}\n\n`)
由于使用响应请求头Content-Type:text/event-stream
开启事件流EventStream,因此可以看到浏览器请求SSE的请求时会有下图的表格,上述代码的id,event,data分别对应下图表格的前三列内容,\n
则表示自动跳转下一个表格单元,因此执行第三个res.write()
时最后跟两个\n
,第一个用于跳转时间列的单元格,第二个用于跳转下一行第一个单元格。
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSE流式发送title>
head>
<body>
<div id="data">div>
<script>
document.addEventListener('keydown', (e) => {
// 按下回车键触发sse接口
if (e.keyCode == 13) {
const sse = new EventSource('http://localhost:3000/api/sse')
sse.addEventListener('open', (e) => {
console.log(e.target);
})
// 接收到服务器发送的数据的监听方式1:
// 后端nodejs自定义了事件名就要把message改成自定义的事件名
sse.addEventListener('lol', (e) => {
document.getElementById('data').innerHTML += e.data
console.log(e);
})
// 监听方式2
// sse.onmessage = (e) => {
// document.getElementById('data').innerHTML += e.data
// console.log(e);
// }
sse.onerror = (e) => {
console.log(e);
}
}
})
script>
body>
html>
以上代码实现了按下回车键后,客户端发送http请求与服务器建立连接,并实例化EventSource对象与服务器保持持久连接后监听服务器返回数据。
使用LiverServer运行html文件。
WebSocket 是一种在单个 TCP 连接上进行全双工通信的网络协议。它是 HTML5 中的一种新特性,能够实现 Web 应用程序和服务器之间的实时通信,比如在线聊天、游戏、数据可视化等。
相较于 HTTP 协议的请求-响应模式,使用 WebSocket 可以建立持久连接,允许服务器主动向客户端推送数据,避免了不必要的轮询请求,提高了实时性和效率。同时,WebSocket 的连接过程也比较简单,可以通过 JavaScript 中的 WebSocket API 进行创建和管理,并且可以和现有的 Web 技术如 HTML、CSS 和 JavaScript 无缝集成。
WebSocket 协议是基于握手协议(Handshake Protocol)的,它在建立连接时使用 HTTP/HTTPS 发送一个初始握手请求,然后服务器响应该请求,建立连接后就可以在连接上进行数据传输了。
总之,WebSocket 提供了一种快速、可靠且灵活的方式,使 Web 应用程序能够实现实时通信,同时也提高了网络性能和用户体验。
为什么要用使用WebSocket?
因为http 通信只能由客户端发起,服务器返回查询结果,HTTP 协议做不到服务器主动向客户端推送信息。服务器有连续的状态变化,客户端要获知就非常麻烦。
我们只能使用轮询:每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。
轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开);
而WebSocket能做到服务器和客户端相互推送信息。
前端的WebSocket对象提供了用于创建和管理WebSocket 连接,以及可以通过该连接发送和接收数据的 API。
使用示例:
const ws = new WebSocket('ws://localhost:8080')
node后端不再使用Express建立服务,而是安装ws库,创建socket服务。
下面我们直接看代码
后端创建socket服务:ws.js
// 要先安装ws和它的声明文件@types/ws
const ws = require('ws')
// 创建 socket 服务 8080端口
const wss = new ws.Server({ port: 8080 }, () => {
console.log("socket服务启动成功8080");
});
// 监听客户端的连接
wss.on('connection', (socket) => {
// 监听客户端的消息
console.log('客户端连接成功');
// 监听客户端发送过来的消息
socket.on('message', (e) => {
console.log(e.toString());
// 单独给发送消息的客户端发送消息
socket.send(e.toString())
// 给所有客户端群发消息
wss.clients.forEach((client) => {
client.send('群发消息:' + e.toString())
})
})
})
以上代码实现了创建8080端口的socket服务,并监听客户端发送过来的消息,并分别将收到的消息发送给一个客户端以及所有客户端。
前端ws.html
:
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSockettitle>
head>
<body>
<div>
<input type="text" id="input">
<button id="send">发送button>
div>
<div id="txt">服务端发给发给客户端的消息:div>div>
<script>
// 建立8080端口的WebSocket连接
// webSocket的协议要是用ws://或者wss:// 就跟http://和https://一样
const ws = new WebSocket('ws://localhost:8080')
// 监听open 表示连接成功
ws.addEventListener('open', function(event){
console.log('连接成功');
})
let input = document.querySelector('#input')
let btn = document.querySelector('#send')
btn.addEventListener('click', function () {
// 发送消息 前后端都使用send发送消息
if(input.value) {
// 给服务器端发送消息
ws.send(input.value)
input.value = ''
}
})
// 监听服务器端发送过来的消息
ws.addEventListener('message', (e) => {
// 渲染消息到页面
document.querySelector('#txt').innerHTML += e.data
})
script>
body>
html>
以上代码实现了建立8080端口的WebSocket连接,并实现点击btn将输入框的内容发送给服务端,最后监听服务端返回的消息实现渲染到页面上。
效果展示:
后端运行socket服务,前端使用LiveServer(会自动开一个5500的端口)打开html文件,可以看到前后端WebSocket连接成功。
这里我们开启两个客户端,分别验证服务端单独发送给客户端和群发消息的效果:
在左边的客户端的输入框输入你好后点击发送,可以得到如下结果
可以看到左右两边都收到了服务端群发的消息,左边的客户端比右边的客户端多了一条它发给客户端的消息。这表明WebSocket成功实现了服务端和客户端之间的相互通信,并且服务端可以给客户端群发消息。
通信模型:SSE是单向通信模型,只能由服务器向客户端推送数据,而WebSocket是双向通信模型,客户端和服务器可以互相发送消息。
连接性:SSE使用长轮询或HTTP流技术,而WebSocket使用持久连接。SSE需要频繁地发起HTTP请求来获取数据【是这样的吗??】,而WebSocket只需在握手阶段建立一次连接,然后保持连接打开。另外WebSocket没有同源限制,客户端可以与任意服务器通信。
实时性:WebSocket提供了更低的延迟和更高的实时性,因为它支持双向通信,可以立即将数据推送给客户端。SSE虽然也可以实现实时性,但由于其单向通信模型,需要服务器定期发送数据。
浏览器支持:WebSocket在现代浏览器中得到广泛支持,包括Chrome、Firefox、Safari等。SSE在大多数现代浏览器中也有支持,但在某些旧版本浏览器中可能存在兼容性问题。
API复杂性:WebSocket提供了更灵活和复杂的API,可以处理更高级的通信需求。SSE相对简单,使用浏览器的原生 EventSource 接口即可。
选择SSE还是WebSocket取决于您的应用需求。如果您只需要服务器向客户端推送数据,并且实时性要求不高,SSE是一个简单可行的选择。如果您需要双向通信,实时性要求高,或需要处理复杂的通信需求,WebSocket可能更适合您的应用。