1. 程式人生 > >websocket通訊之握手,封包,解包

websocket通訊之握手,封包,解包

介紹

WebSocket協議是基於TCP的一種新的協議。WebSocket最初在HTML5規範中被引用為TCP連線,作為基於TCP的套接字API的佔位符。它實現了瀏覽器與伺服器全雙工(full-duplex)通訊。其本質是保持TCP連線,在瀏覽器和服務端通過Socket進行通訊。

上古時期的瀏覽器有些是不支援WebSocket的,下面來介紹如何在瀏覽器中建立一個websocket物件

var socket = new WebSocket("ws://127.0.0.1:8002/xxoo");

socket傳送接收資料

ws.send() # 傳送訊息
ws.onmessage = function (event) {
            console.log(event.data) # 收到資料執行的函式,event.data就是收到的資料
        }
ws.onclose = function (event) {
    // 服務端主動斷開了連線
}

Socket伺服器

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('127.0.0.1', 8002))
sock.listen(5)
# 獲取客戶端socket物件
conn, address = sock.accept()
# 獲取客戶端的【握手】資訊
data = conn.recv(8096)

print(str(data,encoding='utf-8'))

瀏覽器Socket

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Title</title>
</head>
<body>
<script>
ws = new WebSocket('ws://127.0.0.1:8002/')
</script>
</body>
</html>

執行結果

# 瀏覽器列印
GET / HTTP/1.1
Host: 127.0.0.1:8002
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://localhost:53512
Sec-WebSocket-Version: 13
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: _ga=GA1.1.414503566.1517490000; csrftoken=hm3ml70razspGU9n46ay8z7KaouRS1XrFjBgjqU2ANy8lZOKWPZJMHpNDz1QXNZ1
Sec-WebSocket-Key: Gm61RLyES5nWI3UxzOES0g==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

# 瀏覽器端
WebSocket connection to 'ws://127.0.0.1:8002/' failed: Connection closed before receiving a handshake response

當客戶端向服務端傳送連線請求時,不僅連線還會發送【握手】資訊,並等待服務端響應,至此連線才建立成功!

客戶端的響應內容

  • 從請求【握手】資訊中提取 Sec-WebSocket-Key
  • 利用magic_string 和 Sec-WebSocket-Key 進行hmac1加密,再進行base64加密
  • 將加密結果響應給客戶端
import socket
import base64
import hashlib
 
def get_headers(data):
    """
    將請求頭格式化成字典
    :param data:
    :return:
    """
    header_dict = {}
    data = str(data, encoding='utf-8')
 
    for i in data.split('\r\n'):
        print(i)
    header, body = data.split('\r\n\r\n', 1)
    header_list = header.split('\r\n')
    for i in range(0, len(header_list)):
        if i == 0:
            if len(header_list[i].split(' ')) == 3:
                header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ')
        else:
            k, v = header_list[i].split(':', 1)
            header_dict[k] = v.strip()
    return header_dict
 
 
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('127.0.0.1', 8002))
sock.listen(5)
 
conn, address = sock.accept()
data = conn.recv(1024)
headers = get_headers(data) # 提取請求頭資訊
# 對請求頭中的sec-websocket-key進行加密
response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \
      "Upgrade:websocket\r\n" \
      "Connection: Upgrade\r\n" \
      "Sec-WebSocket-Accept: %s\r\n" \
      "WebSocket-Location: ws://%s%s\r\n\r\n"
magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
value = headers['Sec-WebSocket-Key'] + magic_string
ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())
response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url'])
# 響應【握手】資訊
conn.send(bytes(response_str, encoding='utf-8'))

服務端響應後,連線就建成了,接下來就可以收發資料了.

客戶端和服務端傳輸資料時,需要對資料進行【封包】和【解包】。客戶端的JavaScript類庫已經封裝【封包】和【解包】過程,但Socket服務端需要手動實現。

解包詳解

0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

# 根據第二個位元組的後七位判斷頭部大小,
# 127是向後64位,也就是8位元組,頭部共10位元組
# 127是向後16位,也就是2位元組,頭部共4位元組
# Masking-key在資料部分的前四位,後面才是真正資料的加密部分
# 解密過程就將Masking-key與加密的資料進行位運算

官方解釋

The MASK bit simply tells whether the message is encoded. Messages from the client must be masked, so your server should expect this to be 1. (In fact, section 5.1 of the spec says that your server must disconnect from a client if that client sends an unmasked message.) When sending a frame back to the client, do not mask it and do not set the mask bit. We'll explain masking later. Note: You have to mask messages even when using a secure socket.RSV1-3 can be ignored, they are for extensions.

The opcode field defines how to interpret the payload data: 0x0 for continuation, 0x1 for text (which is always encoded in UTF-8), 0x2 for binary, and other so-called "control codes" that will be discussed later. In this version of WebSockets, 0x3 to 0x7 and 0xB to 0xF have no meaning.

The FIN bit tells whether this is the last message in a series. If it's 0, then the server will keep listening for more parts of the message; otherwise, the server should consider the message delivered. More on this later.

Decoding Payload Length

To read the payload data, you must know when to stop reading. That's why the payload length is important to know. Unfortunately, this is somewhat complicated. To read it, follow these steps:

  1. Read bits 9-15 (inclusive) and interpret that as an unsigned integer. If it's 125 or less, then that's the length; you're done. If it's 126, go to step 2. If it's 127, go to step 3.
  2. Read the next 16 bits and interpret those as an unsigned integer. You're done.
  3. Read the next 64 bits and interpret those as an unsigned integer (The most significant bit MUST be 0). You're done.

Reading and Unmasking the Data

If the MASK bit was set (and it should be, for client-to-server messages), read the next 4 octets (32 bits); this is the masking key. Once the payload length and masking key is decoded, you can go ahead and read that number of bytes from the socket. Let's call the data ENCODED, and the key MASK. To get DECODED, loop through the octets (bytes a.k.a. characters for text data) of ENCODED and XOR the octet with the (i modulo 4)th octet of MASK. In pseudo-code (that happens to be valid JavaScript):

 

var DECODED = "";
for (var i = 0; i < ENCODED.length; i++) {
    DECODED[i] = ENCODED[i] ^ MASK[i % 4];
}

 

Now you can figure out what DECODED means depending on your application.

python的解包過程

info = conn.recv(8096)

payload_len = info[1] & 127
if payload_len == 126:
   extend_payload_len = info[2:4]
   mask = info[4:8]
   decoded = info[8:]
elif payload_len == 127:
   extend_payload_len = info[2:10]
   mask = info[10:14]
   decoded = info[14:]
else:
    extend_payload_len = None
    mask = info[2:6]
    decoded = info[6:]

bytes_list = bytearray()
for i in range(len(decoded)):
    chunk = decoded[i] ^ mask[i % 4]
    bytes_list.append(chunk)
body = str(bytes_list, encoding='utf-8')
print(body)

封包

def send_msg(conn, msg_bytes):
    """
    WebSocket服務端向客戶端傳送訊息
    :param conn: 客戶端連線到伺服器端的socket物件,即: conn,address = socket.accept()
    :param msg_bytes: 向客戶端傳送的位元組
    :return: 
    """
    import struct

    token = b"\x81"
    length = len(msg_bytes)
    if length < 126:
        token += struct.pack("B", length)
    elif length <= 0xFFFF:
        token += struct.pack("!BH", 126, length)
    else:
        token += struct.pack("!BQ", 127, length)

    msg = token + msg_bytes
    conn.send(msg)
    return True

框架中使用

  • django: channel
  • flask: gevent-websocket
  • tornado: 內建

Flask

安裝

pip3 install gevent-websocket

作用

  • - 處理Http、Websocket協議的請求 -> socket
  • - 封裝Http、Websocket相關資料 -> request

基本結構

from geventwebsocket.handler import WebSocketHandler
from gevent.pywsgi import WSGIServer
from flask import Flask,render_template,request


app = Flask(__name__)
@app.route('/test')
def test():
	ws = request.environ.get('wsgi.websocket') # 如果是websocket請求會得到socket物件,否則None
        if ws:
            while:
	        ws.receive() # 收資料
	        ws.send(message) # 發資料
	        # ws.close() # 關閉
	return render_template('index.html')
				
if __name__ == '__main__':
	http_server = WSGIServer(('0.0.0.0', 5000,), app, handler_class=WebSocketHandler)
	http_server.serve_forever() 

tornado

Tornado是一個輕量級的Web框架,非同步非阻塞+內建WebSocket功能

安裝

pip3 install tornado 

示例

import tornado
from tornado.web import Application
from tornado.web import RequestHandler
from tornado.websocket import WebSocketHandler

# HTTP繼承RequestHandler類
class IndexHandler(RequestHandler):

	def get(self, *args, **kwargs):
		# self.write('Hello World') 響應字串
		self.render('index.html') # 響應模板

	def post(self, *args, **kwargs):
		user = self.get_argument('user') # 提交資料中取值
		self.write('成功')

WS_LIST = []
# WebSocket繼承WebSocketHandler類
class MessageHandler(WebSocketHandler):

	def open(self, *args, **kwargs):
                # 連線時執行的
		WS_LIST.append(self)

	def on_message(self, message):
                # self.close() 服務端主動斷開連線,
                # 收到資料時執行的,message就是收到的資料
		for ws in WS_LIST:
			ws.write_message(message) # 傳送資料

	def on_close(self):
                # 客戶端關閉連線時執行的
		WS_LIST.remove(self)



settings = {
	'template_path':'templates',
	'static_path':'static',
} # 寫配置

app = Application([
	(r"/index", IndexHandler),
	(r"/message", MessageHandler),
],**settings) # 路由及配置

if __name__ == '__main__':
	app.listen(address='0.0.0.0',port=9999)
	tornado.ioloop.IOLoop.instance().start() # 啟動