Up until now, you’ve seen communication between the Web browser and your backend server occur in the request/response cycle of HTTP 1.1.
The Client makes an HTTP request, like GET /home HTTP/1.1. The Server receives that request, translates it, and returns an HTTP response, like HTTP/1.1 200 OK. One request, one response. That is great for getting data and asking the server to create new resources, but it does not support the demands of Web applications that need “real-time communication” or to receive messages from the server without an HTTP request. The WebSockets standard fills that hole. (That’s a link to the RFC. It’s … dense.)
Check out this link to caniuse.com that tracks the support of WebSockets (and a whole bunch of other things) in browsers for the desktop and mobile. You can see that everything supports WebSockets (except Opera Mini which fails to support pretty much anything, stupid Opera Mini).
Since the technology is now well-supported, it makes sense to learn it so that you can do amazing things in your Web application.
In this article, you will learn about how WebSockets work from the perspectives of the communication between the client and the server.
Just one thing: there are lots of libraries out there for you to use that make this WebSocket thing “easy”. In the same way that this curriculum challenges you to use fetch
rather than some other AJAX library like axios
, learning WebSockets teaches you about the technology and how it works. Once you know that, then you can use any library (like socket.io) to ease your development. But, giving you the deeper knowledge is what this is providing for you.
In the traditional model of request/response, the client makes a connection to the server, makes the request, the server responds on the same connection, then the connection can be closed. The next time your browser wants to make a request to the Web server, it may need to establish that connection, again.
WebSockets create a persistent connection, one that doesn’t close unless it doesn’t get used. This means that the TCP/IP handshake that needs to occur between the browser and server does not need to happen with every single request. This has two benefits:
The way it happens is an extra HTTP header in the HTTP request. Here’s an example.
GET ws://WebSocket.example.com/ HTTP/1.1
Origin: http://example.com
Connection: Upgrade
Host: WebSocket.example.com
Upgrade: WebSocket
So, checkout two things about this:
If the server supports WebSockets, it says “COOL!” and returns something like the following headers in the response.
HTTP/1.1 101 WebSocket Protocol Handshake
Date: Thu, 7 May 2020 17:07:34 GMT
Connection: Upgrade
Upgrade: WebSocket
This confirms that the server is good with upgrading the connection. When both the client and server agree, they just don’t close the connection.
Boom! Persistent!
Once the connection exists between the browser and the server, either of the two can send a message over the connection. It’s a message with a sender and a receiver. It is not a request/response. There is no request. There is no response. There are just two actors sending messages back and forth, like two kids in school passing notes back and forth in class. The server doesn’t have to wait for a request to send a message. The client can sends a message without the expectation of a response.
Just like in TCP/IP, when data gets chopped up into packets and datagrams, messages over WebSockets get chopped up into frames. Each frame contains extra information to help ensure the integrity of the message as it traverses between sender and recipient. It’s not super important to know what those parts are because you’re not writing code to implement the standard; instead, the browser will do it for you automatically, just like using fetch
means you don’t have to format the HTTP request.
Just like the browser has the fetch
method to easily make HTTP 1.1 requests, it provides a WebSocket
class for you to create objects that manage the connection between the browser and the server. You just give the constructor the WebSocket URL that you want your browser to connect to.
// This is EXAMPLE CODE ONLY!
// There is no sockets.example.com!
const socket = new WebSocket('wss://sockets.example.com');
Now, with fetch
, that sends a request and, when a response comes back, the Promise
gets fulfilled and you do stuff with it. That’s not how WebSocket
objects work. They can’t work that way.
Instead of a Promise
, you add event listeners to the WebSocket
object in the same way that you add event listeners to input
or button
elements to capture specific kinds of events. For the WebSocket, the events are
Then, the WebSocket
object has two methods for you to use, send
to send a message to the server, and close
to close the connection. Here’s what some code could look like that uses that socket
opened above.
// This is EXAMPLE CODE ONLY!
// When the socket is open, send a message!
socket.addEventListener('open', () => socket.send('I am LEGENDARY!'));
// When you get a message, add it to your state store.
socket.addEventListener('message', event => {
dispatch(gotMessage(event.data));
});
// Print out that something bad happened
socket.addEventListener('error', () => {
console.error('Something bad happened... :-(');
});
// When the socket closes, update the state
// of the application!
socket.addEventListener('close', () => {
dispatch(justDisconnected());
});
Note: just like with DOM elements where you could use el.onclick = () => {...}
to add an event handler. You can do something like socket.onmessage = () => {...}
, too. But, that’s just not nice because you can’t add more than one listener. So, if you see that in the documentation, somewhere, remember that you can always use addEventListener
rather than the on«event»
properties.
All of that is just provided for you in the browser! There’s a lot of code under all of that to allow your JavaScript that easy-to-use API! Thanks, browser makers!
You can give it a shot yourself. Create a new HTML 5 file with all of the normal stuff and add this code in there.
In the body of the document, put this.
<div>
<button id="connect">Connect</button>
<button id="send-message">Send</button>
<button id="disconnect">Disconnect</button>
</div>
<div id="messages"></div>
Now, create a script
element after the content you just added (so you don’t have to wait for “DOMContentLoaded”). This is just regular-old DOM code with the socket message stuff in there, too. Have a look and try it out! Change the code so that you can see how changes affect it!
This code uses a real WebSocket server, wss://echo.websocket.org!
const connect = document.getElementById('connect');
const disconnect = document.getElementById('disconnect');
const sendMessage = document.getElementById('send-message');
const messages = document.getElementById('messages');
let socket = null;
connect.addEventListener('click', () => {
messages.innerHTML += `<p>Opening WebSocket...</p>`;
socket = new WebSocket("wss://echo.websocket.org/");
socket.addEventListener('open', () => {
messages.innerHTML += `<p>CONNECTED!</p>`;
});
socket.addEventListener('message', event => {
messages.innerHTML += `<p>Received "${event.data}"</p>`;
});
socket.addEventListener('error', () => {
messages.innerHTML += `<p>ERROR</p>`;
});
socket.addEventListener('close', () => {
messages.innerHTML += `<p>Socket closed</p>`;
socket = null;
});
});
disconnect.addEventListener('click', () => {
if (!socket) {
messages.innerHTML += `<p>Socket not open.</p>`;
return;
}
socket.close();
});
sendMessage.addEventListener('click', () => {
if (!socket) {
messages.innerHTML += `<p>Socket not open.</p>`;
return;
}
messages.innerHTML += `<p>Sending "WebSockets are cool!"</p>`;
socket.send('WebSockets are cool!');
});
Here’s an interesting thing. After you play around with the code, refresh the page and connect to the server. Then, just wait. Likely, eventually, the socket will close due to disuse. Many libraries (like socket.io) keep the connection “warm” by sending little ping methods to the server to let the server know that it really does want to keep that connection open. If it doesn’t close, then you have a really good and stable Internet connection!
If that’s the client-side code above, the question that might be bothering you is “How hard is the server-side code?” Well, luckily, it’s just about the same level of ease with the ws package for Node.js.
Because WebSockets are a browser-based technology, the implementations that you will find on the server can vary widely. Luckily, the ws API is an event-driven API, too. It provides these events for you to use to build a WebSocket server using the Server
object.
WebSocket
that allows you to communicate with the client.Then, the server has a close
method which lets it shut down. It’s got some other methods, too, about handling upgrades and stuff, which are outside the scope of this article. You are encouraged to go check out the API docs in a later article.
Those last two are some pretty low-level events that you won’t necessarily have to pay attention to unless you’re doing some really advanced stuff. However, you will want to pay attention to the connection property because that is how you know a client is connected. Then, when the connection gets upgraded, the callback gets a WebSocket
object so that your server can send messages to the browser.
Here’s the code to write an “echo” server like you just used in the client-side stuff above. Put this in a file, install “ws” using npm install ws
, and run it with node «filename»
. Then, change the URL in the HTML file that you created from wss://echo.websocket.org/ to ws://localhost:8080. Everything still works!
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });
server.on('connection', webSocket => {
console.log('client connecting...');
let interval = null;
setInterval(() => webSocket.send('Hello?'), 1000);
webSocket.on('message', message => {
console.log('received: %s', message);
webSocket.send(message);
});
webSocket.on('close', () => {
console.log('Connection closed.');
clearInterval(interval)
});
});
You can see that a server gets created using port 8080. That server then waits for connections with server.on('connection', ...)
. When the connection occurs, the callback gets called and webSocket
gets set to the actual WebSocket
instance that you can use to send (and receive) messages to (and from) the client. Then, it creates an interval that sends a “Hello?” message to the client every second or so.
You subscribe to messages using webSocket.on('message', ...)
. When a message arrives from the browser, the callback gets called with the content of the data in the message
variable. Normally, that’ll be a JSON-formatted string that you can use to do things with your code.
Finally, when the WebSocket
object closes, it prints a message to the console and clears that interval.
That’s how nice ws makes it to write WebSocket-enabled. Thank you, ws!
A really cool thing about ws is that it can track clients for you when you create the Server
object by passing in the clientTracking
option when you construct it. Then, the clients
property on the Server
object will have all of the clients on it so you can broadcast messages to everyone!
Server
object which, then, provides a WebSocket
object nearly identical to the WebSocket
used on the client side.