2026-07-19
This topic has been done to death already online, but I wanted to write my work on it down so that I can reference it in the future should it be necessary. I also found that a lot of the examples I saw were poorly documented, written mostly by LLMs (which I took to mean the author didn’t really understand the code), or were published on a website with a lot of cookie dialogs.
This post was inspired by https://beej.us/guide/bgnet/, and frankly you might just want to go there instead. Pedagogical quality there is a lot better.
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>int sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock_fd < 0)
{
perror("socket");
exit(1);
}
(The function signature is int socket(int domain, int type, int protocol);.)
A socket is an endpoint for communication. On POSIX, it returns a file descriptor that lets us write or read from it. AF_INET stipulates that it uses IPv4 internet protocols and SOCK_STREAM means that it does TCP style two way connection-based byte streams. SOCK_DGRAM is the other option, which are unreliable packet transmissions of some fixed size that do not mandate an ordering or a connection.
IPPROTO_TCP isn’t really strictly required, but makes it obvious to the user that this socket speaks TCP. Alternatively, you can just pass a 0, which usually just infers the default protocol for the socket type. For a socket of this kind, the default is of course TCP.
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(PORT);
local.sin_addr.s_addr = htonl(INADDR_ANY);
Next, we have to configure our local address.
Here are the definitions from the manual page:
struct sockaddr_in {
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port; /* Port number */
struct in_addr sin_addr; /* IPv4 address */
};
struct in_addr {
in_addr_t s_addr;
};
typedef uint32_t in_addr_t;
typedef uint16_t in_port_t;
I’ve declared PORT above by #define PORT 8080. htons just does “host to network short”, which is literally just moving from host byte order to big endian byte order. Host byte order is generally little endian, but if you have a big endian system then this is a no-op.
One implementation I found is as simple as ((__uint16_t) ((((x) >> 8) & 0xff) | (((x) & 0xff) << 8))).
INADDR_ANY means “bind to all local interfaces” – AKA 0.0.0.0. (This includes localhost / loopback / 127.0.0.1, but isn’t limited to it.)
if (bind(sock_fd, (struct sockaddr *)&local, sizeof(local)) < 0)
{
perror("bind");
exit(1);
}
if (listen(sock_fd, 128) < 0)
{
perror("listen");
exit(1);
}
bind assigns the address we just setup to the socket that we created earlier. I consider this to be like giving this socket the name of “127.0.0.1:8080”.
listen means that we will consider this socket passive - that it will listen to connection requests that we connect to with accept later. The second parameter backlog (int listen(int sockfd, int backlog);), allows the queue of pending connections to grow to 128. Any connection past this may receive an error of ECONNREFUSED.
The next section I’ve put in an infinite loop. You can do while(1) {} or for(;;) {} depending on whether you are a nerd or a geek. I’ve been told one of those options makes you “cooler” to general audiences, but never quite figured out which one of those it was.
If you are sensible and put a return 0; right before the closing brace of main, your linter may tell you that this code is unreachable. I haven’t found a way to tell the linter that I know it’s unreachable. You can alternatively just silence the warning, but then this prevents further linting to detect unreachability.
What I would give for https://ziglang.org/documentation/master/#unreachable or https://doc.rust-lang.org/std/macro.unreachable.html… I think there is a version for C, but it has the issue that clangd (a C language server) doesn’t seem to like do anything useful with it. (My tools could also just be borked.)
struct sockaddr_storage their_addr;
socklen_t addr_len = sizeof(their_addr);
int client = accept(sock_fd, (struct sockaddr *)&their_addr, &addr_len);
if (client < 0) {
perror("accept");
break;
}
sockaddr_storage is used to hold the address that the connection is being made from. Here it will also be the local interface if you use netcat or telnet or something to test out your server. accept will allow us to take one of these aforementioned connections and actually start reading from it. client is the new file descriptor that we will read from.
sock_fd in this case is retained in listening mode. It will be used in the loop to call another accept when we want to handle following requests. client, however, does not “listen” for new connections.
Okay, here I originally used read, but I then moved to recv because it made me feel like it was more descriptive. The gist, that I can tell at least, is that recv allows you to pass additional flags compared to read.
I did not pass any flags. ¯\_(ツ)_/¯
Unfortunately, for TCP, you have to keep calling recv in order to actually get all of the bytes it’s holding from you.
char buffer[BUF_SIZE];
ssize_t bytesRead = 0;
for (;;) {
ssize_t res = recv(client, buffer + bytesRead, BUF_SIZE - 1 - bytesRead, 0);
if (res < 0) { perror("recv"); break; }
if (res == 0) break;
bytesRead += res;
buffer[bytesRead] = '\0';
if (strstr(buffer, "\r\n\r\n")) break;
if (bytesRead >= BUF_SIZE - 1) break;
}
Here, we call recv until either recv has errors, has nothing left to give, has a finished message, or where the number of bytes read would be over the limit of the buffer size.
Since strings in C need to be null terminated, we ensure to write a \0 at the end of the buffer.
Next, we can write the message that we’ve constructed. This will depend on exactly your business logic and what sort of protocol you are implementing. I did a really dumped down form of HTTP, but that’s kind of a separate notion.
size_t total = 0;
for (;;) {
ssize_t w = send(client, response + total, response_len - total, 0);
if (w < 0) { perror("send"); break; }
if (w == 0) { break; }
total += (size_t)w;
}