一、websocket
WebSocket协议是基于TCP的一种新的协议。WebSocket最初在HTML5规范中被引用为TCP连接,作为基于TCP的套接字API的占位符。它实现了浏览器与服务器全双工(full-duplex)通信。其本质是保持TCP连接,在浏览器和服务端通过Socket进行通信。
本文将使用Python编写Socket服务端,一步一步分析请求过程!!!
1. 启动服务端
import socketsock
= 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.1Host:
127.0.0.1:8002Connection: Upgrade
Pragma: no
-cacheCache
-Control: no-cacheUpgrade: websocket
Origin: http:
//localhost:63342Sec
-WebSocket-Version: 13Sec
-WebSocket-Key: mnwFxiOlctXFN/DeMt1Amg==Sec
-WebSocket-Extensions: permessage-deflate; client_max_window_bits...
...
提取Sec-WebSocket-Key值并加密:
import socketimport base64import hashlibdef 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_stringac
= 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] & 127if 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
= Nonemask
= 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_bytesconn.send(msg)
return True
5. 基于Python实现简单示例
a. 基于Python socket实现的WebSocket服务端:
#!/usr/bin/env python#
-*- coding:utf-8 -*-import socketimport base64import 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_bytesconn.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
= Noneif not info:
break
payload_len
= info[1] & 127if 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
= Nonemask
= 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 uuidimport jsonimport tornado.ioloopimport tornado.webimport tornado.websocketclass 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()