# WebSockets — Part 1: The Handshake and the Full-Duplex Model

> This topic assumes the Browser Request Lifecycle and HTTP topics for general request/response background. Part 1 covers the connection model and basic API; Part 2 covers origin validation, authentication, reconnection, and message framing considerations — security-relevant material that only makes sense once the basic model here is understood.

## 1. The Core Problem: HTTP Is Request/Response, Not a Conversation

Ordinary HTTP is fundamentally one-shot: the client sends a request, the server sends back a response, and the connection's job (for that exchange) is done. Getting the server to push new data to the client without the client re-asking requires workarounds — polling (ask repeatedly), long-polling (ask and have the server hold the response open until it has something). WebSockets solve this directly: after a one-time handshake, both sides can send messages to each other **at any time, independently**, over a single connection that stays open.

```
HTTP request/response:        Client ──request──>  Server
                               Client <──response── Server
                               (connection then typically closes or is reused for the NEXT unrelated request)

WebSocket (after handshake):  Client <──message──>  Server   (either side, any time, same connection)
                               Client <──message──>  Server
                               Client <──message──>  Server
                               ... connection stays open until explicitly closed ...
```

This is called **full-duplex**: both directions are simultaneously and independently open, unlike HTTP's strict request-then-response turn-taking.

## 2. The Handshake: An HTTP Upgrade Request

A WebSocket connection doesn't start as something different from HTTP — it starts as a completely normal HTTP `GET` request that asks the server to *switch protocols*:

```http
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
```

If the server supports WebSockets on that path, it responds:

```http
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
```

The `101 Switching Protocols` status is the signal: the same underlying TCP connection that carried this HTTP exchange is now repurposed to carry WebSocket frames instead of further HTTP requests. `Sec-WebSocket-Accept` is a hash the server computes from the client's `Sec-WebSocket-Key` (per the spec) — proving the response actually came from a real WebSocket-aware server and wasn't, say, a cached HTTP response or a misconfigured proxy replaying something unrelated.

Because it starts as a real HTTP request, everything you already know about HTTP applies to *this one request*: it goes over TLS if the URL is `wss://` (the WebSocket-secure scheme, analogous to `https://`), it can carry cookies (attached by destination, exactly as covered in the Browser Request Lifecycle topic), and it can be behind a reverse proxy (which must be explicitly configured to forward `Upgrade`/`Connection` headers, or the upgrade will fail — a very common real-world misconfiguration).

## 3. The Browser API

```js
const socket = new WebSocket("wss://example.com/chat");

socket.onopen = () => {
  console.log("connected");
  socket.send("hello server");
};

socket.onmessage = (event) => {
  console.log("received:", event.data);
};

socket.onerror = (event) => {
  console.error("socket error", event);
};

socket.onclose = (event) => {
  console.log("closed:", event.code, event.reason);
};

// later, to close deliberately:
socket.close(1000, "done chatting");
```

- `new WebSocket(url)` immediately starts the handshake — there's no separate "connect" call.
- `.send(data)` works only after `onopen` has fired (the connection is in the `OPEN` state — check `socket.readyState` if unsure); calling it before that throws.
- `.onmessage` fires for every message the server sends, independently of anything the client did — this is the "push" capability plain HTTP doesn't have.
- `.onclose` fires whether the close was clean (`.close()` called, or the server closed normally) or the connection dropped unexpectedly (network failure) — `event.code` and `event.wasClean` distinguish these cases.

## 4. Message Types: Text and Binary

WebSocket messages are either UTF-8 text or binary:

```js
socket.send("a plain text message");           // text frame
socket.send(new Blob([...]));                   // binary frame (Blob)
socket.send(new ArrayBuffer(8));                 // binary frame (ArrayBuffer)

socket.binaryType = "arraybuffer"; // default is "blob" — controls what event.data is for incoming binary frames
socket.onmessage = (event) => {
  if (typeof event.data === "string") {
    // text message
  } else {
    // binary message — a Blob or ArrayBuffer depending on socket.binaryType
  }
};
```

There's no built-in structured message format (no automatic JSON) — applications almost universally adopt a convention of sending JSON-encoded text frames and parsing them on receipt, entirely at the application level; the WebSocket protocol itself has no opinion about message content.

## 5. Under the Hood: One TCP Connection, Framed Messages

After the `101` handshake, the same TCP connection carries a sequence of **WebSocket frames** — a lightweight binary framing format (a few header bytes indicating frame type, payload length, and whether it's the final fragment of a message, followed by the payload) that lets both sides parse out complete messages from the byte stream, including distinguishing text from binary and detecting connection-close signals, without needing anything HTTP-shaped anymore. This is why WebSockets are dramatically more efficient than repeated HTTP polling for high-frequency bidirectional traffic: there's no repeated handshake, no repeated header overhead per message (HTTP headers are relatively large; a WebSocket frame header is a handful of bytes), and no round-trip latency spent re-establishing a request each time — just one persistent connection with minimal per-message overhead in both directions.

![A WebSocket starts as an HTTP upgrade request answered with 101 Switching Protocols, then both sides exchange framed messages over the same TCP connection](diagrams/01-upgrade-and-frames.svg)

## Common Pitfalls

1. **Calling `.send()` before `onopen` has fired.** The socket isn't ready yet — always send from inside (or after) the `onopen` handler, or check `readyState === WebSocket.OPEN` first.
2. **Forgetting a reverse proxy needs explicit configuration to forward the Upgrade handshake.** A default proxy config that works fine for ordinary HTTP will often silently fail (or hang) on a WebSocket upgrade unless `Upgrade`/`Connection` headers are explicitly passed through — see the Proxy and Reverse Proxy topic in the HTTP group.
3. **Assuming `wss://` is optional "for later."** Plain `ws://` sends everything, including any auth token or cookie riding along on the handshake, unencrypted — treat `wss://` as no more optional than `https://` is for a login page.
4. **Not handling `onclose`/`onerror` at all**, leaving the application with no idea the connection dropped — a real client needs reconnection logic (covered in Part 2), not just happy-path handlers.
5. **Expecting message ordering or delivery guarantees the protocol doesn't actually promise beyond TCP's own in-order-per-connection delivery.** If the connection drops and reconnects, any messages in flight during the drop are simply gone — the application, not the protocol, is responsible for detecting gaps and requesting missed state if that matters.

## Quick Reference

```
Handshake: plain HTTP GET + Upgrade: websocket -> server replies 101 Switching Protocols
Scheme: ws:// (plain, avoid) vs wss:// (TLS, use this — analogous to http/https)

new WebSocket(url)      -> starts handshake immediately
.onopen                 -> connection ready, .send() now valid
.send(data)              -> string (text frame) or Blob/ArrayBuffer (binary frame)
.onmessage(event)        -> event.data: string, or Blob/ArrayBuffer per .binaryType
.onerror / .onclose      -> connection problems / termination (event.code, event.wasClean)
.close(code, reason)     -> deliberate clean close

Full-duplex: either side sends anytime, independently — not request/response turn-taking
One TCP connection, lightweight binary frames — no per-message handshake/header overhead
```
