一:簡介
推文:WebSocket 是什麼原理?爲什麼可以實現持久連接?
推文:WebSocket:5分鐘從入門到精通(很好)
WebSocket協議是基於TCP的一種新的協議。WebSocket最初在HTML5規範中被引用爲TCP連接,作爲基於TCP的套接字API的佔位符。它實現了瀏覽器與服務器全雙工(full-duplex)通信。其本質是保持TCP連接,在瀏覽器和服務端通過Socket進行通信。
二:對比:
Http:
socket實現,單工通道(瀏覽器只發起,服務端只做響應),短連接,請求響應
WebSocket:
socket實現,雙工通道,請求響應,推送。socket創建連接,不斷開
三:socket實現步驟
服務端:
1、服務端開啓socket,監聽IP和端口
3、允許連接
5、服務端接收到特殊值【加密sha1,特殊值,migic string="258EAFA5-E914-47DA-95CA-C5AB0DC85B11"】
6、加密後的值發送給客戶端
客戶端:
2、客戶端發起連接請求(IP和端口)
4、客戶端生成一個xxx,【加密sha1,特殊值,migic string="258EAFA5-E914-47DA-95CA-C5AB0DC85B11"】,向服務端發送一段特殊值
7、 客戶端接收到加密的值
注意:這個魔數是固定的 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
四:簡單實現,實現連接
服務端:
# coding:utf8
# __author: Administrator
# date: 2018/6/29 0029
# /usr/bin/env python
import socket,base64,hashlib
def get_headers(data):
'''將請求頭轉換爲字典'''
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[0].split(" ")) == 3:
header_dict['method'],header_dict['url'],header_dict['protocol'] = header_list[0].split(" ")
else:
k,v=header_list[i].split(":",1)
header_dict[k]=v.strip()
return header_dict
sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sock.bind(("127.0.0.1",8080))
sock.listen(5)
#等待用戶連接
conn,addr = sock.accept()
print("conn from ",conn,addr)
#獲取握手消息,magic string ,sha1加密
#發送給客戶端
#握手消息
data = conn.recv(8096)
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'))
瀏覽器:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>
<script>
ws =new WebSocket("ws://127.0.0.1:8080");
ws.onopen = function (ev) { //若是連接成功,onopen函數會執行
console.log(22222)
}
</script>
五:數據接收規則
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 | #Payload len(第二個字節的前七位,最大127)決定頭部的長度
|I|S|S|S| (4) |A| (7) | (16/64) | #若是小於126:Extended payload length擴展頭部長度爲0字節,後面全部爲主體數據
|N|V|V|V| |S| | (if payload len==126/127) | #若是等於126:Extended payload length擴展頭部長度爲2字節,後面全部爲主體數據
| |1|2|3| |K| | | #若是等於127:Extended payload length擴展頭部長度爲8字節,後面全部爲主體數據
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 | #注意:主體數據中的前四位爲mask掩碼,用於後面的消息的解碼,解碼方式爲循環異或操作
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 | #數據過長,需要分部發送,這時需要FIN和opcode
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
數據幀格式:
FIN:1個比特。
如果是1,表示這是消息(message)的最後一個分片(fragment),如果是0,表示不是是消息(message)的最後一個分片(fragment)。
RSV1, RSV2, RSV3:各佔1個比特。
一般情況下全爲0。當客戶端、服務端協商採用WebSocket擴展時,這三個標誌位可以非0,且值的含義由擴展進行定義。如果出現非零的值,且並沒有採用WebSocket擴展,連接出錯。
Opcode: 4個比特。
操作代碼,Opcode的值決定了應該如何解析後續的數據載荷(data payload)。如果操作代碼是不認識的,那麼接收端應該斷開連接(fail the connection)。可選的操作代碼如下:
%x0:表示一個延續幀。當Opcode爲0時,表示本次數據傳輸採用了數據分片,當前收到的數據幀爲其中一個數據分片。
%x1:表示這是一個文本幀(frame)
%x2:表示這是一個二進制幀(frame)
%x3-7:保留的操作代碼,用於後續定義的非控制幀。
%x8:表示連接斷開。
%x9:表示這是一個ping操作。
%xA:表示這是一個pong操作。
%xB-F:保留的操作代碼,用於後續定義的控制幀。
Mask: 1個比特。
操作代碼,Opcode的值決定了應該如何解析後續的數據載荷(data payload)。如果操作代碼是不認識的,那麼接收端應該斷開連接(fail the connection)。可選的操作代碼如下:
%x0:表示一個延續幀。當Opcode爲0時,表示本次數據傳輸採用了數據分片,當前收到的數據幀爲其中一個數據分片。
%x1:表示這是一個文本幀(frame)
%x2:表示這是一個二進制幀(frame)
%x3-7:保留的操作代碼,用於後續定義的非控制幀。
%x8:表示連接斷開。
%x9:表示這是一個ping操作。
%xA:表示這是一個pong操作。
%xB-F:保留的操作代碼,用於後續定義的控制幀。
Payload length:數據載荷的長度,單位是字節。爲7位,或7+16位,或1+64位。
假設數Payload length === x,如果
x爲0~126:數據的長度爲x字節。
x爲126:後續2個字節代表一個16位的無符號整數,該無符號整數的值爲數據的長度。
x爲127:後續8個字節代表一個64位的無符號整數(最高位爲0),該無符號整數的值爲數據的長度。
此外,如果payload length佔用了多個字節的話,payload length的二進制表達採用網絡序(big endian,重要的位在前)。
Masking-key:0或4字節(32位)
所有從客戶端傳送到服務端的數據幀,數據載荷都進行了掩碼操作,Mask爲1,且攜帶了4字節的Masking-key。如果Mask爲0,則沒有Masking-key。 備註:載荷數據的長度,不包括mask key的長度。
Payload data:(x+y) 字節
載荷數據:包括了擴展數據、應用數據。其中,擴展數據x字節,應用數據y字節。
擴展數據:如果沒有協商使用擴展的話,擴展數據數據爲0字節。所有的擴展都必須聲明擴展數據的長度,或者可以如何計算出擴展數據的長度。此外,擴展如何使用必須在握手階段就協商好。如果擴展數據存在,那麼載荷數據長度必須將擴展數據的長度包含在內。
應用數據:任意的應用數據,在擴展數據之後(如果存在擴展數據),佔據了數據幀剩餘的位置。載荷數據長度 減去 擴展數據長度,就得到應用數據的長度。
實現規則解碼:
def get_data(info): #info是我們連接後,接受的數據
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)
body = str(bytes_list, encoding='utf-8')
return body
實現循環獲取數據
服務端代碼
import socket,base64,hashlib
def get_headers(data):
'''將請求頭轉換爲字典'''
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[0].split(" ")) == 3:
header_dict['method'],header_dict['url'],header_dict['protocol'] = header_list[0].split(" ")
else:
k,v=header_list[i].split(":",1)
header_dict[k]=v.strip()
return header_dict
def get_data(info):
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)
body = str(bytes_list, encoding='utf-8')
return body
sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sock.bind(("127.0.0.1",8080))
sock.listen(5)
#等待用戶連接
conn,addr = sock.accept()
print("conn from ",conn,addr)
#獲取握手消息,magic string ,sha1加密
#發送給客戶端
#握手消息
data = conn.recv(8096)
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'))
#可以進行通信
while True:
data = conn.recv(8096)
data = get_data(data)
print(data)
服務端代碼
客戶端代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>
<script>
ws =new WebSocket("ws://127.0.0.1:8080");
ws.onopen = function (ev) { //若是連接成功,onopen函數會執行
console.log(22222);
ws.send("你好");
}
</script>
客戶端代碼
注意:使用控制檯完成發送,而不是刷新頁面,會報錯,因爲我們關閉了連接,試圖將關閉信號字節編碼出錯。這裏我們需要利用mask(第二字節中,1表示連接,0斷開)
六:數據發送規則(需要發送二進制包struct模塊)
def send_msg(conn, msg_bytes):
"""
WebSocket服務端向客戶端發送消息
:param conn: 客戶端連接到服務器端的socket對象,即: conn,address = socket.accept()
:param msg_bytes: 向客戶端發送的字節
:return:
"""
import struct
token = b"\x81" #接收的第一字節,一般都是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
實現發送數據
服務端
# coding:utf8
# __author: Administrator
# date: 2018/6/29 0029
# /usr/bin/env python
import socket,base64,hashlib
def get_headers(data):
'''將請求頭轉換爲字典'''
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[0].split(" ")) == 3:
header_dict['method'],header_dict['url'],header_dict['protocol'] = header_list[0].split(" ")
else:
k,v=header_list[i].split(":",1)
header_dict[k]=v.strip()
return header_dict
def get_data(info):
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)
body = str(bytes_list, encoding='utf-8')
return body
def send_msg(conn, msg_bytes):
"""
WebSocket服務端向客戶端發送消息
:param conn: 客戶端連接到服務器端的socket對象,即: conn,address = socket.accept()
:param msg_bytes: 向客戶端發送的字節
:return:
"""
import struct
token = b"\x81" #接收的第一字節,一般都是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()
sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sock.bind(("127.0.0.1",8080))
sock.listen(5)
#等待用戶連接
conn,addr = sock.accept()
#獲取握手消息,magic string ,sha1加密
#發送給客戶端
#握手消息
data = conn.recv(8096)
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'))
#可以進行通信
while True:
data = conn.recv(8096)
data = get_data(data)
print(data)
send_msg(conn,bytes(data+"geah",encoding="utf-8"))
服務端
前端onmessage 當數據接收會觸發
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>
<script>
ws =new WebSocket("ws://127.0.0.1:8080");
ws.onopen = function (ev) { //若是連接成功,onopen函數會執行
console.log(22222);
ws.send("你好");
}
ws.onmessage = function (ev) {
console.log(ev);
}
</script>
前端onmessage 當數據接收會觸發
七:tornado實現websocket聊天室
tornado服務端
import tornado.ioloop
import tornado.web
import tornado.websocket
import datetime
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.render("s1.html")
def post(self, *args, **kwargs):
pass
users = set()
class ChatHandler(tornado.websocket.WebSocketHandler):
def open(self, *args, **kwargs):
'''客戶端連接'''
print("connect....")
print(self.request)
users.add(self)
def on_message(self, message):
'''有消息到達'''
now = datetime.datetime.now()
content = self.render_string("recv_msg.html",date=now.strftime("%Y-%m-%d %H:%M:%S"),msg=message)
for client in users:
if client == self:
continue
client.write_message(content)
def on_close(self):
'''客戶端主動關閉連接'''
users.remove(self)
st ={
"template_path": "template",#模板路徑配置
"static_path":'static',
}
#路由映射 匹配執行,否則404
application = tornado.web.Application([
("/index",MainHandler),
("/wschat",ChatHandler),
],**st)
if __name__=="__main__":
application.listen(8080)
#io多路複用
tornado.ioloop.IOLoop.instance().start()
前端模板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/nifty.min.css" rel="stylesheet">
<link href="/static/css/demo/nifty-demo-icons.min.css" rel="stylesheet">
<link href="/static/css/demo/nifty-demo.min.css" rel="stylesheet">
<link href="/static/plugins/pace/pace.min.css" rel="stylesheet">
<script src="/static/js/jquery-2.2.4.min.js"></script>
<script src="/static/plugins/pace/pace.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
<script src="/static/js/nifty.min.js"></script>
<script src="/static/js/demo/nifty-demo.min.js"></script>
<script src="/static/plugins/flot-charts/jquery.flot.min.js"></script>
<script src="/static/plugins/flot-charts/jquery.flot.resize.min.js"></script>
<script src="/static/plugins/gauge-js/gauge.min.js"></script>
<script src="/static/plugins/skycons/skycons.min.js"></script>
<script src="/static/plugins/easy-pie-chart/jquery.easypiechart.min.js"></script>
<script src="/static/js/demo/widgets.js"></script>
</head>
<body>
<div id="container" class="effect aside-bright mainnav-sm aside-right aside-in">
<div class="boxed">
<div id="content-container">
<div class="row">
<div class="col-md-8 col-lg-8 col-sm-8">
<!--Chat widget-->
<!--===================================================-->
<div class="panel" style="height: 640px">
<!--Heading-->
<div class="panel-heading">
<h3 class="panel-title">Chat</h3>
</div>
<!--Widget body-->
<div style="height:510px;padding-top:0px;" class="widget-body">
<div class="nano">
<div class="nano-content pad-all">
<ul class="list-unstyled media-block">
</ul>
</div>
</div>
<!--Widget footer-->
<div class="panel-footer" style="height: 90px;">
<div class="row">
<div class="col-xs-9">
<input type="text" placeholder="Enter your text" class="form-control chat-input">
</div>
<div class="col-xs-3">
<button class="btn btn-primary btn-block" οnclick="sendMsg(this);" type="submit">Send</button>
</div>
</div>
</div>
</div>
</div>
<!--===================================================-->
<!--Chat widget-->
</div>
<div class="col-md-4 col-lg-4 col-sm-4">
<aside id="aside-container">
<div id="aside">
<div class="nano has-scrollbar">
<div class="nano-content" tabindex="0" style="right: -17px;">
<!--Nav tabs-->
<!--================================-->
<ul class="nav nav-tabs nav-justified">
<li class="active">
<a href="#demo-asd-tab-1" data-toggle="tab">
<i class="demo-pli-speech-bubble-7"></i>
</a>
</li>
</ul>
<!--================================-->
<!--End nav tabs-->
<!-- Tabs Content -->
<!--================================-->
<div class="tab-content">
<div class="tab-pane fade in active" id="demo-asd-tab-1">
<p class="pad-hor text-semibold text-main">
<span class="pull-right badge badge-success">0</span> Friends
</p>
</div>
</div>
</div>
<div class="nano-pane" style="display: none;"><div class="nano-slider" style="height: 4059px; transform: translate(0px, 0px);"></div></div></div>
</div>
</aside>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
<script>
ws = new WebSocket("ws://127.0.0.1:8080/wschat");
function sendMsg(ths) {
var dt = new Date()
var now_time = dt.toLocaleString();
var msg = $(ths).parents(".row").find(".chat-input").val();
$(ths).parents(".row").find(".chat-input").empty();
var li = '<li class="mar-btm"><div class="media-right"><img src="" class="img-circle img-sm" alt="Profile Picture"></div>';
li += '<div class="media-body pad-hor speech-right"><div class="speech"><a href="#" class="media-heading">遊客</a>';
li += '<p>'+msg+'</p>';
li += '<p class="speech-time">';
li += '<i class="demo-pli-clock icon-fw"></i>'+now_time;
li += '</p></div></div></li>';
$(ths).parents(".widget-body").find(".list-unstyled").append(li);
$(ths).parents(".panel-footer").find(".chat-input").val("");
ws.send(msg);
}
ws.onmessage=function (ev) {
$(".list-unstyled").append(ev.data);
}
</script>
s1.html
消息插件
<li class="mar-btm">
<div class="media-left">
<img src="img/profile-photos/1.png" class="img-circle img-sm" alt="Profile Picture">
</div>
<div class="media-body pad-hor">
<div class="speech">
<a href="#" class="media-heading">遊客</a>
<p>{{msg}}</p>
<p class="speech-time">
<i class="demo-pli-clock icon-fw"></i>{{date}}
</p>
</div>
</div>
</li>
recv_msg.html