TCP Sockets in C: socket, bind, listen, accept, connect, send, recv
What This Concept Is
The BSD socket API is how portable UNIX programs speak TCP and UDP. The server and client sequences are small and worth memorizing.
Server:
socket() -> bind() -> listen() -> accept() -> recv()/send() -> close()
Client:
socket() -> connect() -> send()/recv() -> close()
Key calls:
int socket(int domain, int type, int protocol)-- e.g.,AF_INET,SOCK_STREAM,0. Returns an fd.int bind(int fd, const struct sockaddr *addr, socklen_t n)-- attach the socket to a local IP and port.int listen(int fd, int backlog)-- mark the socket as passive; queue incoming connections.int accept(int fd, struct sockaddr *peer, socklen_t *n)-- block until a client connects; return a new fd for that connection. The original fd stays open to accept more.int connect(int fd, const struct sockaddr *addr, socklen_t n)-- actively open a connection.ssize_t send(int fd, const void *buf, size_t n, int flags)-- likewrite, plus TCP-specific flags.ssize_t recv(int fd, void *buf, size_t n, int flags)-- likeread, plus flags.
TCP sockets are just fds -- once connected, read/write work, so do poll, select, epoll.
Why It Matters Here
Almost every system exposed to the network is, at some layer, making these calls. Web servers, databases, IPC brokers, REST clients -- they all accept or connect. Modern frameworks wrap the API, but the wrappers leak: when your connection hangs, your read returns short, or EADDRINUSE keeps you from restarting, you are debugging at this layer.
Concrete Example
A minimal TCP echo server and client (IPv4, single-threaded, one connection at a time):
/* server.c */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int s = socket(AF_INET, SOCK_STREAM, 0);
if (s < 0) { perror("socket"); exit(1); }
int yes = 1;
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(9000);
if (bind(s, (struct sockaddr*)&addr, sizeof addr) < 0) { perror("bind"); exit(1); }
if (listen(s, 16) < 0) { perror("listen"); exit(1); }
for (;;) {
int c = accept(s, NULL, NULL);
if (c < 0) { perror("accept"); continue; }
char buf[4096];
ssize_t n;
while ((n = recv(c, buf, sizeof buf, 0)) > 0) {
ssize_t off = 0;
while (off < n) {
ssize_t w = send(c, buf + off, n - off, 0);
if (w < 0) break;
off += w;
}
}
close(c);
}
}
/* client.c */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
int main(void) {
int s = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in a = {0};
a.sin_family = AF_INET;
a.sin_port = htons(9000);
inet_pton(AF_INET, "127.0.0.1", &a.sin_addr);
connect(s, (struct sockaddr*)&a, sizeof a);
const char *msg = "hello, socket\n";
send(s, msg, strlen(msg), 0);
char buf[128];
ssize_t n = recv(s, buf, sizeof buf - 1, 0);
if (n > 0) { buf[n] = 0; write(1, buf, n); }
close(s);
}
Two things worth internalizing. SO_REUSEADDR avoids "Address already in use" when you restart the server while an old connection is in TIME_WAIT. The send loop is there because send, like write, can return fewer bytes than requested -- especially under load.
Common Confusion / Misconception
"recv returns when my message arrives." No. TCP is a byte stream. There is no "message." recv returns whenever some bytes are available -- possibly half of your logical message, possibly the tail of one message and the head of the next. Framing (length prefixes, delimiters, HTTP Content-Length) is your job.
"I need a new socket for every connection." The server needs one listening socket (the result of socket/bind/listen). accept returns a different fd per client -- that is the per-connection socket. Closing the connection fd does not close the listener.
"htons and htonl are optional on my little-endian machine." They are mandatory. Network byte order is big-endian by specification. Skipping them means your port and address will be interpreted upside-down on other hosts, and sometimes even locally depending on the kernel path.
Another trap: not handling EINTR on blocking accept/recv/send, or not setting SO_REUSEADDR and discovering that your CI cannot restart the server between tests.
How To Use It
For any TCP program:
- Decide framing before writing a byte. Length prefix, line-delimited, or HTTP-style headers.
- Always loop
sendandrecv. Treat them likewriteandread. - Always
closeboth ends on shutdown; useshutdown(fd, SHUT_WR)if you want to signal end-of-input while still reading responses. - On the server, use
SO_REUSEADDRand a sensiblelisten(backlog)(16-128 is typical). - For more than one connection at a time, either
forkper connection, spawn a thread per connection, or usepoll/epoll/kqueuefor event-driven I/O.
Check Yourself
- Why does
acceptreturn a new fd instead of reusing the listening one? - Why must
sendandrecvalways be looped? - What does
SO_REUSEADDRactually allow the kernel to do?
Mini Drill or Application
Do all four:
- Compile and run the echo server and client above. Connect with
nc 127.0.0.1 9000as a second client. - Make the server
forkper connection so it can serve multiple clients simultaneously. Reap children with aSIGCHLDhandler. - Extend the client to send a 1 MB payload and verify every byte comes back identical. Expect multiple
recvcalls. - Write a minimal HTTP server: accept, read the request line, respond with
HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok. Test withcurl.
Read This Only If Stuck
- K&R 8.2: Low-Level I/O -- the read/write loop pattern transfers directly
- COD 6.5: Connecting Processors, Memory, and I/O Devices -- hardware-side view of networking I/O
- Man page:
man 2 socket - Man page:
man 2 bind - Man page:
man 2 accept - Man page:
man 7 tcp - Beej's Guide to Network Programming