[Python]通過websocket與js客戶端通訊
網站大多使用HTTP協議通訊,而HTTP是無連線的協議。只有客戶端請求時,伺服器端才能發出相應的應答,HTTP請求的包也比較大,如果只是很小的資料通訊,開銷過大。於是,我們可以使用websocket這個協議,用最小的開銷實現面向連線的通訊。
這裡,介紹如何使用Python與前端js進行通訊。
websocket使用HTTP協議完成握手之後,不通過HTTP直接進行websocket通訊。
於是,使用websocket大致兩個步驟:使用HTTP握手,通訊。
js處理websocket要使用ws模組;Python處理則使用socket模組建立TCP連線即可,比一般的socket,只多一個握手以及資料處理的步驟。
握手
過程
包格式
js客戶端先向伺服器端python傳送握手包,格式如下:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
伺服器迴應包格式:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat
其中,Sec-WebSocket-Key是隨機的,伺服器用這些資料構造一個SHA-1資訊摘要。
方法為:key+migic,SHA-1 加密,base-64加密,如下:
Python中的處理程式碼
MAGIC_STRING = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
res_key = base64.b64encode(hashlib.sha1(sec_key + MAGIC_STRING).digest())
握手完整程式碼
js端
js中有處理websocket的類,初始化後自動傳送握手包,如下:
var socket = new WebSocket('ws://localhost:3368');
Python端
Python用socket接受得到握手字串,處理後傳送
HOST = 'localhost'
PORT = 3368
MAGIC_STRING = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
HANDSHAKE_STRING = "HTTP/1.1 101 Switching Protocols\r\n" \
"Upgrade:websocket\r\n" \
"Connection: Upgrade\r\n" \
"Sec-WebSocket-Accept: {1}\r\n" \
"WebSocket-Location: ws://{2}/chat\r\n" \
"WebSocket-Protocol:chat\r\n\r\n"
def handshake(con):
#con為用socket,accept()得到的socket
#這裡省略監聽,accept的程式碼,具體可見blog:http://blog.csdn.net/ice110956/article/details/29830627
headers = {}
shake = con.recv(1024)
if not len(shake):
return False
header, data = shake.split('\r\n\r\n', 1)
for line in header.split('\r\n')[1:]:
key, val = line.split(': ', 1)
headers[key] = val
if 'Sec-WebSocket-Key' not in headers:
print ('This socket is not websocket, client close.')
con.close()
return False
sec_key = headers['Sec-WebSocket-Key']
res_key = base64.b64encode(hashlib.sha1(sec_key + MAGIC_STRING).digest())
str_handshake = HANDSHAKE_STRING.replace('{1}', res_key).replace('{2}', HOST + ':' + str(PORT))
print str_handshake
con.send(str_handshake)
return True
通訊
不同版本的瀏覽器定義的資料幀格式不同,Python傳送和接收時都要處理得到符合格式的資料包,才能通訊。
Python接收
Python接收到瀏覽器發來的資料,要解析後才能得到其中的有用資料。
瀏覽器包格式
固定位元組:
(1000 0001或是1000 0002)這裡沒用,忽略
包長度位元組:
第一位肯定是1,忽略。剩下7個位可以得到一個整數(0 ~ 127),其中
(1-125)表此位元組為長度位元組,大小即為長度;
(126)表接下來的兩個位元組才是長度;
(127)表接下來的八個位元組才是長度;
用這種變長的方式表示資料長度,節省資料位。
mark掩碼:
mark掩碼為包長之後的4個位元組,之後的兄弟資料要與mark掩碼做運算才能得到真實的資料。
兄弟資料:
得到真實資料的方法:將兄弟資料的每一位x,和掩碼的第i%4位做xor運算,其中i是x在兄弟資料中的索引。
完整程式碼
def recv_data(self, num):
try:
all_data = self.con.recv(num)
if not len(all_data):
return False
except:
return False
else:
code_len = ord(all_data[1]) & 127
if code_len == 126:
masks = all_data[4:8]
data = all_data[8:]
elif code_len == 127:
masks = all_data[10:14]
data = all_data[14:]
else:
masks = all_data[2:6]
data = all_data[6:]
raw_str = ""
i = 0
for d in data:
raw_str += chr(ord(d) ^ ord(masks[i % 4]))
i += 1
return raw_str
js端的ws物件,通過ws.send(str)即可傳送
ws.send(str)
Python傳送
Python要包資料傳送,也需要處理,傳送包格式如下
固定位元組:固定的1000 0001(‘\x81′)
包長:根據傳送資料長度是否超過125,0xFFFF(65535)來生成1個或3個或9個位元組,來代表資料長度。
def send_data(self, data):
if data:
data = str(data)
else:
return False
token = "\x81"
length = len(data)
if length < 126:
token += struct.pack("B", length)
elif length <= 0xFFFF:
token += struct.pack("!BH", 126, length)
else:
token += struct.pack("!BQ", 127, length)
#struct為Python中處理二進位制數的模組,二進位制流為C,或網路流的形式。
data = '%s%s' % (token, data)
self.con.send(data)
return True
js端通過回撥函式ws.onmessage()接受資料
ws.onmessage = function(result,nTime){
alert("從服務端收到的資料:");
alert("最近一次傳送資料到現在接收一共使用時間:" + nTime);
console.log(result);
}
最終程式碼
Python服務端
# _*_ coding:utf-8 _*_
__author__ = 'Patrick'
import socket
import threading
import sys
import os
import MySQLdb
import base64
import hashlib
import struct
# ====== config ======
HOST = 'localhost'
PORT = 3368
MAGIC_STRING = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
HANDSHAKE_STRING = "HTTP/1.1 101 Switching Protocols\r\n" \
"Upgrade:websocket\r\n" \
"Connection: Upgrade\r\n" \
"Sec-WebSocket-Accept: {1}\r\n" \
"WebSocket-Location: ws://{2}/chat\r\n" \
"WebSocket-Protocol:chat\r\n\r\n"
class Th(threading.Thread):
def __init__(self, connection,):
threading.Thread.__init__(self)
self.con = connection
def run(self):
while True:
try:
pass
self.con.close()
def recv_data(self, num):
try:
all_data = self.con.recv(num)
if not len(all_data):
return False
except:
return False
else:
code_len = ord(all_data[1]) & 127
if code_len == 126:
masks = all_data[4:8]
data = all_data[8:]
elif code_len == 127:
masks = all_data[10:14]
data = all_data[14:]
else:
masks = all_data[2:6]
data = all_data[6:]
raw_str = ""
i = 0
for d in data:
raw_str += chr(ord(d) ^ ord(masks[i % 4]))
i += 1
return raw_str
# send data
def send_data(self, data):
if data:
data = str(data)
else:
return False
token = "\x81"
length = len(data)
if length < 126:
token += struct.pack("B", length)
elif length <= 0xFFFF:
token += struct.pack("!BH", 126, length)
else:
token += struct.pack("!BQ", 127, length)
#struct為Python中處理二進位制數的模組,二進位制流為C,或網路流的形式。
data = '%s%s' % (token, data)
self.con.send(data)
return True
# handshake
def handshake(con):
headers = {}
shake = con.recv(1024)
if not len(shake):
return False
header, data = shake.split('\r\n\r\n', 1)
for line in header.split('\r\n')[1:]:
key, val = line.split(': ', 1)
headers[key] = val
if 'Sec-WebSocket-Key' not in headers:
print ('This socket is not websocket, client close.')
con.close()
return False
sec_key = headers['Sec-WebSocket-Key']
res_key = base64.b64encode(hashlib.sha1(sec_key + MAGIC_STRING).digest())
str_handshake = HANDSHAKE_STRING.replace('{1}', res_key).replace('{2}', HOST + ':' + str(PORT))
print str_handshake
con.send(str_handshake)
return True
def new_service():
"""start a service socket and listen
when coms a connection, start a new thread to handle it"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.bind(('localhost', 3368))
sock.listen(1000)
#連結佇列大小
print "bind 3368,ready to use"
except:
print("Server is already running,quit")
sys.exit()
while True:
connection, address = sock.accept()
#返回元組(socket,add),accept呼叫時會進入waite狀態
print "Got connection from ", address
if handshake(connection):
print "handshake success"
try:
t = Th(connection, layout)
t.start()
print 'new thread for client ...'
except:
print 'start new thread error'
connection.close()
if __name__ == '__main__':
new_service()
js客戶端
<script>
var socket = new WebSocket('ws://localhost:3368');
ws.onmessage = function(result,nTime){
alert("從服務端收到的資料:");
alert("最近一次傳送資料到現在接收一共使用時間:" + nTime);
console.log(result);
}
</script>