WebSocket
HTML5定義了WebSocket協議,能更好的節省服務器資源和帶寬,並且能夠更實時地進行通訊。
在2008年誕生,2011年成為國際標准。
現在基本所有瀏覽器都已經支持了。
WebSocket是一種在單個TCP連接上進行全雙工通信的協議。在WebSocket API中,瀏覽器和服務器只需要完成一次握手(不是指建立TCP連接的那個三次握手,是指在建立TCP連接后傳輸一次握手數據),兩者之間就直接可以創建持久性的連接,並進行雙向數據傳輸。
Websocket使用ws或wss的統一資源標志符,類似於HTTPS,其中wss表示在TLS之上的Websocket。如:
ws://example.com/wsapi wss://secure.example.com/
Websocket使用和 HTTP 相同的 TCP 端口,可以繞過大多數防火牆的限制。默認情況下,Websocket協議使用80端口;運行在TLS之上時,默認使用443端口。
握手協議
WebSocket 是獨立的、創建在 TCP 上的協議。 報文
Websocket 通過 HTTP/1.1 協議的101狀態碼進行握手。
為了創建Websocket連接,需要通過瀏覽器發出請求,之后服務器進行回應,這個過程通常稱為“握手”(handshaking)。
一個典型的Websocket握手請求如下:
客戶端請求
GET / HTTP/1.1 Upgrade: websocket Connection: Upgrade Host: example.com Origin: http://example.com Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ== Sec-WebSocket-Version: 13
服務器回應
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s= Sec-WebSocket-Location: ws://example.com/
- Connection必須設置Upgrade,表示客戶端希望連接升級。
- Upgrade字段必須設置Websocket,表示希望升級到Websocket協議。
- Sec-WebSocket-Key是隨機的字符串,服務器端會用這些數據來構造出一個SHA-1的信息摘要。把“Sec-WebSocket-Key”加上一個特殊字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后計算SHA-1摘要,之后進行BASE-64編碼,將結果做為“Sec-WebSocket-Accept”頭的值,返回給客戶端。如此操作,可以盡量避免普通HTTP請求被誤認為Websocket協議。
- Sec-WebSocket-Version 表示支持的Websocket版本。RFC6455要求使用的版本是13,之前草案的版本均應當棄用。
- Origin字段是可選的,通常用來表示在瀏覽器中發起此Websocket連接所在的頁面,類似於Referer。但是,與Referer不同的是,Origin只包含了協議和主機名稱。
- 其他一些定義在HTTP協議中的字段,如Cookie等,也可以在Websocket中使用。
優點
- 較少的控制開銷。在連接創建后,服務器和客戶端之間交換數據時,用於協議控制的數據包頭部相對較小。在不包含擴展的情況下,對於服務器到客戶端的內容,此頭部大小只有2至10字節(和數據包長度有關);對於客戶端到服務器的內容,此頭部還需要加上額外的4字節的掩碼。相對於HTTP請求每次都要攜帶完整的頭部,此項開銷顯著減少了。
- 更強的實時性。由於協議是全雙工的,所以服務器可以隨時主動給客戶端下發數據。相對於HTTP請求需要等待客戶端發起請求服務端才能響應,延遲明顯更少;即使是和Comet等類似的長輪詢比較,其也能在短時間內更多次地傳遞數據。
- 保持連接狀態。與HTTP不同的是,Websocket需要先創建連接,這就使得其成為一種有狀態的協議,之后通信時可以省略部分狀態信息。而HTTP請求可能需要在每個請求都攜帶狀態信息(如身份認證等)。 更好的二進制支持。Websocket定義了二進制幀,相對HTTP,可以更輕松地處理二進制內容。
- 可以支持擴展。Websocket定義了擴展,用戶可以擴展協議、實現部分自定義的子協議。如部分瀏覽器支持壓縮等。
- 更好的壓縮效果。相對於HTTP壓縮,Websocket在適當的擴展支持下,可以沿用之前內容的上下文,在傳遞類似的數據時,可以顯著地提高壓縮率。
- 沒有同源限制,客戶端可以與任意服務器通信。
- 可以發送文本,也可以發送二進制數據。
Socket.IO
1 簡介
Socket.IO 本是一個面向實時 web 應用的 JavaScript 庫,現在已成為擁有眾多語言支持的Web即時通訊應用的框架。
Socket.IO 主要使用WebSocket協議。但是如果需要的話,Socket.io可以回退到幾種其它方法,例如Adobe Flash Sockets,JSONP拉取,或是傳統的AJAX拉取,並且在同時提供完全相同的接口。盡管它可以被用作WebSocket的包裝庫,它還是提供了許多其它功能,比如廣播至多個套接字,存儲與不同客戶有關的數據,和異步IO操作。
Socket.IO 不等價於 WebSocket,WebSocket只是Socket.IO實現即時通訊的其中一種技術依賴,而且Socket.IO還在實現WebSocket協議時做了一些調整。
優點:
Socket.IO 會自動選擇合適雙向通信協議,僅僅需要程序員對套接字的概念有所了解。
有Python庫的實現,可以在Python實現的Web應用中去實現IM后台服務。
缺點:
Socket.io並不是一個基本的、獨立的、能夠回退到其它實時協議的WebSocket庫,它實際上是一個依賴於其它實時傳輸協議的自定義實時傳輸協議的實現。該協議的協商部分使得支持標准WebSocket的客戶端不能直接連接到Socket.io服務器,並且支持Socket.io的客戶端也不能與非Socket.io框架的WebSocket或Comet服務器通信。因而,Socket.io要求客戶端與服務器端均須使用該框架。
2 Python服務器端開發
安裝
pip install python-socketio
創建服務器
-
方式1
使用多進程多線程模式的WSGI服務器對接(如uWSGI、gunicorn)
import socketio # create a Socket.IO servers sio = socketio.Server() # 打包成WSGI應用,可以使用WSGI服務器托管運行 app = socketio.WSGIApp(sio) # Flask Django
創建好app對象后,使用uWSGI、或gunicorn服務器運行此對象。
-
方式2
作為Flask、Django 應用中的一部分
from wsgi import app # a Flask, Django, etc. application import socketio # create a Socket.IO server sio = socketio.Server() app = socketio.WSGIApp(sio, app)
創建好app對象后,使用uWSGI、或gunicorn服務器運行此對象。
-
方式3
使用協程的方式運行 (推薦)
import eventlet eventlet.monkey_patch() import socketio import eventlet.wsgi sio = socketio.Server(async_mode='eventlet') # 指明在evenlet模式下 app = socketio.Middleware(sio) eventlet.wsgi.server(eventlet.listen(('', 8000)), app)
說明
因為服務器與客戶端進行即時通信時,會盡可能的使用長連接,所以若服務器采用多進程或多線程方式運行,受限於服務器能創建的進程或線程數,能夠支持的並發連接客戶端不會很高,也就是服務器性能有限。采用協程方式運行服務器,可以提升即時通信服務器的性能。
事件處理
不同於HTTP服務的編寫方式,SocketIO服務編寫不再以請求Request和響應Response來處理,而是對收發的數據以消息(message)來對待,收發的不同類別的消息數據又以事件(event)來區分。
原本HTTP服務編寫中處理請求、構造響應的視圖處理函數在SocketIO服務中改為編寫收發不同事件的事件處理函數。
1)事件處理方法
編寫事件處理方法,可以接收指定的事件消息數據,並在處理方法中對消息數據進行處理。
@sio.on('connect') def on_connect(sid, environ): """ 與客戶端建立好連接后被執行 :param sid: string sid是socketio為當前連接客戶端生成的識別id :param environ: dict 在連接握手時客戶端發送的握手數據(HTTP報文解析之后的字典) """ pass @sio.on('disconnect') def on_disconnect(sid): """ 與客戶端斷開連接后被執行 :param sid: string sid是斷開連接的客戶端id """ pass # 以字符串的形式表示一個自定義事件,事件的定義由前后端約定 @sio.on('my custom event') def my_custom_event(sid, data): """ 自定義事件消息的處理方法 :param sid: string sid是發送此事件消息的客戶端id :param data: data是客戶端發送的消息數據 """ pass
注意
- connect 為特殊事件,當客戶端連接后自動執行
-
disconnect 為特殊事件,當客戶端斷開連接后自動執行
-
connect、disconnect與自定義事件處理方法的函數傳入參數不同
2)發送事件消息
-
群發
sio.emit('my event', {'data': 'foobar'})
-
給指定用戶發送
sio.emit('my event', {'data': 'foobar'}, room=user_sid)
-
給一組用戶發送
SocketIO提供了房間(room)來為客戶端分組
-
sio.enter_room(sid, room_name)
將連接的客戶端添加到一個room
@sio.on('chat') def begin_chat(sid): sio.enter_room(sid, 'chat_users')
注意:當客戶端連接后,socketio會自動將客戶端添加到以此客戶端sid為名的room中
-
sio.leave_room(sid, room_name)
將客戶端從一個room中移除
@sio.on('exit_chat') def exit_chat(sid): sio.leave_room(sid, 'chat_users')
-
sio.rooms(sid)
查詢sid客戶端所在的所有房間
給一組用戶發送消息的示例
@sio.on('my message') def message(sid, data): sio.emit('my reply', data, room='chat_users')
也可在群組發消息時跳過指定客戶端
@sio.on('my message') def message(sid, data): sio.emit('my reply', data, room='chat_users', skip_sid=sid)
-
-
使用
send
發送message
事件消息對於'message'事件,可以使用send方法
sio.send({'data': 'foobar'}) sio.send({'data': 'foobar'}, room=user_sid)
3 Python客戶端
import socketio sio = socketio.Client() @sio.on('connect') def on_connect(): pass @sio.on('event') def on_event(data): pass sio.connect('http://10.211.55.7:8000') sio.wait()