/Programming


Learn to develop things, get out of debt, find answers and take part in coding and programming challenges.


Members: 10
Join


Moderated by: mozzapp
up
1
up
Manon_code 1786987660 [Programming] 0 comments
There's a line I hear a bit too often in programming communities: "sockets are easy, you just open a connection and send data." And sure, technically, that's true. The problem is that sentence hides a dozen decisions the operating system is making behind the scenes, and most people writing code today never had to look at that layer because they live one level above it — Express, Socket.IO, gRPC, whatever. Nothing wrong with that, but at some point the abstraction leaks. A `recv()` returns half a message, a connection just hangs with no error at all, an `EADDRINUSE` shows up out of nowhere after you restart your server too fast. And then you actually need to understand what's underneath. This is about that: what happens when you call `socket()`, `bind()`, `listen()`, `accept()`, and why most of the "weird" behavior of TCP is really just the protocol doing exactly what it's supposed to do. One thing worth untangling right away: a socket isn't a connection. It's a number. A file descriptor, the same kind you'd use to open a file, except instead of pointing to an inode it points to a structure inside the kernel that holds the connection's state — the read and write buffers, the TCP state machine (SYN_SENT, ESTABLISHED, FIN_WAIT_2, that sort of thing), the addresses and ports involved. When you call `read()` on a socket, from your application's point of view it's identical to reading from a file. That's exactly why Unix systems can treat sockets with the same generic I/O calls. All the difference lives on the other side, inside the kernel's network stack, invisible to you. Let's go through the system calls one at a time, because each one does something specific that tends to get glossed over. `socket(AF_INET, SOCK_STREAM, 0)` creates the descriptor. At that point it isn't connected to anything, has no address, no port. It's just an empty structure waiting to be configured. `bind()` ties that descriptor to a local IP address and port — this is where the infamous `EADDRINUSE` usually comes from, which in practice is almost always a previous connection's TIME_WAIT still holding onto the port (more on that later). `listen()` is where things get interesting: it doesn't open any connection, it creates a queue. Two queues, actually — one for connections that already completed the handshake and are waiting for your app to call `accept()`, and another for connections still mid-handshake. The size of that queue is the backlog, and if it fills up, new connection attempts simply get no response until you catch up. And here's the part that trips up most beginners: `accept()` doesn't create a connection, it pulls an already-completed one off the queue and hands you back a brand new socket. The original socket, the one you used for `bind()` and `listen()`, stays alive, still listening, still on the same port. It never talks directly to the client. Every successful `accept()` gives you a separate descriptor, with its own remote address and port pair, and that's the socket you actually use for `send()` and `recv()`. That's what lets a server handle multiple simultaneous connections on the same port — each one lives on its own descriptor, isolated from the others, even though they all came in through port 8080. It's worth pulling up Wireshark at this point, honestly, because reading about the three-way handshake is one thing, watching it happen while your own code runs is another. SYN from the client, SYN-ACK from the server, ACK back from the client — and only after that third packet does the connection move into ESTABLISHED and your `accept()` call return. If you capture traffic while testing a simple client and server on the same machine, the three packets show up almost instantly, but over a real network, with real latency, you can actually feel the RTT in that handshake before a single byte of data gets exchanged. Now, the point I think is the most valuable in the whole piece, and the one that rarely gets explained well: TCP has no concept of a message. None. It's a byte-stream protocol, full stop. If your app makes two `send()` calls back to back, one sending "HI" and another sending "HOW ARE YOU", there's no guarantee at all that the receiving side gets two matching `recv()` calls. It might all arrive together in a single `recv()`, "HIHOW ARE YOU" concatenated. It might arrive split up, half of "HI" in one call and the rest scattered across two more. The kernel decides this based on MTU, Nagle's algorithm, network conditions, whatever's happening in the buffer at that moment — and your application has zero control over it. People coming from HTTP or WebSocket find this strange, because those layers already solve the problem for you, delimiting messages with content-length or their own framing. Raw sockets don't. If your protocol needs discrete messages, it's on you to implement that — a delimiter, a length prefix, something. Ignoring this detail is probably the single most common cause of the weird, hard-to-reproduce bugs I've seen people report with raw socket code. Which brings me to buffers. `send()` is not a delivery guarantee. It copies data into the kernel's send buffer and returns — it can return before the data has even left the network card. And the return value of `send()` is the number of bytes it managed to queue up, which can be less than what you asked for if the buffer is full. Naive code that assumes "I sent 10000 bytes, so `send()` definitely sent all 10000" breaks silently under load. The correct approach is to always check the return value and, if it's smaller than expected, loop and send the rest. On blocking versus non-blocking: start simple, with a plain blocking socket, because it's easier to reason about. But it's worth showing early the problem this creates — a server with a single blocking socket can only handle one client at a time, because `accept()` blocks waiting for the next connection and `recv()` blocks waiting for data to arrive. One slow or misbehaving client freezes the whole server for everyone else. That's the natural hook to bring up `select`, `poll`, `epoll` — no need to implement any of it here, just plant the question "what if I wanted to handle a thousand connections at once without spawning a thousand threads?" and let the reader stay curious. Closing a connection deserves attention because there's more nuance than it looks like. `close()` shuts the descriptor down on both ends, read and write. `shutdown()` lets you close just one side — useful when you want to signal "I'm done sending" but still want to read the other side's response before hanging up for good. And there's a difference between a normal FIN, which is the protocol's polite way of ending things, and an RST, which is more of a "forget it, cancel everything now" — usually triggered when you try writing to a socket the other side has already closed. Then there's TIME_WAIT, that state that shows up after you close a connection and it still lingers in `netstat`'s output for a while. It exists for a real reason: to make sure delayed packets from the old connection don't get mistaken for a new connection reusing the same address-and-port pair. The catch is, if you restart your server too quickly during development, the old port is still sitting in TIME_WAIT and `bind()` fails with `EADDRINUSE`. That's exactly what the `SO_REUSEADDR` option is for — it tells the kernel "let me rebind to this port even if the previous connection is still winding down." Common errors are worth a quick rundown, more like a reference than a story: `ECONNRESET` usually means the other side dropped the connection abruptly, often because its process died or because you wrote to it after it had already closed. `EADDRINUSE` was already covered. A silent timeout — `recv()` just hanging forever with no error — is usually some firewall along the path swallowing traffic without sending back an RST, so TCP just sits there waiting for an ACK that's never coming. With all that groundwork, real code makes sense as a closer. Here's a minimal echo server in C, using nothing but the POSIX standard library, no external dependencies at all: ```c #include <stdio.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> int main() { int server_fd = socket(AF_INET, SOCK_STREAM, 0); int opt = 1; setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(9000); bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)); listen(server_fd, 10); printf("listening on port 9000\n"); while (1) { struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len); char buf[1024]; ssize_t n; while ((n = recv(client_fd, buf, sizeof(buf), 0)) > 0) { ssize_t sent = 0; while (sent < n) { ssize_t s = send(client_fd, buf + sent, n - sent, 0); if (s <= 0) break; sent += s; } } close(client_fd); } } ``` Notice the inner `send()` loop exists exactly because of what I mentioned earlier: nothing guarantees a single `send()` call ships everything at once. And the `recv()` loop runs in a `while` because, again, each call might bring back any arbitrary chunk of what the client sent — not a "complete message." To test it, you can use `nc` (netcat) as a client: `nc localhost 9000`, type anything, watch it echo back. And if you want to see the handshake in action, run `tcpdump -i lo port 9000` in another tab before connecting. At the end of the day, this low-level layer shows up constantly underneath things that look far more sophisticated. A WebSocket is, literally, a TCP socket with an initial HTTP handshake and a message-framing scheme layered on top. A database like Postgres talks to its driver over its own binary protocol running on... a plain TCP socket, with the exact same framing problems described here. Understanding this layer isn't academic curiosity — it's what separates "my code works on my local test" from "my code holds up in production."

A social news and discussion community