1. 程式人生 > >[Python]通過websocket與js客戶端通訊

[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+migicSHA-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

Pythonsocket接受得到握手字串,處理後傳送

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運算,其中ix在兄弟資料中的索引。

完整程式碼

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)

包長:根據傳送資料長度是否超過1250xFFFF(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>

推薦blog