初识webScoket
一般前端跟后端的网络通信都是使用的是HTTP协议,但是HTTP有一个缺陷,通信只能由客户端发起, 例如:
前端展示用户名: 客户端向服务端发送请求,服务端返回数据,但是用户如果更改自己的用户名时客户端不能自动更新数据,需要客户端刷新页面再次请求服务端才能更新数据,那么怎么能让服务端向客户端推送数据,客户端实时自动更新展示数据呢?如果使用HTTP协议的话,可以采用轮询
(就是每隔一段时间客户端向服务端发送请求查询服务端的数据是否更新),这样非常浪费资源不能采取,我们就可以用WebSocket来解决这个问题,类似问题还有聊天室 手机扫码登录的情况
WebSocket
是一种网络通信协议,最大的特点是服务器能够主动向客户端推送数据,客户端也可以主动向服务端推送数据,属于服务器推送技术的一种 特点总结:
- 建立在TCP协议之上
- 于HTTP协议有着良好的兼容性,默认端口也是80和443,并且握手阶段采用HTTP协议,因此握手时不容易屏蔽,能通过各种HTTP代理服务器
- 数据格式比较轻量,性能开销小,通信高效
- 没有同源策略校址,客户端可以与任意服务器通信
- 可以发送文本,也可以发送二进制数据
- 协议标识符是ws/wss,服务器网址就是URL
前端开发中使用
浏览器提供了WebScoket
构造函数,使用时通过new新建一个WebSocket
实例
const ws = new WebSocket('wss://echo.websocket.org')
ws.onopen = function(evt) {
console.log("连接成功啦...")
ws.send("你好 WebSockets!")
};
ws.onmessage = function(evt) {
console.log( 获取发送的消息,evt)
ws.close()
};
ws.onclose = function(evt) {
console.log("断掉连接.")
}
复制代码
常用api
webSocket.onopen
实例对象的nopen
性,用于指定连接成功后的回调函数
ws.onopen = function(evt) {
console.log("连接成功啦...")
ws.send("你好 WebSockets!")
};
复制代码
如果要指定多个回调函数,可以使用addEventListener
ws.addEventListener('open', function (event) {
ws.send('Hello Server!')
});
复制代码
webSocket.readyState
获取饭就实例对象的当前状态
- CONNECTING:值为0,表示正在连接
- OPEN: 值为1, 表示连接成功.可以通信
- CLOSEING: 值为2,表示连接正在关闭
- CLOSED: 值为3,已经关闭,或者打开连接失败
console.log(ws.readState)
复制代码
webSocket.onclose
实例对象的onclose
属性,用于指定;连接关闭后的回调函数 如果要指定多个回调函数,也可使用`addEvenlistener
ws.onclose = function(event) {
const code = event.code
const reason = event.reason
const wasClean = event.wasClean
}
复制代码
webSocket.onmessage
实例对象的onmessage
属性,用于收到服务器数据后的回调函数 如果要指定多个回调函数,也可使用addEvenlistener
注意返回的数据格式可能是文本可用JSON.parse()
进行转换,也可能是二进制数据
ws.onmessage = function(event) {
let data = event.data;
consoole.log('服务端发送的数据',data)
};
复制代码
webSocket.send()
实例对象的send()
方法用于向服务器发送数据。
ws.send('your message');
复制代码
webSocket.onerror
实例对象的onerror
属性,用于指定报错时的回调函数
socket.onerror = function(event) {
// handle error event
};
socket.addEventListener("error", function(event) {
// handle error event
});
复制代码
Vue项目中封装使用
断线重连
我们将分析两种断线的原因
- webSocket超时没有消息自动断开连接
- websocket异常包括服务端出现中断,交互切屏等等客户端异常中断等等
判断是否在在线
- 当客户端第一次发送请求至服务端时会携带唯一标识、以及时间戳,服务端到db或者缓存去查询改请求的唯一标识,如果不存在就存入db或者缓存中
- 当客户端第一次发送请求至服务端时会携带唯一标识、以及时间戳,服务端到db或者缓存去查询改请求的唯一标识,如果不存在就存入db或者缓存中
- 得出的毫秒秒数判断是否大于指定的时间 ,若小于的话就是在线,否则就是离线
解决断线问题
- webSocket超时没有消息自动断开连接
应对:根据服务端设置的超时时长是多少,在小于超时时间内发送心跳包
在下一个定时器,在一定时间间隔下发送一个空包给客户端,客户端反馈一个同样的空包回来,服务器如果在一定时间内收不到客户端发送过来的反馈包,那就只有认定为掉线了
心跳包: 类比于心跳一样每隔固定的时间发送一次,用来告诉服务器客户端还存在保持着长连接,这个包的内容并没有特殊的规定,可以根据项目情况,客户端很服务端做好协商,在TCP中存在心跳包的机制,在TCP的选项中SO_KEEPALIVE
,系统默认设置的是2小时的心跳频率,但是检测不到机器断电,网线拔出,防火墙这些断线,一般可以用于保活
我们可以使用webSocket发送心跳包,心跳检测就是客户端定时的给服务端发送消息,证明客户端时在线的,如果超过一定的时间没有发送就是断线了 心跳检测步骤:
- 客户端每隔一个时间间隔发生一个探测包给服务器
- 客户端发包时启动一个超时定时器
- 服务器端接收到检测包,应该回应一个包
- 如果客户机收到服务器的应答包,则说明服务器正常,删除超时定时器
- 如果客户端的超时定时器超时,依然没有收到应答包,则说明服务器挂了
heartCheck() {
// 心跳机制的时间可以自己与后端约定
this.pingPong = 'ping'; // ws的心跳机制状态值
this.pingInterval = setInterval(() => {
if (this.ws.readyState === 1) {
// 检查ws为链接状态 才可发送
this.ws.send('ping'); // 客户端发送ping
}
}, 10000)
this.pongInterval = setInterval(() => {
if (this.pingPong === 'ping') {
this.closeHandle('pingPong没有改变为pong'); // 没有返回pong 重启webSocket
}
// 重置为ping 若下一次 ping 发送失败 或者pong返回失败(pingPong不会改成pong),将重启
console.log('返回pong')
this.pingPong = 'ping'
}, 20000)
}
复制代码
- websocket异常包括服务端出现中断,交互切屏等等客户端异常中断
应对: 重连处理,通过onclose关闭连接,服务端再次上线时则需要清除之前存的数据 咱们用引入reconnecting-websocket.min.js,ws建立链接方法使用js库api方法:
const ws = new ReconnectingWebSocket(url);
// 断线重连:
reconnectSocket(){
if ('ws' in window) {
ws = new ReconnectingWebSocket(url);
} else if ('MozWebSocket' in window) {
ws = new MozWebSocket(url);
} else {
ws = new SockJS(url);
}
}
复制代码
断网监测支持使用js库:offline.min.js
onLineCheck(){
Offline.check();
console.log(Offline.state,'---Offline.state');
console.log(this.socketStatus,'---this.socketStatus');
if(!this.socketStatus){
console.log('网络连接已断开!');
if(Offline.state === 'up' && websocket.reconnectAttempts > websocket.maxReconnectInterval){
window.location.reload();
}
reconnectSocket();
}else{
console.log('网络连接成功!');
websocket.send("heartBeat");
}
}
// 使用:在websocket断开链接时调用网络中断监测
websocket.onclose => () {
onLineCheck();
};
复制代码
项目中封装
- 新建socket.js文件
用于创建一个全局的WS类,可在每个组件中通过传递不同参数创建不同的webSocket实例
class WebSocketClass {
constructor(url) {
this.instance = null
this.connect(url,param)
}
static getInstance() {
if (!this.instance) {
this.instance = new WebSocketClass()
}
return this.instance
}
connect(url,param) {
// url 长连接地址, param:连接时向服务端发送的参数,可以用于服务端鉴权
this.ws = new WebSocket(`${url}/${cookie}`)
this.ws.onopen = e => {
this.status = 'open'
console.log(`连接成功`, e)
this.heartCheck()
this.getMessage()
}
}
heartCheck() {
// 心跳机制的时间可以自己与后端约定
this.pingPong = 'ping'; // ws的心跳机制状态值
this.pingInterval = setInterval(() => {
if (this.ws.readyState === 1) {
// 检查ws为链接状态 才可发送
this.ws.send('ping'); // 客户端发送ping
}
}, 10000)
this.pongInterval = setInterval(() => {
if (this.pingPong === 'ping') {
this.closeHandle('pingPong没有改变为pong'); // 没有返回pong 重启webSocket
}
// 重置为ping 若下一次 ping 发送失败 或者pong返回失败(pingPong不会改成pong),将重启
console.log('返回pong')
this.pingPong = 'ping'
}, 20000)
}
closeHandle(e = 'err') {
// 因为webSocket并不稳定,规定只能手动关闭(调closeMyself方法),否则就重连
if (this.status !== 'close') {
console.log(`断开,重连websocket`, e)
if (this.pingInterval !== undefined && this.pongInterval !== undefined) {
// 清除定时器
clearInterval(this.pingInterval)
clearInterval(this.pongInterval)
}
this.connect(); // 重连
} else {
console.log(`websocket手动关闭,或者正在连接`)
}
}
getMessage() {
this.ws.onmessage = e => {
if (e.data === 'pong') {
this.pingPong = 'pong'; // 服务器端返回pong,修改pingPong的状态
} else {
}
console.log(e.data)
return e.data
}
}
close() {
clearInterval(this.pingInterval)
clearInterval(this.pongInterval)
this.status = 'close'
this.ws.send('close')
this.ws.close()
console.log('close')
}
}
export default WebSocketClass
复制代码
在组件中使用
xxx.vue
import sokect from './socket.js'
...
method:{
initSocket () {
this.socketObj = new socket('wss://xxxx/message',data)
}
}
复制代码
原理分析
如何实现?
因为WebSocket是基于TCP通信的,WebSocket的长连接本质上就是TCP协议的长连接, 因为TCP本身就是持久连接,通过三次握手在操作系统内核内存汇总维护四元组对象(源ip,源port,目标ip,目标port)建立连接,TCP连接是有通信双方来决定什么时候结束通信,那么TCP自然就是一个持久连接,WebSocket协议实现独占一条TCP通道,它负责从TCP刘确定消息边界,解析出每个独立的消息包。可进行全双工的双向通信,通过PING/PON Frame 数据包在不影响Application的情况下维持住中间网络的连接状态。
WebSocket连接的过程
- 客服端发起http请求,经过3次握手后,建立起TCP连接; http请求里存放websocket支持的版本号等信息,如: Upgrade、Connection、websocket-Version等
Connection: Upgrade
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: IRQYhWINfX5Fh1zdocDl6Q==
Sec-WebSocket-Version: 13
Upgrade: websocket
复制代码
-
Connection
: Upgrade 表示要升级协议。 -
Upgrade
: Websocket 要升级协议到 websocket 协议。 -
Sec-WebSocket-Extensions
: 表示客户端所希望执行的扩展(如消息压缩插件)。 -
Sec-WebSocket-Key
: 主要用于WebSocket协议的校验,对应服务端响应头的Sec-WebSocket-Accept
。 -
Sec-WebSocket-Version
: 表示websocket
的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader
,里面包含服务端支持的版本号。
- 服务器收到客户端的握手请求后,同样采用http协议反馈数据
服务端响应的 HTTP Header 头信息如下:
Connection: upgrade
Sec-Websocket-Accept: TSF8/KitM+yYRbXmjclgl7DwbHk=
Upgrade: websocket
复制代码
-
Connection
: Upgrade 表示要升级协议。 -
Upgrade
: Websocket 要升级协议到 websocket 协议。 -
Sec-Websocket-Accept
: 对应Sec-WebSocket-Key
生成的值,主要是返回给客户端,让客户端对此值进行校验,证明服务端支持WebSocket
。
- 客户端收到来连接成功搞得消息后,开始借助于TCP传输信道进行全双工通信
总结
在使用研究过WebSocket之后较为深刻的理解了一下几点:
- WebSocket 是一种应用层协议,是为了提供 Web 应用程序和服务端全双工而专门制定的。
- WebSocket 和 HTTP 都是基于 TCP 协议实现的, WebSocket 使用 HTTP 来建立连接,但是定义了一系列新的 header 域,这些域在 HTTP 中并不会使用,不用频繁创建及销毁 TCP 请求,减少网络带宽资源的占用,同时也节省服务器资源;
- 没有同源限制,客户端可以与任意服务器通信。 WebSocket 的数据帧有序。
- WebSocket 是纯事件驱动的,一旦连接建立,通过监听事件可以处理到来的数据和改变的连接状态,数据都以帧序列的形式传输。服务端发送数据后,消息和事件会异步到达。
简而言之,WebSocket 是建立在设备 TCP/IP 堆栈之上的一个微型的传输层。其目的是为 Web 应用程序开发人员提供一个基本的尽可能接近原生的 TCP 通信层,同时添加一些抽象来消除某些在 Web 工作方式方面可能存在的兼容性。