websocket
網(wǎng)頁中的絕大多數(shù)請求使用的是 HTTP 協(xié)議,HTTP 是一個無狀態(tài)的應(yīng)用層協(xié)議,它有著即開即用的優(yōu)點,每次請求都是相互獨立的,這對于密集程度較低的網(wǎng)絡(luò)請求來說是優(yōu)點,因為無需創(chuàng)建請求的上下文條件,但是對于密集度或者實時性要求較高的網(wǎng)絡(luò)請求(例如 IM 聊天)場景來說,可能 HTTP 會力不從心,因為每創(chuàng)建一個 HTTP 請求對服務(wù)器來說都是一個很大的資源開銷。這時我們可以考慮一個相對性能較高的網(wǎng)絡(luò)協(xié)議 Socket,他的網(wǎng)頁版本被稱為 Websocket。
1. 背景
近年來,隨著 HTML5 和 w3c 的推廣開來,WebSocket 協(xié)議被提出,它實現(xiàn)了瀏覽器與服務(wù)器的實時通信,使服務(wù)端也能主動向客戶端發(fā)送數(shù)據(jù)。在 WebSocket 協(xié)議提出之前,開發(fā)人員若要實現(xiàn)這些實時性較強的功能,經(jīng)常會使用一種替代性的解決方案——輪詢。
輪詢的原理是采用定時的方式不斷的向服務(wù)端發(fā)送 HTTP 請求,頻繁地請求數(shù)據(jù)。明顯地,這種方法命中率較低,浪費服務(wù)器資源。伴隨著 WebSocket 協(xié)議的推廣,真正實現(xiàn)了 Web 的即時通信。
WebSocket 的原理是通過 JavaScript 向服務(wù)端發(fā)出建立 WebSocket 連接的請求,在 WebSocket 連接建立成功后,客戶端和服務(wù)端可以實現(xiàn)一個長連接的網(wǎng)絡(luò)管道。因為 WebSocket 本質(zhì)上是 TCP 連接,它是一個長連接,除非斷開連接否則無需重新創(chuàng)建連接,所以其開銷相對 HTTP 節(jié)省了很多。
2. API
2.1 創(chuàng)建連接
通過使用新建一個 websocket 對象的方式創(chuàng)建一個新的連接,不過在創(chuàng)建之前需要檢測一下瀏覽器是否支持 Websocket,因為只有支持 HTML5 的瀏覽器才能支持 Websocket,如下:
if(typeof window.WebSocket == 'function'){
var ws = new WebSocket('http://127.0.0.1:8003');//創(chuàng)建基于本地的8003端口的websocket連接
}else alert("您的瀏覽器不支持websocket");
上述代碼會對本地的 8003 接口請求 Websocket 連接,前提是本地的服務(wù)器有進程監(jiān)聽 8003 端口,不然的話會連接失敗。
2.2 創(chuàng)建成功
由于 JavaScript 的各種 IO 操作是基于事件回調(diào)的,所以 Websocket 也不例外,我們需要創(chuàng)建一個連接成功的回調(diào)函數(shù)來處理連接創(chuàng)建成功之后的業(yè)務(wù)處理,如下:
ws.onopen = function(){//通過監(jiān)聽 open 時間來做創(chuàng)建成功的回調(diào)處理
console.log('websocket連接創(chuàng)建成功')
//進行業(yè)務(wù)處理
}
2.3 接收消息
我們辛辛苦苦創(chuàng)建了長連接就是為了發(fā)送或者接收網(wǎng)絡(luò)數(shù)據(jù),那么怎么接收呢,跟上邊提到的意義,還是需要在回調(diào)函數(shù)里處理,一不小心就陷入了回調(diào)地獄了:
ws.onmessage = function(event){
var d = event.data;
//接收到消息之后的業(yè)務(wù)處理
switch(typeof d){//判斷數(shù)據(jù)的類型格式
case "String":
break;
case "blob":
break;
case "ArrayBuffer":
break;
default:
return;
}
}
上述實例通過監(jiān)聽 message 事件對 websocket 的消息進行一定的業(yè)務(wù)處理,這其中需要判斷數(shù)據(jù)類型格式,因為 Websocket 是基于二進制流格式的,傳輸過來的消息可能不一定是基于 utf8 的字符串格式,因此需要對格式進行判斷。
2.4 發(fā)送消息
客戶端通過使用 send 函數(shù)向服務(wù)端發(fā)送數(shù)據(jù),例如:
ws.send("一段測試消息");
可以發(fā)送文本格式,也可以發(fā)送二進制格式,例如:
var input = document.getElementById("file");
input.onchange = function(){
var file = this.files[0];
if(!!file){
//讀取本地文件,以gbk編碼方式輸出
var reader = new FileReader();
reader.readAsBinaryString(file);
reader.onload = function(){
//讀取完畢后發(fā)送消息
ws.send(this.result);
}
}
}
2.5 監(jiān)聽錯誤信息
類似上述提到的如果創(chuàng)建實例失敗的情況,系統(tǒng)會出現(xiàn)異常,但是我們并不能準確判斷出異常的信息,這時需要通過監(jiān)聽錯誤事件來獲取報錯信息,例如:
ws.onerror = function(event){
//這里處理錯誤信息
}
2.6 關(guān)閉連接
當服務(wù)端或者客戶端關(guān)閉 websocket 連接時,系統(tǒng)會觸發(fā)一個關(guān)閉事件,例如:
ws.onclose = function (event){
//這里處理關(guān)閉之后的業(yè)務(wù)
}
2.7 連接的狀態(tài)
通過 websocket 對象的 readyState 屬性可以獲取到當前連接的狀態(tài),其中常用的有4種,通過 websocket 對象的幾種定義常量對比判斷:
switch (ws.readyState){
case WebSocket.CONNECTING:break;//處于正在連接中的狀態(tài)
case WebSocket.OPEN:break;//表示已經(jīng)連接成功
case WebSocket.CLOSING:break;//表示連接正在關(guān)閉
case WebSocket.CLOSE:break;//表示連接已經(jīng)關(guān)閉,或者創(chuàng)建連接失敗
default:break;
}
3. websocket 實例
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<style>
p {
text-align: left;
padding-left: 20px;
}
</style>
</head>
<body>
<div style="width: 700px;height: 500px;margin: 30px auto;text-align: center">
<h1>聊天室實戰(zhàn)</h1>
<div style="width: 700px;border: 1px solid gray;height: 300px;">
<div style="width: 200px;height: 300px;float: left;text-align: left;">
<p><span>當前在線:</span><span id="user_num">0</span></p>
<div id="user_list" style="overflow: auto;">
</div>
</div>
<div id="msg_list" style="width: 598px;border: 1px solid gray; height: 300px;overflow: scroll;float: left;">
</div>
</div>
<br>
<textarea id="msg_box" rows="6" cols="50" onkeydown="confirm(event)"></textarea><br>
<input type="button" value="發(fā)送" onclick="send()">
</div>
</body>
</html>
<script type="text/javascript">
var uname = window.prompt('請輸入用戶名', 'user' + uuid(8, 16));
var ws = new WebSocket("ws://127.0.0.1:8081");
ws.onopen = function () {
var data = "系統(tǒng)消息:連接成功";
listMsg(data);
};
ws.onmessage = function (e) {
var msg = JSON.parse(e.data);
var data = msg.content;
listMsg(data);
};
ws.onerror = function () {
var data = "系統(tǒng)消息 : 出錯了,請退出重試.";
listMsg(data);
};
function confirm(event) {
var key_num = event.keyCode;
if (13 == key_num) {
send();
} else {
return false;
}
}
/**
* 發(fā)送并清空消息輸入框內(nèi)的消息
*/
function send() {
var msg_box = document.getElementById("msg_box");
var content = msg_box.value;
var reg = new RegExp("\r\n", "g");
content = content.replace(reg, "");
var msg = {'content': content.trim(), 'type': 'user'};
sendMsg(msg);
msg_box.value = '';
}
/**
* 將消息內(nèi)容添加到輸出框中,并將滾動條滾動到最下方
*/
function listMsg(data) {
var msg_list = document.getElementById("msg_list");
var msg = document.createElement("p");
msg.innerHTML = data;
msg_list.appendChild(msg);
msg_list.scrollTop = msg_list.scrollHeight;
}
/**
* 將數(shù)據(jù)轉(zhuǎn)為json并發(fā)送
* @param msg
*/
function sendMsg(msg) {
var data = JSON.stringify(msg);
ws.send(data);
}
</script>
上述實例通過使用 websocket 實現(xiàn)了一個簡單的聊天室功能,功能上只實現(xiàn)了接受和發(fā)送消息的功能,在登錄認證和安全性等問題上并沒有做過多的處理,只是為了給大家連貫的展示一下 websocket 在實際項目中的使用。
4. 注意事項
實際項目中使用 websocket 需要注意一些問題 :
- websocket 創(chuàng)建之前需要使用 HTTP 協(xié)議進行一次握手請求,服務(wù)端正確回復相應(yīng)的請求之后才能創(chuàng)建 websocket 連接;
- 創(chuàng)建 websocket 時需要進行一些類似 token 之類的登錄認證,不然任何客戶端都可以向服務(wù)器進行 websocket 連接;
- websocket 是明文傳輸,敏感的數(shù)據(jù)需要進行加密處理;
- 由于 websocket 是長連接,當出現(xiàn)異常時連接會斷開,服務(wù)端的進程也會丟失,所以服務(wù)端最好有守護進程進行監(jiān)控重啟;
- 服務(wù)器監(jiān)聽的端口最好使用非系統(tǒng)性且不常使用的端口,不然可能會導致端口沖突
5. 小結(jié)
本章介紹了 websocket 的前世今生,詳細說明其對應(yīng)的 API 的調(diào)用方式,最后使用了一個簡單的聊天室的例子來對其函數(shù)串通了一下,最后延伸了一下實際項目中使用 websocket 需要注意的地方,希望大家在實際開發(fā)中針對其優(yōu)缺點來選擇合適的使用場景。