WebSocket的簡(jiǎn)單介紹及應(yīng)用
定時(shí)刷新的不足與改進(jìn)
web開(kāi)發(fā)中可能遇到這樣的場(chǎng)景:網(wǎng)頁(yè)里的某一塊區(qū)域里寫(xiě)了一些內(nèi)容,但這些內(nèi)容不是固定的,即使看網(wǎng)頁(yè)的人沒(méi)有做任何操作,它們也會(huì)隨時(shí)間不斷變化。股票行情、活動(dòng)或游戲的榜單都是比較常見(jiàn)的例子。
對(duì)此,一般的做法是用setTimeout()或setInverval()定時(shí)執(zhí)行任務(wù),任務(wù)內(nèi)容是Ajax訪問(wèn)一次服務(wù)器,并在成功拿到返回?cái)?shù)據(jù)后去更新頁(yè)面。
這種定時(shí)刷新的做法會(huì)有這樣一些感覺(jué)不足的地方:
- 頻繁的定時(shí)網(wǎng)絡(luò)請(qǐng)求對(duì)瀏覽器(客戶(hù)端)和服務(wù)器來(lái)說(shuō)都是一種負(fù)擔(dān),尤其是當(dāng)網(wǎng)頁(yè)里有多個(gè)定時(shí)刷新區(qū)域的時(shí)候。
- 某幾次的定時(shí)任務(wù)可能是不必要的,因?yàn)榉?wù)器可能并沒(méi)有新數(shù)據(jù),還是返回了和上一次一樣的內(nèi)容。
- 頁(yè)面內(nèi)容可能不夠新,因?yàn)榉?wù)器可能剛更新了數(shù)據(jù),但下一輪定時(shí)任務(wù)還沒(méi)有開(kāi)始。
造成這些不足的原因歸結(jié)起來(lái),主要還是由于服務(wù)器的響應(yīng)總是被動(dòng)的。HTTP協(xié)議限制了一次通信總是由客戶(hù)端發(fā)起請(qǐng)求,再由服務(wù)器端來(lái)返回響應(yīng)。
因此,如果讓服務(wù)器端也可以主動(dòng)發(fā)送信息到客戶(hù)端,就可以很大程度改進(jìn)這些不足。WebSocket就是一個(gè)實(shí)現(xiàn)這種雙向通信的新協(xié)議。
WebSocket是基于HTTP的功能追加協(xié)議
WebSocket最初由html5提出,但現(xiàn)在已經(jīng)發(fā)展為一個(gè)獨(dú)立的協(xié)議標(biāo)準(zhǔn)。WebSocket可以分為協(xié)議(Protocol)和API兩部分,分別由IETF和W3C制定了標(biāo)準(zhǔn)。
先來(lái)看看WebSocket協(xié)議的建立過(guò)程。
為了實(shí)現(xiàn)WebSocket通信,首先需要客戶(hù)端發(fā)起一次普通HTTP請(qǐng)求(也就是說(shuō),WebSocket的建立是依賴(lài)HTTP的)。請(qǐng)求報(bào)文可能像這樣:
GET ws://websocket.example.com/ HTTP/1.1 Host: websocket.example.com Upgrade: websocket Connection: Upgrade Origin: http://example.com Sec-WebSocket-Key:pAloKxsGSHtpIHrJdWLvzQ== Sec-WebSocket-Version:13
其中HTTP頭部字段Upgrade: websocket和Connection: Upgrade很重要,告訴服務(wù)器通信協(xié)議將發(fā)生改變,轉(zhuǎn)為WebSocket協(xié)議。支持WebSocket的服務(wù)器端在確認(rèn)以上請(qǐng)求后,應(yīng)返回狀態(tài)碼為101 Switching Protocols的響應(yīng):
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: nRu4KAPUPjjWYrnzxDVeqOxCvlM=
其中字段Sec-WebSocket-Accept是由服務(wù)器對(duì)前面客戶(hù)端發(fā)送的Sec-WebSocket-Key進(jìn)行確認(rèn)和加密后的結(jié)果,相當(dāng)于一次驗(yàn)證,以幫助客戶(hù)端確信對(duì)方是真實(shí)可用的WebSocket服務(wù)器。
驗(yàn)證通過(guò)后,這個(gè)握手響應(yīng)就確立了WebSocket連接,此后,服務(wù)器端就可以主動(dòng)發(fā)信息給客戶(hù)端了。此時(shí)的狀態(tài)比較像服務(wù)器端和客戶(hù)端接通了電話,無(wú)論是誰(shuí)有什么信息想告訴對(duì)方,開(kāi)口就好了。
一旦建立了WebSocket連接,此后的通信就不再使用HTTP了,改為使用WebSocket獨(dú)立的數(shù)據(jù)幀(這個(gè)幀有辦法看到,見(jiàn)后文)。
整個(gè)過(guò)程像這樣:

簡(jiǎn)單的應(yīng)用示例
應(yīng)用WebSocket有這樣幾件事要做:
- 選用支持WebSocket的瀏覽器。
- 網(wǎng)頁(yè)內(nèi)添加創(chuàng)建WebSocket的代碼。
- 服務(wù)器端添加使用WebSocket通信的代碼。
服務(wù)器端
以Node的服務(wù)器為例,我們使用ws這個(gè)組件,這樣搭建一個(gè)支持WebSocket的服務(wù)器端:
var request = require("request");
var dateFormat = require("dateformat");
var WebSocket = require("ws"),
WebSocketServer = WebSocket.Server,
wss = new WebSocketServer({
port: 8080,
path: "/guest"
});
// 收到來(lái)自客戶(hù)端的連接請(qǐng)求后,開(kāi)始給客戶(hù)端推消息
wss.on("connection", function(ws) {
ws.on("message", function(message) {
console.log("received: %s", message);
});
sendGuestInfo(ws);
});
function sendGuestInfo(ws) {
request("http://uinames.com/api?region=china",
function(error, response, body) {
if (!error && response.statusCode === 200) {
var jsonObject = JSON.parse(body),
guest = jsonObject.name + jsonObject.surname,
guestInfo = {
guest: guest,
time: dateFormat(new Date(), "HH:MM:ss")
};
if (ws.readyState === WebSocket.OPEN) {
// 發(fā),送
ws.send(JSON.stringify(guestInfo));
// 用隨機(jī)來(lái)“裝”得更像不定時(shí)推送一些
setTimeout(function() {
sendGuestInfo(ws);
}, (Math.random() * 5 + 3) * 1000);
}
}
});
}
這個(gè)例子使用了姓名生成站點(diǎn)uinames的API服務(wù),來(lái)生成{guest: "人名", time: "15:26:01"}這樣的數(shù)據(jù)。函數(shù)sendGuestInfo()會(huì)不定時(shí)執(zhí)行,并把包含姓名和時(shí)間的信息通過(guò)send()方法發(fā)送給客戶(hù)端。另外,注意send()方法需要以字符串形式來(lái)發(fā)送json數(shù)據(jù)。
這就像是服務(wù)器自己在做一些事,然后在需要的時(shí)候會(huì)通知客戶(hù)端一些信息。
客戶(hù)端
客戶(hù)端我們使用原生javascript來(lái)完成(僅支持WebSocket的瀏覽器):
var socket = new WebSocket("ws://localhost:8080/guest");
socket.onopen = function(openEvent) {
console.log("WebSocket conntected.");
};
socket.onmessage = function(messageEvent) {
var data = messageEvent.data,
dataObject = JSON.parse(data);
console.log("Guest at " + dataObject.time + ": " + dataObject.guest);
};
socket.onerror = function(errorEvent) {
console.log("WebSocket error: ", errorEvent);
};
socket.onclose = function(closeEvent) {
console.log("WebSocket closed.");
};
WebSocket的URL格式是ws://與wss://。因此,需要注意下URL地址的寫(xiě)法,這也包括注意WebSocket服務(wù)器端的路徑(如這里的/guest)等信息。因?yàn)槭潜镜氐氖纠赃@里是localhost。
客戶(hù)端代碼的流程很簡(jiǎn)單:創(chuàng)建WebSocket對(duì)象,然后指定onopen、onmessage等事件的回調(diào)即可。其中onmessage是客戶(hù)端與服務(wù)器端通過(guò)WebSocket通信的關(guān)鍵事件,想要在收到服務(wù)器通知后做點(diǎn)什么,寫(xiě)在onmessage事件的回調(diào)函數(shù)里就好了。
效果及分析
通過(guò)node server(假定服務(wù)器端的文件名為server.js)啟動(dòng)WebSocket服務(wù)器后,用瀏覽器打開(kāi)一個(gè)引入了前面客戶(hù)端代碼的html(直接文件路徑file:///就可以),就可以得到像這樣的結(jié)果:

聯(lián)系前面客戶(hù)端的代碼可以想到,實(shí)際從創(chuàng)建WebSocket對(duì)象的語(yǔ)句開(kāi)始,連接請(qǐng)求就會(huì)發(fā)送,并很快建立起WebSocket連接(不出錯(cuò)誤的話),此后就可以收到來(lái)自服務(wù)器端的通知。如果此時(shí)客戶(hù)端還想再告訴服務(wù)器點(diǎn)什么,這樣做:
socket.send("Hello, server!");
服務(wù)器就可以收到:

當(dāng)然,這也是因?yàn)榍懊娣?wù)器端的代碼內(nèi)同樣設(shè)置了message事件的回調(diào)。在這個(gè)客戶(hù)端和服務(wù)器都是javascript的例子中,無(wú)論是服務(wù)器端還是客戶(hù)端,都用send()發(fā)送信息,都通過(guò)message事件設(shè)置回調(diào),形式上可以說(shuō)非常一致。
其他可用的數(shù)據(jù)類(lèi)型
WebSocket的send()可以發(fā)送的消息,除了前面用的字符串類(lèi)型之外,還有兩種可用,它們是Blob和ArrayBuffer。
它們都代表二進(jìn)制數(shù)據(jù),可用于原始文件數(shù)據(jù)的發(fā)送。比如,這是一個(gè)發(fā)送Blob類(lèi)型數(shù)據(jù)以完成向服務(wù)器上傳圖片的例子:
var fileEl = document.getElementById("image_upload");
var file = fileEl.files[0];
socket.send(file);
然后服務(wù)器端可以這樣把文件保存下來(lái):
var fs = require("fs");
wss.on("connection", function(ws) {
ws.on("message", function(message) {
fs.writeFile("upload.png", message, "binary", function(error) {
if (!error) {
console.log("File saved.");
}
});
});
});
在客戶(hù)端接收二進(jìn)制數(shù)據(jù)時(shí),需注意WebSocket對(duì)象有一個(gè)屬性binaryType,初始值為"blob"。因此,如果接收的二進(jìn)制數(shù)據(jù)是ArrayBuffer,應(yīng)在接收之前這樣做:
socket.binaryType = "arraybuffer";
其他WebSocket服務(wù)器端
其他語(yǔ)言來(lái)做WebSocket服務(wù)器是怎樣的呢?下面是一個(gè)php的WebSocket服務(wù)器的例子(使用Ratchet):
<?php
use Ratchet\ConnectionInterface;
use Ratchet\MessageComponentInterface;
require __DIR__ . '/vendor/autoload.php';
class GuestServer implements MessageComponentInterface {
public function onOpen(ConnectionInterface $conn) {
$conn->send('The server is listening to you now.');
}
public function onMessage(ConnectionInterface $conn, $msg) {
$conn->send($this->generateGuestInfo());
}
public function onClose(ConnectionInterface $conn) {
}
public function onError(ConnectionInterface $conn, \Exception $e) {
$conn->close();
}
private function generateGuestInfo() {
$jsonString = file_get_contents('http://uinames.com/api?region=china');
$jsonObject = json_decode($jsonString, true);
$guest = $jsonObject['name'] . $jsonObject['surname'];
$guestInfo = array(
'guest' => $guest,
'time' => date('H:i:s', time()),
);
return json_encode($guestInfo);
}
}
$app = new Ratchet\App('localhost', 8080);
$app->route('/guest', new GuestServer(), array('*'));
$app->run();
?>
這個(gè)例子也同樣是由服務(wù)器返回{guest: "人名", time: "15:26:01"}的json數(shù)據(jù),不過(guò)由于php不像Node那樣可以用setTimeout()很容易地實(shí)現(xiàn)異步定時(shí)任務(wù),這里改為在客戶(hù)端發(fā)送一次任意信息后,再去uinames取得信息并返回。
也可以看到,php搭建的WebSocket服務(wù)器仍然是近似的,主要通過(guò)WebSocket的open、message等事件來(lái)實(shí)現(xiàn)功能。
在Chrome開(kāi)發(fā)工具中查看WebSocket數(shù)據(jù)幀
Chrome開(kāi)發(fā)工具中選擇Network,然后找到WebSocket的那個(gè)請(qǐng)求,里面可以選擇Frames。在Frames里看到的,就是WebSocket的數(shù)據(jù)幀了:

可以看到很像聊天記錄,其中用淺綠色標(biāo)注的是由客戶(hù)端發(fā)送給服務(wù)器的部分。
結(jié)語(yǔ)
總的來(lái)說(shuō),把服務(wù)器和客戶(hù)端拉到了一個(gè)聊天窗口來(lái)辦事,這確實(shí)是很棒的想法。
即使只從形式上說(shuō),WebSocket的事件回調(diào)感覺(jué)也比定時(shí)任務(wù)用起來(lái)要更親切一些。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
JavaScript 動(dòng)態(tài)添加腳本,并觸發(fā)回調(diào)函數(shù)的實(shí)現(xiàn)代碼
JavaScript 動(dòng)態(tài)添加腳本,并觸發(fā)回調(diào)函數(shù)的實(shí)現(xiàn)代碼,需要的朋友可以參考下。2011-01-01
JavaScript回調(diào)函數(shù)callback用法解析
這篇文章主要介紹了JavaScript回調(diào)函數(shù)callback用法解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01
axios/fetch實(shí)現(xiàn)stream流式請(qǐng)求示例詳解
這篇文章主要為大家介紹了axios/fetch實(shí)現(xiàn)stream流式請(qǐng)求示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
javascript使用location.search的示例
本文介紹javascript 使用location.search獲取當(dāng)前地址欄參數(shù)的實(shí)例2013-11-11
JavaScript跨平臺(tái)的開(kāi)源框架NativeScript
本文給大家分享的是一款使用javascript來(lái)構(gòu)建跨平臺(tái)原生移動(dòng)應(yīng)用的開(kāi)源框架--NativeScript,可以使用JavaScript開(kāi)發(fā)跨平臺(tái)、真正原生的iOS, Android 和 Windows 移動(dòng)App。開(kāi)發(fā)人員使用NativeScript提供的庫(kù)來(lái)構(gòu)建應(yīng)用UI,其抽象了各種原生平臺(tái)之間的不同。2015-03-03
基于JavaScript實(shí)現(xiàn)貪吃蛇游戲
這篇文章主要為大家詳細(xì)介紹了基于JavaScript實(shí)現(xiàn)貪吃蛇游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-03-03
Postman如何實(shí)現(xiàn)參數(shù)化執(zhí)行及斷言處理
這篇文章主要介紹了Postman如何實(shí)現(xiàn)參數(shù)化執(zhí)行及斷言處理,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07

