How WebSocket API Establishes Full-Duplex Connections

The WebSocket API establishes persistent, full-duplex communication channels over a single TCP connection by upgrading a standard HTTP request to the WebSocket protocol. Unlike the traditional request-response model of HTTP, WebSockets enable simultaneous, bi-directional data transfer between a client (such as a web browser) and a server with minimal latency and overhead. This article explains the underlying handshake process, the transition to persistent framing, and how to implement the native WebSocket interface in JavaScript.

The WebSocket Handshake (Protocol Upgrade)

The connection process begins with a standard HTTP/1.1 request initiated by the client, known as the WebSocket Handshake. This request asks the server to upgrade the connection protocol:

  1. Client Handshake Request: The browser sends an HTTP GET request containing specific upgrade headers:
    • Connection: Upgrade — Signals the intent to change the protocol.
    • Upgrade: websocket — Specifies the target protocol.
    • Sec-WebSocket-Key — A base64-encoded random key to verify that the server supports the WebSocket protocol.
    • Sec-WebSocket-Version — Specifies the protocol version (usually 13).
  2. Server Handshake Response: If the server supports WebSockets, it accepts the request and responds with an HTTP status code 101 Switching Protocols. The response includes:
    • Connection: Upgrade
    • Upgrade: websocket
    • Sec-WebSocket-Accept — A hashed value generated using the client’s Sec-WebSocket-Key combined with a standard GUID, proving the server recognized the handshake.

Once this handshake completes, the HTTP layer is dropped, and the underlying TCP connection remains open for raw WebSocket traffic.


Maintaining the Full-Duplex Persistent Connection

After the upgrade, the connection switches from HTTP to the WebSocket framing protocol (using the ws:// or encrypted wss:// URI scheme):


Implementing WebSockets in JavaScript

The browser provides a built-in WebSocket object that abstracts the low-level TCP and handshake management into an event-driven interface.

1. Establishing the Connection

Creating a new WebSocket instance immediately initiates the HTTP upgrade handshake:

const socket = new WebSocket('wss://example.com/socket');

2. Listening for Connection Events

The client monitors the connection state using event listeners:

// Triggered when the handshake succeeds and the connection is open
socket.addEventListener('open', (event) => {
  console.log('Connected to WebSocket server.');
  // Send initial data to the server
  socket.send(JSON.stringify({ type: 'INIT', payload: 'Hello Server' }));
});

// Triggered whenever the server pushes data to the client
socket.addEventListener('message', (event) => {
  console.log('Message from server:', event.data);
});

// Triggered when an error occurs
socket.addEventListener('error', (error) => {
  console.error('WebSocket Error:', error);
});

// Triggered when the connection is closed by either party
socket.addEventListener('close', (event) => {
  console.log(`Connection closed: ${event.code} - ${event.reason}`);
});

3. Sending Data

Data can be sent immediately once the readyState is WebSocket.OPEN:

function sendMessage(messageObject) {
  if (socket.readyState === WebSocket.OPEN) {
    socket.send(JSON.stringify(messageObject));
  } else {
    console.warn('Socket is not open. State:', socket.readyState);
  }
}

4. Closing the Connection

Either side can gracefully terminate the connection using the close() method:

socket.close(1000, 'Work complete');

Through this combination of the HTTP upgrade mechanism, lightweight data framing, and JavaScript’s event-driven API, WebSockets provide an efficient foundation for real-time applications such as live chats, multiplayer games, and financial dashboards.