一:簡介
推文: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'))

''' b' GET / HTTP/1.1\r\n Host: 127.0.0.1:8080\r\n Connection: Upgrade\r\n Pragma: no-cache\r\n Cache-Control: no-cache\r\n Upgrade: websocket\r\n Origin: http://localhost:63342\r\n Sec-WebSocket-Version: 13\r\n User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36\r\n Accept-Encoding: gzip, deflate, br\r\n Accept-Language: zh-CN,zh;q=0.8\r\n Sec-WebSocket-Key: +uL/aiakjNABjEoMzAqm6Q==\r\n Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n\r\n' '''
瀏覽器:
<!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>
五:數據接收規則
0 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 ... | +---------------------------------------------------------------+

The MASK bit simply tells whether the message is encoded. Messages from the client must be masked, so your server should expect this to be 1. (In fact, section 5.1 of the spec says that your server must disconnect from a client if that client sends an unmasked message.) When sending a frame back to the client, do not mask it and do not set the mask bit. We'll explain masking later. Note: You have to mask messages even when using a secure socket.RSV1-3 can be ignored, they are for extensions. The opcode field defines how to interpret the payload data: 0x0 for continuation, 0x1 for text (which is always encoded in UTF-8), 0x2 for binary, and other so-called "control codes" that will be discussed later. In this version of WebSockets, 0x3 to 0x7 and 0xB to 0xF have no meaning. The FIN bit tells whether this is the last message in a series. If it's 0, then the server will keep listening for more parts of the message; otherwise, the server should consider the message delivered. More on this later. Decoding Payload Length To read the payload data, you must know when to stop reading. That's why the payload length is important to know. Unfortunately, this is somewhat complicated. To read it, follow these steps: Read bits 9-15 (inclusive) and interpret that as an unsigned integer. If it's 125 or less, then that's the length; you're done. If it's 126, go to step 2. If it's 127, go to step 3. Read the next 16 bits and interpret those as an unsigned integer. You're done. Read the next 64 bits and interpret those as an unsigned integer (The most significant bit MUST be 0). You're done. Reading and Unmasking the Data If the MASK bit was set (and it should be, for client-to-server messages), read the next 4 octets (32 bits); this is the masking key. Once the payload length and masking key is decoded, you can go ahead and read that number of bytes from the socket. Let's call the data ENCODED, and the key MASK. To get DECODED, loop through the octets (bytes a.k.a. characters for text data) of ENCODED and XOR the octet with the (i modulo 4)th octet of MASK. In pseudo-code (that happens to be valid JavaScript): var DECODED = ""; for (var i = 0; i < ENCODED.length; i++) { DECODED[i] = ENCODED[i] ^ MASK[i % 4]; } Now you can figure out what DECODED means depending on your application.
數據幀格式:
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個比特。
表示是否要對數據載荷進行掩碼操作。從客戶端向服務端發送數據時,需要對數據進行掩碼操作;從服務端向客戶端發送數據時,不需要對數據進行掩碼操作。 如果服務端接收到的數據沒有進行過掩碼操作,服務端需要斷開連接。 如果Mask是1,那么在Masking-key中會定義一個掩碼鍵(masking key),並用這個掩碼鍵來對數據載荷進行反掩碼。所有客戶端發送到服務端的數據幀,Mask都是1。 掩碼的算法、用途在下一小節講解。
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"))

<!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>
七: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" onclick="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>
消息插件

<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>
實現效果
游客一:
游客二