長輪詢是與伺服器建立持續連線最簡單的方式,它不使用任何特定通訊協定,例如 WebSocket 或伺服器發送事件。
它非常容易實作,在許多情況下也夠用了。
定期輪詢
取得伺服器新資訊最簡單的方式是定期輪詢。也就是定期向伺服器發出請求:「嗨,我在這裡,你有任何資訊要給我嗎?」例如,每 10 秒一次。
回應時,伺服器首先會自行記錄客戶端已上線,其次會傳送它到目前為止收到的訊息封包。
這很有效,但有缺點
- 訊息傳遞會延遲最多 10 秒(在請求之間)。
- 即使沒有訊息,伺服器每 10 秒仍會收到大量請求,即使使用者已切換到其他地方或已睡著。就效能而言,這是一個很大的負擔。
因此,如果我們討論的是一個非常小的服務,這種方法可能是可行的,但一般來說,它需要改進。
長輪詢
所謂的「長輪詢」是輪詢伺服器的更好方法。
它也很容易實作,而且可以立即傳遞訊息。
流程
- 向伺服器發送請求。
- 伺服器不會關閉連線,直到它有訊息要傳送。
- 當訊息出現時,伺服器會對請求做出回應。
- 瀏覽器會立即發出新的請求。
這種情況,也就是瀏覽器已發送請求並與伺服器保持連線,是此方法的標準。只有在傳遞訊息時,連線才會關閉並重新建立。
如果連線中斷,例如因為網路錯誤,瀏覽器會立即發送新的請求。
執行長請求的客戶端側 subscribe
函式的草圖
async function subscribe() {
let response = await fetch("/subscribe");
if (response.status == 502) {
// Status 502 is a connection timeout error,
// may happen when the connection was pending for too long,
// and the remote server or a proxy closed it
// let's reconnect
await subscribe();
} else if (response.status != 200) {
// An error - let's show it
showMessage(response.statusText);
// Reconnect in one second
await new Promise(resolve => setTimeout(resolve, 1000));
await subscribe();
} else {
// Get and show the message
let message = await response.text();
showMessage(message);
// Call subscribe() again to get the next message
await subscribe();
}
}
subscribe();
如你所見,subscribe
函式會執行提取,然後等待回應,處理它並再次呼叫它自己。
伺服器架構必須能夠處理許多未處理的連線。
某些伺服器架構會為每個連線執行一個程序,導致程序數與連線數相同,而每個程序都會消耗相當多的記憶體。因此,過多的連線只會消耗所有記憶體。
使用 PHP 和 Ruby 等語言編寫的後端通常就是這種情況。
使用 Node.js 編寫的伺服器通常沒有這種問題。
話雖如此,這不是程式語言的問題。包括 PHP 和 Ruby 在內的大多數現代語言都允許實作適當的後端。請務必確保你的伺服器架構可以順利處理許多同時連線。
示範:聊天
以下是聊天示範,你也可以下載並在本地執行(如果你熟悉 Node.js 且可以安裝模組)
// Sending messages, a simple POST
function PublishForm(form, url) {
function sendMessage(message) {
fetch(url, {
method: 'POST',
body: message
});
}
form.onsubmit = function() {
let message = form.message.value;
if (message) {
form.message.value = '';
sendMessage(message);
}
return false;
};
}
// Receiving messages with long polling
function SubscribePane(elem, url) {
function showMessage(message) {
let messageElem = document.createElement('div');
messageElem.append(message);
elem.append(messageElem);
}
async function subscribe() {
let response = await fetch(url);
if (response.status == 502) {
// Connection timeout
// happens when the connection was pending for too long
// let's reconnect
await subscribe();
} else if (response.status != 200) {
// Show Error
showMessage(response.statusText);
// Reconnect in one second
await new Promise(resolve => setTimeout(resolve, 1000));
await subscribe();
} else {
// Got message
let message = await response.text();
showMessage(message);
await subscribe();
}
}
subscribe();
}
let http = require('http');
let url = require('url');
let querystring = require('querystring');
let static = require('node-static');
let fileServer = new static.Server('.');
let subscribers = Object.create(null);
function onSubscribe(req, res) {
let id = Math.random();
res.setHeader('Content-Type', 'text/plain;charset=utf-8');
res.setHeader("Cache-Control", "no-cache, must-revalidate");
subscribers[id] = res;
req.on('close', function() {
delete subscribers[id];
});
}
function publish(message) {
for (let id in subscribers) {
let res = subscribers[id];
res.end(message);
}
subscribers = Object.create(null);
}
function accept(req, res) {
let urlParsed = url.parse(req.url, true);
// new client wants messages
if (urlParsed.pathname == '/subscribe') {
onSubscribe(req, res);
return;
}
// sending a message
if (urlParsed.pathname == '/publish' && req.method == 'POST') {
// accept POST
req.setEncoding('utf8');
let message = '';
req.on('data', function(chunk) {
message += chunk;
}).on('end', function() {
publish(message); // publish it to everyone
res.end("ok");
});
return;
}
// the rest is static
fileServer.serve(req, res);
}
function close() {
for (let id in subscribers) {
let res = subscribers[id];
res.end();
}
}
// -----------------------------------
if (!module.parent) {
http.createServer(accept).listen(8080);
console.log('Server running on port 8080');
} else {
exports.accept = accept;
if (process.send) {
process.on('message', (msg) => {
if (msg === 'shutdown') {
close();
}
});
}
process.on('SIGINT', close);
}
<!DOCTYPE html>
<script src="browser.js"></script>
All visitors of this page will see messages of each other.
<form name="publish">
<input type="text" name="message" />
<input type="submit" value="Send" />
</form>
<div id="subscribe">
</div>
<script>
new PublishForm(document.forms.publish, 'publish');
// random url parameter to avoid any caching issues
new SubscribePane(document.getElementById('subscribe'), 'subscribe?random=' + Math.random());
</script>
瀏覽器程式碼在 browser.js
。
使用範圍
長輪詢在訊息很少時非常有效。
如果訊息很頻繁,那麼上面繪製的請求-接收訊息圖表就會變成鋸齒狀。
每則訊息都是一個單獨的請求,會附帶標頭、驗證負載等。
留言
<code>
標籤,對於多行程式碼,請將它們包覆在<pre>
標籤中,對於超過 10 行的程式碼,請使用沙盒(plnkr、jsbin、codepen…)