Socket programming in C comes down to a handful of system calls from the BSD socket API, the same ones most servers on a Linux machine are built on. What trips people up is their order, the structs each call expects and the fact that TCP delivers a byte stream with no message boundaries. This guide puts together a TCP client and server step by step: one echo server, one client, both compiled with -Wall -Wextra, and the output they printed. Read in order, the listings add up to a complete C socket programming example.
For a longer reference, Beej's Guide to Network Programming is worth keeping open next to the man pages: http://beej.us/guide/bgnet/. It goes deeper into IPv6 and datagram sockets and covers corner cases that do not fit on one page.
Socket programming in C: what a TCP connection needs
A TCP connection has two ends with different jobs. The server creates a socket, attaches it to a port, marks it as listening and then waits. The client creates a socket and asks the kernel to connect it to an address and a port. Once accept() on the server and connect() on the client both return, each side holds a file descriptor that behaves much like a pipe in both directions.
The headers come from POSIX: sys/socket.h for the socket calls, netdb.h for getaddrinfo(), arpa/inet.h for the byte order helpers and unistd.h for close(). Nothing needs to be linked on Linux, because glibc contains all of it. Solaris is an exception, covered at the end.
| Call | Side | What it does |
|---|---|---|
getaddrinfo() | both | turns a host name and a port into ready-made sockaddr structs |
socket() | both | creates the endpoint and returns a descriptor |
bind() | server | attaches the socket to a local address and port |
listen() | server | marks it as passive and sets the queue length |
accept() | server | waits for a client and returns a new descriptor for it |
connect() | client | opens the connection to the server |
send() / recv() | both | write and read bytes |
shutdown() / close() | both | end one direction, or release the descriptor |
The call sequence for a TCP client and server
The diagram shows both programs side by side. The server's listening socket never carries data. Every accept() returns a fresh descriptor for one client, and the listening socket stays open for the next one.
The three-way handshake happens inside the kernel between connect() and accept(). A client can finish connect() before the server has called accept() at all, because the kernel completes the handshake and puts the connection into the backlog queue whose length listen() sets.

Building the server step by step
The skeleton of a socket programming in C TCP server never changes: resolve, create, bind, listen, accept. The one below listens on port 8080, accepts one client at a time and sends back every byte it receives. The fragments in this section are the core of server.c; the full file adds #define PORT "8080", the send_all() helper, main() around these steps and the usual includes (errno.h, netdb.h, stdio.h, string.h, sys/socket.h, sys/types.h, unistd.h) and starts with #define _DEFAULT_SOURCE, which glibc needs to expose NI_MAXHOST and NI_MAXSERV. With only _POSIX_C_SOURCE defined, gcc 14 stops with "'NI_MAXHOST' undeclared".
getaddrinfo instead of hand-filled structs
Older code fills a struct sockaddr_in field by field. getaddrinfo() does that work, handles IPv4 and IPv6 alike and stores the port in network byte order. AI_PASSIVE with a NULL host asks for the wildcard address that bind() needs on a server.
struct addrinfo hints, *res, *p;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; /* IPv4 or IPv6 */
hints.ai_socktype = SOCK_STREAM; /* TCP */
hints.ai_flags = AI_PASSIVE; /* wildcard address for bind() */
int rc = getaddrinfo(NULL, PORT, &hints, &res);
if (rc != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rc));
return 1;
}socket, SO_REUSEADDR and bind
The result is a linked list. The loop tries each entry until one binds, which is the pattern the man page for getaddrinfo(3) uses as well.
int lfd = -1;
for (p = res; p != NULL; p = p->ai_next) {
lfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (lfd == -1)
continue;
int yes = 1;
if (setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes) == -1)
perror("setsockopt");
if (bind(lfd, p->ai_addr, p->ai_addrlen) == 0)
break;
perror("bind");
close(lfd);
lfd = -1;
}
freeaddrinfo(res);
if (lfd == -1)
return 1;Without SO_REUSEADDR the first quick restart during development fails. For a test, a copy of the server with the setsockopt() call removed was killed while a client was connected and started again 0.2 seconds later:
$ ./server_noreuse
bind: Address already in use
bind: Address already in useThe message appears twice because the loop tried both list entries, IPv4 and IPv6. The unmodified server.c restarted in the same situation printed listening on port 8080. The killed process left the port in TIME_WAIT, and only the option allows a new socket to bind over that state. Linux looks at the flag on the old socket as well: a TIME_WAIT left behind by the version without the option blocked even the fixed one.
listen and accept
if (listen(lfd, 16) == -1) {
perror("listen");
return 1;
}
printf("listening on port %s\n", PORT);
fflush(stdout);
for (;;) {
struct sockaddr_storage peer;
socklen_t plen = sizeof peer;
int cfd = accept(lfd, (struct sockaddr *)&peer, &plen);
if (cfd == -1) {
perror("accept");
continue;
}
char host[NI_MAXHOST], serv[NI_MAXSERV];
rc = getnameinfo((struct sockaddr *)&peer, plen, host, sizeof host,
serv, sizeof serv, NI_NUMERICHOST | NI_NUMERICSERV);
printf("connection from %s port %s\n",
rc == 0 ? host : "?", rc == 0 ? serv : "?");
char buf[4096];
ssize_t n;
size_t total = 0;
while ((n = recv(cfd, buf, sizeof buf, 0)) > 0) {
total += (size_t)n;
if (send_all(cfd, buf, (size_t)n) == -1) {
perror("send");
break;
}
}
if (n == -1)
perror("recv");
printf("client done, echoed %zu bytes\n", total);
fflush(stdout);
close(cfd);
}struct sockaddr_storage is large enough for any address family, so the same code accepts IPv4 and IPv6 clients. getnameinfo() with the two numeric flags prints the peer address without a DNS lookup. The backlog of 16 is an arbitrary value for a test server; the kernel caps it at net.core.somaxconn.
The client: connect, send, then read until EOF
The client resolves the host given on the command line, connects, sends everything it reads from standard input and prints what comes back. This is the full file, a complete C socket programming example that compiles without a single warning.
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
static int send_all(int fd, const char *buf, size_t len)
{
while (len > 0) {
ssize_t n = send(fd, buf, len, 0);
if (n == -1) {
if (errno == EINTR)
continue;
return -1;
}
buf += n;
len -= (size_t)n;
}
return 0;
}
int main(int argc, char *argv[])
{
if (argc != 3) {
fprintf(stderr, "usage: %s host port < data\n", argv[0]);
return 2;
}
struct addrinfo hints, *res, *p;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
int rc = getaddrinfo(argv[1], argv[2], &hints, &res);
if (rc != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rc));
return 1;
}
int fd = -1;
for (p = res; p != NULL; p = p->ai_next) {
fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (fd == -1)
continue;
if (connect(fd, p->ai_addr, p->ai_addrlen) == 0)
break;
perror("connect");
close(fd);
fd = -1;
}
freeaddrinfo(res);
if (fd == -1)
return 1;
/* Send everything from stdin, then tell the server that writing is done. */
char buf[65536];
size_t sent = 0, got = 0, calls = 0, small = 0, big = 0;
ssize_t n;
while ((n = read(STDIN_FILENO, buf, sizeof buf)) > 0) {
if (send_all(fd, buf, (size_t)n) == -1) {
perror("send");
return 1;
}
sent += (size_t)n;
}
shutdown(fd, SHUT_WR);
/* recv() returns whatever has arrived: loop until it returns 0 (EOF). */
while ((n = recv(fd, buf, sizeof buf, 0)) > 0) {
fwrite(buf, 1, (size_t)n, stdout);
got += (size_t)n;
calls++;
if (small == 0 || (size_t)n < small)
small = (size_t)n;
if ((size_t)n > big)
big = (size_t)n;
}
if (n == -1)
perror("recv");
fflush(stdout);
fprintf(stderr, "sent %zu, received %zu bytes in %zu recv() calls"
" (smallest %zu, largest %zu)\n", sent, got, calls, small, big);
close(fd);
return 0;
}Compiled and run against the server from the previous section, inside one container:
$ gcc -Wall -Wextra -o server server.c
$ gcc -Wall -Wextra -o client client.c
$ ./server &
listening on port 8080
$ printf 'hello, socket\n' | ./client localhost 8080
connect: Connection refused
hello, socket
sent 14, received 14 bytes in 1 recv() calls (smallest 14, largest 14)The Connection refused line comes from the first address the client tried. localhost resolved to ::1 first, but the server's loop had bound the first entry getaddrinfo() returned, the IPv4 wildcard, so nothing listened on IPv6. The client moved on to 127.0.0.1 and got through, which is the situation the try-every-address loop was written for.
A server can avoid the detour by asking for AF_INET6 only. On Linux with net.ipv6.bindv6only at its default of 0, as in the test container, an IPv6 wildcard socket also accepts IPv4 clients and reports them as IPv4-mapped addresses. The server log with that one-line change, after one client connected to localhost and one to 127.0.0.1:
listening on port 8080
connection from ::1 port 57784
client done, echoed 3 bytes
connection from ::ffff:127.0.0.1 port 47838
client done, echoed 3 bytesNeither client printed a refused connection this time. Systems with bindv6only set to 1, and some BSDs by default, separate the two families, so portable servers either open one socket per family or set the IPV6_V6ONLY option explicitly.
C sockets recv partial read: loop until done
Most C sockets recv partial read bugs start from one assumption: that one send() arrives as one recv(). It does not. A single recv() returns whatever the kernel has buffered, which may be one byte or the whole buffer. The same client fed one megabyte of random data:
$ head -c 1000000 /dev/urandom | ./client 127.0.0.1 8080 > /dev/null
sent 1000000, received 1000000 bytes in 17 recv() calls (smallest 8768, largest 65536)Eight runs with random input took 16 or 17 calls each, and the smallest piece varied between 8768 and 36864 bytes. Loopback is about the friendliest network there is, and the split still changed from run to run. Over a real network, where one TCP segment usually carries at most about 1,460 bytes, C sockets recv partial reads are the normal case, not the exception.
shutdown versus close
The client calls shutdown(fd, SHUT_WR) after sending. That sends a FIN, the server's recv() returns 0, the server finishes echoing and closes its side, and only then does the client's own recv() loop end. A plain close() at that point would have released the descriptor before the echo arrived. Use shutdown() to say "no more data from this side" and close() to free the descriptor when you are finished with it.
Byte order, htons and data on the wire
Network protocols send multi-byte integers in big-endian order, while x86 and most ARM systems store them little-endian. The role of htons in socket programming is to convert a 16-bit value such as a port from host order into network order; htonl() does the same for 32-bit values and ntohs()/ntohl() convert back. A server that fills sin_port by hand and forgets htons(8080) ends up on port 36895. That one missing call is the most common htons in socket programming mistake, and the compiler cannot warn about it. The byte order converter shows why, byte by byte, and lists the ways to check which order a machine uses.

The same rule applies to your own message formats. A length prefix written as a raw uint32_t on a little-endian machine reads as a different number on a big-endian one. Floating-point values are harder, because there is no standard htonf(). The usual approach is to encode them in IEEE 754 format with explicit byte order; the IEEE 754 converter shows the bit layout of a float, and Beej's example pack2.c, released without copyright, packs floats and integers into a portable byte buffer.
Serving several clients with poll()
The server above has one serious limit: while it echoes for one client, every other client waits in the backlog. A test made that visible. One client connected and kept its connection open for two seconds; a second client started 0.2 seconds later. The rest of this section rebuilds the socket programming in C TCP server around poll(), step by step, so the TCP client and server pairs no longer take turns.
server: second client done after 2009 ms
pollserver: second client done after 206 msThe second line comes from a version built around poll(), which asks the kernel which descriptors are ready and serves whichever one is. This is the main loop of pollserver.c; listen_on() contains the getaddrinfo(), socket(), bind() and listen() steps from earlier.
for (;;) {
if (poll(fds, nfds, -1) == -1) {
if (errno == EINTR)
continue;
perror("poll");
return 1;
}
/* A new connection is waiting on the listening socket. */
if (fds[0].revents & POLLIN) {
int cfd = accept(fds[0].fd, NULL, NULL);
if (cfd == -1) {
perror("accept");
} else if (nfds == MAX_FDS) {
close(cfd); /* full: refuse politely */
} else {
fds[nfds].fd = cfd;
fds[nfds].events = POLLIN;
fds[nfds].revents = 0;
nfds++;
printf("fd %d connected, %lu clients\n", cfd,
(unsigned long)(nfds - 1));
fflush(stdout);
}
}
/* Data (or EOF) on a client socket. */
for (nfds_t i = 1; i < nfds; i++) {
if (!(fds[i].revents & (POLLIN | POLLHUP | POLLERR)))
continue;
char buf[4096];
ssize_t n = recv(fds[i].fd, buf, sizeof buf, 0);
if (n > 0 && send_all(fds[i].fd, buf, (size_t)n) == 0)
continue;
if (n == -1)
perror("recv");
printf("fd %d closed\n", fds[i].fd);
fflush(stdout);
close(fds[i].fd);
fds[i] = fds[nfds - 1]; /* move the last entry into the hole */
nfds--;
i--; /* and look at the moved entry next */
}
}Its log from the same test shows both clients connected at once:
fd 4 connected, 1 clients
fd 5 connected, 2 clients
fd 5 closed
fd 4 closedselect() does the same job with fd_set bitmaps and is still found in older code and on Windows. It cannot watch descriptors at or above FD_SETSIZE, which glibc defines as 1024, and the sets must be rebuilt before every call. poll() has neither problem. The send_all() call in this loop still blocks if one client stops reading, so a production server would make the client sockets non-blocking and buffer unsent data.
Errors: errno, perror and gai_strerror
Socket calls return -1 on failure and set errno. perror() prints a prefix and the matching text, which is enough for small tools. getaddrinfo() and getnameinfo() are the exceptions: they return their own codes for gai_strerror(). Both kinds of message, from the client above:
$ ./client 127.0.0.1 8080 < /dev/null
connect: Connection refused
$ ./client no-such-host.invalid 8080 < /dev/null
getaddrinfo: Name or service not knownThe first case had no server running; the second used the .invalid top-level name, which never resolves. A few errno values deserve their own branch. EINTR means a signal interrupted a blocking call and it is safe to repeat it, which send_all() does. ECONNRESET on recv() or EPIPE on send() mean the peer is gone. By default, writing to such a socket also raises SIGPIPE and kills the process unless you ignore that signal or pass MSG_NOSIGNAL to send() on Linux.

Windows, Solaris and other platforms
Winsock on Windows copies the BSD interface closely; the FAQ below lists the header, library and function changes. Two more catch ported code. WSAStartup(MAKEWORD(2, 2), &wsaData) has a partner, WSACleanup(), at program exit. And sockets have the type SOCKET, an unsigned handle, instead of int, so a failed socket() call has to be detected by comparing with INVALID_SOCKET rather than testing for -1 or a negative value. Cygwin and WSL ship the POSIX headers, so socket programming in C examples written for Linux build there without the Winsock changes.
On Solaris and older SunOS releases the socket functions live in separate libraries, so a build needs extra linker flags at the end of the command: cc -o server server.c -lnsl -lsocket -lresolv. Some setups also needed -lxnet. Linux and the BSDs need none of these.
The echo design in this guide hides one more trap. The client writes all of its input before it reads anything. With 4 MB of input the test finished normally; with 16 MB it hung until timeout killed it after eight seconds. Both programs were blocked in send(), each waiting for the other to read, because the socket buffers on both sides had filled up. Where exactly that limit sits depends on net.ipv4.tcp_rmem and tcp_wmem on the machine in question. The fix is the same poll() loop, this time in the client, watching its own socket for POLLOUT and POLLIN together.
