iTranslated by AI
A Deep Dive into WebSocket: Mechanisms, Usage, and Protocol Analysis
Introduction
WebSocket is a bi-directional communication protocol standardized in 2011 as RFC 6455.
Traditional HTTP communication is a system based on a single request-response cycle, which prevents the server from sending events at any arbitrary timing. To detect state changes, clients had to use techniques such as polling.
By using WebSocket, servers can send events to clients at any timing, allowing the construction of highly real-time applications, such as chat applications.

Additionally, WebSocket is used in WebDriver BiDi and the Chrome DevTools Protocol (CDP), which are utilized for browser automated testing. This enables automated testing that leverages bi-directional communication, such as capturing console logs and network traffic, beyond just typical browser operations. [1]
In this article, after reviewing a simple WebSocket sample, we will examine the features defined in RFC 6455 — The WebSocket Protocol and how to use it from various programming languages.
Note that RFC 8441 for using WebSocket over HTTP/2 is outside the scope of this article.
Target Audience and Prerequisites
This article is intended for the following readers:
- People who use WebSocket "somehow" but want to properly understand the specifications based on RFC 6455.
- People who need to handle WebSocket from Node.js / Browser / Python / PowerShell, etc.
- People who have encountered WebSocket in WebDriver BiDi or Chrome DevTools Protocol (CDP) and want to understand what is happening underneath.
The following level of knowledge is assumed:
- Basic mechanism of HTTP (requests/responses, headers, etc.).
- Experience writing simple sample code in any programming language.
- Experience using Wireshark.
Quick Start
First, we will build a simple WebSocket sample using a Node.js server and a browser.
How to Build the Server
npm init -y && npm pkg set type=module
npm install --save ws
Create the following server code.
simple_server.js
simple_server.js
import { WebSocketServer } from 'ws';
const wsServer = new WebSocketServer({
port: 8080,
});
wsServer.on('connection', (ws, req) => {
console.log('connection');
const ip = req.socket.remoteAddress;
ws.on('error', console.error);
ws.on('message', function message(data, isBinary) {
if (isBinary) {
console.log(`receive binary [${ip}]:`, data);
} else {
console.log(`receive text [${ip}]:`, data.toString());
}
// Echo back the received message
ws.send(data);
});
const interval = setInterval(() => {
// Send an event with the current time every 10 seconds
ws.send(`event ${new Date()}`)
}, 10000);
ws.on('close', () => {
console.log('close');
clearInterval(interval);
});
ws.send(`connected ${ip}`);
});
This server sends messages to the client in the following cases:
- When a message is received from a client, it echoes the content back.
- Once every 10 seconds, it sends the current time as text to the client.
To start the server, execute the following command:
node simple_server.js
Explanation of Server-Side Code
In Node.js, this is achieved using the ws library.
-
WebSocketServer Class:
- Creates a new server instance.
-
connection Event of WebSocketServer:
- An event triggered when there is a connection request from a client and the handshake is completed.
- The connection event holds the WebSocket object for communicating with the client.
- Message sending to the client is performed through the send method of the WebSocket object.
-
WebSocket message Event:
- Triggered when a message is received from the client.
- Data can be either text or binary.
- The data received here is content that has had masking, message compression/decompression, and fragmentation resolved as necessary.
How to Build the Client Side
Create the following HTML and open it in Chrome.
simple_server.html
simple_server.html
<html>
<head>
</head>
<body>
<pre id="receive"></pre>
<input type="text" id="message">
<input type="button" id="btnSendText" value="送信(text)">
<input type="button" id="btnSendBin" value="送信(バイナリ)">
<input type="button" id="btnClose" value="CLOSE">
<script>
const receive = document.getElementById("receive");
const message = document.getElementById("message");
const btnSendText = document.getElementById("btnSendText");
const btnSendBin = document.getElementById("btnSendBin");
const btnClose = document.getElementById("btnClose");
const sock = new WebSocket("ws://127.0.0.1:8080/");
sock.addEventListener("open", (e) => {
console.log("Connected.", e);
});
sock.addEventListener("close", (e) => {
console.log("Closed.", e);
});
sock.addEventListener("error", (e) => {
console.log("Error.", e);
});
sock.addEventListener("message", (e) => {
receive.innerText += e.data + "\n";
});
btnSendText.addEventListener("click", (e) => {
sock.send(message.value);
message.value = "";
});
btnSendBin.addEventListener("click", (e) => {
const encoder = new TextEncoder();
const bytes = encoder.encode(message.value);
sock.send(bytes);
message.value = "";
});
btnClose.addEventListener("click", (e) => {
sock.close();
btnSendText.disabled = true;
btnSendBin.disabled = true;
btnClose.disabled = true;
});
</script>
</body>
</html>
Execution Check

The "Send (text)" button sends the text content as text data to the server and retrieves the loopback.
The "Send (binary)" button sends the text content as binary data to the server and retrieves the loopback.
The current time is sent from the server as text once every 10 seconds.
The "CLOSE" button terminates the connection.
In this demo, you can confirm that both client-initiated and server-initiated communications can be performed after connecting via WebSocket.
Checking Send/Receive on the Browser
You can inspect WebSocket communication by checking the Network tab of the developer tools.
A row is created for each WebSocket connection.

Clicking a row allows you to check the details of each WebSocket.
The Headers tab displays the information used for the handshake (described later).

The Messages tab allows you to verify the messages sent and received over the WebSocket.

WebSocket Specification
The behavior of WebSocket is defined in RFC 6455 — The WebSocket Protocol.
WebSocket communication follows these steps:
- The client performs a handshake with the server.
- After connecting, either the client or the server sends messages.
Handshake Procedure
The client performs a handshake with the server using HTTP.
The client sends the following HTTP request:
GET / HTTP/1.1
Host: example.com:8080
Origin: http://example.com:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
The following items are included in the headers of this request:
| Name | Description |
|---|---|
| Host | Required. Includes the host name and optionally the port. |
| Upgrade | Required. The value must include websocket. |
| Connection | Required. The value must include Upgrade. |
| Sec-WebSocket-Key | Required. A base64-encoded 16-byte value, randomly chosen and used only once. |
| Origin | Required for requests from a browser. Informs the server from which origin's JavaScript this WebSocket connection is being initiated. |
| Sec-WebSocket-Version | Required. The protocol version of the connection. Its value is 13. |
| Sec-WebSocket-Protocol | Optional. Specifies the sub-protocols the client wishes to use. Multiple selections are possible. |
| Sec-WebSocket-Extensions | Optional. Specifies any Extensions the client desires. |
If the server accepts the handshake, it returns a response like the following:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
The following items are included in the response headers:
| Name | Description |
|---|---|
| 101 Switching Protocols | HTTP status code. Returned when the handshake is successful and the protocol is switched to WebSocket. |
| Upgrade | Required. The value must include websocket. |
| Connection | Required. The value must include Upgrade. |
| Sec-WebSocket-Accept | Required. A value calculated from the Sec-WebSocket-Key in the request. |
| Sec-WebSocket-Extensions | Optional. Returns information about the Extensions the server can use. |
| Sec-WebSocket-Protocol | Optional. Information about the sub-protocol available to the server. One is selected from the sub-protocols presented by the client. |
Sec-WebSocket-Key and Sec-WebSocket-Accept
The Sec-WebSocket-Key and Sec-WebSocket-Accept are used for the server to confirm that a valid WebSocket opening handshake has been received.
The client sets a base64-encoded 16-byte random value, used only once, as the Sec-WebSocket-Key.
The server concatenates the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" to the Sec-WebSocket-Key from the request, calculates the SHA-1 hash for the result, and sets the base64-encoded 20-byte value to Sec-WebSocket-Accept.
In JavaScript, this can be created with the following code:
Example
// Calculate the WebSocket Accept key
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const acceptKey = crypto
.createHash('sha1')
.update(key + GUID, 'binary')
.digest('base64');
The client verifies whether the Sec-WebSocket-Accept value matches the value calculated from its own Sec-WebSocket-Key; if they do not match, the WebSocket connection fails.
Message Transmission
After the handshake, data is sent in units called frames.
The basic frame format is as follows:

FIN: 1 bit
A single message may be fragmented (broken up) into multiple frames.
A frame with the FIN bit set to 0 is the first or middle frame of the message, and the message continues until a frame with FIN=1 arrives.
RSV1, RSV2, RSV3: 1 bit each
Reserved bits. Usually 0.
May be used when using Extensions.
Opcode: 4 bits
The operation code for interpreting the "Payload data."
- 0x0 represents a continuation frame
- 0x1 represents a text frame
- 0x2 represents a binary frame
- 0x3–7 are reserved for further non-control frames
- 0x8 represents a connection close
- 0x9 represents a ping
- 0xA represents a pong
- 0xB–F are reserved for further control frames
Mask: 1 bit
Defines whether the "Payload data" is masked.
If set to 1, a Masking-key is present. For all frames sent from the client to the server, this bit is set to 1.
Payload length and Extended payload length: 7 bits, 7+16 bits, or 7+64 bits
The length of the "Payload data" (in bytes).
If it is 0–125, that value is the byte length of the Payload data.
If it is 126, the following 2 bytes, interpreted as a 16-bit unsigned integer, indicate the byte length of the Payload data.
If it is 127, the following 8 bytes, interpreted as a 64-bit unsigned integer, indicate the byte length of the Payload data.
Multi-byte length values are expressed in network byte order.
Example of network byte order
If the bytes are [0x12, 0x34], it is interpreted as 0x1234.
Masking-key: 0 or 4 bytes
Only present if the Mask bit is 1.
The Payload Data of all frames sent from the client to the server is masked by the Masking-key. Refer to the section below for the specific method.
Fragmented Transmission
It is possible to construct a text or binary data message using multiple frames using the FIN bit and OpCode.
The following examples show a case of sending in a single frame and a case of sending in multiple frames, where the content of the message itself is the same.
Single Frame (All at once)
- FIN:1 opcode=1 (text data) Payload Data "HelloWorld"
Split (Fragmented)
- FIN:0 opcode=1 (text data) Payload Data: "Hello"
- FIN:1 opcode=0 (continuation frame) Payload Data: "World"
Details of Masking Process
When sending a message from a client to a server, masking must be performed on the Payload Data of the frame.
The client sets a 4-byte random Masking-key and performs an XOR operation on the Payload Data before sending.
The server retrieves the original Payload Data from the Masking-key and the masked Payload Data.
In code, it can be expressed as follows:
// The client generates a random key
const maskingKey = [0x12, 0x54, 0xA1, 0x52];
const payloadData = Buffer.from("test", "utf8");
console.log("original payload:", payloadData);
function mask(payload, maskingKey) {
const payloadLen = payload.length;
const data = Buffer.alloc(payloadLen);
for (let i = 0; i < payloadLen; i++) {
data[i] = payload[i] ^ maskingKey[i % 4];
}
return data;
}
// Masking process for payload data (Client-side processing)
const maskedPayloadData = mask(payloadData, maskingKey);
console.log('masking data:', maskedPayloadData);
// Restoration of payload data (Server-side processing)
console.log('original data:', mask(maskedPayloadData, maskingKey));
Close
If the frame's OpCode is 0x08, it is a Close frame.
It is possible to include the reason for closing in the Payload Data. However, there is no guarantee that this is human-readable data.
No further frames must be sent after sending a Close frame.
When a Close frame is received, a Close frame must be sent in response. After both endpoints have sent and received Close messages, the endpoints consider the WebSocket connection closed and must close the underlying TCP connection.
Ping and Pong
If the frame's OpCode is 0x09, it is a Ping frame.
When a Ping frame is received, a Pong frame must be sent in response unless a Close frame has already been received.
Payload Data may be included.
If the frame's OpCode is 0x0A, it is a Pong frame.
If the Ping frame included Payload Data, the same Payload Data must be included in the Pong frame.
If a Ping frame is received and a Pong frame has not yet been sent for a previous Ping frame, you may choose to send a Pong frame only for the most recently received Ping frame.
A Pong frame may be sent even if a Ping frame has not been received. No response is expected for this.
Extension
WebSocket has Extensions. A prominent example is RFC 7692 Compression Extensions for WebSocket.
When "permessage-deflate" is present in the Sec-WebSocket-Extensions request header during the handshake and is also present in the response header, data compression becomes possible for that WebSocket communication.
This extension allows messages to be sent with compressed data in the Payload Data. Note that the sender has the freedom to choose whether or not to compress.
Whether a frame has been processed by permessage-deflate can be determined by the RSV1 bit being set to 1.
Compression Algorithm
It uses DEFLATE as defined in RFC 1951.
It is a combination of the LZ77 algorithm and Huffman coding.
In the Sec-WebSocket-Extensions header of the handshake, the following items can be configured along with "permessage-deflate":
- client_no_context_takeover
- The client does not use previous messages during compression.
- server_no_context_takeover
- The server does not use previous messages during compression.
- client_max_window_bits
- An option to determine the maximum size of the LZ77 sliding window used in client-side compression.
- server_max_window_bits
- An option to determine the maximum size of the LZ77 sliding window used in server-side compression.
The permessage-deflate.js in the ws library serves as a good reference for the actual implementation.
It is implemented using Node.js's zlib createInflateRaw and createDeflateRaw.
Security Considerations
When permessage-deflate is combined with secure transports such as TLS, the risk of compression side-channel attacks, exemplified by CRIME, has been pointed out.
Briefly explained, in a situation where an attacker can send messages many times while slightly changing the input, if a message containing secret information is compressed, the secret information may be inferred by observing changes in the size after compression.
Experimentation with Various Programming Languages
Here, we will confirm whether the following operations can be performed in each programming language:
- Sending and receiving text
- Sending and receiving binary data
- Fragmented transmission
- Ping and Pong
- Compression using permessage-deflate
Capturing Messages
You will likely want to check the actual telegram content during experiments.
In this case, you can capture WebSocket messages using the following means:
Network Tab of Developer Tools
You can check WebSocket messages using developer tools in browsers like Chrome.
However, low-level telegrams cannot be inspected.
What is possible:
- Verifying telegrams during the handshake.
- Capturing binary or text messages.
What is impossible:
Only message-level capture is possible, so low-level information cannot be confirmed.
- Ping/pong are not captured.[2]
- Fragmented data is captured as a single message.
- Compressed data is also captured as a restored (decompressed) message.
Wireshark
By using Wireshark, you can also check low-level messages.
However, with default settings, unnecessary telegrams are also captured, so appropriate filtering is required.
In this experiment, the following filtering condition is used:
websocket || http
Handshake request example:

Handshake response example:

Ping example:

Pong example:

Example of fragmented transmission:




Example of masked Payload Data:

Payload Data after unmasking can also be verified.

Example of compressed Payload Data:

RSV1 is set to 1, and the compressed content is stored in the Payload Data.
Data before compression can also be verified.

Node.js
Library used
ws: a Node.js WebSocket library
The following sample code shows implementation examples of a Node.js WebSocket server and client.
The server side is designed to send back messages such as ping, pong, fragments, and compressed data depending on the message received.
The client side allows you to input messages to be sent to the server from standard input.
Node.js sample code
Server-side sample
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({
port: 8080,
perMessageDeflate: {
// Compression options as needed. At minimum, true/false is fine.
clientNoContextTakeover: true,
serverNoContextTakeover: true,
},
}, () => {
console.log('created....');
});
wss.on('connection', function connection(ws) {
console.log('connection')
ws.on('error', console.error);
ws.on('message', function message(data, isBinary) {
console.log('received: %s', isBinary, data);
if (data.toString() == 'call_ping') {
console.log('call ping.')
ws.ping();
} else if (data.toString() == 'call_close') {
console.log('call close.')
ws.close();
} else if (data.toString() == 'call_bin') {
console.log('call binary.')
const buf = Buffer.from("abcdefg_bin", "utf8");
ws.send(buf);
} else if (data.toString() == 'call_large') {
console.log('call_large')
ws.send("ABRACADABRA ".repeat(100));
} else if (data.toString() == 'call_fragmentation') {
console.log('call_fragmentation.');
ws.send("........1", { fin: false });
ws.send("........2", { fin: false });
ws.send("........3", { fin: false });
ws.send("........4end", { fin: true });
} else {
ws.send(`${data}`)
}
});
ws.on('pong', function message(data) {
console.log('received pong')
});
ws.on('ping', function message(data) {
console.log('received ping')
});
ws.on('close', function message(data) {
console.log('received close')
});
});
Client-side sample
// client.js
import WebSocket from 'ws'; // For CommonJS: const WebSocket = require('ws');
const url = 'ws://localhost:8080';
const ws = new WebSocket(url);
// When connection opens
ws.on('open', () => {
console.log('connected');
});
// When receiving a message
ws.on('message', (data) => {
// data is a Buffer or a string
console.log('received:', data, data.toString());
});
// On error
ws.on('error', (err) => {
console.error('error:', err);
});
// When connection closes
ws.on('close', (code, reason) => {
console.log('closed:', code, reason.toString());
});
ws.on('pong', function message(data) {
console.log('receive pong')
});
ws.on('ping', function message(data) {
console.log('receive ping')
});
// Read from standard input
process.stdin.setEncoding("utf8");
setTimeout(() => {
console.log("Please input something. (exit to quit)");
}, 100);
process.stdin.on("data", (input) => {
const text = input.trim();
if (text === "exit") {
console.log("Exiting.");
ws.close();
process.exit(0);
}
if (text === "ping") {
ws.ping();
return;
}
ws.send(text);
console.log("Input:", text);
});
Configuring permessage-deflate
The client side performs a handshake with the server using the following implementation.
import WebSocket from 'ws';
const url = 'ws://localhost:8080';
const ws = new WebSocket(url);
The telegram for the request at this time is as follows.

It requests a handshake by including client_max_window_bits in permessage-deflate.
The server side can choose whether to enable or disable compression.
Example where compression is not enabled
const wss = new WebSocketServer({
port: 8080
}, () => {
console.log('created....');
});
You can confirm that Sec-WebSocket-Extensions is not included in the response data.

In this state, messages will not be compressed during the connection.
Example where compression is enabled
const wss = new WebSocketServer({
port: 8080,
perMessageDeflate: {
// Compression options as needed. At minimum, true/false is fine.
clientNoContextTakeover: true,
serverNoContextTakeover: true,
},
}, () => {
console.log('created....');
});
You can confirm that Sec-WebSocket-Extensions is included in the response data.

In this case, data compression may occur.
Since very short messages are typically not compressed, you should verify this with reasonably long messages.
It is also possible to set serverMaxWindowBits and clientMaxWindowBits when enabling compression.
const wss = new WebSocketServer({
port: 8080,
perMessageDeflate: {
// Compression options as needed. At minimum, true/false is fine.
clientNoContextTakeover: true,
serverNoContextTakeover: true,
serverMaxWindowBits: 8,
clientMaxWindowBits: 8
},
}, () => {
console.log('created....');
});

Specifying Subprotocols
To set subprotocols, you need to modify how the WebSocketServer and WebSocket are created.
On the server side, you can describe the subprotocol selection logic in handleProtocols. The candidates for the subprotocols passed by the client are stored in the protocols argument of handleProtocols (e.g., as a Set containing 'chat.v1', 'json.v1'). You then select exactly one of them or return a response that does not use a subprotocol.
const wss = new WebSocketServer({
port: 8080,
perMessageDeflate: {
clientNoContextTakeover: true,
serverNoContextTakeover: true,
serverMaxWindowBits: 8,
clientMaxWindowBits: 8,
},
// ★ Subprotocol selection logic
handleProtocols: (protocols, request) => {
console.log('Client offered protocols:', protocols);
// Example: Adopt "chat.v1" if it exists, otherwise adopt "json.v1"
if (protocols.has('chat.v1')) {
return 'chat.v1';
}
if (protocols.has('json.v1')) {
return 'json.v1';
}
// If none are supported, returning false continues the connection without a subprotocol
// (If subprotocols are mandatory, design it to reject the connection here)
return false;
},
}, () => {
console.log('created....');
});
On the client side, you connect by specifying candidates for the subprotocols.
const ws = new WebSocket(url, ['chat.v1', 'json.v1']);
In the example above, the client specifies two subprotocol candidates, 'chat.v1' and 'json.v1', and the server-side logic will select chat.v1. You can confirm that Sec-WebSocket-Protocol is added to the handshake when this implementation is used.
Request

Response

Sending and Receiving Text and Binary Data
Text or binary messages are received in the message event of the WebSocket class.
ws.on('message', function message(data, isBinary) {
console.log('received: %s', isBinary, data);
}
isBinary determines whether the received data is binary or text. The data received here is content that has had fragments recombined, masking removed, and compression reverted. To send text and binary data, use the send method. It automatically detects the type of data to send it as either text or binary. The following options are available. When sending in fragments, set fin to false for the intermediate data as shown below.
ws.send("........1", { fin: false });
ws.send("........2", { fin: false });
ws.send("........3", { fin: false });
ws.send("........4end", { fin: true });
ping and pong
Detect the occurrence of ping and pong with the ping and pong events of the WebSocket class.
ws.on('pong', function message(data) {
console.log('received pong')
});
ws.on('ping', function message(data) {
console.log('received ping')
});
You can send a ping to the connected peer using the ping method.
You can send a pong to the connected peer using the pong method. A pong can be sent even if a ping has not been received.
Integration with express
If you want to integrate with express, you can achieve this by passing the server object created with http.createServer(app) to the WebSocketServer constructor.
import express from 'express';
import http from 'http';
import { WebSocketServer } from 'ws';
const app = express();
const server = http.createServer(app);
// Express routing etc. here
app.get('/', (req, res) => {
res.send('hello');
});
// Attach the WebSocket server to the HTTP server
const wss = new WebSocketServer({ server, path: '/ws' });
wss.on('connection', (ws, req) => {
console.log('ws connected', req.url);
ws.on('message', (data) => {
console.log('msg', data.toString());
ws.send(`echo: ${data}`);
});
});
server.listen(8080, () => {
console.log('HTTP+WS server listening on :8080');
});
In this case, a client can connect via ws://localhost:8080/ws.
Browser
Most modern browsers support the WebSocket API (WebSockets).
This allows for easy implementation of WebSocket client-side logic.
A sample of this was provided in the "Quick Start" section earlier.
Handshake
You can specify multiple subprotocol candidates in the WebSocket constructor.
const sock = new WebSocket("ws://127.0.0.1:8080/", ["json.v1", "json.v2"]);
Regarding Sec-WebSocket-Extensions in the browser's handshake request, for example in Chrome 142, the following is set:
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n
You cannot control the contents of this request.
After the open event occurs, you can verify how the server accepted the request using the following implementation:
sock.addEventListener("open", (e) => {
console.log("Connected.", e);
// Example: sock.extensions permessage-deflate; client_max_window_bits=8; server_no_context_takeover; client_no_context_takeover; server_max_window_bits=8
console.log('sock.extensions', sock.extensions);
});
Sending and Receiving Text and Binary Data
Messages can be sent using the send method. It sends either text or binary data depending on the type of data.
Unlike in Node.js, no options can be specified, so fragmented transmission is not supported.
Messages are received in the message event. The e.data will contain either text or binary data.
sock.addEventListener("message", async (e) => {
if (typeof e.data === "string") {
// Text message
receive.innerText += "[text] " + e.data + "\n";
} else if (e.data instanceof Blob) {
// Blob → ArrayBuffer
const buf = await e.data.arrayBuffer();
const bytes = new Uint8Array(buf);
const hex = Array.from(bytes)
.map(b => b.toString(16).padStart(2, "0"))
.join(" ");
receive.innerText += "[binary Blob] " + hex + "\n";
} else {
// This is executed when binaryType = 'blob'
receive.innerText += "[unknown]" + e.data + "\n";
}
});
The type of binary data received is controlled by WebSocket.binaryType. If binaryType is "arraybuffer", it returns the ArrayBuffer type; if it is "blob", it returns the Blob type.
ping and pong
The ping/pong event handlers and the ping method are not supported.
However, the browser will automatically respond with a pong to ping commands from the server.
Python
In Python, several libraries exist for implementing WebSocket.
-
websockets
- An asyncio-based library dedicated to WebSocket (both server and client).
- Synchronous APIs are also provided as separate modules (e.g.,
websockets.sync.client). - GitHub Stars: 5.6K, Version: 15.0.1 (2025/03/06).
-
aiohttp
- Asyncio-compatible HTTP client/server + WebSocket support.
- GitHub Stars: 16.1K, Version: 3.13.2 (2025/10/29).
-
websocket-client
- A traditional synchronous WebSocket-only client library.
- GitHub Stars: 3.7K, Version: 1.9.0 (2025/10/08).
- Currently does not support the
permessage-deflateextension.
In this section, we will experiment with websockets, which allows for both asynchronous and synchronous implementations of servers and clients.
WebSocket sample in Python
Prerequisites
Install websockets first.
pip install websockets
Server Example
import asyncio
import websockets
SUPPORTED_SUBPROTOCOLS = ["chat.v1", "json.v1"]
async def handler(websocket): # ★ Does not take path
print("connection")
print("negotiated subprotocol =", websocket.subprotocol)
try:
async for message in websocket:
is_binary = isinstance(message, (bytes, bytearray))
print("received:", is_binary, message)
if not is_binary:
if message == "call_ping":
print("call ping.")
await websocket.ping()
elif message == "call_close":
print("call close.")
await websocket.close()
elif message == "call_bin":
print("call binary.")
buf = "abcdefg_bin".encode("utf-8")
await websocket.send(buf)
elif message == "call_large":
print("call_large")
await websocket.send("ABRACADABRA " * 100)
elif message == "call_fragmentation":
print("call_fragmentation.")
await websocket.send([
"........1",
"........2",
"........3",
"........4end"
])
else:
await websocket.send(message)
else:
await websocket.send(message)
except websockets.exceptions.ConnectionClosedOK:
print("connection closed (OK)")
except websockets.exceptions.ConnectionClosedError as e:
print("connection closed with error:", e)
async def main():
async with websockets.serve(
handler,
host="0.0.0.0",
port=8080,
subprotocols=SUPPORTED_SUBPROTOCOLS,
compression="deflate",
):
print("WebSocket server started on port 8080")
await asyncio.Future()
if __name__ == "__main__":
asyncio.run(main())
Synchronous Client Example
# sync_client.py
import sys
import threading
from websockets.sync.client import connect
from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError
URL = "ws://localhost:8080"
SUBPROTOCOLS = ["chat.v1", "json.v1"]
def recv_loop(ws):
"""Message reception loop from server (separate thread)"""
try:
for message in ws:
if isinstance(message, (bytes, bytearray)):
print("received (binary):", message, message.decode("utf-8", errors="replace"))
else:
print("received:", message)
except ConnectionClosedOK as e:
print("closed:", e.code, e.reason)
except ConnectionClosedError as e:
print("closed with error:", e.code, e.reason)
except Exception as e:
print("recv error:", e)
def main():
# Enable permessage-deflate with compression="deflate"
with connect(
URL,
subprotocols=SUBPROTOCOLS,
compression="deflate",
) as ws:
print("connected, negotiated subprotocol =", ws.subprotocol)
t = threading.Thread(target=recv_loop, args=(ws,), daemon=True)
t.start()
# Reading from standard input
print("Please input something. (type 'exit' to quit)")
for line in sys.stdin:
text = line.strip()
if text == "exit":
print("Exiting.")
ws.close()
break
if text == "ping":
try:
pong_event = ws.ping()
# Wait for pong (wait is optional)
pong_event.wait(timeout=10.0)
print("receive pong")
except TimeoutError:
print("ping timeout")
except ConnectionClosedError:
print("connection already closed")
continue
ws.send(text)
print("Input:", text)
# Exiting the 'with' block automatically completes the close process
t.join(timeout=1.0)
if __name__ == "__main__":
main()
Configuring permessage-deflate
You can enable compression by setting compression="deflate" (or omitting it as it may be the default depending on version), or disable it with compression=None.
Here is an example of the setting on the client side:
from websockets.sync.client import connect
# ...omitted...
with connect(
URL,
subprotocols=SUBPROTOCOLS,
compression="deflate",
) as ws:
print("connected, negotiated subprotocol =", ws.subprotocol)
Example of server settings:
import websockets
# ...omitted...
async with websockets.serve(
handler,
host="0.0.0.0",
port=8080,
subprotocols=SUPPORTED_SUBPROTOCOLS,
compression="deflate",
):
print("WebSocket server started on port 8080")
If you want to perform detailed compression settings, use the extensions property when creating the server.
import websockets
from websockets.extensions import permessage_deflate
# ...omitted...
async with websockets.serve(
handler,
host="0.0.0.0",
port=8080,
subprotocols=SUPPORTED_SUBPROTOCOLS,
compression="deflate",
extensions=[
permessage_deflate.ServerPerMessageDeflateFactory(
server_max_window_bits=11,
client_max_window_bits=11,
compress_settings={"memLevel": 4},
),
],
):
print("WebSocket server started on port 8080")
The handshake in this case looks like this:

For more details, refer to Compression.
Specifying Subprotocols
Subprotocol candidates are set using the subprotocols property.
Server Settings
# ...omitted...
SUPPORTED_SUBPROTOCOLS = ["chat.v1", "json.v1"]
# ...omitted...
async def main():
async with websockets.serve(
handler,
host="0.0.0.0",
port=8080,
subprotocols=SUPPORTED_SUBPROTOCOLS,
compression="deflate",
):
print("WebSocket server started on port 8080")
Client Settings
# ...omitted...
SUBPROTOCOLS = ["chat.v1", "json.v1"]
# ...omitted...
with connect(
URL,
subprotocols=SUBPROTOCOLS,
compression="deflate",
) as ws:
print("connected, negotiated subprotocol =", ws.subprotocol)
By default, the first subprotocol that matches between the client and server is selected. In this example, it would be chat.v1.
To change the subprotocol selection logic on the server side, specify a callback function to select_subprotocol when creating the server.
def select_subprotocol_handler(cnn, subprotocols):
print("select_subprotocol_handler", cnn, subprotocols)
return "json.v1"
async def main():
async with websockets.serve(
handler,
host="0.0.0.0",
port=8080,
subprotocols=SUPPORTED_SUBPROTOCOLS,
select_subprotocol=select_subprotocol_handler,
compression="deflate",
):
print("WebSocket server started on port 8080")
cnn receives a websockets.asyncio.server.ServerConnection object, and subprotocols receives a list of subprotocols provided by the client.
Sending and Receiving Text and Binary Data
Text, binary, and fragmented messages are sent using the send method on the server side and the send method on the client side.
Example of sending text
await websocket.send("test")
Example of sending binary
buf = "abcdefg_bin".encode("utf-8")
await websocket.send(buf)
Example of fragmented transmission
await websocket.send([
"........1",
"........2",
"........3",
"........4end"
])
To receive messages, iterate over the ServerConnection or ClientConnection.
For the server, you can obtain messages by looping with async for message in websocket using websockets.asyncio.server.ServerConnection.
async with websockets.serve(
handler,
host="0.0.0.0",
port=8080,
subprotocols=SUPPORTED_SUBPROTOCOLS,
select_subprotocol=select_subprotocol_handler,
compression="deflate",
):
print("WebSocket server started on port 8080")
await asyncio.Future()
async def handler(websocket): # ★ Does not take path
print("connection")
print("negotiated subprotocol =", websocket.subprotocol)
try:
async for message in websocket:
is_binary = isinstance(message, (bytes, bytearray))
print("received:", is_binary, message)
For synchronous clients, create a separate thread and iterate over websockets.sync.client.ClientConnection in a loop on that thread to receive messages.
# ...omitted...
# Create ClientConnection and loop in a separate thread
# Enable permessage-deflate with compression="deflate"
with connect(
URL,
subprotocols=SUBPROTOCOLS,
compression="deflate",
) as ws:
print("connected, negotiated subprotocol =", ws.subprotocol)
t = threading.Thread(target=recv_loop, args=(ws,), daemon=True)
# ...omitted...
def recv_loop(ws):
"""Message reception loop from server (separate thread)"""
try:
for message in ws:
if isinstance(message, (bytes, bytearray)):
print("received (binary):", message, message.decode("utf-8", errors="replace"))
else:
print("received:", message)
If you want to obtain data in fragments instead of messages, use recv_streaming.
try:
while True:
# Obtain fragments for one message, frame by frame
for fragment in ws.recv_streaming():
if isinstance(fragment, (bytes, bytearray)):
print("received (binary):", fragment, fragment.decode("utf-8", errors="replace"))
else:
print("received:", fragment)
print('....end fragments')
ping and pong
Both the server and client automatically return a pong when a ping is received. However, unlike Node.js's ws, it seems handling these events is not supported directly.
Ping transmission is performed using the ping method.
pong_event = ws.ping()
# Wait for pong (wait is optional)
pong_event.wait(timeout=10.0)
This method also allows you to wait until a pong is returned.
A pong can be sent using the pong method. A pong can be sent even if a ping has not been received.
Client Example in PowerShell
The following shows an implementation example of a WebSocket client on PowerShell 7.5.4 + macOS. When implementing a WebSocket client in .NET environments such as PowerShell, the ClientWebSocket class can be used.
PowerShell sample code
Client-side sample
# PowerShell 7.5.4 + macOS
Add-Type -AssemblyName System.Net.WebSockets
$uri = [Uri]"ws://localhost:8080"
$ws = [System.Net.WebSockets.ClientWebSocket]::new()
# Keep the subprotocol if needed. If the server doesn't specify any, you can comment this out for now.
$ws.Options.AddSubProtocol("chat.v1")
$ws.Options.AddSubProtocol("json.v1")
# Compression options
$deflate = [System.Net.WebSockets.WebSocketDeflateOptions]::new()
$deflate.ClientContextTakeover = $false
$ws.Options.DangerousDeflateOptions = $deflate
$cts = [System.Threading.CancellationTokenSource]::new()
# Connect
$ws.ConnectAsync($uri, $cts.Token).GetAwaiter().GetResult()
Write-Host "connected. SubProtocol =" $ws.SubProtocol
# Reception task (capturing the outer $ws / $cts as is)
$recv_job = {
param($ws)
# Alternative because Write-Host cannot be used in this context
[Console]::WriteLine("start recv_job....")
$buffer = [Net.WebSockets.WebSocket]::CreateClientBuffer(1024,1024)
$ct = [Threading.CancellationToken]::new($false)
$taskResult = ""
while ($ws.State -eq [Net.WebSockets.WebSocketState]::Open) {
$messageResult = ""
do {
$frameResult = $ws.ReceiveAsync($buffer, $ct)
$frame = [Text.Encoding]::UTF8.GetString($buffer, 0, $frameResult.Result.Count)
[Console]::WriteLine("....frame: {0}" -f $frame)
[Console]::WriteLine("......FIN: {0}" -f $frameResult.Result.EndOfMessage)
$messageResult += $frame
} until (
$ws.State -ne [Net.WebSockets.WebSocketState]::Open -or $frameResult.Result.EndOfMessage
)
if (-not [string]::IsNullOrEmpty($messageResult)) {
[Console]::WriteLine("received message: {0}" -f $messageResult)
}
}
}
Write-Output "Starting recv runspace"
$recv_runspace = [PowerShell]::Create()
$recv_runspace.AddScript($recv_job).
AddParameter("ws", $ws).BeginInvoke() | Out-Null
# Send loop
while ($ws.State -eq [System.Net.WebSockets.WebSocketState]::Open) {
Write-Host "Please input something. (type 'exit' to quit)"
$line = Read-Host
if ($null -eq $line) { break }
$text = $line.Trim()
if ($text -eq "exit") {
Write-Host "Exiting."
$ws.CloseAsync(
[System.Net.WebSockets.WebSocketCloseStatus]::NormalClosure,
"bye",
$cts.Token
).GetAwaiter().GetResult()
break
}
if ($text.Length -eq 0) {
continue
}
$bytes = [System.Text.Encoding]::UTF8.GetBytes($text)
$segment = [System.ArraySegment[byte]]::new($bytes, 0, $bytes.Length)
$ws.SendAsync(
$segment,
[System.Net.WebSockets.WebSocketMessageType]::Text,
$true, # EndOfMessage
$cts.Token
).GetAwaiter().GetResult()
}
# Cleanup process
$cts.Cancel()
Configuring permessage-deflate
permessage-deflate can be enabled by setting ClientWebSocketOptions.DangerousDeflateOptions. Refer to the WebSocketDeflateOptions class for the properties that can be specified here.
$ws = [System.Net.WebSockets.ClientWebSocket]::new()
# ...omitted...
$deflate = [System.Net.WebSockets.WebSocketDeflateOptions]::new()
$deflate.ClientContextTakeover = $false
$ws.Options.DangerousDeflateOptions = $deflate
Specifying Subprotocols
Subprotocol candidates are configured using the ClientWebSocketOptions.AddSubProtocol method.
$ws = [System.Net.WebSockets.ClientWebSocket]::new()
# Keep the subprotocol if needed. If the server doesn't specify any, you can comment this out for now.
$ws.Options.AddSubProtocol("chat.v1")
$ws.Options.AddSubProtocol("json.v1")
Sending and Receiving Text and Binary Data
Text or binary data can be sent using the ClientWebSocket.SendAsync method. Switching between text and binary is done via WebSocketMessageType.
Example of sending text
$bytes = [System.Text.Encoding]::UTF8.GetBytes("Hello")
$segment = [System.ArraySegment[byte]]::new($bytes, 0, $bytes.Length)
$ws.SendAsync(
$segment,
[System.Net.WebSockets.WebSocketMessageType]::Text,
$true, # EndOfMessage
$cts.Token
).GetAwaiter().GetResult()
Example of sending binary
$bytes = [System.Text.Encoding]::UTF8.GetBytes("Hello")
$segment = [System.ArraySegment[byte]]::new($bytes, 0, $bytes.Length)
$ws.SendAsync(
$segment,
[System.Net.WebSockets.WebSocketMessageType]::Binary,
$true, # EndOfMessage
$cts.Token
).GetAwaiter().GetResult()
Example of fragmented transmission
By setting the endOfMessage argument, you can control the FIN bit, allowing for fragmented transmission.
$bytes = [System.Text.Encoding]::UTF8.GetBytes("Hello")
$segment = [System.ArraySegment[byte]]::new($bytes, 0, $bytes.Length)
$ws.SendAsync(
$segment,
[System.Net.WebSockets.WebSocketMessageType]::Text,
$false, # Segmented
$cts.Token
).GetAwaiter().GetResult()
$ws.SendAsync(
$segment,
[System.Net.WebSockets.WebSocketMessageType]::Text,
$true, # EndOfMessage
$cts.Token
).GetAwaiter().GetResult()


Message Reception
Messages are received in units of frames, typically in a separate thread. Use ClientWebSocket.ReceiveAsync to receive a ValueWebSocketReceiveResult.
$recv_job = {
param($ws)
# Alternative because Write-Host cannot be used in this context
[Console]::WriteLine("start recv_job....")
$buffer = [Net.WebSockets.WebSocket]::CreateClientBuffer(1024,1024)
$ct = [Threading.CancellationToken]::new($false)
$taskResult = ""
while ($ws.State -eq [Net.WebSockets.WebSocketState]::Open) {
$messageResult = ""
do {
$frameResult = $ws.ReceiveAsync($buffer, $ct)
$frame = [Text.Encoding]::UTF8.GetString($buffer, 0, $frameResult.Result.Count)
[Console]::WriteLine("....frame: {0}" -f $frame)
[Console]::WriteLine("......FIN: {0}" -f $frameResult.Result.EndOfMessage)
$messageResult += $frame
} until (
$ws.State -ne [Net.WebSockets.WebSocketState]::Open -or $frameResult.Result.EndOfMessage
)
if (-not [string]::IsNullOrEmpty($messageResult)) {
[Console]::WriteLine("received message: {0}" -f $messageResult)
}
}
}
Use ValueWebSocketReceiveResult.EndOfMessage to determine if the message has been completely received.
ping and pong
When a ping is received from the server, it automatically responds with a pong. However, there is no method to receive ping or pong events, and neither a ping nor a pong method is provided in the class.
Summary
In this article, we summarized WebSocket. We reviewed the overview of RFC 6455 — The WebSocket Protocol and confirmed how it is implemented in several programming languages.
I hope you have gone from "somehow using" WebSocket to gaining a clearer understanding of what it is doing based on which specifications.
-
Re-introducing WebDriver and Selenium: Understanding History and Internal Operations (CDP/BiDi) ↩︎
-
According to Chrome's "Inspect WebSocket messages," it is written as if ping and pong frames can be read, but in reality, they cannot be verified. This issue has also been observed by others. ↩︎
Discussion