Может быть много студентов, которые используют setInterval для управления ajax, чтобы постоянно запрашивать последние данные с сервера (опрос). См. следующий код:
setInterval(function() {
$.get('/get/data-list', function(data, status) {
console.log(data)
})
}, 5000)
Таким образом, фронтенд будет запрашивать данные из фона каждые 5 секунд.Реализация выглядит очень простой, но есть очень важная проблема, то есть мы не можем контролировать стабильность скорости сети, и мы не можем гарантировать результат предыдущего запроса при отправке следующего запроса. Он успешно вернулся, что неизбежно таит в себе скрытые опасности. Некоторые умные студенты сразу же подумают об использовании setTimeout с рекурсией, чтобы увидеть следующий код:
function poll() {
setTimeout(function() {
$.get('/get/data-list', function(data, status) {
console.log(data)
poll()
})
}, 5000)
}
Когда результат возвращается, следующий запрос задерживается и инициируется.Хотя нет способа гарантировать, что интервал между двумя запросами полностью соответствует, он может, по крайней мере, гарантировать, что ритм возврата данных стабилен.Независимо от его производительности ,структура кода не элегантная.Для решения этой проблемы сервер может долго держать соединение с клиентом для обмена данными.h5 Добавлены WebSocket и EventSource для реализации длительного опроса.Разберем характеристики и сценарии использования два.
WebSocket
что:WebSocket — это средство связи, основанное на протоколе TCP, порты по умолчанию — также 80 и 443, идентификатор протокола — ws (зашифрован как wss), реализует полнодуплексную связь между браузером и сервером и расширяет связь между браузером и сервером, так что сервер также может активно отправлять данные клиенту без междоменных ограничений.
Какая польза:WebSocket используется для решения проблемы, связанной с тем, что http не может быть постоянно подключен.Поскольку он может обмениваться данными в обоих направлениях, его можно использовать для реализации чатов и других функций, активно продвигаемых сервером, таких как погода в реальном времени, биржевые котировки, оставшиеся отображение голосов, уведомление о сообщении и т. д.
EventSource
что:Официальное название EventSource должно быть "Отправленные сервером события" (сокращенно SSE) — события отправки на стороне сервера. EventSource — это просто одноэлементное сообщение, основанное на протоколе http, которое реализует процесс отправки на стороне сервера. Клиент не может отправлять данные на сервер через EventSource. Хорошая новость в том, что у ie нет хорошей совместимости.Конечно, есть такие решения, какnpm install event-source-polyfill. Хотя он не может достичь двусторонней связи, он также имеет некоторые преимущества в функциональном дизайне, например, автоматическую переподключение, идентификаторы событий и возможность отправки случайных событий (Websocket может достигать переподключения с помощью сторонних библиотек, таких как сокет. IO.)
Какая польза:Из-за ограничения на одно взаимодействие EventSource можно использовать только для реализации таких сценариев, как биржевые котировки, рассылка новостей и прогноз погоды в реальном времени, которые требуют, чтобы сервер только отправлял сообщения клиенту. Использование EventSource более удобно, что также является его преимуществом.
Разница между WebSocket и EventSource
- WebSocket основан на протоколе TCP, а EventSource основан на протоколе http.
- EventSource — это односторонняя связь, а websocket — двусторонняя связь.
- EventSource может отправлять только текст, а websocket поддерживает отправку двоичных данных.
- EventSource проще, чем websocket в реализации.
- Eventsource имеет возможность автоматически подключиться (без помощи третьей стороны) и отправлять случайные события.
- Потребление ресурса Websocket слишком большие события более легкой.
- Websocket может быть междоменным, а EventSource требует, чтобы сервер устанавливал заголовки запросов на основе междоменного http.
Случай реализации EventSource
код клиента
// 实例化 EventSource 参数是服务端监听的路由
var source = new EventSource('/EventSource-test')
source.onopen = function (event) { // 与服务器连接成功回调
console.log('成功与服务器连接')
}
// 监听从服务器发送来的所有没有指定事件类型的消息(没有event字段的消息)
source.onmessage = function (event) { // 监听未命名事件
console.log('未命名事件', event.data)
}
source.onerror = function (error) { // 监听错误
console.log('错误')
}
// 监听指定类型的事件(可以监听多个)
source.addEventListener("myEve", function (event) {
console.log("myEve", event.data)
})
Код сервера (node.js)
const fs = require('fs')
const express = require('express') // npm install express
const app = express()
// 启动一个简易的本地server返回index.html
app.get('/', (req, res) => {
fs.stat('./index.html', (err, stats) => {
if (!err && stats.isFile()) {
res.writeHead(200)
fs.createReadStream('./index.html').pipe(res)
} else {
res.writeHead(404)
res.end('404 Not Found')
}
})
})
// 监听EventSource-test路由服务端返回事件流
app.get('/EventSource-test', (ewq, res) => {
// 根据 EventSource 规范设置报头
res.writeHead(200, {
"Content-Type": "text/event-stream", // 规定把报头设置为 text/event-stream
"Cache-Control": "no-cache" // 设置不对页面进行缓存
})
// 用write返回事件流,事件流仅仅是一个简单的文本数据流,每条消息以一个空行(\n)作为分割。
res.write(':注释' + '\n\n') // 注释行
res.write('data:' + '消息内容1' + '\n\n') // 未命名事件
res.write( // 命名事件
'event: myEve' + '\n' +
'data:' + '消息内容2' + '\n' +
'retry:' + '2000' + '\n' +
'id:' + '12345' + '\n\n'
)
setInterval(() => { // 定时事件
res.write('data:' + '定时消息' + '\n\n')
}, 2000)
})
// 监听 6788
app.listen(6788, () => {
console.log(`server runing on port 6788 ...`)
})
Клиентский доступhttp://127.0.0.1:6788/Вы увидите следующий вывод:
Подводя итог связанным API, API на стороне клиента очень просты и все в комментариях, а на стороне сервера есть несколько моментов, на которые следует обратить внимание:
формат потока событий?
Поток событий — это просто поток текстовых данных, и текст должен быть закодирован в формате UTF-8. Каждое сообщение отделяется пустой строкой. Строки комментариев к поведению, начинающиеся с двоеточия, игнорируются.
Для чего нужны аннотации?
Строки комментариев можно использовать для предотвращения истечения времени ожидания соединения, а сервер может периодически отправлять строки комментариев сообщения, чтобы поддерживать соединение.
Какие поля указаны в спецификации EventSource?
event:Тип события. Если это поле указано, когда клиент получает сообщение, событие будет инициировано в текущем объекте EventSource. Тип события — это значение поля этого поля. Вы можете использовать метод addEventListener() для добавления события к текущему EventSource Любой тип именованного события отслеживается в объекте.Если в сообщении нет поля события, будет запущен обработчик события в свойстве onmessage.data:Поле данных сообщения. Если сообщение содержит несколько полей данных, клиент будет использовать новую строку, чтобы объединить их в строку в качестве значения поля.id:Идентификатор события станет значением свойства внутреннего свойства «идентификатор последнего события» текущего объекта EventSource.retry:Целочисленное значение, указывающее время повторного подключения (в миллисекундах), которое игнорируется, если значение поля не является целым числом.
Для чего повторное подключение?
Как упоминалось выше, поле повтора используется для указания времени переподключения, так зачем же нам нужно переподключение? Возьмем в качестве примера узел. Всем известно, что для узла характерен однопоточный асинхронный ввод-вывод. серверная сторона сообщает об ошибке, служба будет остановлена.Конечно, эти исключения будут обрабатываться в процессе разработки узла, но как только служба будет остановлена, вам необходимо использовать такие инструменты, как pm2, чтобы перезапустить операцию.Хотя в это время сервер работает нормально, ссылка клиента EventSource Когда он все еще отключен, он используется для повторного подключения.
Почему в этом случае сообщение заканчивается на \n?
\n — управляющий символ для новой строки Спецификация EventSource предусматривает, что за каждым сообщением следует пустая строка в качестве разделителя, добавление \n в конце означает конец поля, а добавление двух \n означает конец поля. сообщение. (два \n означают, что после новой строки добавляется пустая строка)
Примечание. Если строка текста не содержит двоеточия, вся строка текста будет проанализирована как имя поля, а ее значение поля будет пустым.
Случай реализации WebSocket
Собственный API на стороне клиента для WebSocket
var ws = new WebSocket('ws://localhost:8080')Объект WebSocket используется в качестве конструктора для создания нового экземпляра WebSocket.
ws.onopen = function(){}Используется для указания функции обратного вызова после успешного подключения.
ws.onclose = function(){}Используется для указания функции обратного вызова после закрытия соединения.
ws.onmessage = function(){}Используется для указания функции обратного вызова после получения данных сервера
ws.send('data')Метод send() экземпляра объекта используется для отправки данных на сервер.
socket.onerror = function(){}Используется для указания функции обратного вызова при сообщении об ошибке
Как реализовать WebSocket на стороне сервера
В npm есть много пакетов, которые реализуют websocket, напримерsocket.io, WebSocket-Node, ws и многие другие. В этой статье мы лишь кратко анализируем socket.io и ws. Подробную информацию см. в официальной документации.
В чем разница между socket.io и ws
Socket.io:Socket.io — это библиотека WebSocket, включающая js на стороне клиента и nodejs на стороне сервера. Она автоматически выбирает лучший метод из WebSocket, длительного опроса AJAX, потоковой передачи Iframe и т. д. в зависимости от браузера для реализации сетевых приложений в реальном времени. (Если WebSocket не поддерживается, он будет понижен до опроса AJAX.) Это очень удобно и удобно, и совместимость очень хорошая. Минимальный поддерживаемый браузер - IE5.5. Защитите различия в деталях и проблемах совместимости, а также реализуйте двунаправленную передачу данных между браузерами и устройствами.
ws:В отличие отsocket.ioМодуль, ws — это чистый модуль websocket, не обеспечивает восходящую совместимость и не требует навешивания дополнительных js-файлов на стороне клиента. Клиенту не нужно использовать вторичный инкапсулированный API для связи с собственным API Websocket браузера.
Двунаправленная связь WebSocket на основе socket.io
код клиента
<button id="closeSocket">断开连接</button>
<button id="openSocket">恢复连接</button>
<script src="/socket.io/socket.io.js"></script>
<script>
// 建立连接 默认指向 window.location
let socket = io('http://127.0.0.1:6788')
openSocket.onclick = () => {
socket.open() // 手动打开socket 也可以重新连接
}
closeSocket.onclick = () => {
socket.close() // 手动关闭客户端对服务器的链接
}
socket.on('connect', () => { // 连接成功
// socket.id是唯一标识,在客户端连接到服务器后被设置。
console.log(socket.id)
})
socket.on('connect_error', (error) => {
console.log('连接错误')
})
socket.on('disconnect', (timeout) => {
console.log('断开连接')
})
socket.on('reconnect', (timeout) => {
console.log('成功重连')
})
socket.on('reconnecting', (timeout) => {
console.log('开始重连')
})
socket.on('reconnect_error', (timeout) => {
console.log('重连错误')
})
// 监听服务端返回事件
socket.on('serverEve', (data) => {
console.log('serverEve', data)
})
let num = 0
setInterval(() => {
// 向服务端发送事件
socket.emit('feEve', ++num)
}, 1000)
Код сервера (node.js)
const app = require('express')()
const server = require('http').Server(app)
const io = require('socket.io')(server, {})
// 启动一个简易的本地server返回index.html
app.get('/', (req, res) => {
res.sendfile(__dirname + '/index.html')
})
// 监听 6788
server.listen(6788, () => {
console.log(`server runing on port 6788 ...`)
})
// 服务器监听所有客户端 并返回该新连接对象
// 每个客户端socket连接时都会触发 connection 事件
let num = 0
io.on('connection', (socket) => {
socket.on('disconnect', (reason) => {
console.log('断开连接')
})
socket.on('error', (error) => {
console.log('发生错误')
})
socket.on('disconnecting', (reason) => {
console.log('客户端断开连接但尚未离开')
})
console.log(socket.id) // 获取当前连接进入的客户端的id
io.clients((error, ids) => {
console.log(ids) // 获取已连接的全部客户机的ID
})
// 监听客户端发送的事件
socket.on('feEve', (data) => {
console.log('feEve', data)
})
// 给客户端发送事件
setInterval(() => {
socket.emit('serverEve', ++num)
}, 1000)
})
/*
io.close() // 关闭所有连接
*/
const io = require('socket.io')(server, {})Второй параметр — это элемент конфигурации, который можно передать в следующих параметрах:
- путь: '/socket.io' фиксирует имя пути
- serveClient: false, следует ли обслуживать клиентские файлы
- pingInterval: 10000 временной интервал для отправки сообщений
- pingTimeout: 5000 В течение этого времени не было отключено ни одно соединение для передачи данных.
- происхождение: '*' разрешить перекрестное происхождение
- ...
В приведенной выше реализации на основе socket.ioexpressВ качестве зависимой сервисной основы для связи через сокетsocket.ioВ качестве сокетного коммуникационного модуля реализована двусторонняя передача данных. Наконец, важно отметить, что на стороне сервераemitРазличают следующие три случая:
-
socket.emit(): отправляется клиенту, установившему соединение -
socket.broadcast.emit(): отправляется всем клиентам, кроме клиента, установившего соединение -
io.sockets.emit(): отправить всем клиентам сумму, равную сумме двух вышеуказанных -
io.to(id).emit(): отправить событие клиенту с указанным идентификатором
Двунаправленная связь WebSocket на основе ws
код клиента
let num = 0
let ws = new WebSocket('ws://127.0.0.1:6788')
ws.onopen = (evt) => {
console.log('连接成功')
setInterval(() => {
ws.send(++ num) // 向服务器发送数据
}, 1000)
}
ws.onmessage = (evt) => {
console.log('收到服务端数据', evt.data)
}
ws.onclose = (evt) => {
console.log('关闭')
}
ws.onerror = (evt) => {
console.log('错误')
}
closeSocket.onclick = () => {
ws.close() // 断开连接
}
Код сервера (node.js)
const fs = require('fs')
const express = require('express')
const app = express()
// 启动一个简易的本地server返回index.html
const httpServer = app.get('/', (req, res) => {
res.writeHead(200)
fs.createReadStream('./index.html').pipe(res)
}).listen(6788, () => {
console.log(`server runing on port 6788 ...`)
})
// ws
const WebSocketServer = require('ws').Server
const wssOptions = {
server: httpServer,
// port: 6789,
// path: '/test'
}
const wss = new WebSocketServer(wssOptions, () => {
console.log(`server runing on port ws 6789 ...`)
})
let num = 1
wss.on('connection', (wsocket) => {
console.log('连接成功')
wsocket.on('message', (message) => {
console.log('收到消息', message)
})
wsocket.on('close', (message) => {
console.log('断开了')
})
wsocket.on('error', (message) => {
console.log('发生错误')
})
wsocket.on('open', (message) => {
console.log('建立连接')
})
setInterval(() => {
wsocket.send( ++num )
}, 1000)
})
Приведенный выше код находится вnew WebSocketServerкогда прошел вserver: httpServerЦель состоит в том, чтобы унифицировать порт.Хотя WebSocketServer может использовать другие порты, унифицированный порт все же является лучшим выбором.На самом деле экспресс не занимает напрямую порт 6788, а вызывает встроенный модуль http для создания http.Server и слушать 6788. Express просто регистрирует функцию ответа на http.Server. Точно так же WebSocketServer также может зарегистрировать свою собственную функцию ответа в http.Server, чтобы один и тот же порт мог обрабатываться express и ws соответственно в соответствии с протоколом. Мы получаем ссылку на http.Server, созданную экспрессом, а затем настраиваем ее в wssOptions.server, чтобы позволить WebSocketServer запускаться в соответствии с нашей входящей службой http, которая реализует цель унифицированного порта.
Всегда помните, что браузер по-прежнему отправляет стандартный HTTP-запрос при создании WebSocket. Будь то запрос WebSocket или обычный HTTP-запрос, он будет обработан http.Server. Конкретный метод обработки реализуется функциями обратного вызова, внедряемыми Express и WebSocketServer. WebSocketServer сначала определит, является ли запрос WS-запросом, если да, то обработает запрос, если нет, запрос все равно обрабатывается экспрессом. Таким образом, WS-запросы будут обрабатываться непосредственно WebSocketServer, они вообще не будут проходить через экспресс.
*Репозиторий кейсов: https://github.com/cp0725/YouChat/tree/master/webSocket-eventSource-test* *Некоторые концепции взяты из https://www.w3cschool.cn/socket/*