前言
关于计算机网络,其主要部分是通信,而根据通信方式,我们又将协议划分为三类:单功、半双工、双工;我们常见的 HTTP 协议就是通信协议中最常见的一种半双工协议。怎么区分呢?我们将通信两端区分为client和serve端,如果只能单向传输,比如C -> S那就是单功;比如一端接收请求后可以响应,但不能主动发送信息,那即是半双功;再就是双端均可主动推送,那就是双功,我们今天的主角websocket就是一个双功协议。
它解决了什么问题?
在出现websocket之前,对于【服务器有连续的状态变化,客户端要获知】的情况,一般有下面三种处理方案
- 轮询(polling):客户端定时发起请求状态的任务压力大
- 普通轮询
- 长轮询(long-polling)
- iframe流(streaming):存在client端一直处于请求未关闭状态
- EventSource流:不能跨域
文末会扩展这三个方案的具体细节,但也可以看到,上面三个方案都是存在问题的,这也是websocket被广泛使用的原因
它是如何使用的?
serve.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
let WebSockerServer = require('ws').Server;
let server = new WebSockerServer({
port: 8888
})
// socket 套接字
server.on('connection', function (socket){
console.log('2. 服务端监听到了客户端的连接');
socket.on('message',function (message){
console.log('4. 客户端连接信息', message);
socket.send('5. 服务器说:'+message)
})
})
app.listen(3001)
client
Index.html中的script
let socket = new WebSocket('ws://localhost:8888');
socket.onopen = function () {
console.log('1. 客户端连接上了服务端');
socket.send('3. 你好')
}
socket.onmessage = function (event) {
console.log('6. ' + event.data);
}
现象
特点
在发起升级协议时, 在请求上存在
- 请求头
- Connection : Upgrade
- Upgrade : websocket
- Sec-WebSocket-Version: [websocket版本号]
- Sec-WebSocket-Key: [base64加密key]{与响应头中的
Sec-WebSocket-Accept相对应,如果匹配成功才会升级为ws协议} - 在响应上存在
- 状态码是101 switching Protocols 切换协议成功
- 响应头
- Connection : Upgrade(Http常见keep-alive)
- Upgrade : websocket
websocket前的三种处理方案
轮询
普通轮询:其实就是启动一个定时器,然后不断的发起请求
let clock = document.querySelector('#clock');
setInterval(function (){
let xhr = new XMLHttpRequest;
xhr.open('GET','/clock',true);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
clock.innerHTML = xhr.responseText;
}
}
xhr.send()
},1000)
长轮询:区别于普通轮询,就是将下一次的请求后置在上一次的响应之后
function send() {
let xhr = new XMLHttpRequest;
xhr.open('GET','/clock',true);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
clock.innerHTML = xhr.responseText;
send()
}
}
xhr.send()
}
send()
问题也是显而易见的,客户端会发起很多无意义的请求
iframe
iframe方案有点像jsonp,这个实现的关键在于两点
- iframe会将src返回的内容显示在iframe中(这样就可以返回一个script进行操作)
- 服务端在接收到iframe请求后写出数据但不断开端口连接,这样在数据变化时就可以写出新数据了
Client
<!-- 会默认请求src 并将返回结果回显在iframe中 -->
<iframe src="/clock" frameborder="0"></iframe>
<script>
function setTime(ts) {
document.querySelector('#clock').innerHTML = ts;
}
</script>
serve
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.get('/clock', function (req,res){
res.header('Content-Type', 'text/html');
setInterval(function (){
res.write(`
<script>
parent.setTime("${new Date().toLocaleString()}")
</script>
`)
})
})
app.listen(8000)
问题在于client端一直处于请求未关闭状态,所以会tab栏上会一直转圈圈
EventSource流
EventSource是标准库的一个API,W3C:EventSource
Server-sent events (SSE) is a technology where a browser receives automatic updates from a server via HTTP connection. The Server-Sent Events EventSource API is standardized as part of HTML5[1] by the W3C.
大致翻译下就是:SSE是一种能让浏览器通过HTTP连接自动收到服务器端更新的技术,SSE EventSource 接口被W3C制定为HTML5的一部分。
client
let eventSource = new EventSource('/clock');
// 监听服务器发过来的消息
eventSource.onmessage = function (event) {
console.log('onmessage',event);
let message = event.data;
clock.innerHTML = message;
}
eventSource.onerror = function (event) {
}
server
关键在于三点
Content-Type设置为text/event-stream- 按照
event:message写出数据 - 写出数据以
\r\n结尾
res.header('Content-Type', 'text/event-stream');
setInterval(function (){
// res.write(`
// event:message
// `)
res.write(`
event:message\ndata:${new Date().toLocaleString()}\r\n
`)
},1000)
缺点:
- 无法跨域
- 只能支持服务端向客户端的推送
WebSocket & EventSource 的区别
- WebSocket基于TCP协议,EventSource基于http协议。
- EventSource是单向通信,而websocket是双向通信。
- EventSource只能发送文本,而websocket支持发送二进制数据。
- 在实现上EventSource比websocket更简单。
- EventSource有自动重连接(不借助第三方)以及发送随机事件的能力。
- websocket的资源占用过大EventSource更轻量。
- websocket可以跨域,EventSource基于http跨域需要服务端设置请求头。