一、什么是Websocket
WebSocket 是一种在单个 TCP 连接上进行 全双工 通信的协议,它可以让客户端和服务器之间进行实时的双向通信。
WebSocket 使用一个长连接,在客户端和服务器之间保持持久的连接,从而可以实时地发送和接收数据。
在 WebSocket 中,客户端和服务器之间可以互相发送消息,客户端可以使用 JavaScript 中的 WebSocket API 发送消息到服务器,也可以接收服务器发送的消息。
二、Websocket特点
简单来说,websocket 具有 双向通信,实时性强,支持二进制,控制开销 的特点。
1、协议标识符是ws
(如果加密,则为wss
),服务器网址就是 URL。
2、实时通信,服务器可以随时主动给客户端下发数据。
3、保持连接状态,Websocket需要先创建连接,所以是一种有状态的协议,之后通信时就可以省略部分状态信息。
4、控制开销,连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,头部还需要加上额外的4字节的掩码。
5、实现简单,建立在 TCP 协议之上,服务器端的实现比较容易,并且没有同源限制,客户端可以与任意服务器通信。
6、支持二进制传输,Websocket定义了二进制帧,可以发送文本,也可以发送二进制数据。
7、与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
8、支持扩展,用户可以扩展协议、实现部分自定义的子协议,如部分浏览器支持压缩等。
三、WebSocket与HTTP的区别
websocket和http都是基于TCP的应用层协议,使用的也是 80 端口(若运行在 TLS 之上时,默认使用 443 端口)。
其区别主要就在于连接的性质和通信方式。
WebSocket是一种双向通信的协议,通过一次握手即可建立持久性的连接,服务器和客户端可以随时发送和接收数据。
而HTTP协议是一种请求-响应模式的协议,每次通信都需要发送一条请求并等待服务器的响应。
WebSocket的实时性更好,延迟更低,并且在服务器和客户端之间提供双向的即时通信能力,适用于需要实时数据传输的场景。
四、常见应用场景
- 实时聊天:WebSocket能够提供双向、实时的通信机制,使得实时聊天应用能够快速、高效地发送和接收消息,实现即时通信。
- 实时协作:用于实时协作工具,如协同编辑文档、白板绘画、团队任务管理等,团队成员可以实时地在同一页面上进行互动和实时更新。
- 实时数据推送:用于实时数据推送场景,如股票行情、新闻快讯、实时天气信息等,服务器可以实时将数据推送给客户端,确保数据的及时性和准确性。
- 多人在线游戏:实时的双向通信机制,适用于多人在线游戏应用,使得游戏服务器能够实时地将游戏状态和玩家行为传输给客户端,实现游戏的实时互动。
- 在线客服:WebSocket可以用于在线客服和客户支持系统,实现实时的客户沟通和问题解决,提供更好的用户体验,减少等待时间。
五、websocket实例
为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息"Upgrade: WebSocket"表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。
1)Websocket事件
- onopen: 客户端和服务器建立连接后触发,被称为客户端和服务器之间的初始握手。如果接收到open, 说明已经连接成功,可以进行通信了。
- onmessage: 接收到消息时触发。服务器发送给客户端的消息可包括纯文本消息,二进制数据(Blob消息或者ArrayBuffer消息)。
- onerror: 响应意外故障时触发,在错误之后总是会终止连接。
- onclose:连接关闭时触发。一旦连接关闭后,客户端和服务端将不会再进行消息的收发。也可主动调用close()方法关闭连接。
2)Websocket方法
- send() : 在连接成功后关闭前,发送消息(onopen后和onclose前才可发送消息)
- close() : 关闭连接
3)Websocket对象属性
- readyState:只读属性,表示Websocket的连接状态。
- CONNECTING — 正在连接中,对应的值为 0;
- OPEN — 已经连接并且可以通讯,对应的值为 1;
- CLOSING — 连接正在关闭,对应的值为 2;
- CLOSED — 连接已关闭或者没有连接成功,对应的值为 3。
- bufferedAmount:只读属性。已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。
- Protocol:打开握手期间使用的协议。
4)websocket请求报文
// websocket握手:客户端发送的请求头
GET wss://www.example.cn/webSocket HTTP/1.1 // 使用的https协议, 对应的wss请求
Host: www.example.cn
Connection: Upgrade // 带upgrade头的http1.1消息必须含有connection头,表示任何接受此消息的人都在转发此消息之前处理掉connection中指定的域(即不转发upgrade域)
Upgrade: websocket // 定义转换协议的header域,如果服务器支持,客户端希望使用已经建立好的http(tcp)连接
Sec-WebSocket-Version: 13 // 客户端支持的WebSocket协议的版本列表
Origin: http://example.cn // Origin为安全使用,防止跨站攻击,浏览器一般会使用这个来标识原始域,类似于Referer。但与Referer 不同的是,Origin 只包含了协议和主机名称
Sec-WebSocket-Key: afmbhhBRQuwCLmnWDRWHxw== // 客户端随机生成的字符串,服务器会使用此字段组装成另一个key值(构造出一个SHA-1 的信息摘要)放在握手返回信息里,用于客户端到服务器websocket的初始握手,避免夸协议攻击
Sec-WebSocket-Protocol: chat, superchat // 首标,告诉客户端应用程序可使用的协议
Sec-WebSocket-Extensions: permessage-deflate // 首标,permessage-deflate:协商使用传输数据压缩,client_max_window_bits:擦采用LZ77压缩算法时,滑动窗口相关SIZE大小
// websocket握手:服务器发出的响应头
HTTP/1.1 101 // 服务端响应101状态码、Upgrade和Sec-WebSocket-Accept首标才算连接成功,否则不能连接成功。
Server: nginx/1.12.2
Date: Sat, 11 Aug 2018 13:21:27 GMT
Connection: upgrade // 指定一项或多项协议名,按优先级排序,以逗号分隔,这里表示升级为 WebSocket 协议
Upgrade: websocket
Sec-WebSocket-Accept: sLMyWetYOwus23qJyUD/fa1hztc= // 根据Sec-WebSocket-Key加上特殊字符串,计算SHA-1摘要,再进行 Base64编码协议生成(可尽量避免普通HTTP请求被误认为WebSocket协议)
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15
5)简单应用示例
<template>
<div>
<input type="text" :value="inputVal" />
<button @click="handleSend">send</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const inputVal = ref("");
const handleSend = () => {
socket.send(inputVal.value);
};
// 创建WebSocket对象,并指定服务器的地址
const socket = new WebSocket("ws://82.157.123.54:9010/ajaxchattest");
// 与服务器建立连接:发送消息到服务器
socket.onopen = () => {
console.log("connect: ");
};
// 收到服务器发送的消息:event处理服务器返回的数据
socket.onmessage = (event) => {
console.log("receive: ", event.data);
};
// 连接或通信过程中发生错误
socket.onerror = (event) => {
console.log("errror: ", event.error);
};
// 与服务器断开连接
socket.onclose = (event) => {
console.log("close: ", event.code);
};
</script>
六、websocket心跳机制
1、作用:使 WebSocket 连接保持长连接,避免断开连接的情况发生。同时,心跳机制也可以检查WebSocket连接的状态,及时处理异常情况。还可以减少WebSocket连接及服务器资源的消耗。
2、原理:是利用心跳包及时发送和接收数据,保证WebSocket长连接不被断开。
3、详细流程:
-
客户端建立WebSocket连接。
-
客户端向服务器发送心跳数据包(心跳包是指在一定时间间隔内,WebSocket发送的空数据包),服务器接收并返回一个表示接收到心跳数据包的响应。
-
当服务器没有及时接收到客户端发送的心跳数据包时,服务器会发送一个关闭连接的请求。
-
服务器定时向客户端发送心跳数据包,客户端接收并返回一个表示接收到心跳数据包的响应。
-
当客户端没有及时接收到服务器发送的心跳数据包时,客户端会重新连接WebSocket
4、实现方式
-
使用setInterval定时发送心跳包。对服务器造成很大的压力,因为即使WebSocket连接正常,也要定时发送心跳包,从而消耗服务器资源。
-
在前端监听到WebSocket的onclose()事件时,重新创建WebSocket连接。减轻了服务器的负担,但是在重连时可能会丢失一些数据。
5、WebSocket重连
重连意思就是在WebSocket断开之后重新建立连接,这里指由于异常断开需要重新连接。
常用实现方法有下:
1)前端监听WebSocket的onclose()事件,重新创建WebSocket连接。
2)使用WebSocket插件或库,例如Sockjs、Stompjs等。
3)使用心跳机制检测WebSocket连接状态,自动重连。
4)使用断线重连插件或库,例如ReconnectingWebSocket等。
6、通过WebSocket心跳机制,实现重连
思路: 在建立长连接的时候开启心跳 > 通过和服务端发送信息,得到服务端给返回的信息,然后重置心跳 > 清除时间,再重新开启心跳。(如果网络断开的话,会执行方法,重新连接)
<template>
<div>
<input type="text" :value="inputVal" />
<button @click="handleSend">send</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const inputVal = ref("");
let ws: any = null; // websocket实例
let isConnect: Boolean = false; // 连接标识 避免重复连接
let reciveCode: any = null; // 断线重连后,延迟5秒重新创建WebSocket连接,reciveCode用来存储延迟请求的代码
let timeoutObj: any = null; // 延时发送消息对象(启动心跳时新建这个对象,收到消息后重置这个对象)
// websocket重连
const reConnect = () => {
// 如果已经连上就不在重连了
if (isConnect) {
return;
}
clearTimeout(reciveCode);
// 延迟5秒重连 避免过多次过频繁请求重连
reciveCode = setTimeout(() => {
newWebSocket();
}, 5000);
};
// 创建并初始化websocket实例
const newWebSocket = () => {
// 判断当前环境是否支持Websocket
if (window.WebSocket) {
if (!ws) {
// 创建WebSocket对象,并指定服务器的地址
ws = new WebSocket("ws://82.157.123.54:9010/ajaxchattest");
}
// 与服务器建立连接
ws.onopen = () => {
console.log("connect: ");
isConnect = true;
// 5分钟发一次心跳,比server端设置的连接时间稍微小一点,在接近断开的情况下以通信的方式去重置连接时间。
timeoutObj = setTimeout(() => {
if (isConnect) ws.send(inputVal.value);
}, 300000);
};
// 收到服务器发送的消息
ws.onmessage = (event: any) => {
console.log("receive: ", event, event.data);
clearTimeout(timeoutObj);
};
// 连接或通信过程中发生错误
ws.onerror = (event: any) => {
console.log("errror: ", event.error);
isConnect = false; //连接错误 需要重连
reConnect();
};
// 与服务器断开连接
ws.onclose = (event: any) => {
console.log("close: ", event.code);
isConnect = false;
// 连接错误 需要重连
reConnect();
};
} else {
console.log("当前浏览器不支持websocket");
}
};
const handleSend = () => {
newWebSocket();
if (ws.readyState === ws.OPEN) {
// ws开启状态
ws.send(inputVal.value);
} else if (ws.readyState === ws.CONNECTING) {
// 正在开启状态,则等待1s后重新调用
setTimeout(function () {
handleSend();
}, 1000);
} else {
// 若未开启 ,则等待1s后重新调用
setTimeout(function () {
handleSend();
}, 1000);
}
};
</script>