предисловие
Server Push — это общий термин для определенного типа технологии. В общем случае взаимодействие между клиентом и сервером выглядит следующим образом: клиент инициирует запрос, сервер получает запрос и возвращает ответ, а клиент получает ответ для обработки. Из приведенного выше процесса взаимодействия видно, что если клиент хочет получить данные, ему необходимо самостоятельно инициировать запрос к серверу для получения соответствующих данных.
В большинстве случаев достаточно «активного» поведения клиента. Однако в некоторых сценариях сервер должен «активно» передавать данные клиенту. Например:
- Чат или приложение для разговоров
- Мониторинг данных и статистика в режиме реального времени
- Канбан фондового финансирования и т. д.
Этот тип приложений имеет несколько важных характеристик: требуется высокая производительность в режиме реального времени, и клиент не может ожидать цикла обновления данных, когда сервер получает последние данные, информация должна быть синхронизирована с клиентом. Этот тип сценария приложения называется "Server Push".
Технология «проталкивания сервером» имеет долгую историю: от первоначального простого опроса до COMET, основанного на длительном опросе, до SSE спецификации HTML5 и полнодуплексного протокола WebSocket, технология «проталкивания сервером» продолжает развиваться. В этой статье будут представлены основные принципы и методы реализации этих технологий, которые помогут вам быстро понять и освоить основные принципы различных технологий "проталкивания сервера". Полный демо-адрес будет прикреплен в конце статьи.
1. Простой опрос
Простой опрос — простейший технический способ «решить» эту проблему.
Простой опрос по существу создает таймер на внешнем интерфейсе, запрашивает серверную службу через регулярные промежутки времени и выполняет соответствующую обработку, если есть данные.
function polling() {
fetch(url).then(data => {
process(data);
return;
}).catch(err => {
return;
}).then(() => {
setTimeout(polling, 5000);
});
}
polling();
В начале опроса отправьте запрос на задний конец. После ответа Интервал запросит данные в определенное время, поэтому взаимность цикла. Эффект следующий:
Преимущество такого подхода в том, что он очень прост и практически не требует дополнительной настройки или разработки.
В то же время недостатки тоже весьма очевидны. Во-первых, этот метод, эквивалентный опросу по времени, имеет явную задержку получения данных, для уменьшения задержки интервал опроса можно только сократить, с другой стороны, каждый опрос будет делать полный HTTP-запрос, если Отсутствие обновления данных эквивалентно «пустому» запросу, а также пустой трате ресурсов сервера.
Поэтому интервал опроса должен быть тщательно продуман. Если интервал опроса слишком длинный, пользователи не смогут вовремя получать обновленные данные, если интервал опроса слишком короткий, это вызовет слишком много запросов и увеличит нагрузку на сервер.
2. COMET
С развитием веб-приложений, особенно с разработкой требований к веб-приложениям и технологиями в эпоху web2.0 на основе ajax, все больше внимания стало уделяться чисто браузерной технологии "проталкивания сервера" Алекс Рассел (руководитель проекта Dojo Toolkit) Такая технология "проталкивания сервера", основанная на длительном HTTP-соединении без установки плагинов на стороне браузера, называется "Comet".
Обычно используемый COMET делится на два типа: технология длинного опроса (long-polling) на основе HTTP и режим потока (stream) с длинным соединением на основе iframe.
2.1 Длинный опрос на основе HTTP (long-polling)
В простом опросе мы делаем запросы к серверу через равные промежутки времени. Одна из самых больших проблем этого метода заключается в том, что задержка получения данных ограничена интервалом опроса, и невозможно получить данные, которые служба хочет отправить с первого раза.
Длинный опрос является улучшением этого. После того, как клиент инициирует запрос, сервер будет поддерживать соединение до тех пор, пока данные не будут обновлены в бэкэнде, прежде чем вернуть данные клиенту; клиент снова отправляет запрос после получения ответа и так далее. Картинка стоит тысячи слов о разнице между простым опросом и длинным опросом:
Таким образом, когда у сервера есть данные для отправки, они могут быть вовремя доставлены клиенту.
function query() {
fetchMsg('/longpolling')
.then(function(data) {
// 请求结束,触发事件通知eventbus
eventbus.trigger('fetch-end', {data, status: 0});
});
}
eventbus.on('fetch-end', function (result) {
// 处理服务端返回的数据
process(result);
// 再次发起请求
query();
});
Выше приведена сокращенная версия внешнего кода, который использует шину событий для уведомления об окончании запроса.После получения сообщения об окончанииprocess(result)
обрабатывать необходимые данные при повторном вызовеquery()
Обратиться с просьбой.
На стороне сервера, взяв узел в качестве примера, стороне сервера нужно возвращаться только тогда, когда она прослушивает обновление сообщения/данных.
const app = http.createServer((req, res) => {
// 返回数据的方法
const longPollingSend = data => {
res.end(data);
};
// 当有数据更新时,服务端“推送”数据给客户端
EVENT.addListener(MSG_POST, longPollingSend);
req.socket.on('close', () => {
console.log('long polling socket close');
// 注意在连接关闭时移除监听,避免内存泄露
EVENT.removeListener(MSG_POST, longPollingSend);
});
});
Эффект следующий:
2.2 режим длинного потока соединения на основе iframe
Когда мы встраиваем iframe в страницу и устанавливаем его src, сервер может «непрерывно» выводить контент клиенту через длинное соединение.
Например, мы можем вернуть клиенту фрагмент кода javascript, завернутый в тег скрипта, который будет выполняться в iframe. Итак, если мы предопределим функцию обработчика на родительской странице iframeprocess()
, и каждый раз, когда есть новые данные для отправки, пишите их в ответе соединения<script>parent.process(${your_data})</script>
. Затем этот код в iframe будет вызывать предварительно определенный на родительской страницеprocess()
функция. (Это немного похоже на то, как JSONP передает данные?)
// 在父页面中定义的数据处理方法
function process(data) {
// do something
}
// 创建不可见的iframe
var iframe = document.createElement('iframe');
iframe.style = 'display: none';
// src指向后端接口
iframe.src = '/long_iframe';
document.body.appendChild(iframe);
Бэкэнд по-прежнему использует узел в качестве примера
const app = http.createServer((req, res) => {
// 返回数据的方法,将数据拼装成script脚本返回给iframe
const iframeSend = data => {
let script = `<script type="text/javascript">
parent.process(${JSON.stringify(data)})
</script>`;
res.write(script);
};
res.setHeader('connection', 'keep-alive');
// 注意设置相应头的content-type
res.setHeader('content-type', 'text/html; charset=utf-8');
// 当有数据更新时,服务端“推送”数据给客户端
EVENT.addListener(MSG_POST, iframeSend);
req.socket.on('close', () => {
console.log('iframe socket close');
// 注意在连接关闭时移除监听,避免内存泄露
EVENT.removeListener(MSG_POST, iframeSend);
});
});
Эффект следующий:
Однако в использовании iframe есть небольшой недостаток, поэтому этот iframe эквивалентен тому, что он никогда не загружается, поэтому в браузере всегда будет флаг загрузки.
В целом две технологии COMET, long polling и iframe streaming, имеют хорошую практическую ценность, характеризуются очень сильной совместимостью и не требуют от клиента или сервера поддержки каких-то новых функций. Тем не менее, чтобы решить некоторые проблемы при использовании COMET, рекомендуется рассмотреть некоторые зрелые сторонние библиотеки в производственной среде. Стоит отметить, что Socket.io также возвращается к режиму длительного опроса в браузерах, несовместимых с WebSocket (о чем мы поговорим позже).
Однако технология COMET не является частью стандарта HTML5 и не рекомендуется с точки зрения совместимости со стандартом. (особенно после того, как у нас появятся другие технологии)
3. SSE (Server-Sent Events)
SSE (отправленные сервером события) является частью стандарта HTML5. Принцип его реализации аналогичен режиму длинного соединения на основе iframe, о котором мы упоминали в предыдущем разделе.
Содержимое HTTP-ответа имеет особый тип содержимого — text/event-stream. Этот заголовок ответа идентифицирует содержимое ответа как поток событий. Клиент не будет закрывать соединение, а будет ждать, пока сервер непрерывно отправит результат ответа.
Спецификация SSE относительно проста и в основном делится на две части: часть в браузереEventSource
объект и протокол связи между серверной и браузерной сторонами.
в браузере черезEventSource
конструктор для создания объекта
var source = new EventSource('/sse');
Содержимое ответа SSE можно рассматривать как поток событий, состоящий из различных событий. Эти события запускают внешний интерфейсEventSource
методы на объектах.
// 默认的事件
source.addEventListener('message', function (e) {
console.log(e.data);
}, false);
// 用户自定义的事件名
source.addEventListener('my_msg', function (e) {
process(e.data);
}, false);
// 监听连接打开
source.addEventListener('open', function (e) {
console.log('open sse');
}, false);
// 监听错误
source.addEventListener('error', function (e) {
console.log('error');
});
EventSource
Работает с помощью обработчиков событий. Обратите внимание, что приведенный выше код слушаетmy_msg
События, SSE поддерживает настраиваемые события, события по умолчанию отслеживаютсяmessage
чтобы получить данные.
В SSE каждое событие состоит из двух частей: типа и данных, и каждое событие может иметь необязательный идентификатор. Содержимое различных событий разделено пустыми строками ("\r\n"), содержащими только возврат каретки и перевод строки. Данные для каждого события могут состоять из нескольких строк.
- Тип пустой, что указывает на то, что строка является комментарием и будет игнорироваться при обработке.
- Тип — данные, указывающие, что строка содержит данные. Строки, начинающиеся с данных, могут появляться несколько раз. Все эти строки являются данными для этого события.
- Тип — это событие, указывающее тип события, для объявления которого используется строка. Когда браузер получает данные, он генерирует события соответствующего типа. Например, я настроил выше
my_msg
мероприятие. - Тип — id, который представляет собой идентификатор, используемый этой строкой для объявления события.
- Тип — retry, указывающий, что эта строка используется для объявления времени ожидания браузера перед повторным подключением после отключения.
Видно, что SSE действительно является относительно простой спецификацией протокола, а реализация сервера относительно проста:
const app = http.createServer((req, res) => {
const sseSend = data => {
res.write('retry:10000\n');
res.write('event:my_msg\n');
// 注意文本数据传输
res.write(`data:${JSON.stringify(data)}\n\n`);
};
// 注意设置响应头的content-type
res.setHeader('content-type', 'text/event-stream');
// 一般不会缓存SSE数据
res.setHeader('cache-control', 'no-cache');
res.setHeader('connection', 'keep-alive');
res.statusCode = 200;
res.write('retry:10000\n');
res.write('event:my_msg\n\n');
EVENT.addListener(MSG_POST, sseSend);
req.socket.on('close', () => {
console.log('sse socket close');
EVENT.removeListener(MSG_POST, sseSend);
});
});
Эффект следующий:
Кроме того, мы также можем рассмотреть возможность использования SSE в сочетании с преимуществами HTTP/2. Однако, возможно, не очень хорошая новость заключается в том, что IE/Edge несовместим.
Конечно, вы можете каким-то образом написать совместимый с IE полифилл. Однако, поскольку объект XMLHttpRequest в IE не поддерживает получение части содержимого ответа, вместо него можно использовать только XDomainRequest.Конечно, это также приводит к некоторым небольшим проблемам. Если вас интересуют конкретные детали реализации, вы можете взглянуть на эту библиотеку полифиллов.Yaffle/EventSource.
4. WebSocket
WebSocket, как и протокол http, основан на TCP. На самом деле WebSocket не ограничивается «проталкиванием сервером», это полнодуплексный протокол, подходящий для сценариев, требующих сложной двусторонней передачи данных. Так что есть и более сложные спецификации.
Когда клиент хочет установить соединение WebSocket с сервером, во время процесса рукопожатия между клиентом и сервером клиент сначала отправляет HTTP-запрос на сервер, включаяUpgrade
Заголовок запроса, чтобы сообщить серверу, что клиент хочет установить соединение WebSocket.
Установить соединение WebSocket на стороне клиента очень просто:
var ws = new WebSocket('ws://127.0.0.1:8080');
Конечно, что-то вродеHTTP
а такжеHTTPS
,ws
Соответственно есть иwss
Используется для установки безопасного соединения.
Заголовок запроса на данный момент выглядит следующим образом: (обратите внимание на поле Upgrade)
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cache-Control: no-cache
Connection: Upgrade
Cookie: Hm_lvt_4e63388c959125038aabaceb227cea91=1527001174
Host: 127.0.0.1:8080
Origin: http://127.0.0.1:8080
Pragma: no-cache
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: 0lUPSzKT2YoUlxtmXvdp+w==
Sec-WebSocket-Version: 13
Upgrade: websocket
Сервер обрабатывает запрос после получения запроса, и заголовок ответа выглядит следующим образом
Connection: Upgrade
Origin: http://127.0.0.1:8080
Sec-WebSocket-Accept: 3NOOJEzyscVfEf0q14gkMrpV20Q=
Upgrade: websocket
Указывает, что протокол WebSocket был обновлен.
Обратите внимание, что в приведенном выше заголовке запроса естьSec-WebSocket-Key
, который имеет мало общего с шифрованием и безопасностью.Основная функция — проверить, действительно ли сервер правильно «понимает» WebSocket и действительно ли соединение WebSocket. Сервер будет использоватьSec-WebSocket-Key
, и по фиксированному алгоритму
mask = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // 一个规定的字符串
accept = base64(sha1(key + mask));
генерироватьSec-WebSocket-Accept
Поля заголовка ответа, которые проверяются браузером.
Далее приятна двусторонняя связь между браузером и сервером.
Ввиду недостатка места конкретные спецификации и детали протокола WebSocket (такие как формат фрейма данных, проверка сердцебиения и т. д.) здесь обсуждаться не будут.Таких хороших статей в Интернете также много.Заинтересованные читатели также могут прочитать последняя часть этой статьи.
Ниже приведено краткое введение в использование WebSocket.
На стороне браузера после установления соединения WebSocket вы можете использоватьonmessage
Для прослушивания информации о данных.
var ws = new WebSocket('ws://127.0.0.1:8080');
ws.onopen = function () {
console.log('open websocket');
};
ws.onmessage = function (e) {
var data = JSON.parse(e.data);
process(data);
};
На стороне сервера, потому что протокол Websocket имеет много спецификаций и деталей для решения, рекомендуется использовать некоторые сторонние библиотеки с относительно полной инкапсуляцией. Например, в узлеwebsocket-nodeи знаменитыйsocket.io. Конечно, есть много других языковРеализация с открытым исходным кодом. Узловая часть кода выглядит следующим образом:
const http = require('http');
const WebSocketServer = require('websocket').server;
const app = http.createServer((req, res) => {
// ...
});
app.listen(process.env.PORT || 8080);
const ws = new WebSocketServer({
httpServer: app
});
ws.on('request', req => {
let connection = req.accept(null, req.origin);
let wsSend = data => {
connection.send(JSON.stringify(data));
};
// 接收客户端发送的数据
connection.on('message', msg => {
console.log(msg);
});
connection.on('close', con => {
console.log('websocket close');
EVENT.removeListener(MSG_POST, wsSend);
});
// 当有数据更新时,使用WebSocket连接来向客户端发送数据
EVENT.addListener(MSG_POST, wsSend);
});
Эффект следующий:
напиши в конце
Как особый тип технологии, Server Push играет важную роль в некоторых бизнес-сценариях.Понимание принципов и характеристик различных технологий поможет нам сделать определенный выбор в реальных бизнес-сценариях.
Для того, чтобы облегчить понимание содержания в тексте, я организовал все коды в демо, и заинтересованные друзья могут перейти наздесьЗагрузите его и запустите локально.
использованная литература
- w3c: Server-Sent Events
- w3c: The WebSocket API
- Comet: технология «Server Push», основанная на постоянном соединении HTTP
- События, отправляемые сервером HTML5, практическая разработка
- What is Sec-WebSocket-Key for?
- Deep dive into WebSockets and HTTP/2 with SSE + how to pick the right path
- MDN: Server Sent Event
- The WebSocket Protocol