WebSockets: Revolutionizing Real-Time Web Communication
📒

WebSockets: Revolutionizing Real-Time Web Communication

Tags
Published
July 25, 2024
WebSockets have transformed the landscape of web communication, enabling real-time, bidirectional data exchange between clients and servers. This powerful protocol has become an essential tool for developers building interactive and dynamic web applications. Let's explore what WebSockets are, how they work, and why they're so valuable in modern web development.

What are WebSockets?

WebSockets are a communication protocol that provides full-duplex communication channels over a single TCP connection. Unlike traditional HTTP, which follows a request-response model, WebSockets allow for continuous two-way communication between clients and servers. This means that once a WebSocket connection is established, both parties can send messages to each other at any time without the need for repeated requests.

How WebSockets Work

The WebSocket protocol begins with a handshake process. When a client wants to establish a WebSocket connection, it sends an HTTP request to the server with specific headers indicating its desire to upgrade the connection to WebSockets. 
Request format of the connection
GET ws://websocketexample.com:8181/ HTTP/1.1 Host: localhost:8181 Connection: Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Sec-WebSocket-Version: 13 Sec-WebSocket-Key: b6gjhT32u488lpuRwKaOWs==
Upgrade header denotes the WebSocket handshake while the Sec-WebSocket-Key features Base64-encoded random value. 
The server then responds with an acknowledgment, and the connection is upgraded from HTTP to WebSocket.
Response format of the connection
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: rG8wsswmHTJ85lJgAE3M5RTmcCE=
Sec-WebSocket-Accept, features the zest of value submitted in the Sec-WebSocket-Key request header.
Once the connection is established, the communication switches to the WebSocket protocol. Messages are sent in frames, which consist of various components such as flags, payload length, and the actual payload data. This frame structure allows for efficient parsing and handling of messages on both ends.
Frame Structure
notion image
image source (intercepted and analyzed by Wireshark)
image source (intercepted and analyzed by Wireshark)
  • FIN bit: Indicates if this is the final fragment of a message
  • Opcode: Specifies the type of frame (e.g. text, binary, close, ping, pong)
  • Mask bit: Indicates if the payload is masked
  • Payload length: Size of the payload data
  • Masking key (if masked): Used to decode the payload
  • Payload data: The actual message content
 
 
Below is a sample of code.
Server:
const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); wss.on('connection', function connection(ws) { console.log('New client connected'); ws.on('message', function incoming(message) { console.log('Received: %s', message); ws.send(`Server received: ${message}`); }); ws.on('close', function close() { console.log('Client disconnected'); }); ws.send('Welcome to the WebSocket server!'); }); console.log('WebSocket server is running on ws://localhost:8080');
Server (python):
import asyncio import websockets async def handler(websocket, path): print("New client connected") try: async for message in websocket: print(f"Received: {message}") await websocket.send(f"Server received: {message}") except websockets.ConnectionClosed: print("Client disconnected") start_server = websockets.serve(handler, "localhost", 8080) asyncio.get_event_loop().run_until_complete(start_server) print("WebSocket server is running on ws://localhost:8080") asyncio.get_event_loop().run_forever()
Client:
const socket = new WebSocket('ws://localhost:8080'); // or ws://echo.websocket.org/ an open existing websocket server socket.onopen = function(e) { console.log("[open] Connection established"); }; socket.onmessage = function(event) { console.log(`[message] Data received from server: ${event.data}`); }; socket.onclose = function(event) { if (event.wasClean) { console.log(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`); } else { console.log('[close] Connection died'); } }; socket.onerror = function(error) { console.log(`[error] ${error.message}`); };

An Easy Implementation of the server and the client of WebSocket Protocol

#include <vector> #include <cstdint> #include <string> std::vector<uint8_t> createWebSocketFrame(const std::string& message, bool isFinal = true, uint8_t opcode = 0x01) { std::vector<uint8_t> frame; // First byte: FIN bit and Opcode frame.push_back((isFinal ? 0x80 : 0x00) | opcode); // Second byte: Mask bit (0 for server) and Payload length size_t length = message.length(); if (length <= 125) { frame.push_back(static_cast<uint8_t>(length)); // If the payload length is 125 bytes or less, it can be directly encoded in a single byte. static_cast<uint8_t>(length) ensures that the length is cast to an 8-bit unsigned integer before pushing it onto the frame vector. } else if (length <= 65535) { frame.push_back(126); frame.push_back((length >> 8) & 0xFF); frame.push_back(length & 0xFF); // If the payload length is between 126 and 65535 bytes, it uses an extended payload length format. The first byte pushed is 126, indicating that the next two bytes will represent the payload length. frame.push_back((length >> 8) & 0xFF) takes the higher byte of the 16-bit length (by shifting right 8 bits and masking with 0xFF). frame.push_back(length & 0xFF) takes the lower byte of the 16-bit length. } else { frame.push_back(127); for (int i = 7; i >= 0; --i) { frame.push_back((length >> (i * 8)) & 0xFF); } // If the payload length is larger than 65535 bytes, it uses an extended payload length format that spans 8 bytes. The first byte pushed is 127, indicating that the next eight bytes will represent the payload length. The for loop iterates from i = 7 to i = 0, shifting the length right by multiples of 8 bits and masking with 0xFF to isolate each byte of the 64-bit length, pushing each byte onto the frame vector in big-endian order (most significant byte first). } // Append the message data frame.insert(frame.end(), message.begin(), message.end()); return frame; }
#include <vector> #include <cstdint> #include <string> std::string extractWebSocketFrame(const std::vector<uint8_t>& frame) { if (frame.size() < 2) return ""; bool fin = (frame[0] & 0x80) != 0; uint8_t opcode = frame[0] & 0x0F; bool masked = (frame[1] & 0x80) != 0; uint64_t payload_length = frame[1] & 0x7F; size_t header_length = 2; if (payload_length == 126) { if (frame.size() < 4) return ""; payload_length = (frame[2] << 8) | frame[3]; header_length = 4; } else if (payload_length == 127) { if (frame.size() < 10) return ""; payload_length = 0; for (int i = 0; i < 8; ++i) { payload_length = (payload_length << 8) | frame[2 + i]; } header_length = 10; } if (masked) { uint8_t mask[4]; std::copy(frame.begin() + header_length, frame.begin() + header_length + 4, mask); header_length += 4; std::string payload; for (size_t i = 0; i < payload_length; ++i) { payload += frame[header_length + i] ^ mask[i % 4]; } return payload; } else { return std::string(frame.begin() + header_length, frame.begin() + header_length + payload_length); } }

Benefits of WebSockets

  1. Real-Time Updates: WebSockets enable instant data transmission, making them ideal for applications requiring live updates, such as chat systems, gaming, and financial trading platforms.
  1. Reduced Latency: By eliminating the need for continuous polling, WebSockets significantly reduce latency and improve overall performance.
  1. Efficient Resource Usage: The persistent connection of WebSockets reduces server load and bandwidth consumption compared to traditional polling methods.
  1. Cross-Platform Compatibility: WebSockets are supported by all modern web browsers and are compatible with various platforms, including web, mobile, and desktop applications.
  1. Bi-Directional Communication: Both clients and servers can initiate communication, allowing for more dynamic and interactive applications.

Use Cases for WebSockets

WebSockets excel in scenarios that require real-time, event-driven communication. Some common use cases include:
  • Real-time chat applications
  • Live sports updates and scores
  • Collaborative editing tools
  • Multiplayer online games
  • Financial trading platforms
  • IoT device communication

Implementing WebSockets

While implementing WebSockets from scratch can be complex, there are numerous libraries and frameworks available that simplify the process. For JavaScript developers, the Socket.io library is a popular choice that provides an easy-to-use API for WebSocket communication. For those interested in building a WebSocket server from scratch, it's essential to understand the underlying protocols and frame structures. This involves handling the initial handshake, parsing incoming frames, and constructing outgoing messages correctly.

Is it related to Traditional Sockets?

WebSockets and traditional sockets share a fundamental relationship in the realm of network communication, but they serve different purposes and operate at different levels of the network stack. WebSocket is named with "socket" in its name due to its conceptual and functional similarities to traditional network sockets, while also highlighting its specific use in web communications. Here's a breakdown of the reasoning behind the name:
  1. Similarity to traditional sockets: WebSockets operate on a similar principle to traditional network sockets, providing a communication endpoint for sending and receiving data across a network. Like traditional sockets, WebSockets enable bidirectional communication between two endpoints.
  1. Persistent connection: WebSockets maintain a persistent connection between the client and server, much like how traditional sockets keep a connection open for ongoing communication. This persistent nature is a key feature that distinguishes WebSockets from the standard HTTP request-response model.
  1. Full-duplex communication: WebSockets allow for full-duplex communication, meaning data can be sent in both directions simultaneously. This is similar to how traditional sockets operate, allowing for real-time, bidirectional data exchange.
  1. Emphasis on web usage: The "Web" prefix in WebSocket emphasizes its specific design for web-based applications and browsers. It indicates that this technology is tailored for web environments, unlike traditional sockets which are more general-purpose.
  1. API similarity: The WebSocket API in web browsers provides methods and events that are conceptually similar to working with traditional sockets, such as opening connections, sending data, and handling incoming messages.
By incorporating "socket" into its name, WebSocket conveys its role as a communication endpoint for web applications, while also indicating its similarities to and evolution from traditional socket programming. This naming helps developers understand its purpose and functionality in the context of web development and real-time communication.

Considerations and Alternatives

While WebSockets offer significant advantages, they may not be the best solution for every scenario. Some considerations include:
  • Scalability: Managing many concurrent WebSocket connections can be challenging and resource-intensive for servers.
  • Firewall Issues: Some firewalls and proxy servers may have issues with long-lived connections.
  • Fallback Mechanisms: It's important to implement fallback options for environments where WebSockets are not supported.
Alternative approaches, such as long polling or server-sent events, may be more appropriate in certain situations. Some platforms, like PubNub, even prefer long polling for its reliability and scalability in various networking environments.

Conclusion

WebSockets have revolutionized real-time web communication, enabling developers to create more interactive and responsive applications. By providing a persistent, bidirectional communication channel, WebSockets offer a powerful tool for building modern web experiences. As the web continues to evolve, WebSockets will undoubtedly play a crucial role in shaping the future of real-time applications.