WebSocket
最近项目中使用到了WebSocket,在这里总结一下使用的方法和遇到的问题
首先介绍下什么是WebSocket
WebSocket是一种网络传输层协议,可在单个TCP链接上进行全双工通信,
位于OSI模型的应用层,WebSocket允许服务端向客户端主动推送数据。浏览器和我武器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
为什么要使用WebSocket(WebSocket与传统HTTP有什么优势)
- 客户端与服务端只建立一个TCP连接,可以使用更少的连接
- 服务器可以推送数据到客户端,比HTTP请求响应模式更灵活,更高效
- 更轻量级的协议头,减少数据传送量
WebSocket 握手
客户端建立连接时,通过HTTP发起请求报文
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13
与HTTP请求协议有区别的部分:
Upgrade: websocket Connection: Upgrade
这两个字段表示请求服务器端升级为WebSocket。
Sec-WebSocket-Key
用安全校验:
Sec-WebSocket-Key的值是随机生成的Base64编码的字符串。服务器端接收之后将其与字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11相连,行成dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11,然后通过sha1安全散列算法计算出结果,在进行Base64编码,最后返回给客户端。
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
上面两个字段指定子协议和版本号
服务端处理完请求后,响应报文如下:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade
状态码101表示切换协议,更新应用层协议为WebSocket协议,并在当前的套接字上应用新的协议
Sec-WebSocket-Accep:
表示服务端基于Sec-WebSocket-Key生成的字符串
Sec-WebSocket-Protocol:
表示最终使用的协议
客户端代码
let ws = new WebSocket('ws://192.168.10.40:3000/') ws.onopen = mes=>{ console.log(mes,'aaa') }
服务端代码
const net = require('net'); const crypto = require('crypto'); const wsGUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; net.createServer(function(socket) { socket.on('data', function(buffer) { // data 是buffer需要转化 const data = buffer.toString(), key = getWebSocketKey(data), acceptKey = crypto.createHash('sha1').update(key + wsGUID).digest('base64'), headers = [ 'HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', 'Sec-WebSocket-Accept: ' + acceptKey ]; socket.write(headers.concat('','').join('\r\n')); }) }).listen(3000) function getWebSocketKey(dataStr) { var match = dataStr.match(/Sec\-WebSocket\-Key:\s(.+)\r\n/); if (match) { return match[1]; } }
客户端的API
1.websocket 构造函数
WebSocket 对象作为一个构造函数,用于新建 WebSocket 实例。
var ws = new WebSocket('ws://localhost:8080');
执行上面语句之后,客户端就会与服务器进行连接。下面这张图是实例对象的所有属性和方法清单
2.webSocket.readyState
readyState属性返回实例对象的当前状态,共有四种。
console.log(ws.readyState) // 我们可以输入看当前是什么状态
- CONNECTING:值为0,表示正在连接。
- OPEN:值为1,表示连接成功,可以通信了。
- CLOSING:值为2,表示连接正在关闭。
- CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
3.webSocket.onopen
实例对象的onopen属性,用于指定连接成功后的回调函数。
ws.onopen = function () { ws.send('Hello Server!'); }
如果要指定多个回调函数,可以使用
addEventListener
方法。
ws.addEventListener('open', function (event) { ws.send('Hello Server!'); });
4.webSocket.onclose
实例对象的onclose属性,用于指定连接关闭后的回调函数。
ws.onclose = function(event) { var code = event.code; var reason = event.reason; var wasClean = event.wasClean; // handle close event };
5.webSocket.onmessage
实例对象的onmessage属性,用于指定收到服务器数据后的回调函数
ws.onmessage = function(event) { var data = event.data; // 处理数据 }; ws.addEventListener("message", function(event) { var data = event.data; // 处理数据 });
我们可以使用这个回调函数处理返回的数据相当于我们平时使用axios返回数据成功的then方法
6.webSocket.send()
实例对象的send()方法用于向服务器发送数据
ws.send('your message');
7.webSocket.bufferedAmount
实例对象的bufferedAmount属性,表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束。
var data = new ArrayBuffer(10000000); socket.send(data); if (socket.bufferedAmount === 0) { // 发送完毕 } else { // 发送还没结束 }
8.webSocket.onerror
实例对象的onerror属性,用于指定报错时的回调函数。
socket.onerror = function(event) { // handle error event }; socket.addEventListener("error", function(event) { // handle error event });
完整示例
服务端
import userInfoModel from '../model/userinfo'; import addendanceInfoModel from '../model/addentance'; import Websocket from 'ws'; import * as http from 'http'; const wss = new Websocket.Server({port: 3004}); wss.on('connection',function(ws:Websocket,req:http.IncomingMessage) { ws.on('message',async function(){ try{ // 获取基本数据 const userData = await userInfoModel.find(); ws.send(JSON.stringify(userData)); // 获取出勤数据 const addendanceData = await addendanceInfoModel.find(); ws.send(JSON.stringify(addendanceData)) }catch(err){ ws.close() } }) })
前端websocket
// Ws 封装一系列websock方法 // getInstance 获取当前类的实例 // initConnect 初始化连接,返回promise,连接成功resolve,失败reject // send 发送数据,包括断开重新连接 export default class Ws{ constructor(){ // 初始化连接 this.initConnect() } static getInstance() { if(!this.instance){ this.instance = new Ws() } return this.instance; } initConnect(){ // 创建ws实例 return new Promise((resolve,reject)=>{ this.ws = new WebSocket('ws://192.168.10.40:3004/'); this.ws.onmessage = event => this.message(JSON.parse(event.data)); this.ws.onopen = event => resolve(event); this.ws.onclose = event => reject(event); }) } async send(data){ // 如果为连接断开就重新连接 if(this.ws.readyState == 3 ){ // 初始化连接 await this.initConnect(); // 连接成功发送消息 data && this.ws.send(JSON.stringify(data)) }else{ // 发送消息 data && this.ws.send(JSON.stringify(data)) } } close() { if(this.ws.readyState == 3){ return; } this.ws.close(); } }
前端组件部分
<script> import Ws from '@/assets/websocket.js'; export default { name: 'HelloWorld', mounted() { this.ws = Ws.getInstance(); this.ws.message = this.onmessage; }, methods:{ onmessage(data) { console.log(data,'数据') }, OnClick() { this.ws.send({}) } } } </script>
websocket 遇到的坑
如果前端发送数据到后端,后端响应时间过长,可能会超出服务器的读取时间,排查问题,最后发现nginx设置了超时代理
proxy_read_timeout 60s; proxy_send_timeout 60s;
proxy_read_timeout 60s:
设置从后端读取超时,默认60s,他决定了nginx会等待多长时间获得请求的响应,超时nginx会关闭连接
proxy_send_timeout 60s;
设置发送请求给上游服务器的超时时间,如果60s内上游服务器没有收到任何响应,nginx关闭连接
我们只需找运维同学帮我们把代理时间延长,就不会出现超时时间了
我们在代码中也可以设置断开重连
initConnect(){ // 创建ws实例 return new Promise((resolve,reject)=>{ this.ws = new WebSocket('ws://192.168.10.40:3004/'); this.ws.onmessage = event => this.message(JSON.parse(event.data)); this.ws.onopen = event => resolve(event); this.ws.onclose = event => reject(event); }) } async send(data){ // 如果为连接断开就重新连接 if(this.ws.readyState == 3 ){ // 初始化连接 await this.initConnect(); // 连接成功发送消息 data && this.ws.send(JSON.stringify(data)) }else{ // 发送消息 data && this.ws.send(JSON.stringify(data)) } }
上面代码中,每次给后端发送数据,判断当前websocket连接状态,当状态是3(连接已断开)时,重新进行连接,连接成功给后端发送数据。