websocket
網(wǎng)頁中的絕大多數(shù)請求使用的是 HTTP 協(xié)議,HTTP 是一個(gè)無狀態(tài)的應(yīng)用層協(xié)議,它有著即開即用的優(yōu)點(diǎn),每次請求都是相互獨(dú)立的,這對于密集程度較低的網(wǎng)絡(luò)請求來說是優(yōu)點(diǎn),因?yàn)闊o需創(chuàng)建請求的上下文條件,但是對于密集度或者實(shí)時(shí)性要求較高的網(wǎng)絡(luò)請求(例如 IM 聊天)場景來說,可能 HTTP 會(huì)力不從心,因?yàn)槊縿?chuàng)建一個(gè) HTTP 請求對服務(wù)器來說都是一個(gè)很大的資源開銷。這時(shí)我們可以考慮一個(gè)相對性能較高的網(wǎng)絡(luò)協(xié)議 Socket,他的網(wǎng)頁版本被稱為 Websocket。
1. 背景
近年來,隨著 HTML5 和 w3c 的推廣開來,WebSocket 協(xié)議被提出,它實(shí)現(xiàn)了瀏覽器與服務(wù)器的實(shí)時(shí)通信,使服務(wù)端也能主動(dòng)向客戶端發(fā)送數(shù)據(jù)。在 WebSocket 協(xié)議提出之前,開發(fā)人員若要實(shí)現(xiàn)這些實(shí)時(shí)性較強(qiáng)的功能,經(jīng)常會(huì)使用一種替代性的解決方案——輪詢。
輪詢的原理是采用定時(shí)的方式不斷的向服務(wù)端發(fā)送 HTTP 請求,頻繁地請求數(shù)據(jù)。明顯地,這種方法命中率較低,浪費(fèi)服務(wù)器資源。伴隨著 WebSocket 協(xié)議的推廣,真正實(shí)現(xiàn)了 Web 的即時(shí)通信。
WebSocket 的原理是通過 JavaScript 向服務(wù)端發(fā)出建立 WebSocket 連接的請求,在 WebSocket 連接建立成功后,客戶端和服務(wù)端可以實(shí)現(xiàn)一個(gè)長連接的網(wǎng)絡(luò)管道。因?yàn)?WebSocket 本質(zhì)上是 TCP 連接,它是一個(gè)長連接,除非斷開連接否則無需重新創(chuàng)建連接,所以其開銷相對 HTTP 節(jié)省了很多。
2. API
2.1 創(chuàng)建連接
通過使用新建一個(gè) websocket 對象的方式創(chuàng)建一個(gè)新的連接,不過在創(chuàng)建之前需要檢測一下瀏覽器是否支持 Websocket,因?yàn)橹挥兄С?HTML5 的瀏覽器才能支持 Websocket,如下:
if(typeof window.WebSocket == 'function'){
var ws = new WebSocket('http://127.0.0.1:8003');//創(chuàng)建基于本地的8003端口的websocket連接
}else alert("您的瀏覽器不支持websocket");
上述代碼會(huì)對本地的 8003 接口請求 Websocket 連接,前提是本地的服務(wù)器有進(jìn)程監(jiān)聽 8003 端口,不然的話會(huì)連接失敗。
2.2 創(chuàng)建成功
由于 JavaScript 的各種 IO 操作是基于事件回調(diào)的,所以 Websocket 也不例外,我們需要?jiǎng)?chuàng)建一個(gè)連接成功的回調(diào)函數(shù)來處理連接創(chuàng)建成功之后的業(yè)務(wù)處理,如下:
ws.onopen = function(){//通過監(jiān)聽 open 時(shí)間來做創(chuàng)建成功的回調(diào)處理
console.log('websocket連接創(chuàng)建成功')
//進(jìn)行業(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;
}
}
上述實(shí)例通過監(jiān)聽 message 事件對 websocket 的消息進(jìn)行一定的業(yè)務(wù)處理,這其中需要判斷數(shù)據(jù)類型格式,因?yàn)?Websocket 是基于二進(jìn)制流格式的,傳輸過來的消息可能不一定是基于 utf8 的字符串格式,因此需要對格式進(jìn)行判斷。
2.4 發(fā)送消息
客戶端通過使用 send 函數(shù)向服務(wù)端發(fā)送數(shù)據(jù),例如:
ws.send("一段測試消息");
可以發(fā)送文本格式,也可以發(fā)送二進(jìn)制格式,例如:
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)聽錯(cuò)誤信息
類似上述提到的如果創(chuàng)建實(shí)例失敗的情況,系統(tǒng)會(huì)出現(xiàn)異常,但是我們并不能準(zhǔn)確判斷出異常的信息,這時(shí)需要通過監(jiān)聽錯(cuò)誤事件來獲取報(bào)錯(cuò)信息,例如:
ws.onerror = function(event){
//這里處理錯(cuò)誤信息
}
2.6 關(guān)閉連接
當(dāng)服務(wù)端或者客戶端關(guān)閉 websocket 連接時(shí),系統(tǒng)會(huì)觸發(fā)一個(gè)關(guān)閉事件,例如:
ws.onclose = function (event){
//這里處理關(guān)閉之后的業(yè)務(wù)
}
2.7 連接的狀態(tài)
通過 websocket 對象的 readyState 屬性可以獲取到當(dāng)前連接的狀態(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 實(shí)例
<!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>聊天室實(shí)戰(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>當(dāng)前在線:</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)消息 : 出錯(cuò)了,請退出重試.";
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)容添加到輸出框中,并將滾動(dòng)條滾動(dòng)到最下方
*/
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>
上述實(shí)例通過使用 websocket 實(shí)現(xiàn)了一個(gè)簡單的聊天室功能,功能上只實(shí)現(xiàn)了接受和發(fā)送消息的功能,在登錄認(rèn)證和安全性等問題上并沒有做過多的處理,只是為了給大家連貫的展示一下 websocket 在實(shí)際項(xiàng)目中的使用。
4. 注意事項(xiàng)
實(shí)際項(xiàng)目中使用 websocket 需要注意一些問題 :
- websocket 創(chuàng)建之前需要使用 HTTP 協(xié)議進(jìn)行一次握手請求,服務(wù)端正確回復(fù)相應(yīng)的請求之后才能創(chuàng)建 websocket 連接;
- 創(chuàng)建 websocket 時(shí)需要進(jìn)行一些類似 token 之類的登錄認(rèn)證,不然任何客戶端都可以向服務(wù)器進(jìn)行 websocket 連接;
- websocket 是明文傳輸,敏感的數(shù)據(jù)需要進(jìn)行加密處理;
- 由于 websocket 是長連接,當(dāng)出現(xiàn)異常時(shí)連接會(huì)斷開,服務(wù)端的進(jìn)程也會(huì)丟失,所以服務(wù)端最好有守護(hù)進(jìn)程進(jìn)行監(jiān)控重啟;
- 服務(wù)器監(jiān)聽的端口最好使用非系統(tǒng)性且不常使用的端口,不然可能會(huì)導(dǎo)致端口沖突
5. 小結(jié)
本章介紹了 websocket 的前世今生,詳細(xì)說明其對應(yīng)的 API 的調(diào)用方式,最后使用了一個(gè)簡單的聊天室的例子來對其函數(shù)串通了一下,最后延伸了一下實(shí)際項(xiàng)目中使用 websocket 需要注意的地方,希望大家在實(shí)際開發(fā)中針對其優(yōu)缺點(diǎn)來選擇合適的使用場景。