websocket應用 手動實現的websocket 你所見過的websocket 你一定見過在網站中,有一個游客聊天的聊天框,比如人人影視。這個聊天框是如何實現即時通訊的呢,就是用到了websocket 你可以打開瀏覽器的network,會看到有個ws://xxxxx,這就代表了是websocke ...
websocket應用
手動實現的websocket
你所見過的websocket
你一定見過在網站中,有一個游客聊天的聊天框,比如人人影視。這個聊天框是如何實現即時通訊的呢,就是用到了websocket
你可以打開瀏覽器的network,會看到有個ws://xxxxx,這就代表了是websocket做的
那麼什麼是websocket?
websocket就是一套協議。
看名字,雖然有個websocket,但他和http協議一樣,也要走socket。
不同的是:http是短連接,處理完一個請求就斷開;
websocket是連上就不斷開,一直不斷開,屬於雙工通道,服務端可以主動給客戶端推送消息,客戶端也可以主動給服務端推送消息
當某一個客戶端發送一條消息,服務端接收以後,再推送給所有的客戶端,所以才會呈現出所有人都在即時通訊的效果
服務端當然就是我們寫的程式了,那客戶端是瀏覽器,所以還需要瀏覽器支持才行。不要以為瀏覽器是都支持的,如果所有人都用chrome,前端開發工程師估計就沒什麼工作了。還有,如果所有的瀏覽器都支持,騰訊的webQQ,web微信,也不會使用長輪詢來做這個事了。
來看一下具體的代碼實現
import socket
import base64
import hashlib
def get_headers(data):
"""
將請求頭格式化成字典
:param data:
:return:
"""
header_dict = {}
data = str(data, encoding='utf-8')
for i in data.split('\r\n'):
print(i)
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
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()
data = conn.recv(1024)
headers = get_headers(data) # 提取請求頭信息
# 對請求頭中的sec-websocket-key進行加密
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://%s%s\r\n\r\n"
magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' #固定的,魔法字元串就是這個字元串
value = headers['Sec-WebSocket-Key'] + magic_string
ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest()) #把返回消息加密
response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url'])
# 響應【握手】信息
conn.send(bytes(response_str, encoding='utf-8'))
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)): #上面解密的最終結果,就是拿到這個decode,就是瀏覽器發來的真實的數據(加密的)
chunk = decoded[i] ^ mask[i % 4] #按位異或
bytes_list.append(chunk)
body = str(bytes_list, encoding='utf-8')
print(body)
客戶端向服務端發送的請求里,有Sec-WebSocket-Key
這樣一個key,服務端回消息的時候,就要拿到這個key,加密後再發給瀏覽器,瀏覽器會判斷自己加密後的值,與瀏覽器處理的是否一致,一致才能連接。加密的方式,用到一個magic_string
,其實就是一段固定的字元串258EAFA5-E914-47DA-95CA-C5AB0DC85B11
,加密後打包發給瀏覽器,瀏覽器驗證通過後就可以通訊了,再來看看客戶端:
客戶端就直接用瀏覽器運行這個html文件就行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="dist/css/bootstrap.css">
</head>
<body>
<div>
<input type="text" id="txt"/>
<input type="button" id="btn" value="提交" onclick="sendMsg();"/>
<input type="button" id="close" value="關閉連接" onclick="closeConn();"/>
</div>
<div id="content"></div>
<script type="text/javascript">
var socket = new WebSocket("ws://127.0.0.1:8002");
socket.onopen = function () {
/* 與伺服器端連接成功後,自動執行 */
var newTag = document.createElement('div');
newTag.innerHTML = "【連接成功】";
document.getElementById('content').appendChild(newTag);
};
socket.onmessage = function (event) {
/* 伺服器端向客戶端發送數據時,自動執行 */
var response = event.data;
var newTag = document.createElement('div');
newTag.innerHTML = response;
document.getElementById('content').appendChild(newTag);
};
socket.onclose = function (event) {
/* 伺服器端主動斷開連接時,自動執行 */
var newTag = document.createElement('div');
newTag.innerHTML = "【關閉連接】";
document.getElementById('content').appendChild(newTag);
};
function sendMsg() {
var txt = document.getElementById('txt');
socket.send(txt.value);
txt.value = "";
}
function closeConn() {
socket.close();
var newTag = document.createElement('div');
newTag.innerHTML = "【關閉連接】";
document.getElementById('content').appendChild(newTag);
}
</script>
<script></script>
</body>
</html>
這裡面有三個方法:
- 連接上後,onopen會自動執行
- 發消息時,onmessage自動執行
- 斷開連接,onclose自動執行
客戶端發送給服務端的數據,還有一層加密,必須通過解密才能拿到正確的消息
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)): #上面解密的最終結果,就是拿到這個decode,就是瀏覽器發來的真實的數據(加密的)
chunk = decoded[i] ^ mask[i % 4] #按位異或
bytes_list.append(chunk)
body = str(bytes_list, encoding='utf-8')
這段就是解密的過程,用到位運算
Django預設是不支持websocket的,雖然有個第三方的channels插件
但是tornado預設就支持
tornado實現websocket
如果用tornado,客戶端不能直接用瀏覽器運行了,而應該是運行tornado的一個模板文件
服務端代碼:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import uuid
import json
import tornado.ioloop
import tornado.web
import tornado.websocket
class IndexHandler(tornado.web.RequestHandler):
def get(self):
self.render('index.html')
class ChatHandler(tornado.websocket.WebSocketHandler):
# 用戶存儲當前聊天室用戶
waiters = set()
# 用於存儲歷時消息
messages = []
def open(self):
"""
客戶端連接成功時,自動執行
:return:
"""
ChatHandler.waiters.add(self)
uid = str(uuid.uuid4())
self.write_message(uid)
# 下麵這段代碼是給新加入的用戶,顯示歷史信息的
for msg in ChatHandler.messages:
# {'uid':'xxx','message':asdfasd}
content = self.render_string('message.html', **msg)
self.write_message(content)
def on_message(self, message):
"""
客戶端連發送消息時,自動執行
:param message:
:return:
"""
msg = json.loads(message)
ChatHandler.messages.append(msg)
for client in ChatHandler.waiters:
content = client.render_string('message.html', **msg)
client.write_message(content)
def on_close(self):
"""
客戶端關閉連接時,,自動執行
:return:
"""
ChatHandler.waiters.remove(self)
def run():
settings = {
'template_path': 'templates', # 配置模板文件
'static_path': 'static', # 配置靜態文件路徑
}
application = tornado.web.Application([ # 配置路由
(r"/", IndexHandler),
(r"/chat", ChatHandler),
], **settings)
application.listen(8009)
tornado.ioloop.IOLoop.instance().start()
if __name__ == "__main__":
run()
模板文件(客戶端代碼):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Python聊天室</title>
</head>
<body>
<div>
<input type="text" id="txt"/>
<input type="button" id="btn" value="提交" onclick="sendMsg();"/>
<input type="button" id="close" value="關閉連接" onclick="closeConn();"/>
</div>
<div id="container" style="border: 1px solid #dddddd;margin: 20px;min-height: 500px;">
</div>
<script src="/static/jquery-3.2.1.js"></script>
<script type="text/javascript">
$(function () {
wsUpdater.start();
});
var wsUpdater = {
socket: null,
uid: null,
start: function() {
var url = "ws://192.168.16.200:8009/chat";
wsUpdater.socket = new WebSocket(url);
wsUpdater.socket.onmessage = function(event) {
if(wsUpdater.uid){
wsUpdater.showMessage(event.data);
}else{
wsUpdater.uid = event.data;
}
}
},
showMessage: function(content) {
$('#container').append(content);
}
};
function sendMsg() {
var msg = {
uid: wsUpdater.uid,
message: $("#txt").val()
};
wsUpdater.socket.send(JSON.stringify(msg));
}
</script>
</body>
</html>
原理都一樣,但是用tornado實現起來,就清爽多了。
ps:再說一下騰訊的長輪詢,如果你登錄webQQ,或者web微信,你可以在network裡面找到 pending的字樣,這就是表示是使用的長輪詢。
長輪詢與輪詢的區別就是:
輪詢是過來以後看到沒消息就立馬去走了,但是長輪詢不會立馬走,而是在這等30秒(約定的時間)之後,如果一直沒有消息,才返回,下一次來在等30秒,直到有消息了,這樣有個缺點就是,拿到的消息並不是即時的。那騰訊這麼大的公司,為什麼不用性能更好的websocket呢?原因就是他是個大公司,必須要考慮相容性,必須要保證所有的瀏覽器都能使用才行。
你可以從這裡拿到完整 的示例代碼