WebScoket簡介
在之前的web應用中,雙向通訊機制每每藉助輪詢或是長輪詢來實現,可是這兩種方式都會或多或少的形成資源的浪費,且是非實時的。還有http長鏈接,可是本質上仍是Request與Response,只是減小握手鍊接次數,雖然減小了部分開銷,但仍然會形成資源的浪費、實時性不強等問題。javascript
WebSocket做爲一種解決web應用雙向通訊的協議由HTML5規範引出(RFC6455傳送門),是一種創建在TCP協議基礎上的全雙工通訊的協議。IE瀏覽器用的不是websocket,是輪詢。html
WebSocket 是什麼?
WebSocket 是一種網絡通訊協議。RFC6455 定義了它的通訊標準。java
WebSocket 是 HTML5 開始提供的一種在單個 TCP 鏈接上進行全雙工通信的協議。node
爲何須要 WebSocket ?
瞭解計算機網絡協議的人,應該都知道:HTTP 協議是一種無狀態的、無鏈接的、單向的應用層協議。它採用了請求/響應模型。通訊請求只能由客戶端發起,服務端對請求作出應答處理。nginx
這種通訊模型有一個弊端:HTTP 協議沒法實現服務器主動向客戶端發起消息。git
這種單向請求的特色,註定了若是服務器有連續的狀態變化,客戶端要獲知就很是麻煩。大多數 Web 應用程序將經過頻繁的異步JavaScript和XML(AJAX)請求實現長輪詢。輪詢的效率低,很是浪費資源(由於必須不停鏈接,或者 HTTP 鏈接始終打開)。github
所以,工程師們一直在思考,有沒有更好的方法。WebSocket 就是這樣發明的。WebSocket 鏈接容許客戶端和服務器之間進行全雙工通訊,以便任一方均可以經過創建的鏈接將數據推送到另外一端。WebSocket 只須要創建一次鏈接,就能夠一直保持鏈接狀態。這相比於輪詢方式的不停創建鏈接顯然效率要大大提升。web
WebSocket 如何工做?
Web瀏覽器和服務器都必須實現 WebSockets 協議來創建和維護鏈接。因爲 WebSockets 鏈接長期存在,與典型的HTTP鏈接不一樣,對服務器有重要的影響。json
基於多線程或多進程的服務器沒法適用於 WebSockets,由於它旨在打開鏈接,儘量快地處理請求,而後關閉鏈接。任何實際的 WebSockets 服務器端實現都須要一個異步服務器。flask
HTTP 和 WebSocket 有什麼關係?
Websocket 其實是一個新協議,跟 HTTP 協議基本沒有關係,只是爲了兼容現有瀏覽器的握手規範而已,也就是說它是 HTTP 協議上的一種補充。
Html 和 HTTP 有什麼關係?
Html 是超文本標記語言,是一種用於建立網頁的標準標記語言。它是一種技術標準。Html5 是它的最新版本。
Http 是一種網絡通訊協議。其自己和 Html 沒有直接關係。
WebSocket 代理
若是把 WebSocket 的通訊當作是電話鏈接,Nginx 的角色則像是電話接線員,負責將發起電話鏈接的電話轉接到指定的客服。
Nginx 從 1.3 版開始正式支持 WebSocket 代理。若是你的 web 應用使用了代理服務器 Nginx,那麼你還須要爲 Nginx 作一些配置,使得它開啓 WebSocket 代理功能。
如下爲參考配置:
server { # this section is specific to the WebSockets proxying location /socket.io { proxy_pass http://app_server_wsgiapp/socket.io; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 600; } }
與Socket、HTTP的關係
不少人剛接觸WebSocket確定會與Socket混淆,這裏放出OSI模型
理解socket
Socket是應用層與TCP/IP協議族通訊的中間軟件抽象層,socket是對TCP/IP協議的封裝,自己是是一組接口(API)。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket接口後面,對用戶來講,一組簡單的接口就是所有,讓Socket去組織數據,以符合指定的協議。
其實站在你的角度上看,socket就是一個模塊。咱們經過調用模塊中已經實現的方法創建兩個進程之間的鏈接和通訊。
也有人將socket說成ip+port,由於ip是用來標識互聯網中的一臺主機的位置,而port是用來標識這臺機器上的一個應用程序。 因此咱們只要確立了ip和port就能找到一個應用程序,而且使用socket模塊來與之通訊。
TCP是傳輸層的協議,WebScoket和HTTP都是基於TCP協議的高層(應用層)協議,因此從本質上講,WebSocket和HTTP是處於同一層的兩種不一樣的協議。可是WebSocket使用了HTTP完成了握手鍊接,根據RFC6455文檔中1.5節設計哲♂學中描述,是爲了簡單和兼容性考慮。具體握手操做咱們會在後面提到。
因此總的來講,WebSocket與Socket因爲層級不一樣,關係也僅僅是在某些環境中WebSocket可能經過Socket來使用TCP協議和名字比較像。和HTTP是同一層面的不一樣協議(最大的區別WebSocket是持久化協議而HTTP不是)。
這裏取網上流傳度很高的例子介紹
輪詢:
客戶端(發請求,創建連接):啦啦啦,有沒有新信息(Request)
服務端:沒有(Response)
客戶端(發請求,創建連接):啦啦啦,有沒有新信息(Request)
服務端:沒有。。(Response)
客戶端(發請求,創建連接):啦啦啦,有沒有新信息(Request)
服務端:你好煩啊,沒有啊。。(Response)
客戶端(發請求,創建連接):啦啦啦,有沒有新消息(Request)
服務端:好啦好啦,有啦給你。(Response)
客戶端(發請求,創建連接):啦啦啦,有沒有新消息(Request)
服務端:。。。。。沒。。。。沒。。。沒有(Response)長輪詢:
客戶端(發請求,創建連接):啦啦啦,有沒有新信息,沒有的話就等有了才返回給我吧(Request)
等等等。。。。。
服務端:額。。 等待到有消息的時候。。來 給你(Response)
客戶端(發請求,創建連接):啦啦啦,有沒有新信息,沒有的話就等有了才返回給我吧(Request)WebSocket:
客戶端:啦啦啦,我要創建Websocket協議,須要的服務:chat,Websocket協議版本:17(HTTP Request)
服務端:ok,確認,已升級爲Websocket協議(HTTP Protocols Switched)
客戶端:麻煩你有信息的時候推送給我噢。。
服務端:ok,有的時候會告訴你的。
服務端:balabalabalabala
客戶端:balabalabalabala
服務端:哈哈哈哈哈啊哈哈哈哈
服務端:笑死我了哈哈哈哈哈哈哈
從上面的例子能夠看出,不論是輪詢仍是長輪詢,本質都是不斷地發送HTTP請求,而後由服務端處理返回結果,並非真正意義上的雙向通訊。並且帶來的後果是大量的資源被浪費(HTTP請求),服務端須要快速的處理請求,還要考慮併發等問題。而WebSocket解決了這些問題,經過握手操做後就創建了持久鏈接,以後客戶端和服務端在鏈接斷開以前均可以發送消息,實現真正的全雙工通訊。
WebScoket協議
這裏主要提一下協議中比較重要的握手和發送數據
1、握手
以前有說到,WebSocket的握手是用HTTP請求來完成的,這裏咱們來看一下RFC6455文檔中一個客戶端握手的栗子
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13
能夠發現,這和一個通常的HTTP請求頭沒啥區別,須要注意的是(這裏講重點,具體還請看協議文檔):
- 根據協議規範,握手必須是一個HTTP請求,請求的方法必須是GET,HTTP版本不能夠低於1.1。
- 請求頭必須包含Upgrade屬性名,其值必須包含"websocket"。
- 請求頭必須包含Connection屬性名,其值必須包含"Upgrade"。
- 請求頭必須包含Sec-WebSocket-Key屬性名,其值是16字節的隨機數的被base64編碼後的值
- 若是請求來自瀏覽器必須包含Origin屬性名
- 請求頭必須包含Sec-WebSocket-Version屬性名,其值必須是13
若是請求不符合規範,服務端會返回400 bad request。若是服務端選擇接受鏈接,則會返回好比:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat
首先不一樣於普通的HTTP請求這裏返回101,而後Upgrade和Connection同上都是規定好的,Sec-WebSocket-Accept是由請求頭的Sec-WebSocket-Key加上字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11以後再進行SHA1加密和BASE64編碼獲得的值。返回的狀態碼爲101,表示贊成客戶端協議轉換請求,並將它轉換爲websocket協議。
在握手成功以後,WebSocket鏈接創建,雙向通訊即可以開始了。
2、數據幀類型
- 0x0 表示一個繼續幀
- 0x1 表示一個文本幀
- 0x2 表示一個二進制幀
- 0x3-7 爲之後的非控制幀
- 0x8 表示一個鏈接關閉幀
- 0x9 表示一個ping
- 0xA 表示一個pong
- 0xB-F 爲之後的控制幀
大部分都十分明瞭,這裏來講說Ping,Pong幀:WebSocket用Ping,Pong幀來維持心跳,當接收到Ping幀,終端必須發送一個Pong幀響應,除非它已經接收到一個關閉幀,它應該儘快返回Pong幀做爲響應。Pong幀必須包含與被響應Ping幀的應用程序數據徹底相同的數據。一個Pong幀可能被主動發送,但通常沒必要須返回響應,也能夠作特殊處理。
3、發送數據
在WebSocket協議,數據使用幀來傳輸。一個基本的協議幀以下
WebSocket 客戶端
在客戶端,沒有必要爲 WebSockets 使用 JavaScript 庫。實現 WebSockets 的 Web 瀏覽器將經過 WebSockets 對象公開全部必需的客戶端功能(主要指支持 Html5 的瀏覽器)。
客戶端 API
如下 API 用於建立 WebSocket 對象。
var Socket = new WebSocket(url, [protocol] );
以上代碼中的第一個參數 url, 指定鏈接的 URL。第二個參數 protocol 是可選的,指定了可接受的子協議。
WebSocket 屬性
如下是 WebSocket 對象的屬性。假定咱們使用了以上代碼建立了 Socket 對象:
屬性 | 描述 |
---|---|
Socket.readyState | 只讀屬性 readyState 表示鏈接狀態,能夠是如下值:0 - 表示鏈接還沒有創建。1 - 表示鏈接已創建,能夠進行通訊。2 - 表示鏈接正在進行關閉。3 - 表示鏈接已經關閉或者鏈接不能打開。 |
Socket.bufferedAmount | 只讀屬性 bufferedAmount 已被 send() 放入正在隊列中等待傳輸,可是尚未發出的 UTF-8 文本字節數。 |
WebSocket 事件
如下是 WebSocket 對象的相關事件。假定咱們使用了以上代碼建立了 Socket 對象:
事件 | 事件處理程序 | 描述 |
---|---|---|
open | Socket.onopen | 鏈接創建時觸發 |
message | Socket.onmessage | 客戶端接收服務端數據時觸發 |
error | Socket.onerror | 通訊發生錯誤時觸發 |
close | Socket.onclose | 鏈接關閉時觸發 |
WebSocket 方法
如下是 WebSocket 對象的相關方法。假定咱們使用了以上代碼建立了 Socket 對象:
方法 | 描述 |
---|---|
Socket.send() | 使用鏈接發送數據 |
Socket.close() | 關閉鏈接 |
示例
// 初始化一個 WebSocket 對象 var ws = new WebSocket("ws://localhost:9998/echo"); // 創建 web socket 鏈接成功觸發事件 ws.onopen = function () { // 使用 send() 方法發送數據 ws.send("發送數據"); alert("數據發送中..."); }; // 接收服務端數據時觸發事件 ws.onmessage = function (evt) { var received_msg = evt.data; alert("數據已接收..."); }; // 斷開 web socket 鏈接成功觸發事件 ws.onclose = function () { alert("鏈接已關閉..."); };
WebSocket 服務端
WebSocket 在服務端的實現很是豐富。Node.js、Java、C++、Python 等多種語言都有本身的解決方案。
如下,介紹我在學習 WebSocket 過程當中接觸過的 WebSocket 服務端解決方案。
Node.js
經常使用的 Node 實現有如下三種。
簡單的websocket聊天
py
from flask import Flask, request from geventwebsocket.handler import WebSocketHandler from gevent.pywsgi import WSGIServer from geventwebsocket.websocket import WebSocket # 作語法提示用 import json app = Flask(__name__) @app.route("/ws") def ws(): user_socket = request.environ.get("wsgi.websocket") # type:WebSocket while 1: msg = user_socket.receive() print(msg) user_socket.send(json.dumps({"id": msg})) if __name__ == '__main__': http_serv = WSGIServer(("0.0.0.0", 5000), app, handler_class=WebSocketHandler) http_serv.serve_forever()
html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <script> var ws = new WebSocket("ws://127.0.0.1:5000/ws"); ws.onmessage = function (data) { var msg = JSON.parse(data.data); console.log(msg) } </script> </body> </html>
羣聊
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <p> 發送消息:<input type="text" id="msg"> <button onclick="send_msg()">發送</button> </p> <div id="chat" style="width:500px;height: 500px;"> </div> <script> var ws = new WebSocket("ws://192.168.19.25:5000/ws"); //本身的ip ws.onmessage = function (data) { //onmessage接收到數據時執行的方法,相似於success // var msg = JSON.parse(data.data); // console.log(msg) console.log(data.data); var ptag=document.createElement("p"); //生成p標籤 ptag.innerText=data.data; document.getElementById("chat").appendChild(ptag); }; function send_msg() { var msg=document.getElementById("msg").value; //取到msg的值 ws.send(msg); } </script> </body> </html>
py
from flask import Flask, request,render_template from geventwebsocket.handler import WebSocketHandler from gevent.pywsgi import WSGIServer from geventwebsocket.websocket import WebSocket import json app = Flask(__name__) user_socket_list=[] @app.route("/ws") def ws(): user_socket = request.environ.get("wsgi.websocket") # type:WebSocket user_socket_list.append(user_socket) print(len(user_socket_list),user_socket_list) while 1: try: msg = user_socket.receive() for usocket in user_socket_list: if usocket!=user_socket: usocket.send(msg) print(msg) except: user_socket_list.remove(user_socket) @app.route("/chat") def chat(): return render_template("ws2.html") if __name__ == '__main__': http_serv = WSGIServer(("0.0.0.0", 5000), app, handler_class=WebSocketHandler) http_serv.serve_forever()
單聊
py
from flask import Flask, request, render_template from geventwebsocket.handler import WebSocketHandler from gevent.pywsgi import WSGIServer from geventwebsocket.websocket import WebSocket # 作語法提示用 import json app = Flask(__name__) user_socket_dict = {} # 通常定義字典,方便數據交互 @app.route("/ws/<username>") # 動態傳參 def ws(username): user_socket = request.environ.get("wsgi.websocket") # type:WebSocket user_socket_dict[username] = user_socket print(len(user_socket_dict), user_socket_dict) while 1: try: msg = json.loads(user_socket.receive()) to_user = msg.get("to_user") # 發給誰 content = msg.get("msg") # 發送的內容 usocket = user_socket_dict.get(to_user) recv_msg = {"from_user": username, "msg": content} # 接收數據的格式 usocket.send(json.dumps(recv_msg)) except: pass @app.route("/chat") def chat(): return render_template("ws3.html") if __name__ == '__main__': http_serv = WSGIServer(("0.0.0.0", 5000), app, handler_class=WebSocketHandler) http_serv.serve_forever()
html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <input type="text" id="username"> <button onclick="open_ws()">連接服務器</button> <p>給:<input type="text" id="to_user"></p> <p> 發送消息:<input type="text" id="msg"> <button onclick="send_msg()">發送</button> </p> <div id="chat" style="width:500px;height: 500px;"> </div> <script type="text/javascript"> var ws = null; function open_ws() { var username = document.getElementById("username").value; //經過綁定事件來添加動態參數 ws = new WebSocket("ws://192.168.19.25:5000/ws/" + username); //本身的ip ws.onmessage = function (data) { console.log(data.data); var msg = JSON.parse(data.data); var ptag = document.createElement("p"); ptag.innerText = msg.from_user + ":" + msg.msg; document.getElementById("chat").appendChild(ptag); }; } function send_msg() { var msg = document.getElementById("msg").value; var to_user = document.getElementById("to_user").value; var send_obj = {to_user: to_user, msg: msg}; ws.send(JSON.stringify(send_obj)); } </script> </body> </html>
位運算
位運算符比通常的算術運算符速度要快,並且能夠實現一些算術運算符不能實現的功能。若是要開發高效率程序,位運算符是必不可少的。位運算符用來對二進制位進行操做,包括:按位與(&)、按位或(|)、按位異或(^)、按位取反(~)、按位左移(<<)、按位右移(>>)。下面就給你們介紹位運算符的詳細用法。