python websocket學習使用

前言html

今天看了一些資料,記錄一下心得。

websocket是html5引入的一個新特性,傳統的web應用是經過http協議來提供支持,若是要實時同步傳輸數據,須要輪詢,效率低下

websocket是相似socket通訊,web端鏈接服務器後,握手成功,一直保持鏈接,能夠理解爲長鏈接,這時服務器就能夠主動給客戶端發送數據,實現數據的自動更新。

使用websocket須要注意瀏覽器和當前的版本,不一樣的瀏覽器提供的支持不同,所以設計服務器的時候,須要考慮。


 

進一步簡述前端

 

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,這裏就不過多贅述了。                                                                              

 

Python的Websocket客戶端:Websocket-Client

 

 

Websocket-Client 是 Python 上的 Websocket 客戶端。它只支持 hybi-13,且全部的 Websocket API 都支持同步。

Installation

This module is tested on Python 2.7 and Python 3.x.html5

Type "python setup.py install" or "pip install websocket-client" to install.python

Caution!web

from v0.16.0, we can install by "pip install websocket-client" for python 3.瀏覽器

This module depend on服務器

  • six
  • backports.ssl_match_hostname for Python 2.x

 

 

Python經過websocket與js客戶端通訊示例分析

這裏,介紹如何使用 Python 與前端 js 進行通訊。websocket

websocket 使用 HTTP 協議完成握手以後,不經過 HTTP 直接進行 websocket 通訊。網絡

因而,使用 websocket 大體兩個步驟:使用 HTTP 握手,通訊。socket

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==
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>
相關文章
相關標籤/搜索