人工智能   

【首页】

python之websocket


【2020-12-27】 【人工智能】


一、websocket

WebSocket协议是基于TCP的一种新的协议。WebSocket最初在HTML5规范中被引用为TCP连接,作为基于TCP的套接字API的占位符。它实现了浏览器与服务器全双工(full-duplex)通信。其本质是保持TCP连接,在浏览器和服务端通过Socket进行通信。

本文将使用Python编写Socket服务端,一步一步分析请求过程!!!

1. 启动服务端


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)

# 等待用户连接

conn, address = sock.accept()

...

...

...


启动Socket服务器后,等待用户【连接】,然后进行收发数据。

2. 客户端连接

当客户端向服务端发送连接请求时,不仅连接还会发送【握手】信息,并等待服务端响应,至此连接才创建成功!

3. 建立连接【握手】

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(1024)

...

...

...

conn.send(´响应【握手】信息´)


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

  • 从请求【握手】信息中提取 Sec-WebSocket-Key
  • 利用magic_string 和 Sec-WebSocket-Key 进行hmac1加密,再进行base64加密
  • 将加密结果响应给客户端

注:magic string为:258EAFA5-E914-47DA-95CA-C5AB0DC85B11

请求【握手】信息为:


GET /chatsocket HTTP/1.1

Host: 127.0.0.1:8002

Connection: Upgrade

Pragma: no-cache

Cache-Control: no-cache

Upgrade: websocket

Origin: http://localhost:63342

Sec-WebSocket-Version: 13

Sec-WebSocket-Key: mnwFxiOlctXFN/DeMt1Amg==

Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

...

...


提取Sec-WebSocket-Key值并加密:


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(´

´):

print(i)

header, body = data.split(´

´, 1)

header_list = header.split(´

´)

for i in range(0, len(header_list)):

if i == 0:

if len(header_list.split(´ ´)) == 3:

header_dict[´method´], header_dict[´url´], header_dict[´protocol´] = header_list.split(´ ´)

else:

k, v = header_list.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

"

"Upgrade:websocket

"

"Connection: Upgrade

"

"Sec-WebSocket-Accept: %s

"

"WebSocket-Location: ws://%s%s

"

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´))

...

...

...


4.客户端和服务端收发数据

客户端和服务端传输数据时,需要对数据进行【封包】和【解包】。客户端的JavaScript类库已经封装【封包】和【解包】过程,但Socket服务端需要手动实现。

第一步:获取客户端发送的数据【解包】


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 ^ mask[i % 4]

bytes_list.append(chunk)

body = str(bytes_list, encoding=´utf-8´)

print(body)


解包详细过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 ... |
+---------------------------------------------------------------+

第二步:向客户端发送数据【封包】


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


5. 基于Python实现简单示例

a. 基于Python socket实现的WebSocket服务端:


#!/usr/bin/env python

# -*- coding:utf-8 -*-

import socket

import base64

import hashlib

def get_headers(data):

"""

将请求头格式化成字典

:param data:

:return:

"""

header_dict = {}

data = str(data, encoding=´utf-8´)

header, body = data.split(´

´, 1)

header_list = header.split(´

´)

for i in range(0, len(header_list)):

if i == 0:

if len(header_list.split(´ ´)) == 3:

header_dict[´method´], header_dict[´url´], header_dict[´protocol´] = header_list.split(´ ´)

else:

k, v = header_list.split(´:´, 1)

header_dict[k] = v.strip()

return header_dict

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

def run():

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

sock.bind((´127.0.0.1´, 8003))

sock.listen(5)

conn, address = sock.accept()

data = conn.recv(1024)

headers = get_headers(data)

response_tpl = "HTTP/1.1 101 Switching Protocols

"

"Upgrade:websocket

"

"Connection:Upgrade

"

"Sec-WebSocket-Accept:%s

"

"WebSocket-Location:ws://%s%s

"

value = headers[´Sec-WebSocket-Key´] + ´258EAFA5-E914-47DA-95CA-C5AB0DC85B11´

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´))

while True:

try:

info = conn.recv(8096)

except Exception as e:

info = None

if not info:

break

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 ^ mask[i % 4]

bytes_list.append(chunk)

body = str(bytes_list, encoding=´utf-8´)

send_msg(conn,body.encode(´utf-8´))

sock.close()

if __name__ == ´__main__´:

run()


b. 利用JavaScript类库实现客户端



6. 基于Tornado框架实现Web聊天室

Tornado是一个支持WebSocket的优秀框架,其内部原理正如1~5步骤描述,当然Tornado内部封装功能更加完整。

以下是基于Tornado实现的聊天室示例:


#!/usr/bin/env python

# -*- coding:utf-8 -*-

import uuid

import json

import tornado.ioloop

import tornado.web

import tornado.websocket

class IndexHandler(tornado.web.RequestHandler):

def get(self):

self.render(´index.html´)

class ChatHandler(tornado.websocket.WebSocketHandler):

# 用户存储当前聊天室用户

waiters = set()

# 用于存储历时消息

messages = []

def open(self):

"""

客户端连接成功时,自动执行

:return:

"""

ChatHandler.waiters.add(self)

uid = str(uuid.uuid4())

self.write_message(uid)

for msg in ChatHandler.messages:

content = self.render_string(´message.html´, **msg)

self.write_message(content)

def on_message(self, message):

"""

客户端连发送消息时,自动执行

:param message:

:return:

"""

msg = json.loads(message)

ChatHandler.messages.append(message)

for client in ChatHandler.waiters:

content = client.render_string(´message.html´, **msg)

client.write_message(content)

def on_close(self):

"""

客户端关闭连接时,,自动执行

:return:

"""

ChatHandler.waiters.remove(self)

def run():

settings = {

´template_path´: ´templates´,

´static_path´: ´static´,

}

application = tornado.web.Application([

(r"/", IndexHandler),

(r"/chat", ChatHandler),

], **settings)

application.listen(8888)

tornado.ioloop.IOLoop.instance().start()

if __name__ == "__main__":

run()








copyright©2018-2021 ynn.gotopie.com