updates
This commit is contained in:
parent
94c83e9e50
commit
cc5213b09e
79 changed files with 1341 additions and 357 deletions
95
5-network/10-long-polling/article.md
Normal file
95
5-network/10-long-polling/article.md
Normal file
|
@ -0,0 +1,95 @@
|
|||
# Long polling
|
||||
|
||||
Long polling is the simplest way of having persistent connection with server, that doesn't use any specific protocol like WebSocket or Server Side Events.
|
||||
|
||||
Being very easy to implement, it's also good enough in a lot of cases.
|
||||
|
||||
## Regular Polling
|
||||
|
||||
The simplest way to get new information from the server is polling.
|
||||
|
||||
That is, periodical requests to the server: "Hello, I'm here, do you have any information for me?". For example, once in 10 seconds.
|
||||
|
||||
In response, the server first takes a notice to itself that the client is online, and second - sends a packet of messages it got till that moment.
|
||||
|
||||
That works, but there are downsides:
|
||||
1. Messages are passed with a delay up to 10 seconds.
|
||||
2. Even if there are no messages, the server is bombed with requests every 10 seconds. That's quite a load to handle for backend, speaking performance-wise.
|
||||
|
||||
So, if we're talking about a very small service, the approach may be viable.
|
||||
|
||||
But generally, it needs an improvement.
|
||||
|
||||
## Long polling
|
||||
|
||||
Long polling -- is a better way to poll the server.
|
||||
|
||||
It's also very easy to implement, and delivers messages without delays.
|
||||
|
||||
The flow:
|
||||
|
||||
1. A request is sent to the server.
|
||||
2. The server doesn't close the connection until it has a message.
|
||||
3. When a message appears - the server responds to the request with the data.
|
||||
4. The browser makes a new request immediately.
|
||||
|
||||
The situation when the browser sent a request and has a pending connection with the server, is standard for this method. Only when a message is delivered, the connection is reestablished.
|
||||
|
||||

|
||||
|
||||
Even if the connection is lost, because of, say, a network error, the browser immediately sends a new request.
|
||||
|
||||
A sketch of client-side code:
|
||||
|
||||
```js
|
||||
async function subscribe() {
|
||||
let response = await fetch("/subscribe");
|
||||
|
||||
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();
|
||||
```
|
||||
|
||||
The `subscribe()` function makes a fetch, then waits for the response, handles it and calls itself again.
|
||||
|
||||
```warn header="Server should be ok with many pending connections"
|
||||
The server architecture must be able to work with many pending connections.
|
||||
|
||||
Certain server architectures run a process per connect. For many connections there will be as many processes, and each process takes a lot of memory. So many connections just consume it all.
|
||||
|
||||
That's often the case for backends written in PHP, Ruby languages, but technically isn't a language, but rather implementation issue.
|
||||
|
||||
Backends written using Node.js usually don't have such problems.
|
||||
```
|
||||
|
||||
## Demo: a chat
|
||||
|
||||
Here's a demo:
|
||||
|
||||
[codetabs src="longpoll" height=500]
|
||||
|
||||
## Area of usage
|
||||
|
||||
Long polling works great in situations when messages are rare.
|
||||
|
||||
If messages come very often, then the chart of requesting-receiving messages, painted above, becomes saw-like.
|
||||
|
||||
Every message is a separate request, supplied with headers, authentication overhead, and so on.
|
||||
|
||||
So, in this case, another method is preferred, such as [Websocket](info:websocket) or [Server Sent Events](info:server-sent-events).
|
BIN
5-network/10-long-polling/long-polling.png
Normal file
BIN
5-network/10-long-polling/long-polling.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
BIN
5-network/10-long-polling/long-polling@2x.png
Normal file
BIN
5-network/10-long-polling/long-polling@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
54
5-network/10-long-polling/longpoll.view/browser.js
Normal file
54
5-network/10-long-polling/longpoll.view/browser.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
// 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();
|
||||
|
||||
}
|
18
5-network/10-long-polling/longpoll.view/index.html
Normal file
18
5-network/10-long-polling/longpoll.view/index.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
<!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 to avoid any caching issues
|
||||
new SubscribePane(document.getElementById('subscribe'), 'subscribe?random=' + Math.random());
|
||||
</script>
|
87
5-network/10-long-polling/longpoll.view/server.js
Normal file
87
5-network/10-long-polling/longpoll.view/server.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
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) { // if run by pm2 have this defined
|
||||
process.on('message', (msg) => {
|
||||
if (msg === 'shutdown') {
|
||||
close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
process.on('SIGINT', close);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue