WebSocket был предложен в 2008 году, а его протокол связи был сформулирован в качестве стандарта в 2011 году.
В отличие от http, веб-сокет поддерживает полнодуплексную связь (т. е. двустороннюю связь между клиентом и службой).До появления веб-сокета клиент и сервер обычно поддерживали длинные соединения с помощью опроса http и Comet.
Однако это, несомненно, приведет к потреблению ресурсов на стороне сервера, поскольку HTTP-запросы содержат длинные заголовочные файлы и передают мало полезной информации, что очень ресурсоемко.
Так родился websocket.Он не только экономит ресурсы и пропускную способность, но также может обеспечить долгосрочные связи.Пока клиент активно обменивается рукопожатием с сервером один раз, может быть реализована связь в реальном времени и технология push.
Я также писал статьи по теме раньше:Сокет чат,Используйте JS+socket.io+WebRTC+nodejs+express для создания простой версии удаленного видеочата.Однако модули используются Socket.IO, а глубокой оптимизации нет, и в обычной работе использовать его непросто. Иногда передний или задний конец будет отключен, и другая сторона не знает, когда веб-сервер или внутренний сервер перезапущен, передний конец не гарантирует соединение.
Итак, в этой статье мы будем использовать веб-сокет, чтобы сделать простую демонстрацию и добавить функции сердцебиения, отключения и повторного подключения.
Первый — это сервер, использующий node+ws-модульСоздайте службу веб-сокетов, создайте новый server.js в папке сервера и загрузите модуль ws после инициализации npm.
npm init -y
npm i ws
Внедрите модуль ws и создайте простую службу веб-сокетов.
const WebSocket = require('ws');
const port = 1024//端口
const pathname = '/ws/'//访问路径
new WebSocket.Server({port}, function () {
console.log('websocket服务开启')
}).on('connection', connectHandler)
function connectHandler (ws) {
console.log('客户端连接')
ws.on('error', errorHandler)
ws.on('close', closeHandler)
ws.on('message', messageHandler)
}
function messageHandler (e) {
console.info('接收客户端消息')
this.send(e)
}
function errorHandler (e) {
console.info('客户端出错')
}
function closeHandler (e) {
console.info('客户端已断开')
}
Интерфейсная часть также создает клиент доступа ws.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>
Title</title>
</head>
<body>
<script type="module">
const name = 'test'//连接用户名
let wsUrl = 'ws://127.0.0.1:1024/ws/?name=' + name
const ws = new WebSocket(wsUrl)
ws.onopen = function (e) {
console.log('开启')
ws.send(JSON.stringify({
ModeCode: "message",
msg: 'hello'
}))
}//连接上时回调
ws.onclose = function (e) {
console.log('关闭')
}//断开连接时回调
ws.onmessage = function (e) {
let data = JSON.parse(e.data)
console.log('收到消息' + data.msg)
ws.close()
}//收到服务端消息
ws.onerror = function (e) {
console.log('出错')
}//连接出错
</script>
</body>
</html>
Результаты фронтальной печати:
Сервер выводит результат:
Приведенные выше эффекты показывают, что достигается простейшее соединение ws.Далее давайте оптимизируем его.Чтобы уменьшить связь, мы сначала вводимeventBusОпубликуйте и подпишитесь, а затем создайте новый класс веб-сокета, который наследуется от нативного веб-сокета, потому что в нем мы должны делать сердцебиение и переподключение.
На стороне сервера мы сначала улучшаем сервер и проверяем соединение ws через фильтр обновления http.
Добавьте службу http на исходный сервер и выполните проверку пути
const http = require('http');
const server = http.createServer()
server.on("upgrade", (req, socket, head) => {//通过http.server过滤数据
let url = new URL(req.url, `http://${req.headers.host}`)
let name = url.searchParams.get('name')//获取连接标识
if(!checkUrl(url.pathname, pathname)) {//未按标准
socket.write('未按照标准访问');
socket.destroy();
return;
}
})
server.listen(port, () => {
console.log('服务开启')
})
//验证url标准
function checkUrl (url, key) {//判断url是否包含key
return - ~ url.indexOf(key)
}
После завершения httpServer мы улучшим службу websocket, сохраним каждого подключенного пользователя через прокси и реализуем добавления и удаления, и получим следующий полный сервер
const http = require('http');
const WebSocket = require('ws');
const port = 1024//端口
const pathname = '/ws/'//访问路径
const server = http.createServer()
class WebSocketServer extends WebSocket.Server {
constructor () {
super(...arguments);
this.webSocketClient = {}//存放已连接的客户端
}
set ws (val) {//代理当前的ws,赋值时将其初始化
this._ws = val
val.t = this;
val.on('error', this.errorHandler)
val.on('close', this.closeHandler)
val.on('message', this.messageHandler)
}
get ws () {
return this._ws
}
messageHandler (e) {
console.info('接收客户端消息')
let data = JSON.parse(e)
switch(data.ModeCode) {
case 'message':
console.log('收到消息' + data.msg)
this.send(e)
break;
case 'heart_beat':
console.log(`收到${this.name}心跳${data.msg}`)
this.send(e)
break;
}
}
errorHandler (e) {
this.t.removeClient(this)
console.info('客户端出错')
}
closeHandler (e) {
this.t.removeClient(this)
console.info('客户端已断开')
}
addClient (item) {//设备上线时添加到客户端列表
if(this.webSocketClient[item['name']]) {
console.log(item['name'] + '客户端已存在')
this.webSocketClient[item['name']].close()
}
console.log(item['name'] + '客户端已添加')
this.webSocketClient[item['name']] = item
}
removeClient (item) {//设备断线时从客户端列表删除
if(!this.webSocketClient[item['name']]) {
console.log(item['name'] + '客户端不存在')
return;
}
console.log(item['name'] + '客户端已移除')
this.webSocketClient[item['name']] = null
}
}
const webSocketServer = new WebSocketServer({noServer: true})
server.on("upgrade", (req, socket, head) => {//通过http.server过滤数据
let url = new URL(req.url, `http://${req.headers.host}`)
let name = url.searchParams.get('name')//获取连接标识
if(!checkUrl(url.pathname, pathname)) {//未按标准
socket.write('未按照标准访问');
socket.destroy();
return;
}
webSocketServer.handleUpgrade(req, socket, head, function (ws) {
ws.name = name//添加索引,方便在客户端列表查询某个socket连接
webSocketServer.addClient(ws);
webSocketServer.ws = ws
});
})
server.listen(port, () => {
console.log('服务开启')
})
//验证url标准
function checkUrl (url, key) {//判断url是否包含key
return - ~ url.indexOf(key)
}
Когда соединение отключено, только клиент может активно получить доступ к серверу для повторного подключения, поэтому у клиента больше функций, чем у сервера.Мы оптимизируем веб-сокет клиента и добавляем некоторые простые функции управления (подключение, отправка сообщения, отключение), кнопка, вот что следует отметить: текущее соединение должно быть закрыто перед следующим соединением, в противном случае это приведет к одновременному подключению нескольких клиентов, что приведет к снижению производительности.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>
Title</title>
</head>
<body>
<button id="connect">
连接
</button>
<button disabled
id="sendMessage">
发送
</button>
<button disabled
id="destroy">
关闭
</button>
<script type="module">
const name = 'test'//连接用户名
let connect = document.querySelector('#connect'),//连接按钮
sendMessage = document.querySelector('#sendMessage'),//发送按钮
destroy = document.querySelector('#destroy'),//关闭按钮
wsUrl = 'ws://127.0.0.1:1024/ws/?name=' + name,//连接地址
ws;
connect.addEventListener('click', connectWebSocket)
sendMessage.addEventListener('click', function (e) {
ws.send(JSON.stringify({
ModeCode: "message",
msg: 'hello'
}))
})
destroy.addEventListener('click', function (e) {
ws.close()
ws = null
})
function connectWebSocket () {
if(!ws) {//第一次执行,初始化或ws断开时可执行
ws = new WebSocket(wsUrl)
initWebSocket()
}
}
function initWebSocket () {
ws.onopen = function (e) {
setButtonState('open')
console.log('开启')
}//连接上时回调
ws.onclose = function (e) {
setButtonState('close')
console.log('关闭')
}//断开连接时回调
ws.onmessage = function (e) {
let data = JSON.parse(e.data)
console.log('收到消息' + data.msg)
}//收到服务端消息
ws.onerror = function (e) {
setButtonState('close')
console.log('出错')
}//连接出错
}
/*
* 设置按钮是否可点击
* @param state:open表示开启状态,close表示关闭状态
*/
function setButtonState (state) {
switch(state) {
case 'open':
connect.disabled = true
sendMessage.disabled = false
destroy.disabled = false
break;
case 'close':
connect.disabled = false
sendMessage.disabled = true
destroy.disabled = true
break;
}
}
</script>
</body>
</html>
Эффект следующий:
На этом этапе демо-версия нашего веб-сокета может быть запущена вручную. На этой основе мы инкапсулируем его и используем для внешней связи через eventBus. Конкретный процесс будет реализован вместе со следующим пульсом.
Механизм сердцебиения веб-сокета: как следует из названия, клиент отправляет на сервер уникальное пульсирующее сообщение через равные промежутки времени. Каждый раз, когда сервер получает сообщение, ему нужно только вернуть сообщение. В это время, если они все еще подключены, клиент Сообщение будет получено.Если оно не получено, это означает, что соединение отключено.В это время клиент будет активно переподключаться, чтобы завершить цикл.
Реализация сердцебиения также очень проста, вам нужно только использовать функцию обратного вызова для обработки задержки при первом подключении, в это время вам также нужно установить тайм-аут сердцебиения, если клиент отправляет сообщение в течение определенного периода времени, но сервер не возвращается, считается отключенным. Далее я пойму сердцебиение
//this.heartBeat ---> time:心跳时间间隔 timeout:心跳超时间隔
/*
* 心跳初始函数
* @param time:心跳时间间隔
*/
function startHeartBeat (time) {
setTimeout(() => {
this.sendMsg({
ModeCode: ModeCode.HEART_BEAT,
msg: new Date()
})
this.waitingServer()
}, time)
}
//延时等待服务端响应,通过webSocketState判断是否连线成功
function waitingServer () {
this.webSocketState = false//在线状态
setTimeout(() => {
if(this.webSocketState) {
this.startHeartBeat(this.heartBeat.time)
return
}
console.log('心跳无响应,已断线')
this.close()
//重连操作
}, this.heartBeat.timeout)
}
После того, как реализация пульса завершена, его нужно только вызвать в ws.onopen, и эффект будет следующим:
Затем идет часть повторного подключения.На самом деле вам нужно только создать новый обратный вызов задержки, который похож на сердцебиение, за исключением того, что он используется при сбое соединения, поэтому я не буду объяснять это здесь. Вот полная версия кода:
Часть веб-сокета:
import eventBus
from "./eventBus.js"
const ModeCode = {//websocket消息类型
MSG: 'message',//普通消息
HEART_BEAT: 'heart_beat'//心跳
}
export default class MyWebSocket extends WebSocket {
constructor (url, protocols) {
super(url, protocols);
return this
}
/*
* 入口函数
* @param heartBeatConfig time:心跳时间间隔 timeout:心跳超时间隔 reconnect:断线重连时间间隔
* @param isReconnect 是否断线重连
*/
init (heartBeatConfig, isReconnect) {
this.onopen = this.openHandler//连接上时回调
this.onclose = this.closeHandler//断开连接时回调
this.onmessage = this.messageHandler//收到服务端消息
this.onerror = this.errorHandler//连接出错
this.heartBeat = heartBeatConfig
this.isReconnect = isReconnect
this.reconnectTimer = null//断线重连时间器
this.webSocketState = false//socket状态 true为已连接
}
openHandler () {
eventBus.emitEvent('changeBtnState', 'open')//触发事件改变按钮样式
this.webSocketState = true//socket状态设置为连接,做为后面的断线重连的拦截器
this.heartBeat && this.heartBeat.time ? this.startHeartBeat(this.heartBeat.time) : ""//是否启动心跳机制
console.log('开启')
}
messageHandler (e) {
let data = this.getMsg(e)
switch(data.ModeCode) {
case ModeCode.MSG://普通消息
console.log('收到消息' + data.msg)
break;
case ModeCode.HEART_BEAT://心跳
this.webSocketState = true
console.log('收到心跳响应' + data.msg)
break;
}
}
closeHandler () {//socket关闭
eventBus.emitEvent('changeBtnState', 'close')//触发事件改变按钮样式
this.webSocketState = false//socket状态设置为断线
console.log('关闭')
}
errorHandler () {//socket出错
eventBus.emitEvent('changeBtnState', 'close')//触发事件改变按钮样式
this.webSocketState = false//socket状态设置为断线
this.reconnectWebSocket()//重连
console.log('出错')
}
sendMsg (obj) {
this.send(JSON.stringify(obj))
}
getMsg (e) {
return JSON.parse(e.data)
}
/*
* 心跳初始函数
* @param time:心跳时间间隔
*/
startHeartBeat (time) {
setTimeout(() => {
this.sendMsg({
ModeCode: ModeCode.HEART_BEAT,
msg: new Date()
})
this.waitingServer()
}, time)
}
//延时等待服务端响应,通过webSocketState判断是否连线成功
waitingServer () {
this.webSocketState = false
setTimeout(() => {
if(this.webSocketState) {
this.startHeartBeat(this.heartBeat.time)
return
}
console.log('心跳无响应,已断线')
try {
this.close()
} catch(e) {
console.log('连接已关闭,无需关闭')
}
this.reconnectWebSocket()
}, this.heartBeat.timeout)
}
//重连操作
reconnectWebSocket () {
if(!this.isReconnect) {
return;
}
this.reconnectTimer = setTimeout(() => {
eventBus.emitEvent('reconnect')
}, this.heartBeat.reconnect)
}
}
раздел index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>
Title</title>
</head>
<body>
<button id="connect">
连接
</button>
<button disabled
id="sendMessage">
发送
</button>
<button disabled
id="destroy">
关闭
</button>
<script type="module">
import eventBus
from "./js/eventBus.js"
import MyWebSocket
from './js/webSocket.js'
const name = 'test'//连接用户名
let connect = document.querySelector('#connect')
let sendMessage = document.querySelector('#sendMessage')
let destroy = document.querySelector('#destroy')
let myWebSocket,
wsUrl = 'ws://127.0.0.1:1024/ws/?name=' + name
eventBus.onEvent('changeBtnState', setButtonState)//设置按钮样式
eventBus.onEvent('reconnect', reconnectWebSocket)//接收重连消息
connect.addEventListener('click', reconnectWebSocket)
sendMessage.addEventListener('click', function (e) {
myWebSocket.sendMsg({
ModeCode: "message",
msg: 'hello'
})
})
destroy.addEventListener('click', function (e) {
myWebSocket.close()
})
function reconnectWebSocket () {
if(!myWebSocket) {//第一次执行,初始化
connectWebSocket()
}
if(myWebSocket && myWebSocket.reconnectTimer) {//防止多个websocket同时执行
clearTimeout(myWebSocket.reconnectTimer)
myWebSocket.reconnectTimer = null
connectWebSocket()
}
}
function connectWebSocket () {
myWebSocket = new MyWebSocket(wsUrl);
myWebSocket.init({//time:心跳时间间隔 timeout:心跳超时间隔 reconnect:断线重连时
time: 30 * 1000,
timeout: 3 * 1000,
reconnect: 10 * 1000
}, true)
}
/*
* 设置按钮是否可点击
* @param state:open表示开启状态,close表示关闭状态
*/
function setButtonState (state) {
switch(state) {
case 'open':
connect.disabled = true
sendMessage.disabled = false
destroy.disabled = false
break;
case 'close':
connect.disabled = false
sendMessage.disabled = true
destroy.disabled = true
break;
}
}
</script>
</body>
</html>
Конечный эффект заключается в следующем: даже если серверная служба закрыта или сеть отключена, клиент может поддерживать состояние повторного подключения.
Наконец, спасибо, что прочитали это. Если у вас есть какие-либо вопросы по статье, пожалуйста, укажите и обсудите
Прикрепите исходный код:Gitee