ホームページ >ウェブフロントエンド >jsチュートリアル >WebSocketを使った通信方法
今回は、WebSocket通信の使い方と、WebSocket通信を使用する際の注意点を紹介します。以下は実際の事例ですので、見ていきましょう。
WebSocketとは何ですか?
WebSocket は、クライアントとサーバー間の双方向データ送信のための標準プロトコルです。ただし、これは HTTP とは関係ありません。TCP に基づいた独立した実装です。
以前は、クライアントがサーバーの処理の進行状況を知りたい場合は、常に Ajax を使用してポーリングし、ブラウザーに数秒ごとにサーバーにリクエストを送信させる必要がありました。これにより、サーバーに多大な負荷がかかりました。サーバ。もう 1 つの種類のポーリングは、電話をかけるのと同様で、メッセージが受信されるまで切断されません。つまり、クライアントが接続を開始した後、メッセージがない場合は応答が返されます。クライアントに返されない場合、接続フェーズは常にブロックされます。
そして、WebSocket は HTTP のこれらの問題を解決します。サーバーがプロトコルのアップグレード (HTTP -> WebSocket) を完了すると、サーバーはアクティブに情報をクライアントにプッシュできるようになり、ポーリングによって引き起こされる同期遅延の問題が解決されます。 WebSocket は HTTP ハンドシェイクを 1 回だけ必要とするため、サーバーは接続が閉じるまでクライアントと通信し続けることができ、サーバーが HTTP プロトコルを繰り返し解析する必要がなくなり、リソースのオーバーヘッドが削減されます。
新しい標準の進歩に伴い、WebSocket はより成熟し、さまざまな主流ブラウザの WebSocket サポートが強化されています (IE の下位バージョン、IE 10 以下とは互換性がありません)。時間。
WebSocketを使用する場合、jsはwsプロトコルをサポートしており、軽くカプセル化されたSocketプロトコルに似ていますが、以前はSocket接続を自分で維持する必要がありました。より標準的な方法で実行できるようになりました。
上の図をもとにWebSocketの通信プロセスを詳しく説明していきます。
接続の確立
クライアントリクエストメッセージヘッダー
クライアントリクエストメッセージ:
GET / HTTP/1.1 Upgrade: websocket Connection: Upgrade Host: example.com Origin: http://example.com Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ== Sec-WebSocket-Version: 13
従来のHTTPメッセージとの違い:
Upgrade: websocket Connection: Upgrade
これらの2行は、WebSocketプロトコルが開始されたことを示します。
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ== Sec-WebSocket-Version: 13
Sec-WebSocket-Key はブラウザーによってランダムに生成され、悪意のある接続または意図しない接続に対する基本的な保護を提供します。
Sec-WebSocket-Version は WebSocket のバージョンを示します。当初は WebSocket プロトコルが多すぎて、各メーカーが独自のプロトコル バージョンを持っていましたが、現在は統一されています。サーバーがこのバージョンをサポートしていない場合は、サーバーがサポートするバージョン番号を含む Sec-WebSocket-Versionheader を返す必要があります。
WebSocket オブジェクトを作成します:
var ws = new websocket("ws://127.0.0.1:8001");
ws は WebSocket プロトコルを使用することを意味し、その後にアドレスとポートが続きます
完全なクライアント コード:
<script type="text/javascript"> var ws; var box = document.getElementById('box'); function startWS() { ws = new WebSocket('ws://127.0.0.1:8001'); ws.onopen = function (msg) { console.log('WebSocket opened!'); }; ws.onmessage = function (message) { console.log('receive message: ' + message.data); box.insertAdjacentHTML('beforeend', '<p>' + message.data + '</p>'); }; ws.onerror = function (error) { console.log('Error: ' + error.name + error.number); }; ws.onclose = function () { console.log('WebSocket closed!'); }; } function sendMessage() { console.log('Sending a message...'); var text = document.getElementById('text'); ws.send(text.value); } window.onbeforeunload = function () { ws.onclose = function () {}; // 首先关闭 WebSocket ws.close() }; </script>
サーバー応答メッセージ ヘッダー
まず、サーバー応答メッセージ:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat
1 行ずつ説明しましょう
まず第一に、101 ステータス コード は、サーバーがクライアントのリクエストを理解し、別のプロトコルを使用してリクエストを完了するようにクライアントに通知することを示します。アップグレード メッセージ ヘッダー
次に、Sec-WebSocket-Accept はサーバーによって確認され、暗号化された Sec-WebSocket-Key です
最後に、Sec-WebSocket-Protocol は使用される最終プロトコルを表します。
Sec-WebSocket-Accept の計算方法:
Sec-WebSocket-Key を 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 と結合します。
SHA1 を通じてダイジェストを計算し、base64 に変換します。文字列。
注意:Sec-WebSocket-Key/Sec-WebSocket-Accept 的换算,只能带来基本的保障,但连接是否安全、数据是否安全、客户端 / 服务端是否合法的 ws 客户端、ws 服务端,其实并没有实际性的保证。
创建主线程,用于实现接受 WebSocket 建立请求:
def create_socket(): # 启动 Socket 并监听连接 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.bind(('127.0.0.1', 8001)) # 操作系统会在服务器 Socket 被关闭或服务器进程终止后马上释放该服务器的端口,否则操作系统会保留几分钟该端口。 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.listen(5) except Exception as e: logging.error(e) return else: logging.info('Server running...') # 等待访问 while True: conn, addr = sock.accept() # 此时会进入 waiting 状态 data = str(conn.recv(1024)) logging.debug(data) header_dict = {} header, _ = data.split(r'\r\n\r\n', 1) for line in header.split(r'\r\n')[1:]: key, val = line.split(': ', 1) header_dict[key] = val if 'Sec-WebSocket-Key' not in header_dict: logging.error('This socket is not websocket, client close.') conn.close() return magic_key = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' sec_key = header_dict['Sec-WebSocket-Key'] + magic_key key = base64.b64encode(hashlib.sha1(bytes(sec_key, encoding='utf-8')).digest()) key_str = str(key)[2:30] logging.debug(key_str) response = 'HTTP/1.1 101 Switching Protocols\r\n' \ 'Connection: Upgrade\r\n' \ 'Upgrade: websocket\r\n' \ 'Sec-WebSocket-Accept: {0}\r\n' \ 'WebSocket-Protocol: chat\r\n\r\n'.format(key_str) conn.send(bytes(response, encoding='utf-8')) logging.debug('Send the handshake data') WebSocketThread(conn).start()
进行通信
服务端解析 WebSocket 报文
Server 端接收到 Client 发来的报文需要进行解析
Client 包格式
FIN: 占 1bit
0:不是消息的最后一个分片
1:是消息的最后一个分片
RSV1, RSV2, RSV3:各占 1bit
一般情况下全为 0。当客户端、服务端协商采用 WebSocket 扩展时,这三个标志位可以非
0,且值的含义由扩展进行定义。如果出现非零的值,且并没有采用 WebSocket 扩展,连接出错。
Opcode: 4bit
%x0:表示一个延续帧。当 Opcode 为 0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片;
%x1:表示这是一个文本帧(text frame);
%x2:表示这是一个二进制帧(binary frame);
%x3-7:保留的操作代码,用于后续定义的非控制帧;
%x8:表示连接断开;
%x9:表示这是一个心跳请求(ping);
%xA:表示这是一个心跳响应(pong);
%xB-F:保留的操作代码,用于后续定义的控制帧。
Mask: 1bit
表示是否要对数据载荷进行掩码异或操作。
0:否
1:是
Payload length: 7bit or (7 + 16)bit or (7 + 64)bit
表示数据载荷的长度
0~126:数据的长度等于该值;
126:后续 2 个字节代表一个 16 位的无符号整数,该无符号整数的值为数据的长度;
127:后续 8 个字节代表一个 64 位的无符号整数(最高位为 0),该无符号整数的值为数据的长度。
Masking-key: 0 or 4bytes
当 Mask 为 1,则携带了 4 字节的 Masking-key;
当 Mask 为 0,则没有 Masking-key。
掩码算法:按位做循环异或运算,先对该位的索引取模来获得 Masking-key 中对应的值 x,然后对该位与 x 做异或,从而得到真实的 byte 数据。
注意:掩码的作用并不是为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题。
Payload Data: 载荷数据
解析 WebSocket 报文代码如下:
def read_msg(data): logging.debug(data) msg_len = data[1] & 127 # 数据载荷的长度 if msg_len == 126: mask = data[4:8] # Mask 掩码 content = data[8:] # 消息内容 elif msg_len == 127: mask = data[10:14] content = data[14:] else: mask = data[2:6] content = data[6:] raw_str = '' # 解码后的内容 for i, d in enumerate(content): raw_str += chr(d ^ mask[i % 4]) return raw_str
服务端发送 WebSocket 报文
返回时不携带掩码,所以 Mask 位为 0,再按载荷数据的大小写入长度,最后写入载荷数据。
struct 模块解析
struct.pack(fmt, v1, v2, ...)
按照给定的格式 fmt,把数据封装成字符串 ( 实际上是类似于 C 结构体的字节流 )
struct 中支持的格式如下表:
Format | C Type | Python type | Standard size |
---|---|---|---|
x | pad byte | no value | |
c | char | bytes of length 1 | 1 |
b | signed char | integer | 1 |
B | unsigned char | integer | 1 |
? | _Bool | bool | 1 |
h | short | integer | 2 |
H | unsigned short | integer | 2 |
i | int | integer | 4 |
I | unsigned int | integer | 4 |
l | long | integer | 4 |
L | unsigned long | integer | 4 |
q | long long | integer | 8 |
Q | unsigned long long | integer | 8 |
n | ssize_t | integer | |
N | size_t | integer | |
e | -7 | float | 2 |
f | float | float | 4 |
d | double | float | 8 |
s | char[] | bytes | |
p | char[] | bytes | |
P | void * | integer |
为了同 C 语言中的结构体交换数据,还要考虑有的 C 或 C++ 编译器使用了字节对齐,通常是以 4 个字节为单位的 32 位系统,故而 struct 根据本地机器字节顺序转换。可以用格式中的第一个字符来改变对齐方式,定义如下:
Character | Byte order | Size | Alignment |
---|---|---|---|
@ | native | native | native |
= | native | standard | none |
< | little-endian | standard | none |
> | big-endian | standard | none |
! | network (= big-endian) | standard | none |
发送 WebSocket 报文代码如下:
def write_msg(message): data = struct.pack('B', 129) # 写入第一个字节,10000001 # 写入包长度 msg_len = len(message) if msg_len <= 125: data += struct.pack('B', msg_len) elif msg_len <= (2 ** 16 - 1): data += struct.pack('!BH', 126, msg_len) elif msg_len <= (2 ** 64 - 1): data += struct.pack('!BQ', 127, msg_len) else: logging.error('Message is too long!') return data += bytes(message, encoding='utf-8') # 写入消息内容 logging.debug(data) return data
总结
没有其他能像 WebSocket 一样实现全双工传输的技术了,迄今为止,大部分开发者还是使用 Ajax 轮询来实现,但这是个不太优雅的解决办法,WebSocket 虽然用的人不多,可能是因为协议刚出来的时候有安全性的问题以及兼容的浏览器比较少,但现在都有解决。如果你有这些需求可以考虑使用 WebSocket:
多个用户之间进行交互;
需要频繁地向服务端请求更新数据。
比如弹幕、消息订阅、多玩家游戏、协同编辑、股票基金实时报价、视频会议、在线教育等需要高实时的场景。
相信看了本文案例你已经掌握了方法,更多精彩请关注php中文网其它相关文章!
推荐阅读:
以上がWebSocketを使った通信方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。