一、websocket是什麼javascript
Websocket是html5提出的一個協議規範,參考rfc6455。php
websocket約定了一個通訊的規範,經過一個握手的機制,客戶端(瀏覽器)和服務器(webserver)之間能創建一個相似tcp的鏈接,從而方便c-s之間的通訊。在websocket出現以前,web交互通常是基於http協議的短鏈接或者長鏈接。html
WebSocket是爲解決客戶端與服務端實時通訊而產生的技術。websocket協議本質上是一個基於tcp的協議,是先經過HTTP/HTTPS協議發起一條特殊的http請求進行握手後建立一個用於交換數據的TCP鏈接,此後服務端與客戶端經過此TCP鏈接進行實時通訊。前端
注意:此時再也不須要原HTTP協議的參與了。html5
二、websocket的優勢java
之前web server實現推送技術或者即時通信,用的都是輪詢(polling),在特色的時間間隔(好比1秒鐘)由瀏覽器自動發出請求,將服務器的消息主動的拉回來,在這種狀況下,咱們須要不斷的向服務器發送請求,然而HTTP request 的header是很是長的,裏面包含的數據可能只是一個很小的值,這樣會佔用不少的帶寬和服務器資源。jquery
而最比較新的技術去作輪詢的效果是Comet – 用了AJAX。但這種技術雖然可達到全雙工通訊,但依然須要發出請求(reuqest)。web
WebSocket API最偉大之處在於服務器和客戶端能夠在給定的時間範圍內的任意時刻,相互推送信息。 瀏覽器和服務器只須要要作一個握手的動做,在創建鏈接以後,服務器能夠主動傳送數據給客戶端,客戶端也能夠隨時向服務器發送數據。 此外,服務器與客戶端之間交換的標頭信息很小。算法
WebSocket並不限於以Ajax(或XHR)方式通訊,由於Ajax技術須要客戶端發起請求,而WebSocket服務器和客戶端能夠彼此相互推送信息;json
所以從服務器角度來講,websocket有如下好處:
三、websocket的協議規範
1.基於flash的握手協議
使用場景是IE的多數版本,由於IE的多數版本不都不支持WebSocket協議,以及FF、CHROME等瀏覽器的低版本,尚未原生的支持WebSocket。此處,server惟一要作的,就是準備一個WebSocket-Location域給client,沒有加密,可靠性不好。
2.基於md5加密方式的握手協議
其中 Sec-WebSocket-Key1,Sec-WebSocket-Key2 和 [8-byte security key] 這幾個頭信息是web server用來生成應答信息的來源,依據 draft-hixie-thewebsocketprotocol-76 草案的定義。
web server基於如下的算法來產生正確的應答信息:
1. 逐個字符讀取 Sec-WebSocket-Key1 頭信息中的值,將數值型字符鏈接到一塊兒放到一個臨時字符串裏,同時統計全部空格的數量;
2. 將在第(1)步裏生成的數字字符串轉換成一個整型數字,而後除以第(1)步裏統計出來的空格數量,將獲得的浮點數轉換成整數型;
3. 將第(2)步裏生成的整型值轉換爲符合網絡傳輸的網絡字節數組;
4. 對 Sec-WebSocket-Key2 頭信息一樣進行第(1)到第(3)步的操做,獲得另一個網絡字節數組;
5. 將 [8-byte security key] 和在第(3)、(4)步裏生成的網絡字節數組合併成一個16字節的數組;
6. 對第(5)步生成的字節數組使用MD5算法生成一個哈希值,這個哈希值就做爲安全密鑰返回給客戶端,以代表服務器端獲取了客戶端的請求,贊成建立websocket鏈接
3.基於sha加密方式的握手協議
也是目前見的最多的一種方式,這裏的版本號目前是須要13以上的版本。
客戶端請求:
GET /ls HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: www.qixing318.com
Sec-WebSocket-Origin: http://www.qixing318.com
Sec-WebSocket-Key: 2SCVXUeP9cTjV+0mWB8J6A==
Sec-WebSocket-Version: 13
服務器返回:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket Connection:
Upgrade Sec-WebSocket-Accept: mLDKNeBNWz6T9SxU+o0Fy/HgeSw=
其中 server就是把客戶端上報的key拼上一段GUID( 「258EAFA5-E914-47DA-95CA-C5AB0DC85B11″),拿這個字符串作SHA-1 hash計算,而後再把獲得的結果經過base64加密,最後再返回給客戶端。
-格式:\r\n
-建立連接以後默認不斷開
四、基於sha加密的Opening Handshake(握手環節)
客戶端發起鏈接Handshake請求
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-Accept
服務器端在接收到的Sec-WebSocket-Key密鑰後追加一段神奇字符串「258EAFA5-E914-47DA-95CA-C5AB0DC85B11」,並將結果進行sha-1哈希,而後再進行base64加密返回給客戶端(就是Sec-WebSocket-Key)。 好比:
function encry($req) { $key = $this->getKey($req); $mask = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; # 將 SHA-1 加密後的字符串再進行一次 base64 加密 return base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true)); }
若是加密算法錯誤,客戶端在進行校檢的時候會直接報錯。若是握手成功,則客戶端側會出發onopen事件。HTTP/1.1 101 Switching Protocols
101爲服務器返回的狀態碼,全部非101的狀態碼都表示handshake並未完成。
Data Framing
Websocket協議經過序列化的數據幀傳輸數據。數據封包協議中定義了opcode、payload length、Payload data等字段。其中要求:
針對上狀況,發現錯誤的一方可向對方發送close幀(狀態碼是1002,表示協議錯誤),以關閉鏈接。
具體數據幀格式以下圖所示:
這裏的長度表示遵循一個原則,用最少的字節表示長度(儘可能減小沒必要要的傳輸)。舉例說,payload真實長度是124,在0-125之間,必須用前7位表示;不容許長度1是126或127,而後長度2是124,這樣違反原則。
Payload data
應用層數據
接收到客戶端數據後的解析規則以下:
示例代碼:
while True:
# 對數據進行解密
# send_msg(conn, bytes('alex', encoding='utf-8'))
# send_msg(conn, bytes('SB', encoding='utf-8'))
# info = conn.recv(8096)
# print(info)
info = conn.recv(8096)
payload_len = info[1] & 127
if 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 = None
mask = info[2:6]
decoded = info[6:]
bytes_list = bytearray()
for i in range(len(decoded)):
chunk = decoded[i] ^ mask[i % 4]
bytes_list.append(chunk)
msg = str(bytes_list, encoding='utf-8')
rep = msg + 'sb'
send_msg(conn,bytes(rep,encoding='utf-8'))
五、原理代碼:
import socket import hashlib import base64 def get_headers(data): """ 將請求頭格式化成字典 :param data: :return: """ header_dict = {} data = str(data, encoding='utf-8') header, body = data.split('\r\n\r\n', 1) header_list = header.split('\r\n') for i in range(0, len(header_list)): if i == 0: if len(header_list[i].split(' ')) == 3: header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ') else: k, v = header_list[i].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_bytes conn.send(msg) return True 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() # WebSocket發來的鏈接 # 1. 獲取握手數據 data = conn.recv(1024) headers = get_headers(data) # 2. 對握手信息進行加密: magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' value = headers['Sec-WebSocket-Key'] + magic_string ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest()) # 3. 返回握手信息 response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \ "Upgrade:websocket\r\n" \ "Connection: Upgrade\r\n" \ "Sec-WebSocket-Accept: %s\r\n" \ "WebSocket-Location: ws://127.0.0.1:8002\r\n\r\n" response_str = response_tpl % (ac.decode('utf-8'),) conn.sendall(bytes(response_str, encoding='utf-8')) # 以後,才能進行首發數據。 while True: # 對數據進行解密 # send_msg(conn, bytes('alex', encoding='utf-8')) # send_msg(conn, bytes('SB', encoding='utf-8')) # info = conn.recv(8096) # print(info) info = conn.recv(8096) payload_len = info[1] & 127 if 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 = None mask = info[2:6] decoded = info[6:] bytes_list = bytearray() for i in range(len(decoded)): chunk = decoded[i] ^ mask[i % 4] bytes_list.append(chunk) msg = str(bytes_list, encoding='utf-8') rep = msg + 'sb' send_msg(conn,bytes(rep,encoding='utf-8'))
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>WebSocket協議學習</h1> <script type="text/javascript"> // 向 127.0.0.1:8002 發送一個WebSocket請求 var socket = new WebSocket("ws://127.0.0.1:8002"); socket.onmessage = function (event) { /* 服務器端向客戶端發送數據時,自動執行 */ var response = event.data; console.log(response); }; </script> </body> </html>
一、Flask中應用: pip3 install gevent-websocket
from flask import Flask,request,render_template,session,redirect
import uuid
import json
from geventwebsocket.handler import WebSocketHandler
from gevent.pywsgi import WSGIServer
app = Flask(__name__)
app.secret_key = 'asdfasdf'
GENTIEMAN = {
'1':{'name':'鋼彈','count':0},
'2':{'name':'鐵錘','count':0},
'3':{'name':'閆帥','count':0},
}
WEBSOCKET_DICT = {
}
@app.before_request
def before_request():
if request.path == '/login':
return None
user_info = session.get('user_info')
if user_info:
return None
return redirect('/login')
@app.route('/login',methods=['GET','POST'])
def login():
if request.method == "GET":
return render_template('login.html')
else:
uid = str(uuid.uuid4())
session['user_info'] = {'id':uid,'name':request.form.get('user')}
return redirect('/index')
@app.route('/index')
def index():
return render_template('index.html',users=GENTIEMAN)
@app.route('/message')
def message():
# 1. 判斷究竟是否是websocket請求?
ws = request.environ.get('wsgi.websocket')
if not ws:
return "請使用WebSocket協議"
# ----- ws鏈接成功 -------
current_user_id = session['user_info']['id']
WEBSOCKET_DICT[current_user_id] = ws
while True:
# 2. 等待用戶發送消息,並接受
message = ws.receive() # 帥哥ID
# 關閉:message=None
if not message:
del WEBSOCKET_DICT[current_user_id]
break
# 3. 獲取用戶要投票的帥哥ID,並+1
old = GENTIEMAN[message]['count']
new = old + 1
GENTIEMAN[message]['count'] = new
data = {'user_id': message, 'count': new,'type':'vote'}
# 4. 給全部客戶端推送消息
for conn in WEBSOCKET_DICT.values():
conn.send(json.dumps(data))
return 'close'
@app.route('/notify')
def notify():
data = {'data': "你的訂單已經生成,請及時處理;", 'type': 'alert'}
print(WEBSOCKET_DICT)
for conn in WEBSOCKET_DICT.values():
conn.send(json.dumps(data))
return '發送成功'
if __name__ == '__main__':
http_server = WSGIServer(('192.168.11.143', 5000), app, handler_class=WebSocketHandler)
http_server.serve_forever()
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form method="post"> <input type="text" name="user"> <input type="submit" value="提交"> </form> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>投票系統:參與投票的人</h1> <ul> {% for k,v in users.items() %} <li id="user_{{k}}" ondblclick="vote('{{k}}')">{{v.name}} <span>{{v.count}}</span> </li> {% endfor %} </ul> <script src="{{ url_for('static',filename='jquery-3.3.1.min.js')}}"></script> <script> var socket = new WebSocket("ws://192.168.11.143:5000/message"); socket.onmessage = function (event) { /* 服務器端向客戶端發送數據時,自動執行 */ var response = JSON.parse(event.data); // {'user':1,'count':new} if(response.type == 'vote'){ var nid = '#user_' + response.user_id; $(nid).find('span').text(response.count) }else{ alert(response.data); } }; /* 我要給某人投票 */ function vote(id) { socket.send(id); } </script> </body> </html>
二、Django應用:channel
三、Tornado應用:本身有