Sockets

A socket is a file descriptor for the network. The same calls that read and write files work on it, and the addresses are structures with a family, an IP address and a port. This lesson uses the loopback interface, so both ends run on this machine and no external host is involved; the server and the client are two processes created with fork from lesson 67.

Addresses and getaddrinfo

getaddrinfo turns a host name and a service into a list of socket addresses that the system can use. The list may hold an IPv6 entry, an IPv4 entry, or both, and the program tries them in order. With localhost the answer comes from /etc/hosts, so no network is needed.

#include <netdb.h> #include <stdio.h> #include <string.h> int main(void) { struct addrinfo hints; struct addrinfo *result = NULL; memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; int status = getaddrinfo("localhost", "8080", &hints, &result); if (status != 0) { printf("error: %s\n", gai_strerror(status)); return 1; } for (struct addrinfo *entry = result; entry != NULL; entry = entry->ai_next) { printf("family %d, type %d\n", entry->ai_family, entry->ai_socktype); } freeaddrinfo(result); return 0; }

gcc -Wall -Wextra resolve.c -o resolve
./resolve

family 10, type 1
family 2, type 1

10 is AF_INET6 and 2 is AF_INET; type 1 is SOCK_STREAM for both. The IPv6 entry comes first on this system, and a client that tries the list in order connects over whichever family works. The result must be freed with freeaddrinfo.

TCP: bind, listen, accept

A TCP server creates a socket, binds it to an address, listens and accepts. Port zero asks the kernel for a free port, and getsockname reads the number back. The server prints it, then forks a client that connects to the same address and sends four bytes.

#include <arpa/inet.h> #include <netinet/in.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <sys/wait.h> #include <unistd.h> int main(void) { int server = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in address; memset(&address, 0, sizeof address); address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); address.sin_port = 0; if (bind(server, (struct sockaddr *) &address, sizeof address) != 0) { perror("bind"); return 1; } if (listen(server, 1) != 0) { perror("listen"); return 1; } socklen_t length = sizeof address; if (getsockname(server, (struct sockaddr *) &address, &length) != 0) { return 1; } int port = ntohs(address.sin_port); printf("listening on port %d\n", port); pid_t pid = fork(); if (pid == 0) { int client = socket(AF_INET, SOCK_STREAM, 0); if (client < 0) { _exit(1); } if (connect(client, (struct sockaddr *) &address, sizeof address) != 0) { perror("connect"); _exit(1); } write(client, "ping", 4); close(client); _exit(0); } int connection = accept(server, NULL, NULL); char buffer[8] = {0}; ssize_t got = read(connection, buffer, sizeof buffer - 1); printf("server read %zd: %s\n", got, buffer); close(connection); close(server); waitpid(pid, NULL, 0); return 0; }

gcc -Wall -Wextra tcp.c -o tcp
./tcp

listening on port 39129
server read 4: ping

The port is chosen by the kernel and changes on every run, so the printed number is only this run's. The server accepted one connection, read the four bytes and closed, which ends the client's stream. A real server loops over accept and handles each connection until EOF.

UDP: a datagram

UDP keeps the message boundary: one sendto becomes one recvfrom of the same size, with no connection and no retransmission. The receiving socket is bound once, the port is read back from the kernel, and the child sends a five-byte datagram to it.

#include <arpa/inet.h> #include <netinet/in.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <sys/wait.h> #include <unistd.h> int main(void) { int server = socket(AF_INET, SOCK_DGRAM, 0); struct sockaddr_in address; memset(&address, 0, sizeof address); address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); address.sin_port = 0; if (bind(server, (struct sockaddr *) &address, sizeof address) != 0) { return 1; } socklen_t length = sizeof address; getsockname(server, (struct sockaddr *) &address, &length); printf("datagram port %d\n", ntohs(address.sin_port)); pid_t pid = fork(); if (pid == 0) { int client = socket(AF_INET, SOCK_DGRAM, 0); sendto(client, "dgram", 5, 0, (struct sockaddr *) &address, sizeof address); close(client); _exit(0); } char buffer[8] = {0}; ssize_t got = recvfrom(server, buffer, sizeof buffer - 1, 0, NULL, NULL); printf("received %zd: %s\n", got, buffer); close(server); waitpid(pid, NULL, 0); return 0; }

gcc -Wall -Wextra udp.c -o udp
./udp

datagram port 57407
received 5: dgram

The received size is five, the number of bytes in the datagram, and the port again varies. A datagram that arrives when the buffer is full is truncated, and one that never arrives is simply lost; the protocol trades the reliability of TCP for the message shape and the lower overhead.

The byte order of a port

The port in a sockaddr_in is in network byte order, which is big-endian. On x86_64 the host is little-endian, so the number has to go through htons before the call and through ntohs after it. Assigning 8080 directly binds a different port, and the program shows which:

#include <arpa/inet.h> #include <netinet/in.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <unistd.h> int main(void) { int server = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in address; memset(&address, 0, sizeof address); address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); address.sin_port = 8080; if (bind(server, (struct sockaddr *) &address, sizeof address) != 0) { perror("bind"); return 1; } socklen_t length = sizeof address; getsockname(server, (struct sockaddr *) &address, &length); printf("asked for 8080, bound %d\n", ntohs(address.sin_port)); close(server); return 0; }

gcc -Wall -Wextra htons.c -o htons
./htons

asked for 8080, bound 36895

8080 is 0x1F90, and the missing conversion hands the kernel 0x901F, which is 36895. The fix is address.sin_port = htons(8080), and the matching ntohs on the value read back. The server still starts, which is what makes the mistake quiet: it listens on a port the client does not expect.

Exercises

  1. Change the TCP exchange to send a reply from the server and read it in the client before both close.
  2. Print the address returned by accept with inet_ntop and check that it is 127.0.0.1.
  3. Make the UDP program send two datagrams and read them with two calls; check that the boundaries are preserved.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does getaddrinfo return?

    Answer

    A list of addresses to try in order

    The result is a linked list of socket addresses, and the caller frees it with freeaddrinfo.

  2. What does binding port zero ask the system to do?

    Answer

    Choose a free port for the socket

    The kernel picks an unused port, and getsockname reads the chosen number back.

  3. How does UDP differ from TCP in this lesson?

    Answer

    One send is one received datagram

    UDP keeps the message boundary and does not retransmit, so the size of the received datagram matches the send.

  4. Why convert a port with htons before assigning it?

    Answer

    The socket address stores network byte order

    The field is big-endian on every machine, and htons converts from the host order.

  5. Which families does the resolver print for localhost?

    Answer

    10 and then 2, both stream sockets

    AF_INET6 is 10 and AF_INET is 2, and both entries have SOCK_STREAM type 1.

  6. What does this program report after the client sends four bytes? #include <arpa/inet.h> #include <netinet/in.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <sys/wait.h> #include <unistd.h> int main(void){int s=socket(AF_INET,SOCK_STREAM,0);struct sockaddr_in a;memset(&a,0,sizeof a);a.sin_family=AF_INET;a.sin_addr.s_addr=htonl(INADDR_LOOPBACK);a.sin_port=0;bind(s,(struct sockaddr*)&a,sizeof a);listen(s,1);socklen_t l=sizeof a;getsockname(s,(struct sockaddr*)&a,&l);pid_t p=fork();if(p==0){int c=socket(AF_INET,SOCK_STREAM,0);connect(c,(struct sockaddr*)&a,sizeof a);write(c,"ping",4);close(c);_exit(0);}int c=accept(s,0,0);char b[8]={0};ssize_t n=read(c,b,7);printf("%zd %s\n",n,b);close(c);close(s);waitpid(p,0,0);return 0;}

    Answer

    4 ping

    The client writes ping, and the server reads the four bytes before the connection closes.

  7. What does this program report for its five-byte datagram? #include <arpa/inet.h> #include <netinet/in.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <sys/wait.h> #include <unistd.h> int main(void){int s=socket(AF_INET,SOCK_DGRAM,0);struct sockaddr_in a;memset(&a,0,sizeof a);a.sin_family=AF_INET;a.sin_addr.s_addr=htonl(INADDR_LOOPBACK);a.sin_port=0;bind(s,(struct sockaddr*)&a,sizeof a);socklen_t l=sizeof a;getsockname(s,(struct sockaddr*)&a,&l);pid_t p=fork();if(p==0){int c=socket(AF_INET,SOCK_DGRAM,0);sendto(c,"dgram",5,0,(struct sockaddr*)&a,sizeof a);close(c);_exit(0);}char b[8]={0};ssize_t n=recvfrom(s,b,7,0,0,0);printf("%zd %s\n",n,b);close(s);waitpid(p,0,0);return 0;}

    Answer

    5 dgram

    The datagram is five bytes, and recvfrom returns exactly that count.

  8. What port does this program print when it assigns 8080 without htons? #include <arpa/inet.h> #include <netinet/in.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <unistd.h> int main(void){int s=socket(AF_INET,SOCK_STREAM,0);struct sockaddr_in a;memset(&a,0,sizeof a);a.sin_family=AF_INET;a.sin_addr.s_addr=htonl(INADDR_LOOPBACK);a.sin_port=8080;bind(s,(struct sockaddr*)&a,sizeof a);socklen_t l=sizeof a;getsockname(s,(struct sockaddr*)&a,&l);printf("%d\n",ntohs(a.sin_port));close(s);return 0;}

    Answer

    36895

    The bytes are swapped, so 0x1F90 is read as 0x901F, which is 36895.

  9. What is the visible effect of a missing htons?

    Answer

    The server listens on a different port than expected

    The server still starts, but on a port the client does not use, which makes the mistake quiet.

  10. Why can one read on a TCP connection return only part of the request?

    Answer

    TCP is a stream without message boundaries

    The bytes arrive as a stream, so a reader must loop until the expected amount or the end-of-file.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev