Client-Server Communication
Learning Objectives
- You know of the differences between long and short polling.
- You know how to implement naive polling on the client and server.
In the previous chapters of this part, we’ve focused on client-side functionality and state management. From this chapter on, we’ll move our focus to client-server communication. We’ll start by revisiting the basics of APIs, then moving on to basics of checking whether there are updates on the server side.
Brief API reminder
Web applications typically use the request-response pattern to build Application Programming Interfaces (APIs). In this model, a client sends a request to the server, which then returns a response. Figure 1 illustrates this interaction: the client sends a GET request to a specific URI, and the server replies with JSON-formatted data.
Many APIs follow Representational State Transfer (REST) architectural style. In contemporary REST applications for the web, resources are identified by URIs, data is commonly formatted in JSON, and standard HTTP methods (such as GET, POST, PUT, and DELETE) are used to define interactions.
For instance, a REST API that provides task information might assign unique URI paths to individual tasks, use HTTP methods to perform operations on these tasks, and return data in JSON format.
The original definition of REST had little to do with APIs as they are currently discussed, but rather focused on the architectural principles of the web. These principles included the use of URIs to identify resources, the use of standard methods for interacting with resources, and the use of agreed-upon representation formats for exchanging information.
For a deeper dive into REST, see Roy Fielding’s dissertation Architectural Styles and the Design of Network-based Software Architectures.
Long and short polling
Most web applications implement the request-response pattern synchronously. That is, the client sends a request and waits for the server’s reply. However, when generating a response takes a long time or when updates must be checked frequently, the client may use polling to request new data from the server. Two common polling methods are long polling and short polling.
Long polling
In long polling, the client sends a request, and the server holds the connection open until either new data is available or a timeout occurs. When the connection times out, it is closed and the client immediately sends a new request. Alternatively, if the server sends data before the timeout, the client processes the response and can then decide whether to request again. This is visualized in Figure 2 below.
Short polling
With short polling, the client sends a request and the server responds immediately. If new data is available, it is returned with the response; if not, the server indicates that no update is yet available. After a brief wait, the client sends another request to check for updates. This is illustrated in Figure 3 below.
Benefits and downsides
Both polling methods allow the client to retrieve near-real-time updates. However, they have some drawbacks:
- Resource overhead: Every polling request requires establishing a new connection and transmitting HTTP header information, which can waste resources — especially with short polling if requests are very frequent.
- Connection management: Long polling keeps connections open for extended periods, potentially leading to many simultaneous open connections.
- Timeouts: In long polling, connections eventually time out, so the client must handle reconnections and resubmit requests as needed.
Chat application example — short polling
To illustrate the polling mechanisms, let’s first look into a simple chat application that uses short polling. The server-side has two endpoints, one for sending messages and another for retrieving messages. The client-side sends messages to the server and requests new messages at regular intervals.
Server-side functionality
A simple server-side implementation could be as follows (we intentionally omit e.g. database functionality):
// imports etc
let messages = [];
app.post("/api/messages", async (c) => {
const message = await c.req.json();
message.date = Date.now();
messages.unshift(message);
messages = messages.slice(0, 10);
return c.json({ status: "ok" });
});
app.get("/api/messages", (c) => {
const since = parseInt(c.req.query("since") ?? 0);
return c.json(messages.filter((m) => m.date > since));
});
// export app
Above, whenever a message is sent to the server, it is added to the message list, which stores at most 10 messages. When a client requests messages, the messages that are returned include either all the messages or messages where the date
property is greater than the query parameter since
.
Client-side functionality
The corresponding client-side functionality would include functions for sending and retrieving messages. One possible implementation would be as follows, where the application polls the server every 3 seconds for new messages:
<script>
let message = $state("");
let messages = $state([]);
const sendMessage = async () => {
await fetch("/api/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ message }),
});
message = "";
};
const getMessages = async () => {
const since = messages.length > 0 ? messages[0].date : 0;
const response = await fetch("/api/messages?since=" + since);
const newMessages = await response.json();
if (newMessages.length > 0) {
messages = [...newMessages, ...messages];
messages = messages.slice(0, 10);
}
};
if (!import.meta.env.SSR) {
setInterval(getMessages, 3000);
}
</script>
<p>Chat!</p>
<input type="text" bind:value={message} />
<button onclick={() => sendMessage()}>Send</button>
<ul>
{#each messages as message}
<li>{message.message}</li>
{/each}
</ul>
The above example assumes that the server is running on the same host as the client, or at least, that they are running behind a load balancer that routes requests between them. Due to this, the client does not need to add the server’s address to the fetch requests.
Chat application example — long polling
To change the above to an application that uses long polling, we would need to modify both client- and server-side functionality. First, on the client, we would remove the setInterval
call, call getMessages
once when the page is loaded, and add a getMessages
call at the end of the getMessages
function.
<script>
// states and functionality for sending messages
const getMessages = async () => {
// same as before
// new line:
getMessages();
};
if (!import.meta.env.SSR) {
getMessages();
}
</script>
<!-- chat elements -->
With the above change to the client-side code, if we do not do any changes to the server, the client polls the server as fast as it can.
Then, on the server, we would need to modify the /api/messages
endpoint to wait for new messages before responding. A naive approach would be to add a loop that checks for new messages and, if none exist, wait a while to check for new messages again. Once the server finds new messages, it responds with them. Otherwise, after looping for a while, the server would respond with an empty array. Below is a simple implementation of this:
// ...
app.get("/api/messages", async (c) => {
const since = parseInt(c.req.query("since") ?? 0);
for (let i = 0; i < 30; i++) {
const newMessages = messages.filter((m) => m.date > since);
if (newMessages.length > 0) {
return c.json(newMessages);
}
await new Promise((resolve) => setTimeout(resolve, 3000));
}
return c.json([]);
});
Altogether, the above examples are rather simple. There is, for example, no error handling, no authentication, and no database. However, they illustrate the basic principles of short and long polling.