一、WebSocket介紹:
1,WebSocket是什么?
WebSocket是一種在單個(gè)TCP連接上進(jìn)行全雙工通信的協(xié)議。在WebSocket API中,瀏覽器和服務(wù)器只需要完成一次握手(不是指建立TCP連接的那個(gè)三次握手,是指在建立TCP連接后傳輸一次握手?jǐn)?shù)據(jù)),兩者之間就直接可以創(chuàng)建持久性的連接,并進(jìn)行雙向數(shù)據(jù)傳輸。
WebSocket是H5新增的一重通信協(xié)議,為了更好的節(jié)省服務(wù)器資源和帶寬,并且能夠更實(shí)時(shí)地進(jìn)行通訊。
2,WebSocket與TCP、HTTP之間有什么關(guān)系?
WebSocket和HTTP都是基于TCP創(chuàng)建的,所以發(fā)送消息之前都要?jiǎng)?chuàng)建連接進(jìn)行三次握手。
WebSocket依賴HTTP協(xié)議的一次握手,一次握手成功之后,就不需要HTTP了,而是使用TCP的傳輸通道,開始收發(fā)數(shù)據(jù),這就是上面所說的雙向連接。
3,WebSocket使用場景?
- 用戶下了訂單,運(yùn)營管理后臺需要向運(yùn)營人員推送訂單通知。
- 用戶A關(guān)注了用戶B,需要向用戶B推送消息
- 有一些游戲,既支持手機(jī)端,也支持web端時(shí),需要用到。
- 網(wǎng)頁版即時(shí)聊天、彈幕等
4,為什么選擇WebSocket而不是其他?
- HTTP/1.x 版本不支持服務(wù)器主動推送,只能在客戶端發(fā)起請求后做出回應(yīng)。
- HTTP/2.x版本支持服務(wù)器主動推送,但HTTP/2 還未全面實(shí)施
- 輪詢 :每隔一段時(shí)間就會想服務(wù)器發(fā)送請求,詢問是否有新的消息,必須不停連接,或者連接始終打開,效率低下,浪費(fèi)資源。
- Comet (基于長連接)長輪詢是在打開一條連接以后保持,等待服務(wù)器推送來數(shù)據(jù)再關(guān)閉的方式。依然需要反復(fù)發(fā)出請求,而且長連接也會消耗服務(wù)器資源。
綜上所述:HTTP、輪詢、長輪詢都不太合適。而WebSocket呢?看下面介紹:
5,典型的Websocket握手請求如下:
客戶端請求:
GET / HTTP/1.1
Upgrade: websocket
#Upgrade字段必須設(shè)置Websocket,表示希望升級到Websocket協(xié)議
Connection: Upgrade
#Connection必須設(shè)置Upgrade,表示客戶端希望連接升級
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: N9cRrP/n9NdMgdcy2VJFQghu==
#Sec-WebSocket-Key是一串隨機(jī)的字符串,服務(wù)器使用SHA算法,base64編碼之后,將結(jié)果做為“Sec-WebSocket-Accept”頭的值,返回給客戶端。如此操作,可以**盡量避免普通HTTP請求被誤認(rèn)為Websocket協(xié)議**
Sec-WebSocket-Version: 13
服務(wù)器回應(yīng):
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: BooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Location: ws://example.com/
握手過程解析:
第一步:服務(wù)端開啟socket,監(jiān)聽某個(gè)ip和端口。等待連接...
第二步:客戶端connect連接服務(wù)端(ip,端口)
第三步:服務(wù)端允許連接...
第四部:客戶端發(fā)送握手請求,客戶端自己生成的一個(gè)特殊值發(fā)給服務(wù)器(魔法字符串migic string),發(fā)送之前客戶端會自己先加密,保存。
第五步:服務(wù)器接收到特殊字符串后,經(jīng)過SHA1加密,加密后把值發(fā)給客戶端
第六步:客戶端接收到加密后的值后,與之前的保存的值校驗(yàn),如果校驗(yàn)通過,說明對方也是websocket協(xié)議,ok,握手成功,可以雙方收發(fā)數(shù)據(jù)了。
Websocket總結(jié):
- 節(jié)省資源開銷。服務(wù)器到客戶端的內(nèi)容,頭部大小只有2至10字節(jié)(和數(shù)據(jù)包長度有關(guān)),而HTTP請求每次都要攜帶完整的頭部。
- 更強(qiáng)的實(shí)時(shí)性。因?yàn)閰f(xié)議是全雙工的,所以服務(wù)器可以隨時(shí)主動給客戶端下發(fā)數(shù)據(jù)。
- 保持連接狀態(tài)。Websocket需要先創(chuàng)建連接,這就使得其成為一種有狀態(tài)的協(xié)議,之后通信時(shí)可以省略部分狀態(tài)信息。而HTTP請求可能需要在每個(gè)請求都攜帶狀態(tài)信息(如身份認(rèn)證等)。
- 更好的二進(jìn)制支持。Websocket定義了二進(jìn)制幀,相對HTTP,可以更輕松地處理二進(jìn)制內(nèi)容。
- 可擴(kuò)展。Websocket定義了擴(kuò)展,用戶可以擴(kuò)展協(xié)議、實(shí)現(xiàn)部分自定義的子協(xié)議。如部分瀏覽器支持壓縮等。
- 更好的壓縮效果。相對于HTTP壓縮,Websocket在適當(dāng)?shù)臄U(kuò)展支持下,可以沿用之前內(nèi)容的上下文,在傳遞類似的數(shù)據(jù)時(shí),可以顯著地提高壓縮率。
- 沒有同源限制??蛻舳丝梢耘c任意服務(wù)器通信。
- 可以發(fā)送文本,也可以發(fā)送二進(jìn)制數(shù)據(jù)。
二、Socket.IO介紹:
1,Socket.IO是什么?
Socket.IO 基于WebSocket協(xié)議,現(xiàn)在已成為Web即時(shí)通訊應(yīng)用的框架,WebSocket只是Socket.IO實(shí)現(xiàn)即時(shí)通訊的其中一種技術(shù)依賴。
2,Socket.IO優(yōu)點(diǎn):
Socket.IO 會自動選擇合適雙向通信協(xié)議
有Python庫的實(shí)現(xiàn),可以在Python實(shí)現(xiàn)的Web應(yīng)用中去實(shí)現(xiàn)IM后臺服務(wù)。
https://python-socketio.readthedocs.io/en/latest/server.html
3,Socket.IO缺點(diǎn):
Socket.io要求客戶端與服務(wù)器端均須使用該框架。
三、python 服務(wù)器端開發(fā)
1,安裝:
pip install python-socketio
2,服務(wù)器部署方案:
下面主要使用協(xié)程eventlet庫部署服務(wù)器,其他服務(wù)器部署方案,后面會詳細(xì)介紹。
選擇協(xié)程方案的原因:
- 使用eventlet部署的Socket.IO服務(wù)器可以訪問長輪詢和WebSocket傳輸。
- 也可以使用協(xié)程gevent部署,只不過gevent不支持websocket,要安裝gevent-websocket 包才行。
- 若服務(wù)器采用多進(jìn)程或多線程方式,受限于服務(wù)器能創(chuàng)建的進(jìn)程或線程數(shù),能夠支持的并發(fā)連接客戶端不會很高,服務(wù)器性能有限。
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)
3,事件處理方法如下:
@sio.on('connect')
def on_connect(sid, environ):
"""
與客戶端建立好連接后被執(zhí)行
:param sid: string sid是socketio為當(dāng)前連接客戶端生成的識別id
:param environ: dict 在連接握手時(shí)客戶端發(fā)送的握手?jǐn)?shù)據(jù)(HTTP報(bào)文解析之后的字典)
"""
pass
@sio.on('disconnect')
def on_disconnect(sid):
"""
與客戶端斷開連接后被執(zhí)行
:param sid: string sid是斷開連接的客戶端id
"""
pass
# 以字符串的形式表示一個(gè)自定義事件,事件的定義由前后端約定
@sio.on('my custom event')
def my_custom_event(sid, data):
"""
自定義事件消息的處理方法
:param sid: string sid是發(fā)送此事件消息的客戶端id
:param data: data是客戶端發(fā)送的消息數(shù)據(jù)
"""
pass
提示:
SocketIO:對收發(fā)的數(shù)據(jù)以消息(message)來對待,收發(fā)的不同類別的消息數(shù)據(jù)又以事件(event)來區(qū)分。
connect 為特殊事件,當(dāng)客戶端連接后自動執(zhí)行
disconnect 為特殊事件,當(dāng)客戶端斷開連接后自動執(zhí)行
connect、disconnect與自定義事件處理方法的函數(shù)傳入?yún)?shù)不同
4,發(fā)送消息(emit)
發(fā)送消息可以單獨(dú)發(fā)給某個(gè)用戶,也可以群發(fā),SocketIO提供了房間(room)來為客戶端分組,也可以發(fā)到某個(gè)房間,參考如下方法:
1.發(fā)給某個(gè)用戶(指定用戶id)
sio.emit('my event', {'data': 'foobar'}, room=user_sid)
2.群發(fā)(不指定id號)
sio.emit('my event', {'data': 'foobar'})
3.將連接的客戶端放入一個(gè)room中 : sio.enter_room(sid, room_name)
@sio.on('chat')
def begin_chat(sid):
sio.enter_room(sid, 'chat_users')
4.將客戶從某個(gè)房間移除: sio.leave_room(sid, room_name)
@sio.on('exit_chat')
def exit_chat(sid):
sio.leave_room(sid, 'chat_users')
5.查詢sid所在的房間,給一組用戶發(fā)送消息:sio.rooms(sid)
@sio.on('my message')
def message(sid, data):
sio.emit('my reply', data, room='chat_users')
6.也可跳過某個(gè)客戶端,使用 skip_sid參數(shù)
@sio.event
def message(sid, data):
sio.emit('my reply', data, room='chat_users', skip_sid=sid)
7.使用send發(fā)送message事件消息
sio.send({'data': 'foobar'})
sio.send({'data': 'foobar'}, room=user_sid)
機(jī)器人聊天服務(wù)實(shí)現(xiàn)
第一步:創(chuàng)建AI文件夾,創(chuàng)建server.py
import socketio
# 創(chuàng)建sio對象
sio = socketio.Server(async_mode='eventlet')
app = socketio.Middleware(sio)
第二步:創(chuàng)建chart.py聊天室
import time
from server import sio
@sio.on("connect")
def on_connect(sid, environ):
"""
與客戶端建立好連接后被執(zhí)行
:param sid: string sid是socketio為當(dāng)前連接客戶端生成的識別id
:param environ: dict 在連接握手時(shí)客戶端發(fā)送的握手?jǐn)?shù)據(jù)(HTTP報(bào)文解析之后的字典)
# sio.emit(消息事件類型, 消息數(shù)據(jù)內(nèi)容, 接收人)
"""
data = {
"msg": "hello carry", #發(fā)送的內(nèi)容
'timestamp': round(time.time() * 1000) #時(shí)間戳
}
sio.emit("message", data, room=sid)
@sio.on("message")
def on_message(sid, data):
"""
客戶端發(fā)送消息,on_message執(zhí)行
:param sid: string sid是socketio為當(dāng)前連接客戶端生成的識別id
:param environ: dict 在連接握手時(shí)客戶端發(fā)送的握手?jǐn)?shù)據(jù)(HTTP報(bào)文解析之后的字典)
# sio.emit(消息事件類型, 消息數(shù)據(jù)內(nèi)容, 接收人)
"""
resp_data = {
"msg": "hello carry~~~,接收到你的消息:{}".format(data.get('msg')),
'timestamp': round(time.time() * 1000)
}
sio.emit("message", resp_data, room=sid)
運(yùn)行main.py
import eventlet
eventlet.monkey_patch()
#使用猴子補(bǔ)丁,在遇到阻塞時(shí),不用修改源代碼,也能執(zhí)行下面代碼
from server import app
import chat
# 創(chuàng)建eventlet服務(wù)器對象
sock = eventlet.listen(('0.0.0.0', 8000))
# 啟動socketio服務(wù)器
eventlet.wsgi.server(sock, app)
溫馨提示:
1.測試之前需要在谷歌上安裝Firecamp插件

2.啟動終端命令:
python main.py
3.點(diǎn)擊網(wǎng)址后面小火苗(Firecamp)標(biāo)志,進(jìn)入上圖,點(diǎn)擊Socket.IO進(jìn)入以下圖片,流程及步驟。

參考資料:
AIOHTTP:https://aiohttp.readthedocs.io/en/stable/
python-socket:https://python-socketio.readthedocs.io/en/latest/server.html
flask-socket:https://flask-socketio.readthedocs.io/en/latest/