A simple full-stack JavaScript application lets you see what happens under the hood in the WebSockets communication protocol. Credit: Shutterstock / UfaBizPhoto WebSockets is a network communication protocol that enables two-way client-server communication. WebSockets are often used for applications that require instantaneous updates, using a persistent duplex channel atop HTTP to support real-time interactions without constant connection negotiation. Server push is one of the many popular use cases for WebSockets. This article takes a code-first look at both sides of the WebSockets equation in JavaScript, using Node.js on the server and vanilla JavaScript in the browser. The WebSocket protocol Once upon a time, duplex communication or server push over HTTP, in the browser, required considerable trickery. These days, WebSockets is an official part of HTTP. It works as an “upgrade” connection to the normal HTTP connection. WebSockets let you send arbitrary data back and forth between the browser client and your back end. Either side can initiate new messages, so you have the infrastructure for a wide range of real-time applications that require ongoing communication or broadcast. Developers use the WebSockets protocol for games, chat apps, live streaming, collaborative apps, and more. The possibilities are endless. For the purpose of this article, we’ll create a simple server and client, then use them to look under the hood at what happens during a WebSockets communication. Create a simple server To start, you’ll need a /server directory with two subdirectories in it, /client and /server. Once you have those, you’ll need a very simple Node server that establishes a WebSocket connection and echoes back whatever is sent to it. Next, move into the /websockets/server and start a new project: $ npm init Next we need the ws project, which we’ll use for WebSocket support: $ npm install ws With that in place, we can sketch out a simple echo server, in echo.js: // echo.js const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 3000 }); wss.on('connection', (ws) => { console.log('Client connected'); ws.on('message', (message) => { console.log('Received message:', message); ws.send(message); // Echo the message back to the client }); ws.on('close', () => { console.log('Client disconnected'); }); }); console.log(‘server started’); Here, we’re listening on port 3000 and then listening for the connection event on the WebSocket.server object. Once a connection occurs, we get the socket object (ws) as an argument to the callback. Using that, we listen for two more events: message and close. Any time the client sends a message, it calls the onMessage handler and passes us the message. Inside that handler, we us the ws.send() method to send the echo response. Notice that ws.send() also lets us send messages whenever we need to, so we could push an update to the client based on some other event, like an update from a service or a message from another client. The onClose handler lets us do work when the client disconnects. In this case, we just log it. Test the socket server It would be nice to have a simple way to test the socket server from the command line, and the Websocat tool is great for that purpose. It's a simple installation procedure, as described here, and there are many examples for using it. Now start the server: /websockets/server $ node echo.js Background it with Ctrl-z and $ bg, then run the following: $ ./websocat.x86_64-unknown-linux-musl -t --ws-c-uri=wss://localhost:3000/ - ws-c:cmd:'socat - ssl:echo.websocket.org:443,verify=0' That will establish an open WebSocket connection that lets you type into the console and see responses. You’ll get an interaction like so: $ node echo.js Server started ^Z [1]+ Stopped node echo.js matthewcarltyson@dev3:~/websockets/server$ bg [1]+ node echo.js & matthewcarltyson@dev3:~/websockets/server$ ./websocat.x86_64-unknown-linux-musl -t --ws-c-uri=wss://localhost:3000/ - ws-c:cmd:'socat - ssl:echo.websocket.org:443,verify=0' Request served by 7811941c69e658 An echo test An echo test Works Works ^C matthewcarltyson@dev3:~/websockets/server$ fg node echo.js ^C Create the client Now, let’s move into the /websockets/client directory and create a webpage we can use to interact with the server. Keep the server running in the background and we’ll access it from the client. First, create an index.html file like so: // index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>WebSocket Client</title> </head> <body> <h1>WebSocket Client</h1> <input type="text" id="message" placeholder="Enter message"> <button id="send-btn">Send</button> <div id="output"></div> <script src="script.js"></script> </body> </html> This just provides a text input and a button to submit it. It doesn’t do anything itself, just provides the DOM elements we’ll need in the script file that we include: // script.js const wsUri = "ws://localhost:3000"; const outputDiv = document.getElementById("output"); const messageInput = document.getElementById("message"); const sendButton = document.getElementById("send-btn"); let websocket; function connect() { websocket = new WebSocket(wsUri); websocket.onopen = function (event) { outputDiv.innerHTML += "Connected to server!"; }; websocket.onmessage = function (event) { const receivedMessage = event.data; outputDiv.innerHTML += "Received: " + receivedMessage + ""; }; websocket.onerror = function (event) { outputDiv.innerHTML += "Error: " + event.error + ""; }; websocket.onclose = function (event) { outputDiv.innerHTML += "Connection closed."; }; } sendButton.addEventListener("click", function () { const message = messageInput.value; if (websocket && websocket.readyState === WebSocket.OPEN) { websocket.send(message); messageInput.value = ""; } else { outputDiv.innerHTML += "Error: Connection not open."; } }); connect(); // Connect immediately This script sets up several event handlers using the browser-native API. We start up the WebSocket as soon as the script is loaded and watch for open, onclose, onmessage, and onerror events. Each one appends its updates to the DOM. The most important one is onmessage, where we accept the message from the server and display it. The Click handler on the button itself takes the input typed in by the user (messageInput.value) and uses the WebSocket object to send it to the server with the send() function. Then we reset the value of the input to a blank string. Assuming the back end is still running and available at ws://localhost:3000, we can now run the front end. We can use http-server as a simple way to run the front end. It’s a simple way to host static files in a web server, akin to Python’s http module or Java’s Simple Web Server, but for Node. It can be installed as a global NPM package or simply run with npx, from the client directory: /websockets/client/ $ npx http-server -o When we run the previous command and visit the page, we get the form as we should. But when we enter a message in the input and hit Send, it says: Received: [object Blob] If you look in the browser dev console, everything is going over the WebSocket channel (the ws tab within the network tab). The question is, why is it coming back as a blob? If you look at the server console, it says: Client connected Received message: <Buffer 6f 6d 20 6d 61 6e 69 20 70 61 64 6d 65 20 68 75 6d> So now we know the problem is on the server. The problem is that a more recent version of the ws module doesn't automatically decode the message into strings, and instead just gives you the binary buffer. It’s a quick fix in the echo.js onmessage handler: ws.on('message', (message, isBinary) => { message = isBinary ? message : message.toString(); console.log('Received message:', message); ws.send(message); }); We use the second parameter to the callback, isBinary, and if the handler is receiving a string, we do a quick conversion to a string using message.toString(). Conclusion This quick tour has illuminated the underlying mechanisms of a WebSocket client-server communication without any obfuscation from frameworks. As you've seen, the basics of using WebSockets advanced capabilities are straightforward. Just using simple callbacks and message sending gives you full duplex and asynchronous communication using a browser-standard API and a popular Node library. In many projects, of course, you’d want to use something like React on the front end and Node or a similar runtime on the back end. Fortunately, once you know the basics, these frameworks are easy to integrate. The discussion and examples here intentionally glossed over security, which like every area of web development adds a layer of complexity to be managed on both sides of the stack. Scalability and error handling round out the additional issues we'd need to address in a real-world WebSockets implementation. Related content feature 14 great preprocessors for developers who love to code Sometimes it seems like the rules of programming are designed to make coding a chore. Here are 14 ways preprocessors can help make software development fun again. By Peter Wayner Nov 18, 2024 10 mins Development Tools Software Development feature Designing the APIs that accidentally power businesses Well-designed APIs, even those often-neglected internal APIs, make developers more productive and businesses more agile. By Jean Yang Nov 18, 2024 6 mins APIs Software Development news Spin 3.0 supports polyglot development using Wasm components Fermyon’s open source framework for building server-side WebAssembly apps allows developers to compose apps from components created with different languages. By Paul Krill Nov 18, 2024 2 mins Microservices Serverless Computing Development Libraries and Frameworks news Go language evolving for future hardware, AI workloads The Go team is working to adapt Go to large multicore systems, the latest hardware instructions, and the needs of developers of large-scale AI systems. By Paul Krill Nov 15, 2024 3 mins Google Go Generative AI Programming Languages Resources Videos