原文:http://www.cnblogs.com/jinjiangongzuoshi/p/5062092.htmlhtml
前言前端
今天看了一些資料,記錄一下心得。
websocket是html5引入的一個新特性,傳統的web應用是經過http協議來提供支持,若是要實時同步傳輸數據,須要輪詢,效率低下
websocket是相似socket通訊,web端鏈接服務器後,握手成功,一直保持鏈接,能夠理解爲長鏈接,這時服務器就能夠主動給客戶端發送數據,實現數據的自動更新。
使用websocket須要注意瀏覽器和當前的版本,不一樣的瀏覽器提供的支持不同,所以設計服務器的時候,須要考慮。
進一步簡述html5
websocket是一個瀏覽器和服務器通訊的新的協議,通常而言,瀏覽器和服務器通訊最經常使用的是http協議,可是http協議是無狀態的,每次瀏覽器請求信息,服務器返回信息後這個瀏覽器和服務器通訊的信道就被關閉了,這樣使得服務器若是想主動給瀏覽器發送信息變得不可能了,服務器推技術在http時代的解決方案一個是客戶端去輪詢,或是使用comet技術,而websocket則和通常的socket同樣,使得瀏覽器和服務器創建了一個雙工的通道。
具體的websocket協議在rfc6455裏面有,這裏簡要說明一下。websocket通訊須要先有個握手的過程,使得協議由http轉變爲webscoket協議,而後瀏覽器和服務器就能夠利用這個socket來通訊了。
首先瀏覽器發送握手信息,要求協議轉變爲websocket
GET / HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
服務器接收到信息後,取得其中的Sec-WebSocket-Key,將他和一個固定的字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11作拼接,獲得的字符串先用sha1作一下轉換,再用base64轉換一下,就獲得了迴應的字符串,這樣服務器端發送回的消息是這樣的
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
這樣握手就完成了,用python來實現這段握手過程的話就是下面這樣。
def handshake(conn):
key =None
data = conn.recv(8192)
if not len(data):
return False
for line in data.split('\r\n\r\n')[0].split('\r\n')[1:]:
k, v = line.split(': ')
if k =='Sec-WebSocket-Key':
key =base64.b64encode(hashlib.sha1(v +'258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest())
if not key:
conn.close()
return False
response ='HTTP/1.1 101 Switching Protocols\r\n'\
'Upgrade: websocket\r\n'\
'Connection: Upgrade\r\n'\
'Sec-WebSocket-Accept:'+ key +'\r\n\r\n'
conn.send(response)
return True
握手過程完成以後就是信息傳輸了,websocket的數據信息格式是這樣的。
+-+-+-+-+-------+-+-------------+-------------------------------+
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 ... |
+---------------------------------------------------------------+
值得注意的是payload len這項,表示數據的長度有多少,若是小於126,那麼payload len就是數據的長度,若是是126那麼接下來2個字節是數據長度,若是是127表示接下來8個字節是數據長度,而後後面會有四個字節的mask,真實數據要由payload data和mask作異或才能獲得,這樣就能夠獲得數據了。發送數據的格式和接受的數據相似,具體細節能夠去參考rfc6455,這裏就不過多贅述了。
Websocket-Client 是 Python 上的 Websocket 客戶端。它只支持 hybi-13,且全部的 Websocket API 都支持同步。
This module is tested on Python 2.7 and Python 3.x.python
Type "python setup.py install" or "pip install websocket-client" to install.web
Caution!瀏覽器
from v0.16.0, we can install by "pip install websocket-client" for python 3.服務器
This module depend onwebsocket
這裏,介紹如何使用 Python 與前端 js 進行通訊。網絡
websocket 使用 HTTP 協議完成握手以後,不經過 HTTP 直接進行 websocket 通訊。socket
因而,使用 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-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
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