Websockets, I’m sure you have heard of them before.
Websockets are very interesting, but most of the time, they are covered under abstractions provided by libraries, and ’cause of that, we can’t really appreciate their elegance. So, in this blog, we are gonna remove every abstraction, and understand websockets in pure form — what they are, why we need them, and lastly, how they work.
Before diving into what websockets are, let’s establish why there is even a need for them. If you already know the crux, you can skip this section, but if not, stick around.
So, in ancient times, there was HTTP 1.0 protocol. (Sorry for the dramatic buildup), so, the idea of HTTP 1.0 protocol is simple, for each request, we create a new TCP Connection, pretty simple right ? .
That’s called ‘connectionmaxxing’. Yes, i made it up.
This is super inefficient and eats up the resources. Imagine you’re calling a friend just to say one sentence, then hanging up, and doing that for every sentence.

The Fix — persistence
HTTP 1.1 introduced a new HTTP header, Keep-Alive. Now, when you connect to a server, a TCP connection is opened, and this gets reused for any further requests made.
Also, HTTP 1.1 lays the foundation for WebSocket Protocol.

Websockets
The core philosophy of WebSockets is bi-directional communication. Unlike traditional HTTP request/response cycles, where the client requests, server responds (boooring), websockets allow the client and server to talk freely. Once WebSocket connection is established, both parties can send data independently.
The Connection flow
Like everything, websocket connections starts out as a plain old HTTP request.
But there’s a twist, the ‘Upgrade’ header.
So here’s the flow:

Let’s go more nerdy:
Here’s the actual Request to the server :
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
And the Response :
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
The Sec-Websocket-Key is random string sent by the client while initiating the handshake. Server takes this key, appends GUID “258EAFA5-E914–47DA-95CA-C5AB0DC85B11” to it. Then it computes the SHA-1 hash of combined string, base64 encodes it and send it back in the Sec-WebSocket-Accept header. If it matches with client’s expected value, the handshake is accepted, rejected otherwise.
Where do you use websockets ?
Let’s talk some cons (cause nothing’s perfect)
Websocket is a stateful protocol, meaning the server needs to keep track of each connection. So, if the server dies, the connection dies too. This makes horizontal scaling tricky. You can write more code, add a Database to store websockets, but that’s just extra work.
Load-balancing with websockets is tricky, particularly at layer 7.
To get it to work, you have to maintain two connections:
This is not ideal for Scalability.
Apart from that, you have to deal with timeouts.
Websocket connections can basically stay open forever, but firewalls, proxies, and load balancers have a limit for which a connection can stay open. Past that, the connection is terminated. Most implementations of websockets implement a ping/pong mechanism to keep the connection alive.
Let’s write some code (yay).
If you wish to create a websocket server from scratch, you must first invent the universe.
Ok not that scratch maybe… but we will remove abstractions as much as we can and build the core ourselves.
Since Websockets begin via an HTTP Request, we need a Basic HTTP server.
The idea of an HTTP Server is simple:
Let’s start by importing necessary header files:
#include <sys/socket.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <string.h>
#define PORT 6969 // :)
#define BACKLOG 10 // number of pending connections queue will store
#define BUFFER_SIZE 10240 // 10mb buffer
#define MAX_HEADERS 100
const char *PLACEHOLDER_RESPONSE = "<HTML><HEAD><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\">\r\n<TITLE>ABCD</TITLE></HEAD><BODY>\r\n<H1>XYZ</H1>\r\n</BODY></HTML>\r\n\r\n";
typedef struct
{
char *key;
char *value;
} header_t;
typedef struct
{
char method[16];
char target[1024];
char version[16];
header_t headers[MAX_HEADERS];
int header_count;
} http_request_t;
int main()
{
int server_fd;
struct sockaddr_in server_adress;
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket");
exit(EXIT_FAILURE);
}
server_adress.sin_addr.s_addr = INADDR_ANY;
server_adress.sin_family = AF_INET;
server_adress.sin_port = htons(PORT);
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1)
{
perror("setsocketopt");
close(server_fd);
exit(EXIT_FAILURE);
}
if (bind(server_fd, (struct sockaddr *)&server_adress, sizeof(server_adress)) == -1)
{
perror("binding");
close(server_fd);
exit(EXIT_FAILURE);
}
if (listen(server_fd, BACKLOG) == -1)
{
perror("listen");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Listening on port : %d\n", PORT);
while (1)
{
struct sockaddr_in client_adress;
size_t client_addr_len = sizeof(client_adress);
int *client_fd = malloc(sizeof(int));
if ((*client_fd = accept(server_fd, (struct sockaddr *)&client_adress, (socklen_t *)&client_addr_len)) == -1)
{
perror("accept");
continue;
}
pthread_t tid;
pthread_create(&tid, NULL, handle_client, (void *)client_fd);
pthread_detach(tid);
}
}
void *handle_client(void *arg)
{
int client_fd = *((int *)arg); // type-cast void * to int * and dereference it
char *buffer = (char *)malloc(BUFFER_SIZE * sizeof(char));
ssize_t bytes_received = recv(client_fd, buffer, BUFFER_SIZE, 0);
if (bytes_received > 0)
{
buffer[bytes_received] = '\0';
http_request_t req;
parse_request(buffer, &req);
printf("Method: %s\n", req.method);
printf("Target: %s\n", req.target);
printf("Version: %s\n", req.version);
const char *http_headers =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"Content-Length: %zu\r\n"
"\r\n";
const char *body = PLACEHOLDER_RESPONSE;
char response[BUFFER_SIZE];
size_t body_len = strlen(body);
int header_len = snprintf(response, BUFFER_SIZE, http_headers, body_len);
strncat(response, body, BUFFER_SIZE - header_len - 1);
send(client_fd, response, strlen(response), 0);
}
close(client_fd);
free(arg);
free(buffer);
return NULL;
}
Request Parser
void parse_request(const char *request, http_request_t *parsed)
{
char *req_copy = strdup(request);
if (!req_copy)
{
perror("strdup");
return;
}
char *header_end = strstr(req_copy, "\r\n\r\n");
*header_end = '\0';
char *line = strtok(req_copy, "\r\n");
sscanf(line, "%15s %1023s %15s", parsed->method, parsed->target, parsed->version);
parsed->header_count = 0;
while ((line = strtok(NULL, "\r\n")) != NULL)
{
char *colon = strstr(line, ": ");
if (colon)
{
*colon = '\0';
char *key = line;
char *value = colon + 2;
parsed->headers[parsed->header_count].key = strdup(key);
parsed->headers[parsed->header_count].value = strdup(value);
parsed->header_count++;
}
}
free(req_copy);
}
Here’s the whole code : server.c
From client side,
curl -v 127.0.0.1:6969
* Trying 127.0.0.1:6969...
* Connected to 127.0.0.1 (127.0.0.1) port 6969
> GET / HTTP/1.1
> Host: 127.0.0.1:6969
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/html
< Content-Length: 149
<
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>ABCD</TITLE></HEAD><BODY>
<H1>XYZ</H1>
</BODY></HTML>
From Server side,
Listening on port : 6969
Method: GET
Target: /
Version: HTTP/1.1
A typical Websocket handshake looks like this,
curl -v 127.0.0.1:6969/websocket
-H "Upgrade: websocket" \
-H "connection: Upgrade" \
-H "sec-websocket-key: dGhlIHNhbXBsZSBub25jZQ=="
We just need to parse the request, validate the headers, and send back the base64 hashed key, along with status code 101.
Validating the Handshake
bool is_valid_ws_handshake(http_request_t request)
{
// check method
bool is_get = strcmp(request.method, "GET") == 0;
char *version_dup;
if ((version_dup = strdup(request.version)) == NULL)
{
perror("strdup version_dup");
return false;
}
// check http version
char *protocol = strtok(version_dup, "/");
char *version_number = strtok(NULL, "\0");
free(version_dup);
double version_num = atof(version_number);
bool version_compat = version_num >= 1.1;
// check headers
bool upgrade_header = header_exists(request.headers, request.header_count, "Upgrade", "websocket");
bool connection_header = header_exists(request.headers, request.header_count, "connection", "Upgrade");
bool sec_websocket_key_header = header_exists(request.headers, request.header_count, "sec-websocket-key", NULL);
bool headers_valid = upgrade_header && connection_header && sec_websocket_key_header;
return is_get && headers_valid;
}
after validating the handshake, we need to complete the handshake. For that, we will take the sec-websocket-key and do the magic. And send it along 101 status code.
Generating the sec-websocket-accept key
char *generate_sec_websocket_accept_key(char *sec_websocket_key)
{
size_t key_len = strlen(sec_websocket_key);
size_t GUID_len = strlen(GUID);
size_t combined_len = key_len + GUID_len;
char *combined = malloc(combined_len + 1);
if (!combined)
{
perror("combined malloc");
return NULL;
}
strcpy(combined, sec_websocket_key);
strcat(combined, GUID);
unsigned char hashed[SHA_DIGEST_LENGTH];
size_t len = strlen(combined);
SHA1(combined, len, hashed);
char *encoded = b64_encode(hashed);
return encoded;
}
Finally, we will complete the handshake.
void handle_ws_handshake(http_request_t req, int client_fd)
{
const char *ws_headers =
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: %s\r\n"
"\r\n";
char *client_key = get_sec_websocket_key(req.headers, req.header_count);
char *transformed_key = generate_sec_websocket_accept_key(client_key);
char response[BUFFER_SIZE];
int header_len = snprintf(response, BUFFER_SIZE, ws_headers, transformed_key);
send(client_fd, response, strlen(response), 0);
}
Here’s the updated handle_client() function.
void *handle_client(void *arg)
{
int client_fd = *((int *)arg); // type-cast void * to int * and dereference it
char *buffer = (char *)malloc(BUFFER_SIZE * sizeof(char));
ssize_t bytes_received = recv(client_fd, buffer, BUFFER_SIZE, 0);
if (bytes_received > 0)
{
buffer[bytes_received] = '\0';
http_request_t req;
parse_request(buffer, &req);
printf("Method: %s\n", req.method);
printf("Target: %s\n", req.target);
printf("Version: %s\n", req.version);
for (int i = 0; i < req.header_count; i++)
{
printf("%s : %s\n", req.headers[i].key, req.headers[i].value);
}
const char *http_headers =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"Content-Length: %zu\r\n"
"\r\n";
if (strcmp(req.target, "/websocket") == 0)
{
if (is_valid_ws_handshake(req))
{
handle_ws_handshake(req, client_fd);
handle_ws_request(client_fd);
}
}
else
{
const char *body = PLACEHOLDER_RESPONSE;
char response[BUFFER_SIZE];
size_t body_len = strlen(body);
int header_len = snprintf(response, BUFFER_SIZE, http_headers, body_len);
strncat(response, body, BUFFER_SIZE - header_len - 1);
send(client_fd, response, strlen(response), 0);
}
}
close(client_fd);
free(arg);
free(buffer);
return NULL;
}
We are ready to receive websocket requests on /websocket path. Atleast till the Handshake. Let’s try it out
Let’s try with a browser now,

On The Server,
Listening on port : 6969
Method: GET
Target: /websocket
Version: HTTP/1.1
Host : localhost:6969
Connection : Upgrade
Pragma : no-cache
Cache-Control : no-cache
User-Agent : Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Upgrade : websocket
Origin : https://duckduckgo.com
Sec-WebSocket-Version : 13
Accept-Encoding : gzip, deflate, br, zstd
Accept-Language : en-US,en;q=0.9
Sec-WebSocket-Key : wB8akPQhn4vst/tD+RwLmA==
Sec-WebSocket-Extensions : permessage-deflate; client_max_window_bits
Yayyy. Our Handshake is working.
Now comes the interesting bit, Websocket Frames.
Here’s what we are talking about.
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
Prolly looks scary, but its quite simple actually.
A Websocket frame is made up of 4 parts:
Checkout Section 5.2 of RFC 6455 for more details.
I would like to go over Masking a bit.
Masking means obfuscating the data. A client always masks the data before sending, this may not be always true for the server.
The purpose of masking isn’t to add any encryption. Rather, it’s used to differentiate it from an HTTP Request. It’s also to prevent proxies and caches to cache the Request.
How Masking works ?
The client takes each byte of the payload and XORs it with one byte from a randomly generated 4-byte masking key. The key bytes repeat in a cycle. Applying the same XOR operation again on the masked data reverses it back to the original payload.
Let’s understand how are we handling a Websocket Request:
void handle_ws_request(int client_fd)
{
while (1)
{
unsigned char meta[2]; // for reading frame metadata
ssize_t n = recv(client_fd, meta, 2, 0);
if (n <= 0)
break;
unsigned char fin = (meta[0] & 0x80) >> 7;
unsigned char opcode = meta[0] & 0x0F;
unsigned char masked = (meta[1] & 0x80) >> 7;
uint64_t payload_len = meta[1] & 0x7F;
if (payload_len < 126)
{
}
else if (payload_len == 126)
{
char real_payload_len[2];
recv(client_fd, real_payload_len, 2, 0);
payload_len = (real_payload_len[0] << 8) | real_payload_len[1];
}
else if (payload_len == 127)
{
char real_payload_len[8];
recv(client_fd, real_payload_len, 8, 0);
uint64_t real_len = 0;
for (int i = 0; i < 8; i++)
{
real_len = (real_len << 8) | real_payload_len[i];
}
payload_len = real_len;
}
char *pl_buffer = malloc(payload_len);
if (masked)
{
unsigned char masking_key[4];
ssize_t key_size = recv(client_fd, masking_key, 4, 0);
recv(client_fd, pl_buffer, payload_len, 0);
for (int i = 0; i < payload_len; i++)
{
pl_buffer[i] = pl_buffer[i] ^ masking_key[i % 4];
}
printf("Payload: %.*s\n", (int)payload_len, pl_buffer);
}
else
{
recv(client_fd, pl_buffer, payload_len, 0);
}
}
return;
}
Firstly, we read the first 2 bytes of the frame, to get information about the Frame.
unsigned char meta[2]; // for reading frame metadata
ssize_t n = recv(client_fd, meta, 2, 0);
if (n <= 0)
break;
unsigned char fin = (meta[0] & 0x80) >> 7;
unsigned char opcode = meta[0] & 0x0F;
unsigned char masked = (meta[1] & 0x80) >> 7;
uint64_t payload_len = meta[1] & 0x7F;
First byte containes the FIN bit, which tells if its a final frame, next 3 bits are reserve bits, which are used for indicating extension. Next 4 bits are the opcode, which basically tells the interpretation of the data. Next 1 bit is the masked bit, if its 1, it means the data is masked. Next 7 bits tell the payload length.
The last 7 bits are interesting, as, it can only hold upto 127, so, for larger payload, there is a trick.
if (payload_len < 126)
{
}
else if (payload_len == 126)
{
char real_payload_len[2];
recv(client_fd, real_payload_len, 2, 0);
payload_len = (real_payload_len[0] << 8) | real_payload_len[1];
}
else if (payload_len == 127)
{
char real_payload_len[8];
recv(client_fd, real_payload_len, 8, 0);
uint64_t real_len = 0;
for (int i = 0; i < 8; i++)
{
real_len = (real_len << 8) | real_payload_len[i];
}
payload_len = real_len;
}
Extended Payload Length:
Next we can just put everything together, and read the payload data.
Client Side :

Server side:
Listening on port : 6969
Method: GET
Target: /websocket
Version: HTTP/1.1
Host : localhost:6969
Connection : Upgrade
Pragma : no-cache
Cache-Control : no-cache
User-Agent : Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Upgrade : websocket
Origin : https://duckduckgo.com
Sec-WebSocket-Version : 13
Accept-Encoding : gzip, deflate, br, zstd
Accept-Language : en-US,en;q=0.9
Sec-WebSocket-Key : E5AO9X0FkPCACiKsdpmDMw==
Sec-WebSocket-Extensions : permessage-deflate; client_max_window_bits
payload len : 5
Payload: hello
payload len : 126
Payload: helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
Everything’s working perfectly . yayyyy.
Grab the full code from here : websockets
So, websocket is a full duplex, stateful, binary protol, that upgrades an HTTP connection to a persistent channel, that allows client and server to talk freely, without initiating new connection each time.
Thank you for reading till the end <3.
Wanna connect?, follow me on X.