Source: CS341 Simple TCP Client Example + HTTP
Tags: TCP client, socket programming, C sockets, getaddrinfo, struct addrinfo, connect, socket system call, AF_INET, SOCK_STREAM, UIUC, CS341
Difficulty: Intermediate Prerequisites: OSI model and TCP vs UDP concepts (see companion notes). Basic C programming, including structs and pointers.
Once you understand that TCP is a reliable, connection-oriented protocol at the transport layer, the next question is: how do you use it in code? This material walks through the C system calls needed to resolve a hostname, create a socket, and connect to a remote server. These three steps (resolve, create, connect) form the skeleton of every TCP client you will write in this course. The code pattern here is reusable, and exam questions frequently ask you to fill in the blanks of this exact sequence.
To make a TCP connection in C, you resolve the server's address with getaddrinfo, create a socket with socket, and establish the connection with connect. The struct addrinfo holds address information, and you configure it with AF_INET (IPv4) and SOCK_STREAM (TCP) before passing it through the call chain.
getaddrinfo
A POSIX function that translates a hostname and service name (or port number) into a linked list of struct addrinfo results, each containing a socket address the program can use to connect. Think of it as a phonebook lookup: you give it a name like "illinois.edu" and a port like "80", and it hands back the address(es) you need.
Signature: int getaddrinfo(char host, char service, addrinfo hints, addrinfo *res);
struct addrinfo
A C structure that holds address information for a network host. It contains fields for the address family (ai_family), socket type (ai_socktype), protocol (ai_protocol), and the actual socket address (ai_addr with length ai_addrlen). The ai_next pointer chains multiple results together as a linked list.
struct addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
socklen_t ai_addrlen;
struct sockaddr *ai_addr;
char *ai_canonname;
struct addrinfo *ai_next;
};
memset
A C library function used to zero out a block of memory. When setting up a hints struct, you call memset(&hints, 0, sizeof(hints)) to clear any garbage values before filling in the fields you care about. Failing to do this can cause unpredictable behaviour because uninitialised struct fields may contain random data.
AF_INET
The address family constant for IPv4. Set hints.ai_family = AF_INET to tell getaddrinfo you want IPv4 addresses. For IPv6, you would use AF_INET6. For either, use AF_UNSPEC.
SOCK_STREAM
The socket type constant for a TCP (stream-oriented) socket. Set hints.ai_socktype = SOCK_STREAM to request a TCP connection. The alternative is SOCK_DGRAM for UDP.
socket (system call)
Creates an endpoint for communication and returns a file descriptor.
Signature: int socket(int domain, int type, int protocol);
In a typical TCP client, you pass the ai_family and ai_socktype from the resolved addrinfo result, with 0 for protocol (letting the system choose the default for the given family/type pair).
connect (system call)
Initiates a TCP connection from the local socket to the remote address.
Signature: int connect(int socket, struct sockaddr *address, socklen_t address_len);
You pass the socket file descriptor from socket(), and the ai_addr and ai_addrlen from the resolved addrinfo result.
File descriptor (socket context)
Once a TCP connection is established, the socket file descriptor behaves like a regular file descriptor. You can use read and write (or send and recv) to transfer data over the connection.
Every TCP client in C follows the same three steps:
Resolve the address with getaddrinfo.
Create the socket with socket.
Connect with connect.
After step 3, the socket is ready for read/write.
Declare two struct addrinfo variables: hints (your search criteria) and *result (the output pointer).
Zero out hints with memset(&hints, 0, sizeof(hints)).
Set hints.ai_family = AF_INET for IPv4.
Set hints.ai_socktype = SOCK_STREAM for TCP.
Call getaddrinfo("illinois.edu", "80", &hints, &result).
First argument: the hostname.
Second argument: the service (port number as a string, or a service name like "http").
Third argument: pointer to your hints struct.
Fourth argument: pointer to a pointer that will receive the linked list of results.
Check the return value. If it is non-zero, an error occurred. Use gai_strerror(s) to get a human-readable error message.
Call socket(result->ai_family, result->ai_socktype, 0).
The first two arguments come from the resolved addrinfo.
The third argument (0) lets the OS pick the default protocol for the family/type combination (TCP in this case).
The return value is a file descriptor (sock_fd). If it is -1, the call failed.
Call connect(sock_fd, result->ai_addr, result->ai_addrlen).
First argument: the socket file descriptor from step 2.
Second argument: the socket address from the resolved addrinfo.
Third argument: the length of that address.
If the return value is -1, the connection failed.
int main() {
struct addrinfo hints, *result; // line 02
memset(&hints, 0, sizeof(hints)); // line 03
hints.ai_family = AF_INET; // line 04
hints.ai_socktype = SOCK_STREAM; // line 05
int s = getaddrinfo("illinois.edu", "80", &hints, &result); // line 06
if (s != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
exit(1);
}
int sock_fd = socket(result->ai_family, result->ai_socktype, 0); // line 09
if (sock_fd == -1) { perror("socket"); exit(1); }
int ok = connect(sock_fd, result->ai_addr, result->ai_addrlen); // line 11
if (ok == -1) { perror("connect"); exit(1); }
// Connection is now open. Use read/write on sock_fd.
}
getaddrinfo returns 0 on success and a non-zero error code on failure. Use gai_strerror() to decode it.
socket and connect return -1 on failure. Use perror() to print the system error message.
Always check return values. Networking code that skips error checks will silently fail in unpredictable ways.
The call chain, in order:
getaddrinfo(host, port, &hints, &result)
|
v
socket(result->ai_family, result->ai_socktype, 0)
|
v
connect(sock_fd, result->ai_addr, result->ai_addrlen)
|
v
read / write on sock_fd
This exact three-step pattern is how command-line tools like curl and wget start their connections under the hood. Any program that fetches a web page, sends an API request, or connects to a database server over TCP uses the same resolve-create-connect sequence (or a library wrapper around it).
"You can skip the memset on hints." You cannot safely skip it. Uninitialised struct fields in C contain garbage. If ai_protocol or ai_flags happen to hold non-zero garbage, getaddrinfo may return unexpected results or fail.
"The second argument to getaddrinfo is an integer port." It is a string, not an integer. Pass "80", not 80.
"socket() connects to the server." socket() only creates the endpoint. connect() is the call that actually reaches out to the remote host.
"AF_INET means TCP." AF_INET specifies the address family (IPv4), not the protocol. SOCK_STREAM is what specifies TCP. You can have AF_INET with SOCK_DGRAM for UDP over IPv4.
⚠️ The fill-in-the-blank version of this code is a very common exam format. Know every blank.
⚠️ Know what each argument to getaddrinfo, socket, and connect represents, not just the value.
⚠️ Know why memset is necessary (clearing uninitialised memory).
⚠️ Know the difference between AF_INET (address family) and SOCK_STREAM (socket type). They answer different questions.
⚠️ Be able to explain the purpose of the hints struct vs the result pointer.
Fill in the blank: hints.ai_socktype = ______ for a TCP socket.
True or False: getaddrinfo returns -1 on failure.
Fill in the blank: The second argument to getaddrinfo is the ______ (as a string).
True or False: After a successful connect, you can use read and write on the socket file descriptor.
Fill in the blank: memset(&hints, 0, sizeof(______)) clears the struct before use.
Answers: 1. SOCK_STREAM. 2. False (it returns a non-zero error code, not necessarily -1). 3. Port number or service name. 4. True. 5. hints.
Q: What are the three system calls (in order) needed to establish a TCP client connection in C?
A: getaddrinfo (resolve the address), socket (create the endpoint), connect (initiate the connection).
Q: Why do you call memset on the hints struct before setting its fields?
A: To zero out any uninitialised memory. C does not automatically initialise local struct variables, so fields you do not explicitly set may contain garbage values that cause unexpected behaviour.
Q: What do AF_INET and SOCK_STREAM specify, respectively?
A: AF_INET specifies the address family (IPv4). SOCK_STREAM specifies the socket type (stream-oriented, i.e. TCP).
Q: What is the second argument to getaddrinfo, and what type is it?
A: The service name or port number, passed as a C string (e.g. "80" or "http"), not an integer.
Q: Fill in the blanks for the connect call: connect(sock_fd, ______, ______);
A: connect(sock_fd, result->ai_addr, result->ai_addrlen);
Q: After connect returns 0, how do you send data to the server?
A: Use write(sock_fd, buffer, length) or send(sock_fd, buffer, length, flags) on the socket file descriptor.
This builds directly on the TCP vs UDP material from the companion notes. The reason you set SOCK_STREAM is because TCP is a stream-oriented protocol, as covered there. In later lectures, you will see the server side of this pattern (bind, listen, accept), which mirrors these client-side calls. The read/write interface on TCP sockets also connects to the Unix philosophy of "everything is a file," which appears throughout systems programming.
TCP client, socket programming, C sockets, getaddrinfo, struct addrinfo, socket system call, connect system call, AF_INET, SOCK_STREAM, memset, file descriptor, network programming, POSIX sockets, three-step TCP client, CS341, UIUC, systems programming, client-server model