Python GeventWebSocket

WebSocket 介绍

什么是 WebSocket ?

WebSocket 是独立的、创建在 TCP 上的协议。

Websocket 通过 HTTP/1.1 协议的101状态码进行握手。

为了创建 Websocket 连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为 “握手)”(handshaking)。

  • HTTP 是运行在 TCP 协议传输层的应用协议 ,WebSocket 是通过 HTTP 协议协商如何连接,然后独立运行在 TCP 协议传输层上的 应用协议。
  • WebSocket 是一个持久化的协议,相对于 HTTP 这种非持久的协议来说。

使用 Python scoket

  • 服务端暂时只是获取 HTTPWebsocket 请求头。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import socket
    import base64
    import hashlib

    HOST = "127.0.0.1"
    PORT = 9999

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    # 端口重用
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind((HOST, PORT))
    s.listen()
    conn, addr = s.accept()
    # 获取客户端的握手信息
    with conn:
    # print('Connected by', addr)
    while True:
    data = conn.recv(1024)
    frmat = data.decode()
    print(frmat)
    if not data: break
    conn.sendall(data)

  • WebSocket 客户端代码,只做连接

    1
    2
    3
    4
    5
    6
    7
    <script type="text/javascript"> 
    var ws = new WebSocket("ws://127.0.0.1:9999")

    ws.onmessage = function (event) {
    console.log(event.data);
    };
    </script>

以下为获取请求头

原始 HTTP 请求头( 上图 ) 对比 WebSocket

image-20190322203358514

+=

image-20190322203504577

获取 Sec-WebSocket-Key 的值

1
2
3
4
5
6
7
8
9
def get_headers(data):
header_dict = {}
header_str = data.decode("utf8")
for i in header_str.split("\r\n"):
if str(i).startswith("Sec-WebSocket-Key"):
header_dict["Sec-WebSocket-Key"] = i.split(":")[1].strip()

return header_dict

请求和响应的握手信息需要遵循规则:

  • 从请求握手信息中提取 S·ec-WebSocket-Key 的值
  • 利用 magic_stringSec-WebSocket-Key 的值 ,进行 hmac1(sha1) 加密,再进行 base64 加密
  • 将结果做为 “ Sec-WebSocket-Accept” 头的值,返回给客户端。

magic_string特殊字符串 「 258EAFA5-E914-47DA-95CA-C5AB0DC85B11` 」

再来介绍一下ready state 表示的四种状态

  • CONNECTING (0):表示还没建立连接;
  • OPEN (1): 已经建立连接,可以进行通讯;
  • CLOSING (2):通过关闭握手,正在关闭连接;
  • CLOSED (3):连接已经关闭或无法打开;

2019-03-22 at 10.41 PM

客户端 接受客户端信息解密,流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

hashstr = b'\x81\x8b\xf1\x9c*=\x99\xf9FQ\x9e\xb0]R\x83\xf0N'

# 将第二个字节也就是 \x83 第9-16位 进行与127进行位运算
payload = hashstr[1] & 127
print(payload)
if payload == 127:
extend_payload_len = hashstr[2:10]
mask = hashstr[10:14]
decoded = hashstr[14:]
# 当位运算结果等于127时,则第3-10个字节为数据长度
# 第11-14字节为mask 解密所需字符串
# 则数据为第15字节至结尾

if payload == 126:
extend_payload_len = hashstr[2:4]
mask = hashstr[4:8]
decoded = hashstr[8:]
# 当位运算结果等于126时,则第3-4个字节为数据长度
# 第5-8字节为mask 解密所需字符串
# 则数据为第9字节至结尾


if payload <= 125:
extend_payload_len = None
mask = hashstr[2:6]
decoded = hashstr[6:]

# 当位运算结果小于等于125时,则这个数字就是数据的长度
# 第3-6字节为mask 解密所需字符串
# 则数据为第7字节至结尾

str_byte = bytearray()

for i in range(len(decoded)):
byte = decoded[i] ^ mask[i % 4]
str_byte.append(byte)

print(str_byte.decode("utf8"))

加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import struct
msg_bytes = "hello".encode("utf8")
token = b"\x81"
length = len(msg_bytes)

if length < 126:
token += struct.pack("B", length)
elif length == 126:
token += struct.pack("!BH", 126, length)
else:
token += struct.pack("!BQ", 127, length)

msg = token + msg_bytes

print(msg)

完整代码示例

不再赘述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import socket
import base64
import hashlib
import struct

HOST = "127.0.0.1"
PORT = 9999

# 服务器回应,响应头
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://127.0.0.1:9999\r\n\r\n"


def get_headers(data):
# 获取 sec-websocket-key
header_dict = {}
header_str = data.decode("utf8")
for i in header_str.split("\r\n"):

if str(i).startswith("Sec-WebSocket-Key"):
header_dict["Sec-WebSocket-Key"] = i.split(":")[1].strip()

return header_dict


def decrypt_client_info(msg):
'''解密客户端发送的信息'''

hashstr = msg
# 将第二个字节也就是 \x83 第9-16位 进行与127进行位运算
payload = hashstr[1] & 127
print(payload)
if payload == 127:
extend_payload_len = hashstr[2:10]
mask = hashstr[10:14]
decoded = hashstr[14:]
# 当位运算结果等于127时,则第3-10个字节为数据长度
# 第11-14字节为mask 解密所需字符串
# 则数据为第15字节至结尾

if payload == 126:
extend_payload_len = hashstr[2:4]
mask = hashstr[4:8]
decoded = hashstr[8:]
# 当位运算结果等于126时,则第3-4个字节为数据长度
# 第5-8字节为mask 解密所需字符串
# 则数据为第9字节至结尾

if payload <= 125:
extend_payload_len = None
mask = hashstr[2:6]
decoded = hashstr[6:]

# 当位运算结果小于等于125时,则这个数字就是数据的长度
# 第3-6字节为mask 解密所需字符串
# 则数据为第7字节至结尾

str_byte = bytearray()

for i in range(len(decoded)):
byte = decoded[i] ^ mask[i % 4]
str_byte.append(byte)

return str_byte.decode("utf8")


# 发送消息
def send_msg(msg, conn):
# 服务端回应,防止粘包现象 使用了 struct 模块
msg_bytes = msg.encode("utf8")
token = b"\x81"
length = len(msg_bytes)

if length < 126:
token += struct.pack("B", length)
elif length == 126:
token += struct.pack("!BH", 126, length)
else:
token += struct.pack("!BQ", 127, length)
msg = token + msg_bytes
conn.send(msg)


with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# 端口重用
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))
s.listen()
conn, addr = s.accept()
# 获取客户端的握手信息
with conn:
# print('Connected by', addr)
data = conn.recv(1024)
headers = get_headers(data) # 提取请求头信息
# 对请求头中的 sec-websocket-key 进行加密
# 固定特殊字符串
magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
value = headers['Sec-WebSocket-Key'] + magic_string
print(value)
ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())

response_str = response_tpl % (ac.decode('utf-8'))
# 响应握手信息
conn.send(response_str.encode("utf8"))
while True:
msg = conn.recv(8096)
print(decrypt_client_info(msg))
send_msg('你好啊发送发送发送', conn)

HTML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div>
<input type="text" id="txt"/>
<input type="button" id="btn" value="发送消息" onclick="sendMsg();"/>
<input type="button" id="close" value="关闭连接" onclick="closeConn();"/>
</div>
<div id="content"></div>

<script type="text/javascript">
var socket = new WebSocket("ws://127.0.0.1:9999");

socket.onopen = function () {
/* 与服务器端连接成功后,自动执行 */

var newTag = document.createElement('div');
newTag.innerHTML = "【连接成功】";
document.getElementById('content').appendChild(newTag);
};

socket.onmessage = function (event) {
/* 服务器端向客户端发送数据时,自动执行 */
var response = event.data;
var newTag = document.createElement('div');
newTag.innerHTML = response;
document.getElementById('content').appendChild(newTag);
};

socket.onclose = function (event) {
/* 服务器端主动断开连接时,自动执行 */
var newTag = document.createElement('div');
newTag.innerHTML = "【关闭连接】";
document.getElementById('content').appendChild(newTag);
};
// socket.addEventListener('message', function (event) {
// console.log('Message from server ', event.data);
//
// });


function sendMsg() {
var txt = document.getElementById('txt');
socket.send(txt.value);
txt.value = "";
}

function closeConn() {
socket.close();
var newTag = document.createElement('div');
newTag.innerHTML = "【关闭连接】";
document.getElementById('content').appendChild(newTag);
}

</script>
</body>
</html>

gevent 实现 websocker demo 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

import json
import os
import sys
from gevent import monkey

monkey.patch_all()
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)

from pymongo import MongoClient
from flask import render_template, Flask
from werkzeug.debug import DebuggedApplication

from geventwebsocket import WebSocketServer, WebSocketApplication, Resource

from mechat.blueprints.ai_baidu import Baidu_AI

baidu_ai = Baidu_AI()

mgclient = MongoClient("127.0.0.1", 27017)
MONGO_DB = mgclient["chat_mess_room"]

flask_app = Flask(__name__)
flask_app.debug = True


class ChatApplication(WebSocketApplication):
def on_open(self):
print('connect')

def on_message(self, message):
if message is None:
return

message = json.loads(message)
# a = {'msg_type': 'message', 'nickname': 'unknown', 'message': '你啊'}

if message['msg_type'] == 'message':
im = message['message']
tuningre = baidu_ai.tuning(im)
cur_time = message.get('cur_time')
the_robot = {'msg_type': 'message', 'nickname': '图灵机器人的爸爸', 'message': tuningre, 'the_robot': "robot",
'cur_time': cur_time}

self.broadcast(message)
self.broadcast(the_robot)
MONGO_DB.groups.insert_many([message, the_robot])



elif message['msg_type'] == 'update_clients':
self.send_client_list(message)

def send_client_list(self, message):
current_client = self.ws.handler.active_client
current_client.nickname = message['nickname']

self.ws.send(json.dumps({
'msg_type': 'update_clients',
'clients': [
getattr(client, 'nickname', 'anonymous')
for client in self.ws.handler.server.clients.values()
]
}))

def broadcast(self, message):
for client in self.ws.handler.server.clients.values():
# client.ws.send(json.dumps({
# 'msg_type': 'message',
# 'nickname': message['nickname'],
# 'message': message['message']
# }))
client.ws.send(json.dumps(message))

def on_close(self, reason):
print('Connectiion closed')


@flask_app.route('/')
def home():
te = [{"nickname": "baozi", "body": '你的名字叫啥'},
{"nickname": "baozi", "body": '做个自我介绍啊'},
{"nickname": "baozi", "body": '发图啊'}]

re = list(MONGO_DB.groups.find({}, {"_id": 0}))

# return render_template("home2.html")
return render_template("home2.html", messages=re)


if __name__ == '__main__':
WebSocketServer(('192.168.1.2', 9999), Resource([
('^/chat', ChatApplication),
('^/.*', DebuggedApplication(flask_app))
]), debug=False).serve_forever()


V2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from __future__ import print_function

import json

from gevent import monkey

monkey.patch_all()

from flask import Flask, render_template
from werkzeug.debug import DebuggedApplication

from geventwebsocket import WebSocketServer, WebSocketApplication, Resource

websocket = Flask(__name__)
websocket.debug = True


class ChatApplication(WebSocketApplication):
''' webSocket 消息发送'''

def on_open(self):
print("Some client connected!")

def on_message(self, message):
if message is None:
return

message = json.loads(message)

if message.get('msg_type') == 'send_':
print(message)
self.share_music_friend(message)

elif message.get('msg_type') == 'accept_':
self.accept_client_list(message)
else:
pass

def share_music_friend(self, message):
current_client = self.ws.handler.active_client
# current_client.from_id = message.get('toy_id')
current_client.from_id = message.get('toy_id', message.get("from_user"))

for client in self.ws.handler.server.clients.values():
if getattr(client, 'from_id', 'anonymous') == message['to_user']:
client.ws.send(json.dumps(message))

def accept_client_list(self, message):
print(message)
current_client = self.ws.handler.active_client
current_client.from_id = message.get('toy_id', message.get("from_user"))

def on_close(self, reason):
print("Connection closed!")


@websocket.route('/toy')
def toy():
return render_template('toy.html')


WebSocketServer(
("192.168.1.2", 8001),

Resource([
('^/sendone', ChatApplication),
('^/.*', DebuggedApplication(websocket))
]),

debug=False
).serve_forever()

WebSocket

Writing WebSocket servers

https://developer.mozilla.org/en-US/docs/Web/API/WebSocket