первый
Длинное соединение: Несколько пакетов данных могут быть отправлены непрерывно по одному соединению.Во время соединения, если пакеты данных не отправляются, обеим сторонам необходимо отправить пакеты проверки соединения.
TCP/IP: TCP/IP относится к транспортному уровню, который в основном решает проблему передачи данных в сети и только передает данные. Однако для передаваемых данных не предусмотрена стандартная инкапсуляция, анализ и т. д., что затрудняет идентификацию передаваемых данных, поэтому для инкапсуляции и анализа данных существует протокол прикладного уровня, например протокол HTTP.
HTTP: HTTP — это протокол прикладного уровня, который инкапсулирует и анализирует передаваемые данные. Начиная с HTTP 1.1, длинные соединения были включены по умолчанию, то есть Connection: Keep-alive отображается в заголовке запроса. Но это длинное соединение просто сохраняется (сервер может указать клиенту сохранить время Keep-Alive:timeout=200;max=20;) Этот TCP-канал, непосредственно запрос-ответ, не требует создания другого канала соединения, и достигает оптимизации производительности. Но сама HTTP-связь по-прежнему является запросом-ответом.
socket: В отличие от HTTP, сокет не является протоколом, это инкапсуляция интерфейса протокола транспортного уровня (который в основном можно понимать как TCP/IP) на программном уровне. Мы знаем, что протокол транспортного уровня предназначен для передачи данных в сети, поэтому сокет является интерфейсом на обоих концах канала передачи. Таким образом, для внешнего интерфейса сокет также может быть просто понят как абстрактный протокол для TCP/IP.
WebSocket: WebSocket упакован как протокол прикладного уровня в виде сокета, так что клиент и удаленный сервер могут установить полнодуплексную связь через Интернет. Websocket предоставляет две схемы URL, ws и wss.Английский документ соглашенияа такжекитайский перевод
WebSocket API
Создайте соединение WebSocket с помощью конструктора WebSocket, возвращающего экземпляр websocket. Через этот экземпляр мы можем прослушивать события, которые знают, когда возобновляется соединение, когда отправляется сообщение, когда возникает ошибка и когда соединение закрывается. Мы можем использовать узел для создания сервера WebSocket, чтобы увидеть,github. Вы также можете позвонитьwebsocket.orgДемонстрационный сервер сайтаdemos.kaazing.com/echo/.
мероприятие
//创建WebSocket实例,可以使用ws和wss。第二个参数可以选填自定义协议,如果多协议,可以以数组方式
var socket = new WebSocket('ws://demos.kaazing.com/echo');
-
open
Сервер инициирует соответствующий запрос на подключение WebSocket.
socket.onopen = (event) => { socket.send('Hello Server!'); };
-
message
У сервера есть данные ответа для запуска
socket.onmessage = (event) => { debugger; console.log(event.data); };
-
error
Запускается по ошибке и закрывает соединение. В настоящее время его можно обрабатывать по мере необходимости в соответствии с сообщением об ошибке.
socket.onerror = (event) => { console.log('error'); }
-
close
连接关闭时触发,这在两端都可以关闭。另外如果连接失败也是会触发的。 针对关闭一般我们会做一些异常处理,关于异常参数: 1. socket.readyState 2 正在关闭 3 已经关闭 2. event.wasClean [Boolean] true 客户端或者服务器端调用close主动关闭 false 反之 3. event.code [Number] 关闭连接的状态码。socket.close(code, reason) 4. event.reason [String] 关闭连接的原因。socket.close(code, reason) socket.onclose = (event) => { debugger; }
метод
-
send
отправить (данные) метод передачи данные могут быть String/Blob/ArrayBuffer/ByteBuffer и т. д.
Следует отметить, что использовать send для отправки данных необходимо после установления соединения. Обычно отправляется после запуска события onopen:
socket.onopen = (event) => { socket.send('Hello Server!'); };
Если вам нужно отреагировать на другие события, а затем отправить сообщение, то есть передать сокет экземпляра WebSocket другому методу для использования, потому что вы не обязательно знаете, подключен ли еще сокет при отправке, поэтому вы можете проверить, значение свойства readyState равно константе OPEN, то есть посмотреть, подключен ли еще сокет.
btn.onclick = function startSocket(){ //判断是否连接是否还存在 if(socket.readyState == WebSocket.OPEN){ var message = document.getElementById("message").value; if(message != "") socket.send(message); } }
-
close
Используйте метод close([code[reason]]) для закрытия соединения. И код, и причина являются необязательными
// 正常关闭 socket.close(1000, "closing normally");
постоянный
постоянное имя | стоимость | описывать |
---|---|---|
CONNECTING | 0 | Соединение не было открыто |
OPEN | 1 | Соединение открыто для общения |
CLOSING | 2 | соединение закрывается |
CLOSED | 3 | соединение было закрыто |
Атрибуты
Имя свойства | тип значения | описывать |
---|---|---|
binaryType | String | Строка, представляющая двоичный тип данных, передаваемый соединением. По умолчанию «клякса». |
bufferedAmount | Number | Только чтение. Если данные, отправленные с помощью метода send(), слишком велики, несмотря на то, что метод send() будет выполнен немедленно, данные не будут переданы немедленно. Браузер будет кэшировать данные, выходящие из приложения, вы можете использовать свойство bufferedAmount, чтобы проверить размер данных, которые были поставлены в очередь, но еще не переданы. В определенной степени насыщения сети можно избежать. |
protocol | String/Array | В конструкторе параметр протокола позволяет серверу узнать протокол WebSocket, используемый клиентом. В сокете экземпляра он пуст до установления соединения, а имя протокола определяется клиентом и сервером после установления соединения. |
readyState | String | Только чтение. Текущее состояние соединения, которому соответствуют константы. |
extensions | String | Расширение выбора сервера. В настоящее время это просто пустая строка или список расширений, согласованных по соединению. |
Простая реализация WebSocket
Протокол WebSocket состоит из двух частей: рукопожатия и передачи данных.
Среди них рукопожатие, несомненно, является ключом и обязательным условием всего.
Пожать руки
-
Запрос рукопожатия клиента
//创建WebSocket实例,可以使用ws和wss。第二个参数可以选填自定义协议,如果多协议,可以以数组方式 var socket = new WebSocket('ws://localhost:8081', [protocol]);
Причина появления WebSocket в том, что браузер может реализовать полнодуплексную связь с сервером и широкое использование протокола HTTP на стороне браузера (конечно не все для браузера, но в основном для браузера). Таким образом, рукопожатие WebSocket — это обновление HTTP-запроса. Пример заголовка запроса клиента WebSocket:
GET /chat HTTP/1.1 //必需。 Host: server.example.com // 必需。WebSocket服务器主机名 Upgrade: websocket // 必需。并且值为" websocket"。有个空格 Connection: Upgrade // 必需。并且值为" Upgrade"。有个空格 Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== // 必需。其值采用base64编码的随机16字节长的字符序列。 Origin: http://example.com //浏览器必填。头域(RFC6454)用于保护WebSocket服务器不被未授权的运行在浏览器的脚本跨源使用WebSocket API。 Sec-WebSocket-Protocol: chat, superchat //选填。可用选项有子协议选择器。 Sec-WebSocket-Version: 13 //必需。版本。
Клиент WebSocket отправляет указанный выше запрос на сервер. Если вызывается API WebSocket браузера, браузер автоматически выполнит указанные выше заголовки запроса.
-
Ответ сервера на рукопожатие
Сервер должен доказать клиенту, что он получил рукопожатие клиента WebSocket, чтобы предотвратить прием сервером соединений, отличных от WebSocket, не позволяя злоумышленникам отправлять тщательно сконструированные пакеты через XMLHttpRequest или отправлять форму для обмана сервера WebSocket. Сервер объединяет две части информации для формирования ответа. Первая часть информации поступает из поля Sec-WebSocket-Key заголовка рукопожатия клиента, такого как Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==. Для этого поля заголовка сервер берет значение поля заголовка (сначала необходимо удалить пробелы) и объединяет глобально уникальный идентификатор (GUID, [RFC4122]) в виде строки: 258EAFA5-E914-47DA-95CA. -C5AB0DC85B11, это значение не может использоваться сетевыми терминалами, которые не понимают протокол WebSocket. Затем выполните кодирование хэша SHA-1 (160-битное), затем кодирование base64 и верните результат в виде рукопожатия сервера. детали следующим образом:
请求头:Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jZQ== 取值,字符串拼接后得到:"dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; SHA-1后得到: 0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6 0x46 0x06 0xcf 0x38 0x59 0x45 0xb20xbe 0xc4 0xea Base64后得到: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= 最后的结果值作为响应头Sec-WebSocket-Accept 的值。
Наконец, формируется ответ на рукопожатие на стороне сервера WebSocket:
HTTP/1.1 101 Switching Protocols //必需。响应头。状态码为101。任何非101的响应都为握手未完成。但是HTTP语义是存在的。 Upgrade: websocket // 必需。升级类型。 Connection: Upgrade //必需。本次连接类型为升级。 Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK+xOo= //必需。表明服务器是否愿意接受连接。如果接受,值就必须是通过上面算法得到的值。
Конечно, в заголовке ответа есть несколько необязательных полей. Основным необязательным полем является Sec-WebSocket-Protocol, которое является ответом на результат выбора подпротокола Sec-WebSocket-Protocol, указанный в клиентском запросе. Конечно, файлы cookie также возможны.
//handshaking.js const crypto = require('crypto'); const cryptoKey = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; // 计算握手响应accept-key let challenge = (reqKey) => { reqKey += cryptoKey; // crypto.vetHashes()可以获得支持的hash算法数组,我这里得到46个 reqKey = reqKey.replace(/\s/g,""); // crypto.createHash('sha1').update(reqKey).digest()得到的是一个Uint8Array的加密数据,需要将其转为base64 return crypto.createHash('sha1').update(reqKey).digest().toString('base64'); } exports.handshaking = (req, socket, head) => { let _headers = req.headers, _key = _headers['sec-websocket-key'], resHeaders = [], br = "\r\n"; resHeaders.push( 'HTTP/1.1 101 WebSocket Protocol Handshake is OK', 'Upgrade: websocket', 'Connection: Upgrade', 'Sec-WebSocket-Origin: ' + _headers.origin, 'Sec-WebSocket-Location: ws://' + _headers.host + req.url, ); let resAccept = challenge(_key); resHeaders.push('Sec-WebSocket-Accept: '+ resAccept + br, head); socket.write(resHeaders.join(br), 'binary'); }
-
рукопожатие закрыть
Закрытие рукопожатия Рукопожатие можно закрыть, используя метод, при котором TCP напрямую закрывает соединение. Но близкое рукопожатие TCP не всегда надежно, особенно при наличии перехватывающих прокси-серверов и других посредников. Также возможно, чтобы любой конец отправил кадр с данными с указанным контрольным порядковым номером (например, код состояния 1002, ошибка протокола), чтобы начать рукопожатие закрытия.Когда другая сторона получает этот кадр закрытия, она должна закрыть соединение.
передача информации
В протоколе WebSocket на этапе передачи данных для связи используются фреймы (кадры данных), а фреймы делятся на разные типы, в основном: текстовые данные, двоичные данные. Из соображений безопасности и во избежание перехвата сети кадр данных, отправляемый клиентом, должен быть замаскирован перед отправкой на сервер, независимо от того, находится ли он в протоколе безопасности TLS. Сервер должен закрыть соединение и отправить код состояния 1002, если он не получает замаскированный фрейм данных. Сервер не может маскировать данные, отправляемые клиенту, и клиент должен закрыть соединение, если он получает замаскированный фрейм данных.
Так что же представляет собой кадр данных, полученный нашим сервером?
-
Фрейм данных
Передача данных WebSocket должна следовать определенному формату данных — фрейму данных (frame).
Каждый столбец представляет собой байт, байт имеет 8 бит, и каждый бит представляет собой двоичное число.
плавник:Определяет, является ли этот кадр данных последним кадром блока.
1 为最后一帧 0 不是最后一帧。需要分为多个帧传输
рсв1-3:Значение по умолчанию — 0. Расширение согласования получения определяется как ненулевое значение.код операции:Код операции определяет, что представляют собой данные, если это не значение в определении, соединение будет прервано. Занимая четыре бита, он может представлять десятичное число от 0 до 15 или шестнадцатеричное.
%x0 表示一个继续帧 %x1 表示一个文本帧 %x2 表示一个二进制帧 %x3-7 为以后的非控制帧保留 %x8 表示一个连接关闭 %x9 表示一个ping %x10 表示一个pong %x11-15 为以后的控制帧保留
в маске:Один бит второго байта определяет, существует ли ключ маскирования. И используйте маску маскирующего ключа для анализа данных полезной нагрузки.
1 客户端发送数据到服务端 0 服务端发送数据到客户端
длина полезной нагрузки:Указывает общую длину данных полезной нагрузки. Занимает 7 бит, или 7+2 байта, или 7+8 байт.
0-125,则是payload的真实长度 126,则后面2个字节形成的16位无符号整型数的值是payload的真实长度,125<数据长度<65535 127,则后面8个字节形成的64位无符号整型数的值是payload的真实长度,数据长度>65535
маскирующий ключ:0 или 4 байта, существуют только при маскировании 1, 4 байта, иначе 0, используются для расшифровки нужных нам данных
данные полезной нагрузки:Нужные нам данные, если masked равно 1, то данные будут зашифрованы, а настоящие данные можно получить, выполнив операцию XOR и расшифровав через ключ маскировки.
-
О фреймах данных
Поскольку данные, полученные сервером WebSocket, могут быть непрерывными кадрами данных, сообщение может быть отправлено в нескольких кадрах. Но использование fin для границ сообщения проблематично.
Я отправил строку размером 27378 байт, всего сервер получил 2 фрейма, fin обоих фреймов равен 1, а длина полезных данных двух фреймов, рассчитанная по спецификации, равна 27372, что на 6 байт меньше. Недостающие 6 байтов на самом деле равны 2 внутренним байтам плюс 4 байта maskingKey, что означает, что второй фрейм является чистым фреймом данных. Что тут происходит? ?
Исходя из результатов, предполагается, что формат данных второго полученного нами кадра не является форматом кадра, что указывает на то, что данные сначала не отправляются кадрами (фрагментами). Вместо этого кадр пакетируется и отправляется.
Фрагментация
Основная цель сегментирования — разрешить отправку сообщения неизвестного размера в начале сообщения, но его не нужно буферизовать. Если сообщение нельзя фрагментировать, конечной точке придется буферизовать все сообщение, чтобы подсчитать его длину до появления первого байта. Для сегментирования сервер или промежуточное ПО могут выбрать буфер соответствующего размера и, когда буфер заполнится, записать сегмент в сеть.
Наше сообщение размером 27378 байт заведомо знает длину сообщения, поэтому даже если сообщение очень большое, по спецификации длина данных 1 кадра теоретически равна 0
Подводя итог, когда клиент отправляет данные, их все равно нужно вручную разбивать на фреймы (шардировать), иначе они будут отправляться по одному фрейму, и малый объем данных не имеет значения, если это большой объем данные, они будут автоматически пакетированы и отправлены сокетом. Разница между этим и автоматическим кадрированием (фрагментацией), объявленным спецификацией протокола WebSocket, должна быть вызвана тем, что различные браузеры срезают углы в реализации спецификации протокола WebSocket. Итак, мы видим, что плагины, такие как socket.io, будут иметь клиентский интерфейс, который должен заново реализовать спецификацию протокола WebSocket. Исходя из принципа, в качестве примера возьмем передачу небольшого количества данных (одиночного кадра).
-
Разобрать фрейм данных
//dataHandler.js // 收集本次message的所有数据 getData(data, callback) { this.getState(data); // 如果状态码为8说明要关闭连接 if(this.state.opcode == 8) { this.OPEN = false; this.closeSocket(); return; } // 如果是心跳pong,回一个ping if(this.state.opcode == 10) { this.OPEN = true; this.pingTimes = 0;// 回了pong就将次数清零 return; } // 收集本次数据流数据 this.dataList.push(this.state.payloadData); // 长度为0,说明当前帧位最后一帧。 if(this.state.remains == 0){ let buf = Buffer.concat(this.dataList, this.state.payloadLength); //使用掩码maskingKey解析所有数据 let result = this.parseData(buf); // 数据接收完成后回调回业务函数 callback(this.socket, result); //重置状态,表示当前message已经解析完成了 this.resetState(); }else{ this.state.index++; } } // 收集本次message的所有数据 getData(data, callback) { this.getState(data); // 收集本次数据流数据 this.dataList.push(this.state.payloadData); // 长度为0,说明当前帧位最后一帧。 if(this.state.remains == 0){ let buf = Buffer.concat(this.dataList, this.state.payloadLength); //使用掩码maskingKey解析所有数据 let result = this.parseData(buf); // 数据接收完成后回调回业务函数 callback(this.socket, result); //重置状态,表示当前message已经解析完成了 this.resetState(); }else{ this.state.index++; } } // 解析本次message所有数据 parseData(allData, callback){ let len = allData.length, i = 0; for(; i < len; i++){ allData[i] = allData[i] ^ this.state.maskingKey[ i % 4 ];// 异或运算,使用maskingKey四个字节轮流进行计算 } // 判断数据类型,如果为文本类型 if(this.state.opcode == 1) allData = allData.toString(); return allData; }
-
Соберите кадр данных, который необходимо отправить
// 组装数据帧,发送是不需要掩码加密 createData(data){ let dataType = Buffer.isBuffer(data);// 数据类型 let dataBuf, // 需要发送的二进制数据 dataLength,// 数据真实长度 dataIndex = 2; // 数据的起始长度 let frame; // 数据帧 if(dataType) dataBuf = data; else dataBuf = Buffer.from(data); // 也可以不做类型判断,直接Buffer.form(data) dataLength = dataBuf.byteLength; // 计算payload data在frame中的起始位置 dataIndex = dataIndex + (dataLength > 65535 ? 8 : (dataLength > 125 ? 2 : 0)); frame = new Buffer.alloc(dataIndex + dataLength); //第一个字节,fin = 1,opcode = 1 frame[0] = parseInt(10000001, 2); //长度超过65535的则由8个字节表示,因为4个字节能表达的长度为4294967295,已经完全够用,因此直接将前面4个字节置0 if(dataLength > 65535){ frame[1] = 127; //第二个字节 frame.writeUInt32BE(0, 2); frame.writeUInt32BE(dataLength, 6); }else if(dataLength > 125){ frame[1] = 126; frame.writeUInt16BE(dataLength, 2); }else{ frame[1] = dataLength; } // 服务端发送到客户端的数据 frame.write(dataBuf.toString(), dataIndex); return frame; }
-
Обнаружение сердцебиения
// 心跳检查 sendCheckPing(){ let _this = this; let timer = setTimeout(() => { clearTimeout(timer); if (_this.pingTimes >= 3) { _this.closeSocket(); return; } //记录心跳次数 _this.pingTimes++; if(_this.pingTimes == 100000) _this.pingTimes = 0; _this.sendCheckPing(); }, 5000); } // 发送心跳ping sendPing() { let ping = Buffer.alloc(2); ping[0] = parseInt(10001001, 2); ping[1] = 0; this.writeData(ping); }
закрыть соединение
Клиент напрямую вызывает метод close, а сервер может использовать метод socket.end.
наконец
WebSocket делает внешний интерфейс в определенной степени более полезным, что, несомненно, радует, но многие неопределенности в его спецификации также вызывают большое сожаление. Поскольку браузер не полностью реализует спецификацию WebSocket, все еще необходимо выполнить множество оптимизаций.Эта статья реализует только WebSocket, и многие аспекты безопасности и стабильности в течение этого периода должны быть улучшены в приложении. Конечно, также неплохо использовать относительно зрелый подключаемый модуль, такой как socket.io.