Client Code
Ephemeral Ports
is_seqnum_cl.c
The Client’s Job
The client program (is_seqnum_cl) connects to the sequence number server, tells it how many sequence numbers it wants, and prints the starting number it received.
The client is simpler than the server because it doesn’t need bind() or listen(). It just creates a socket and calls connect(). The kernel automatically picks a free ephemeral port for the client side of the connection โ no manual port assignment needed.
The client takes two arguments:
- Argument 1 (required): hostname of the server (e.g. “localhost”)
- Argument 2 (optional): how many sequence numbers to request (default = 1)
Compare this to the server โ notice how much simpler the client is. No bind, no listen, no accept. Just create a socket, connect, talk, close.
| Step | Function | What Happens |
|---|---|---|
| 1 | getaddrinfo() |
Resolve server hostname + port into socket address structures |
| 2 | socket() |
Create socket fd |
| 3 | connect() |
Connect to server. Kernel assigns ephemeral port automatically. |
| 4 | write() |
Send request to server (how many numbers we want) |
| 5 | readLine() |
Read server’s response (the starting sequence number) |
| 6 | close() |
Close the connection |
The client’s hints are almost identical to the server’s but without AI_PASSIVE. The node is the actual server hostname, not NULL.
struct addrinfo hints;
struct addrinfo *result, *rp;
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_canonname = NULL;
hints.ai_addr = NULL;
hints.ai_next = NULL;
hints.ai_family = AF_UNSPEC; /* IPv4 or IPv6 โ we don't care */
hints.ai_socktype = SOCK_STREAM; /* TCP */
hints.ai_flags = AI_NUMERICSERV; /* Port is numeric, not a name */
/* NOTE: No AI_PASSIVE โ this is a client, not a server */
/* argv[1] is the server hostname, PORT_NUM is "50000" */
if (getaddrinfo(argv[1], PORT_NUM, &hints, &result) != 0)
errExit("getaddrinfo");
The key difference from the server: the first argument is the actual server hostname (from command line), and there is no AI_PASSIVE flag. The client wants to connect to a specific host, not bind to all interfaces.
The client uses the same loop pattern as the server, but calls connect() instead of bind():
int cfd;
for (rp = result; rp != NULL; rp = rp->ai_next) {
/* Try to create a socket with this address */
cfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (cfd == -1)
continue; /* socket() failed, try next */
/* Try to connect to the server */
if (connect(cfd, rp->ai_addr, rp->ai_addrlen) != -1)
break; /* connect() succeeded! Exit the loop. */
/* connect() failed: close socket and try next address */
close(cfd);
}
if (rp == NULL)
fatal("Could not connect to any address");
freeaddrinfo(result); /* Done with address list */
When connect() succeeds, the kernel has done several things automatically:
| What connect() does automatically | Detail |
|---|---|
| Assigns ephemeral port | A temporary port from the system’s ephemeral port range |
| Completes TCP 3-way handshake | SYN โ SYN-ACK โ ACK with the server |
| Returns | 0 on success, -1 on failure (server not listening, refused, etc.) |
/* reqLenStr is the number of sequence numbers we want, as a string */
/* Default is "1\n" if no 2nd argument given */
char *reqLenStr = (argc > 2) ? argv[2] : "1";
char seqNumStr[INT_LEN];
ssize_t numRead;
/* Send: how many sequence numbers do we want? */
/* We send it as a newline-terminated string */
if (write(cfd, reqLenStr, strlen(reqLenStr)) != strlen(reqLenStr))
fatal("Partial/failed write (reqLenStr)");
/* Some implementations need a newline at the end โ add it */
if (write(cfd, "\n", 1) != 1)
fatal("Partial/failed write (newline)");
/* Read the server's response: the starting sequence number */
numRead = readLine(cfd, seqNumStr, INT_LEN);
if (numRead == -1)
errExit("readLine");
if (numRead == 0)
fatal("Server closed connection unexpectedly");
/* Print the sequence number */
printf("Sequence number: %s", seqNumStr);
close(cfd);
return 0;
The protocol is symmetric: both sides exchange newline-terminated ASCII strings. This simple design makes the server easy to test with telnet and avoids any issues with network byte ordering of integers.
When the client calls connect() without having called bind() first, the kernel automatically assigns a port number to the client socket. This temporary, kernel-chosen port is called an ephemeral port.
“Ephemeral” means “short-lived” โ the port only exists for the duration of the connection. Once the client closes the socket, that port becomes available again.
| Side | IP Address | Port | Who assigns it? |
|---|---|---|---|
| Server | 0.0.0.0 (all interfaces) | 50000 (fixed, well-known) | Programmer, via bind() |
| Client | 192.168.1.50 (its own IP) | 33273 (temporary) | Kernel automatically |
From the example output in the PDF, you can see the ephemeral ports incrementing:
$ ./is_seqnum_cl localhost
Connection from (localhost, 33273) <-- client got port 33273
$ ./is_seqnum_cl localhost 10
Connection from (localhost, 33274) <-- next client got port 33274
$ ./is_seqnum_cl localhost
Connection from (localhost, 33275) <-- another, port 33275
The server’s log shows the client’s ephemeral port because of the getnameinfo() call on the address returned by accept().
Linux allocates ephemeral ports from a specific range. You can see and change this range using the proc filesystem:
/* Check the current ephemeral port range */
$ cat /proc/sys/net/ipv4/ip_local_port_range
32768 60999
/* This means Linux uses ports 32768 to 60999 for ephemeral ports */
/* About 28,000 ports available for client connections */
How Linux allocates them: instead of always starting from the low end (which would be predictable and create hot spots), Linux optimizes by starting allocations from different points in the range to minimize hash table collisions in the kernel’s internal table of socket bindings.
When the upper limit is reached, the kernel wraps around and starts looking for free ports from the low end of the range again.
| What you can do with ip_local_port_range | Command |
|---|---|
| Read current range | cat /proc/sys/net/ipv4/ip_local_port_range |
| Expand range (more client connections) | echo "1024 65535" > /proc/sys/net/ipv4/ip_local_port_range |
| Make permanent | Add to /etc/sysctl.conf: net.ipv4.ip_local_port_range = 1024 65535 |
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include "is_seqnum.h" /* PORT_NUM, INT_LEN */
int main(int argc, char *argv[])
{
char *reqLenStr; /* Sequence length to request */
char seqNumStr[INT_LEN]; /* Sequence number received from server */
int cfd;
ssize_t numRead;
struct addrinfo hints, *result, *rp;
if (argc < 2 || strcmp(argv[1], "--help") == 0) {
fprintf(stderr, "Usage: %s server-host [sequence-len]\n", argv[0]);
return 1;
}
/* Prepare hints: client mode, no AI_PASSIVE */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_NUMERICSERV; /* Port is numeric */
/* Resolve server hostname */
if (getaddrinfo(argv[1], PORT_NUM, &hints, &result) != 0)
errExit("getaddrinfo");
/* Walk the address list, try to connect */
for (rp = result; rp != NULL; rp = rp->ai_next) {
cfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (cfd == -1) continue;
if (connect(cfd, rp->ai_addr, rp->ai_addrlen) != -1)
break; /* Success */
close(cfd);
}
if (rp == NULL) fatal("Could not connect to any address");
freeaddrinfo(result);
/* Default: request 1 sequence number */
reqLenStr = (argc > 2) ? argv[2] : "1";
/* Send the request as a newline-terminated string */
if (write(cfd, reqLenStr, strlen(reqLenStr)) != strlen(reqLenStr))
fatal("Partial write (reqLen)");
if (write(cfd, "\n", 1) != 1)
fatal("Partial write (newline)");
/* Read the server's response */
numRead = readLine(cfd, seqNumStr, INT_LEN);
if (numRead == -1) errExit("readLine");
if (numRead == 0) fatal("Unexpected EOF from server");
printf("Sequence number: %s", seqNumStr);
close(cfd);
return 0;
}
| Aspect | Server (is_seqnum_sv) | Client (is_seqnum_cl) |
|---|---|---|
| getaddrinfo node | NULL (wildcard) | argv[1] (server hostname) |
| AI_PASSIVE flag | Yes (for binding) | No |
| bind() | Yes (port 50000) | No (kernel assigns ephemeral port) |
| listen() | Yes (BACKLOG=50) | No |
| accept() | Yes (waits for client) | No |
| connect() | No | Yes (connects to server) |
| Runs how long | Forever (infinite loop) | One request then exits |
| SIGPIPE handling | Ignored (SIG_IGN) | Not needed |
Here is what the full demo from the book looks like, with explanation:
/* Terminal 1: Start the server in the background */
$ ./is_seqnum_sv &
[1] 4075
/* Terminal 2: Client 1 requests 1 sequence number (default) */
$ ./is_seqnum_cl localhost
Connection from (localhost, 33273) /* server prints this */
Sequence number: 0 /* client prints this */
/* Client 2 requests 10 sequence numbers */
$ ./is_seqnum_cl localhost 10
Connection from (localhost, 33274)
Sequence number: 1 /* starts from 1, client gets 1-10 */
/* Client 3 requests 1 sequence number */
$ ./is_seqnum_cl localhost
Connection from (localhost, 33275)
Sequence number: 11 /* counter advanced by 10, now at 11 */
Notice: each client gets a different ephemeral port (33273, 33274, 33275) and the sequence counter correctly advances based on how many numbers each client claimed.
Both the server and client include a common header file that defines shared constants. This is good practice โ it keeps the port number and buffer size in one place:
/* is_seqnum.h - shared between server and client */
#ifndef IS_SEQNUM_H
#define IS_SEQNUM_H
#include <netdb.h>
#include <sys/socket.h>
#define PORT_NUM "50000" /* Port the server listens on, as a string */
#define INT_LEN 30 /* Buffer size for integer-as-string */
/* readLine(): read characters until newline or buffer full.
Returns number of chars read, 0 on EOF, -1 on error. */
ssize_t readLine(int fd, void *buffer, size_t n);
#endif
readLine() is a custom function (not from standard library) that reads one character at a time from the socket until it sees a newline, making it easy to handle newline-delimited text protocols.
Q1. What is an ephemeral port and how does a client get one?
An ephemeral port is a temporary port number assigned by the kernel to a client socket. When a client calls connect() without having called bind() first, the kernel automatically picks a free port from the ephemeral range (32768โ60999 on Linux by default) and assigns it to the socket.
Q2. Why doesn’t the client need to call bind()?
bind() is needed on the server to attach the socket to a well-known port that clients can find. The client doesn’t care which port it uses locally โ any port that’s free will do. The kernel handles this automatically when the client calls connect().
Q3. What does connect() return on failure and what are common reasons?
It returns -1 and sets errno. Common reasons include: ECONNREFUSED (no server listening on that port), ETIMEDOUT (no response from server), ENETUNREACH (no route to host), and ECONNRESET (server actively rejected the connection).
Q4. What is the ephemeral port range on Linux and how do you change it?
The default range is 32768โ60999, controlled by /proc/sys/net/ipv4/ip_local_port_range. You can expand it by writing a new range to that file or by adding it to /etc/sysctl.conf for persistence across reboots.
Q5. Why does the client use AF_UNSPEC instead of AF_INET?
Using AF_UNSPEC makes the client protocol-independent โ it will work whether the server is reachable via IPv4 or IPv6. If the server supports IPv6, the client can connect via IPv6 without any code changes. If only IPv4 is available, the client falls back to IPv4.
Q6. In the demo, why do the ephemeral ports increment sequentially (33273, 33274, 33275)?
The Linux kernel allocates ephemeral ports in a way that minimizes hash table collisions for its internal socket table. When ports are allocated quickly in succession from the same machine, they tend to be adjacent numbers. The kernel wraps around to the low end of the range when the high end is exhausted.
Q7. What is the purpose of the readLine() function? Why not use fgets()?
readLine() reads from a socket file descriptor one character at a time until a newline is found. fgets() works on FILE* streams, and while you can use fdopen() to wrap a socket fd in a FILE*, mixing raw read()/write() calls with buffered I/O on the same fd causes problems. A custom readLine() on the raw fd avoids this.
Q8. Could this client be compiled and linked without the server? Why or why not?
Yes, they are separate programs compiled independently. They share the header file is_seqnum.h for constants and the readLine() function declaration, but the actual implementations are in separate .c files linked into separate executables. The server and client only communicate at runtime, not at compile time.
| Topic | Key Point |
|---|---|
| getaddrinfo() | Single function handling IPv4+IPv6; replaces gethostbyname(); always use it |
| addrinfo hints | Zero with memset, then fill: ai_family=AF_UNSPEC, ai_socktype=SOCK_STREAM |
| AI_PASSIVE | Server flag: with NULL node โ wildcard address for binding |
| sockaddr_storage | Universal address buffer: large enough for IPv4 or IPv6 |
| SO_REUSEADDR | Set before bind() so server can restart without TIME_WAIT issues |
| SIGPIPE | Servers must ignore it to avoid dying when writing to disconnected clients |
| Ephemeral ports | Kernel assigns automatically to clients; range in /proc/sys/net/ipv4/ip_local_port_range |
| getnameinfo() | Convert sockaddr โ readable host/port strings for logging |
| Iterative server | One client at a time; simple but doesn’t scale; OK for learning |
| Text protocol | Newline-terminated strings: easy to debug with telnet, no byte-order issues |
โ Part 1: Overview โ Part 2: Server EmbeddedPathashala Home
