本文原鏈接:https://cloud.tencent.com/developer/article/1115496
https://cloud.tencent.com/developer/article/1193011
webSocket與ajax、web
先看一個有道釋義:

其實釋義的挺形象的,下面我來一一解釋哈:
1、聊天室:webSocket有名的應用就是聊天室了;
2、服務:webSocket提供客戶端請求的服務器和服務;
3、套接字:源IP地址和目的IP地址以及源端口號和目的端口號的組合叫套接字,webSocket就是服務端和客戶端的結合;
4、協議:webSocket是基於TCP的一種新的網絡協議。
一、webSocket與ajax
作為一個碼了還算久代碼的前端,說起webSocket,腦子里最先閃現的當然就是ajax ajax ajax......ajax是啥,ajax剛出來時,可謂轟動一時,讓我們愉快地告別那種提交一個表單必須得填完所有信息,然后再把數據轉給服務器驗證,結果發現有一個小小的輸入框里輸錯了信息,然后又改掉重新提交走着重復的路的痛苦時代,所以它最大的貢獻就是局部刷新。當然,不是說有了webSocket,它就out了,ajax現在依舊好用。下面稍微比較了下ajax和webSocket:
1、ajax
(1)瀏覽器主動發送消息給服務器;
(2)非實時數據交互(異步,局部刷新)。

原生寫法:
四部曲:ajax對象、建立連接、發送請求、獲取相應。
更通俗的用打電話來比喻,那就是:電話、撥號、說話、聽到對方回應。demo
//創建一個ajax對象(想打電話,首先得有電話這個對象)
var XHR = null; if (window.XMLHttpRequest) { // 非IE內核 XHR = new XMLHttpRequest(); } else if (window.ActiveXObject) { // IE內核,早期IE的版本寫法不同 XHR = new ActiveXObject("Microsoft.XMLHTTP"); } else { XHR = null; }
if(XHR){ //建立連接(撥號) XHR.open("GET", "ajaxServer.action",true); //發送請求(說話) XHR.send(); //獲取響應(聽到對方回應) XHR.onreadystatechange = function () { // readyState值說明 // 0,初始化,XHR對象已經創建,還未執行open // 1,載入,已經調用open方法,但是還沒發送請求 // 2,載入完成,請求已經發送完成 // 3,交互,可以接收到部分數據
// 4,交互完畢 // status值說明 // 200:成功 // 404:沒有發現文件、查詢或URl // 500:服務器產生內部錯誤 if (XHR.readyState == 4 && XHR.status == 200) { // 這里可以對返回的內容做處理 // 一般會返回JSON或XML數據格式 console.log(XHR.responseText); // 主動釋放,JS本身也會回收的 XHR = null; } }; }
JQuery寫法(so easy,媽媽再也不用擔心我的學習啦):
$.ajax({ type:"post", url:url, async:true, data:params, dataType:"json", success:function(res){ console.log(res); }, error:function(jqXHQ){ alert("發生錯誤:"+jqXHQ.status); } });
2、webSocket
(1)實現了瀏覽器與服務器全雙工(full-duplex)通信——允許服務器主動發送信息給客戶端;
(2)實時數據交互。
WebSocket 握手過程
WebSocket 協議本質上是一個基於 TCP 的協議,WebSocket
連接與 TCP 連接的建立過程類似[4]。但與傳統的基於 TCP 連 接的協議有所不同的是 WebSocket 協議需要從 HTTP 協議“過 渡”而來,而這個“過度”過程也被稱為 WebSocket 協議的握手 過程。因此想要建立基於 WebSocket 的 Web 應用必須首先了解 WebSocket 協議的握手機制,如圖 2 給出了 WebSocket 協議握手 過程的示意圖。
首先由瀏覽器客戶端向服務器發起 WebSocket 握手請求報 文,這個報文是基於 HTTP 協議的,它告訴服務器客戶端想要升 級當前的 HTTP 協議為 WebSocket 協議。服務器收到客戶端的 WebSocket 握手請求報文之后會對報頭進行解析,如果服務器 理解客戶端握手請求報頭並且滿足升級為 WebSocket 協議的條 件,便會向客戶端發送握手應答報文,這個應答報文同樣是基於 HTTP 協議的。客戶端收到服務器的應答報文后會對該報文進 行一次驗證,驗證成功之后便會成功升級為 WebSocket 協議,如 果驗證失敗客戶端將會主動斷開連接。建立了 WebSocket 連接 之后,雙方便可以進行全雙工通信,
WebSocket 握手過程與 TCP 握手過程類似,但是 WebSocket 協議握手采用了更加簡潔的方式。
相對於傳統的 TCP 握手,WebSocket 握手協議有以下明顯 的特點: 首先,WebSocket 整個握手過程只需要兩次握手,相對 於 TCP 的三次握手,WebSocket 簡化並且加入了自己的規則。 其次,WebSocket 握手協議是基於 HTTP 協議的,相對於字節流 的解析,ASCII 序列解析起來更加簡便。最后,WebSocket 握手 協議引入了隨機序列認證機制,易於實現。
兩次握手成功預示着雙方接下來將升級當前的 HTTP 協議 為 WebSocket 協議。WebSocket 握手請求報文的報頭部分除了 必須包含必要的 HTTP 字段[5],還須遵循通用消息格式[RFC 822],同時又要包含和 WebSocket 緊密聯系的字段[6],如 Web- Socket 協議的版本信息、客戶端隨即生成的 Key、必要的 GET 請 求方法等。

// Create WebSocket connection.
var socket = new WebSocket('ws://localhost:8080'); //創建一個webSocket實例 // Connection opened socket.addEventListener('open', function (event) { //一旦服務端響應WebSocket連接請求,就會觸發open事件 socket.send('Hello Server!'); }); // Listen for messages socket.addEventListener('message', function (event) { //當消息被接受會觸發消息事件 console.log('Message from server', event.data); });
二、webSocket API
既然上面寫了一部分代碼,那不如把API全都貼出來,哈哈哈。
首先,創建一個webSocket實例:
var socket = new WebSocket('ws://localhost:8080');
然后再看下面的的API。
1、事件
(1)open
一個用於連接打開事件的事件監聽器。當readyState
的值變為 OPEN 的時候會觸發該事件。該事件表明這個連接已經准備好接受和發送數據。這個監聽器會接受一個名為"open"的事件對象。
socket.onopen = function(e) { console.log("Connection open..."); };
或者:
socket.addEventListener('open', function (event) { console.log("Connection open..."); });
(2)message
一個用於消息事件的事件監聽器,這一事件當有消息到達的時候該事件會觸發。這個Listener會被傳入一個名為"message"的 MessageEvent
對象。
socket.onmessage = function(e) { console.log("message received", e, e.data); };
(3)error
當錯誤發生時用於監聽error事件的事件監聽器。會接受一個名為“error”的event對象。
socket.onerror = function(e) { console.log("WebSocket Error: " , e); };
(4)close
用於監聽連接關閉事件監聽器。當 WebSocket 對象的readyState 狀態變為 CLOSED 時會觸發該事件。這個監聽器會接收一個叫close的 CloseEvent
對象。
socket.onclose = function(e) { console.log("Connection closed", e); };
2、方法
(1)send
通過WebSocket連接向服務器發送數據。
一旦在服務端和客戶端建立了全雙工的雙向連接,可以使用send方法去發送消息,當連接是open的時候send()方法傳送數據,當連接關閉或獲取不到的時候回拋出異常。
一個通常的錯誤是人們喜歡在連接open之前發送消息。如下所示:
// 這將不會工作
var socket= new WebSocket("ws://localhost:8080") socket.send("Initial data");
應該等待open事件觸發后再發送消息,正確的姿勢如下:
var socket= new WebSocket("ws://localhost:8080") socket.onopen = function(e) { socket.send("Initial data"); }
(2)close
關閉WebSocket連接或停止正在進行的連接請求。如果連接的狀態已經是closed
,這個方法不會有任何效果。
使用close方法來關閉連接,如果連接已經關閉,這方法將什么也不做。調用close方法后,將不能再發送數據。close方法可以傳入兩個可選的參數,code(numerical)和reason(string),以告訴服務端為什么終止連接。
socket.close(1000, "Closing normally"); //1000是狀態碼,代表正常結束。
3、屬性
屬性名 |
類型 |
描述 |
---|---|---|
binaryType |
DOMString |
一個字符串表示被傳輸二進制的內容的類型。取值應當是"blob"或者"arraybuffer"。 "blob"表示使用DOMBlob 對象,而"arraybuffer"表示使用 ArrayBuffer 對象。 |
bufferedAmount |
unsigned long |
調用 send() 方法將多字節數據加入到隊列中等待傳輸,但是還未發出。該值會在所有隊列數據被發送后重置為 0。而當連接關閉時不會設為0。如果持續調用send(),這個值會持續增長。只讀。 |
extensions |
DOMString |
服務器選定的擴展。目前這個屬性只是一個空字符串,或者是一個包含所有擴展的列表。 |
protocol |
DOMString |
一個表明服務器選定的子協議名字的字符串。這個屬性的取值會被取值為構造器傳入的protocols參數。 |
readyState |
unsigned short |
連接的當前狀態。取值是 Ready state constants之一。只讀。 |
url |
DOMString |
傳入構造器的URL。它必須是一個絕對地址的URL。只讀。 |
4、常量
Ready state 常量
常量 |
值 |
描述 |
---|---|---|
CONNECTING |
0 |
連接還沒開啟。 |
OPEN |
1 |
連接已開啟並准備好進行通信。 |
CLOSING |
2 |
連接正在關閉的過程中。 |
CLOSED |
3 |
連接已經關閉,或者連接無法建立。 |
三、webSocket與HTTP
webSocket和http同為協議,大家心里肯定會想它倆之間有什么聯系,當然,我也好奇,所以就有了下面的研究結果,呵呵呵呵~~
大家都知道,webSocket是H5的一種新協議(這樣看來和http是沒什么關系),本質是通過http/https協議進行握手后創建一個用於交換數據的TCP連接,服務端與客戶端通過此TCP連接進行實時通信。也就是說,webSocket是http協議上的一種補充。
相對於HTTP這種非持久的協議來說,Websocket是一個持久化的協議。
以php的生命周期為例:
在http1.0中,一個request,一個response,一個周期就結束了。
在http1.1中,有了keep-alive,可以發送多個Request,接收多個Response。但在http中永遠是一個request對應一個response。而且這個response是被動的,不能主動發起。
這時候webSocket就派上用場了。
四、webSocket原理
首先,先來看一張http的Request Headers:

再看一張webSocket的:

以及webSocket的Response Headers:

I guess,無論熟不熟悉http,想必都看出了區別,哈哈哈。接下來就要對這些東西進行講解啦:
(1)Upgrade和Connection
Upgrade: websocket
Connection: Upgrade
這個就是webSocket的核心,告訴Apache、ngix等服務器:注意啦,我發起的是webSocket協議,快點幫我找到對應的助理處理~ 不是那個老土的http。
(2)Sec-WebSocket-Key、Sec-WebSocket-Extensions和Sec-WebSocket-Version
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Extensions: chat, superchat Sec-WebSocket-Version: 13
這個很好理解啦,首先,Sec-WebSocket-Key是一個Base64 encode的值,這個是瀏覽器隨機生成的,告訴服務器:尼好,我是webSocket,這是我的ID卡,讓我過去吧。
然后,Sec-WebSocket-Extensions:協議擴展, 某類協議可能支持多個擴展,通過它可以實現協議增強
最后,Sec-WebSocket-Version是告訴服務器所使用的webSocket Draft(協議版本)。喏,我是小喵4.1版本哆啦A夢,哈哈哈哈哈哈哈哈。
然后只要服務器返回了上面我放的那一系列balabala的東西,就代表已經接受請求,webSocket建立成功啦!
(3)Sec-WebSocket-Accept和Sec-WebSocket-Extensions
請求時,webSocket會自帶加密過的ID卡過來讓服務端驗證;
對應的,接受請求之后,服務端也得搞一個安全卡(Accept頭域的值就是Key的值,是由瀏覽器發過來的Sec-WebSocket-Key生成的)來證明是我同意你通過的,而不是什么肯蒙拐騙的壞銀->
就這樣,原理部分就說完啦,握手成功!
五、webSocket的作用
說webSocket之前,先說一下ajax輪詢和long poll。
1、ajax輪詢:
ajax輪詢很簡單,就是讓瀏覽器隔個幾秒就發送一次請求,詢問服務器是否有新信息。
客戶端:hello hello,有沒有新信息(Request) 服務端:沒有(Response) 客戶端:hello hello,有沒有新信息(Request) 服務端:沒有。。(Response) 客戶端:hello hello,有沒有新信息(Request) 服務端:你好煩啊,沒有啊。。(Response) 客戶端:hello hello,有沒有新消息(Request) 服務端:有啦有啦,here you are(Response) 客戶端:hello hello,有沒有新消息(Request) 服務端:。。沒。。。。沒。。。沒。。。。(Response)
2、long poll
long poll和ajax輪詢原理很像,不過long poll是阻塞模型,簡單來說,就是一直給你打電話,直到你接聽為止。
客戶端:hello hello,有沒有新信息,沒有的話就等有了再返回給我吧(Request) 服務端:額。。。 (。。。。等待到有消息的時候。。。。) 有了,給你(Response)
很明顯,ajax輪詢和long poll弊大於利:
(1)被動性
上面這兩種方式都是客戶端先主動消息給服務端,然后等待服務端應答,要知道,等待總是難熬的,如果服務端能主動發消息多好,這也就是缺點之一:被動性。
(2)非常消耗資源
ajax輪詢 需要服務器有很快的處理速度和資源(速度);
long poll 需要有很高的並發,也就是說同時接待客戶的能力(場地大小)。
so,當ajax輪詢和long poll碰上503(啊啊啊啊啊,game over)
這時候,神奇的webSocket又派上用場了。
3、webSocket
(1)被動性
首先,解決被動性:
客戶端:hello hello,我要建立webSocket協議,擴展服務:chat,Websocket,協議版本:17(HTTP Request)
服務端:ok,確認,已升級為webSocket協議(HTTP Protocols Switched)
客戶端:麻煩你有信息的時候推送給我噢。。
服務端:ok,有的時候會告訴你的。
服務端:balabalabalabala
服務端:balabalabalabala
服務端:哈哈哈哈哈啊哈哈哈哈
服務端:笑死我了哈哈哈哈哈哈哈
就這樣,只需要一次http請求,就會有源源不斷的信息傳送了,是不是很方便。
(2)消耗資源問題
首先,了解一下,我們所用的程序是要經過兩層代理的,即http協議在Nginx等服務器的解析下,然后再傳送給相應的Handler(PHP等)來處理。簡單地說,我們有一個非常快速的接線員(Nginx),他負責把問題轉交給相應的客服(Handler) 。
本身接線員基本上速度是足夠的,但是每次都卡在客服(Handler)了,老有客服處理速度太慢,導致客服不夠。
webSocket就解決了這樣一個難題,建立后,可以直接跟接線員建立持久連接,有信息的時候客服想辦法通知接線員,然后接線員再統一轉交給客戶。
這樣就可以解決客服處理速度過慢的問題了。
同時,在傳統的方式上,要不斷的建立,關閉HTTP協議,由於HTTP是非狀態性的,每次都要重新傳輸鑒別信息,來告訴服務端你是誰。
雖然接線員很快速,但是每次都要聽這么一堆,效率也會有所下降的,同時還得不斷把這些信息轉交給客服,不但浪費客服的處理時間,而且還會在網路傳輸中消耗過多的流量/時間。
但是webSocket只需要一次http握手,所以說整個通訊過程是建立在一次連接/狀態中,也就避免了http的非狀態性,服務端會一直知道你的信息,直到你關閉請求,這樣就解決了接線員要反復解析http協議,還要查看identity info的信息。
六、Socket.io
既然說到了webSocket,就難免扯到socket.io。
有人說socket.io就是對webSocket的封裝,並且實現了webSocket的服務端代碼。可以這樣說,但不完全正確。
在webSocket沒有出現之前,實現與服務端的實時通訊可以通過輪詢來完成任務。Socket.io將webSocket和輪詢(Polling)機制以及其它的實時通信方式封裝成了通用的接口,並且在服務端實現了這些實時機制的相應代碼。也就是說,webSocket僅僅是Socket.io實現實時通信的一個子集。
下面直接上一個用socket.io做的小小聊天室吧。
(1)首先你得有node,然后安裝socket.io。
$ npm install socket.io
(2)服務器端(index.js)
'use strict'; module.exports = require('./lib/express'); var app = require('express')(); var http = require('http').Server(app); var io = require('socket.io')(http); app.get('/', function(req, res){ res.sendFile(__dirname + '/index.html'); }); io.on('connection', function(socket){ socket.on('message',function(msg){ console.log(msg); socket.broadcast.emit('chat',msg); //廣播消息 }) }); http.listen(3000);
(3)客戶端
先引入js文件:
<script src="/socket.io/socket.io.js"></script>
交互代碼(index.html):
<!DOCTYPE html><html><head> <meta charset="UTF-8"> <title>聊天室</title> <style> body,div,ul,li{margin: 0;padding: 0;list-style: none;} .auto{margin: auto;} .l{text-align: left;} .r{text-align: right;} .flex{display: box;display: -webkit-box;display: -moz-box;display: -ms-flexbox;display: -webkit-flex;display: flex;-webkit-box-pack: center;-webkit-justify-content: center;-moz-justify-content: center;-ms-justify-content: center;-o-justify-content: center;justify-content: center;-webkit-box-align: center;-webkit-align-items: center;-moz-align-items: center;-ms-align-items: center;-o-align-items: center;align-items: center;} .chat-box{background: #f1f1f1;width: 56vw;padding:2vw;height:36vw;border:1px solid #ccc;margin-top: 2vw;} .chat-li{display:inline-block;margin-top: 5px;background: #5CB85C;border-radius: 5px;padding: 3px 10px;color: #fff;} .other-chat-li{background: #fff;color: #333;} .send-box{width: 60vw;border:1px solid #ccc;justify-content: space-between;border-top: 0;} .send-text{width: 50vw; border: none; padding: 10px;outline:0;} .send{width: 10vw;background: #5cb85c; border: none; padding: 10px;color: #fff;cursor: pointer;} .chat-name{color: #f00;} .other-box,.self-box{width: 50%;height:100%;} </style> </head> <body>