Networking in C · beginner · ~8 min
## What you will learn - Open a TCP connection from a client program to a listening server using `connect()`. - Fill in a `struct sockaddr_in` correctly (family, port, address) and pass it to `connect()` with the right cast and length. - Interpret the return value of a blocking `connect()` (`0` vs `-1`) and read `errno` to tell *why* it failed. - Recognize the most common connect errors — `ECONNREFUSED`, `ETIMEDOUT`, `EADDRNOTAVAIL` — and what each one tells you about the network. - Understand what a *non-blocking* `connect()` does (`EINPROGRESS`) and why it matters for responsive programs. - Use the connected socket as an ordinary file descriptor with `read()`/`write()`, then clean it up with `close()`.
When two programs talk over a network, one side waits for callers (the server) and the other side reaches out to start the conversation (the client). connect() is the single system call that performs the client's job: it takes a socket you already created and drives the TCP three-way handshake to a server you name by address and port.
This lesson builds directly on struct sockaddr_in — addresses for IPv4. There, you learned how to describe where a server lives: an address family (AF_INET), a port number in network byte order (htons), and an IPv4 address. connect() is what you do with that filled-in structure — you hand it to the kernel and say "reach this place."
In plain terms: a socket starts life as an unconnected endpoint, like a phone that is powered on but not dialing anyone. connect() is dialing the number. If someone picks up (a server is listen()ing and accept()s you), the call succeeds and you now have a two-way pipe. If the line is dead, busy, or never answers, connect() tells you so.
Client-side connect() is everywhere: a web browser fetching a page, a database driver opening a session, a chat app reaching its server, a package manager downloading from a mirror. Once you can connect(), you are ready for the next lesson, send() and recv() — moving bytes, which covers actually exchanging data over the pipe connect() opened.
Terminology you will see: the handshake is the SYN → SYN-ACK → ACK exchange that establishes a TCP connection; blocking means the call pauses your program until it finishes; errno is the global integer the kernel sets to explain why a call failed.
connect() is the entry point for almost every networked client on Earth. If you have ever loaded a web page, sent an email, or run git pull, a connect() happened on your behalf.
Getting it right matters for three concrete reasons:
connect() is the difference between a clear "connection refused" message and a mysterious crash three functions down.connect() to an unreachable host can stall your program for minutes (the OS default TCP timeout). Real clients use timeouts or non-blocking connects so the user is not staring at a frozen window.errno value after a failed connect() is the first diagnostic any network engineer reads. ECONNREFUSED means "the host is up but nothing is listening there," while ETIMEDOUT means "I could not even reach the host." Those point at completely different problems (wrong port vs. firewall/down host), and knowing the difference saves hours of debugging.A TCP client follows a fixed sequence: create → describe destination → connect → use → close.
socket() connect() read()/write() close()
| create | handshake | exchange | release
v endpoint v to server v bytes v fd
[ unconnected ] -> [ connecting... ] -> [ ESTABLISHED ] -> [ CLOSED ]
connect() is the middle step that moves the socket from "just an endpoint" to "a live connection to a specific peer."
When to use: every time your program is the one initiating a TCP conversation.
When NOT to: servers do not call connect() on their listening socket — they call bind() + listen() + accept(). connect() is strictly the client's job.
Pitfall: calling read()/write() before connect() returns 0 — the socket is not connected yet, so the kernel rejects the I/O.
Knowledge check: In one sentence, why does a server never call connect() on the socket it listens on?
connect() does not just "set a destination" — it performs a real network exchange before returning success.
Client Server
| ---------- SYN ---------> | "I want to talk"
| <------ SYN + ACK ------- | "OK, and I want to talk too"
| ---------- ACK ---------> | "Confirmed"
| |
|====== connection ESTABLISHED ======|
For a blocking socket, connect() returns 0 only after all three packets have completed. That is why a slow or unreachable server makes connect() pause: it is waiting for the SYN-ACK that may never come.
How it works internally: the kernel sends the SYN, retransmits it if there is no reply (with backoff), and either receives the SYN-ACK (success), receives a RST packet (refused), or gives up after the timeout (timed out).
Pitfall: assuming connect() is instant. On loopback it nearly is; across the Internet it has real latency and can fail.
Knowledge check: If a server sends back a TCP RST instead of a SYN-ACK, which errno will the client most likely see?
By default a socket is blocking: connect() does not return until the handshake completes or fails. This is simple to reason about but can freeze your program.
A socket switched to non-blocking mode (via fcntl(fd, F_SETFL, O_NONBLOCK)) makes connect() return immediately:
| Mode | Return on a slow handshake | Meaning |
|---|---|---|
| Blocking | waits, then 0 or -1 |
finished |
| Non-blocking | -1, errno == EINPROGRESS |
handshake started, not done yet |
With non-blocking mode you then use select()/poll()/epoll() to learn when the socket becomes writable, which signals the handshake finished. You check the actual outcome with getsockopt(fd, SOL_SOCKET, SO_ERROR, ...).
blocking: connect() ----[ waits for handshake ]----> returns 0/-1
non-blocking: connect() --> returns -1 (EINPROGRESS) immediately
|
poll() until writable
|
getsockopt(SO_ERROR) -> 0 = success
When to use non-blocking: GUIs, servers, or any client that must stay responsive or manage many connections at once.
Pitfall: treating EINPROGRESS as a fatal error. It is normal and expected for a non-blocking connect — it is not a failure.
Knowledge check: Predict the return value and errno of the first connect() call on a freshly created non-blocking socket to a real but distant server.
When connect() returns -1, the reason lives in errno. The same -1 can mean very different things:
| errno | Plain meaning | Typical cause |
|---|---|---|
ECONNREFUSED |
host is up, nothing listening on that port | wrong port, server not started |
ETIMEDOUT |
no answer at all | host down or firewall dropping packets |
EADDRNOTAVAIL |
no usable source port/address | ran out of ephemeral ports (rare in labs) |
ENETUNREACH |
no route to that network | bad routing / no network |
perror("connect") prints a human-readable version of the current errno. Always read errno immediately after the failed call, before any other library call can overwrite it.
Pitfall: calling something else (even printf) between the failed connect() and reading errno, which can clobber the value.
Knowledge check: You start a client against 127.0.0.1:8080 but forgot to start the server. Which errno do you expect, and why is it not ETIMEDOUT?
connect() signature#include <sys/socket.h>
int connect(int sockfd,
const struct sockaddr *addr, // pointer to a filled address struct
socklen_t addrlen); // size of that struct
Annotated call for an IPv4 TCP client:
struct sockaddr_in a;
memset(&a, 0, sizeof a); // zero every byte first
a.sin_family = AF_INET; // IPv4
a.sin_port = htons(8080); // port in NETWORK byte order
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // 127.0.0.1
int rc = connect(fd,
(struct sockaddr *)&a, // cast: connect wants the generic type
sizeof a); // tell the kernel the struct size
Key points:
connect() takes a struct sockaddr *, but you fill a struct sockaddr_in — hence the cast. This is the standard "generic vs. specific" socket address pattern from the prereq lesson.sizeof a (the specific struct's size), not sizeof(struct sockaddr).0 on success, -1 on error (then read errno).connect() doesconnect(fd, &addr, sizeof addr);
This call starts the TCP handshake (the three-step exchange that opens a TCP connection) with the server at addr.
By default, connect() blocks — your program pauses there until one of two things happens:
0.-1 with errno set.ECONNREFUSED — nothing is listening on the target port.ETIMEDOUT — the remote host did not answer in time.EADDRNOTAVAIL — you ran out of source ports (rare in lab settings).Once connect() returns 0, treat fd like any other file descriptor:
write() to send data.read() to receive data.#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#define SERVER_PORT 8080
int main(void) {
/* 1. Create a TCP/IPv4 socket. */
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
perror("socket");
return 1;
}
/* 2. Describe the destination: loopback, port 8080. */
struct sockaddr_in addr;
memset(&addr, 0, sizeof addr); /* zero padding bytes too */
addr.sin_family = AF_INET;
addr.sin_port = htons(SERVER_PORT); /* host -> network byte order */
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
/* 3. Perform the TCP handshake (blocking by default). */
if (connect(fd, (struct sockaddr *)&addr, sizeof addr) < 0) {
perror("connect"); /* prints e.g. "connect: Connection refused" */
close(fd); /* clean up even on the error path */
return 1;
}
/* 4. Connected: the socket is now an ordinary file descriptor. */
const char *msg = "ping\n";
if (write(fd, msg, strlen(msg)) < 0) {
perror("write");
close(fd);
return 1;
}
/* 5. Read the server's reply. */
char buf[64];
ssize_t n = read(fd, buf, sizeof buf - 1); /* leave room for '\0' */
if (n < 0) {
perror("read");
close(fd);
return 1;
}
if (n > 0) {
buf[n] = '\0'; /* read() does NOT null-terminate */
printf("server replied: %s", buf);
}
/* 6. Release the descriptor. */
close(fd);
return 0;
}
What it does: creates an IPv4 TCP socket, fills in the loopback address on port 8080, connects to a server listening there, sends ping\n, reads up to 63 bytes of reply, prints it, and closes the socket. Every system call is error-checked, and fd is closed on every exit path.
Expected output (assuming a server is listening on 127.0.0.1:8080 that echoes back what it receives):
server replied: ping
If no server is running on that port, there is no crash — you get a clean error instead:
connect: Connection refused
Edge cases to note: read() returning 0 means the peer closed the connection (end of stream), which is different from -1 (an error). write() and read() may transfer fewer bytes than requested over real networks — the next lesson, send() and recv(), covers handling partial transfers. Compile with: cc -std=c11 -Wall -o client client.c.
Walking through the example in execution order:
socket(AF_INET, SOCK_STREAM, 0) asks the kernel for a new TCP/IPv4 endpoint and returns a small non-negative integer (the file descriptor, e.g. 3). At this point the socket exists but points nowhere. We check fd < 0 because socket creation can fail (e.g. resource limits).
memset(&addr, 0, sizeof addr) sets every byte of the address struct to 0. This matters because struct sockaddr_in has padding bytes (sin_zero) that must be zeroed; leaving them as garbage can confuse the kernel.
Filling the fields: sin_family = AF_INET selects IPv4. sin_port = htons(8080) converts the port from host byte order to network byte order — on a little-endian machine the two bytes of 8080 get swapped. sin_addr.s_addr = htonl(INADDR_LOOPBACK) sets the address to 127.0.0.1, also in network byte order.
connect(fd, (struct sockaddr *)&addr, sizeof addr) is the heart of the lesson. The kernel sends a SYN to 127.0.0.1:8080. Because the socket is blocking, the call does not return until the handshake finishes.
| Scenario | Kernel sees | connect() returns |
errno |
|---|---|---|---|
| Server listening | SYN-ACK | 0 |
(unset) |
| Nothing on port | RST | -1 |
ECONNREFUSED |
| Host unreachable | nothing, times out | -1 |
ETIMEDOUT |
If it returns < 0, perror("connect") reads errno and prints a readable reason, we close(fd), and exit.
write(fd, msg, strlen(msg)) now works because the socket is ESTABLISHED. The bytes p i n g \n are handed to the kernel's send buffer and travel to the server.
read(fd, buf, sizeof buf - 1) blocks until the server sends something. Say the server echoes ping\n: read returns 5 and copies those 5 bytes into buf. We set buf[5] = '\0' so printf has a proper C string — read() itself never adds a terminator. If read returned 0, the server closed; if -1, it errored.
close(fd) sends a FIN, tears down the connection, and frees the descriptor so the program leaks nothing.
connect()/* WRONG */
connect(fd, (struct sockaddr *)&addr, sizeof addr);
write(fd, "hi", 2); /* writing into a socket that may never have connected */
Why it is wrong: if connect() failed, fd is not connected, and write() will fail (or worse, you misread later errors). You also lose the clear errno diagnostic.
/* CORRECT */
if (connect(fd, (struct sockaddr *)&addr, sizeof addr) < 0) {
perror("connect");
close(fd);
return 1;
}
Prevent it: treat every system call as fallible; check < 0 immediately.
htons on the portaddr.sin_port = 8080; /* WRONG: host byte order */
Why it is wrong: on a little-endian CPU the bytes are reversed, so you actually try to connect to port 36895. The fix is always htons:
addr.sin_port = htons(8080); /* CORRECT */
Recognize it: you get ECONNREFUSED even though the server is clearly running on 8080.
connect(fd, (struct sockaddr *)&addr, sizeof(struct sockaddr)); /* fragile */
Why it is risky: always pass sizeof addr (the size of the actual sockaddr_in you filled). Using sizeof(struct sockaddr) happens to be the same size for IPv4 but is the wrong intent and breaks for other families.
/* WRONG assumption: this never hangs */
connect(fd, ...); /* to a host that is down -> can block for ~minutes */
Why it is wrong: a blocking connect() to an unreachable host waits for the full TCP timeout, freezing your program. Fix: for responsive clients, use a non-blocking socket plus poll(), or set a connect timeout. Recognize it: your program "hangs" with no output when the network is bad.
read()ssize_t n = read(fd, buf, sizeof buf);
printf("%s", buf); /* WRONG: buf may not be a C string */
Fix: read at most sizeof buf - 1 and set buf[n] = '\0' before printing.
#include <sys/socket.h>.#include <netinet/in.h> and #include <arpa/inet.h>.connect call — you forgot the (struct sockaddr *) cast on your sockaddr_in.errno!)connect: Connection refused (ECONNREFUSED) — the host is reachable but no process is listening on that port. Check the server is actually running and on the same port. Verify with ss -ltn (Linux) or lsof -iTCP -sTCP:LISTEN (macOS).connect: Connection timed out (ETIMEDOUT) — packets are being dropped or the host is down/firewalled. This is a network reachability problem, not a wrong-port problem.connect() — blocking connect to an unresponsive host. Add a timeout or use non-blocking mode.buf without null-terminating after read().write does nothing visible — the server may not be echoing; test with a simple listener like nc -l 8080 on the server side.htons() the port and htonl() the address?perror it to see the real errno?127.0.0.1) as expected, not a stale address?A quick way to prove your client works without writing a server: run nc -l 8080 (netcat listening) in one terminal, then run your client in another.
Although connect() itself is about the network, the surrounding code has the usual C safety concerns:
memset(&addr, 0, sizeof addr) before filling fields. Uninitialized padding bytes are undefined behavior to rely on and can cause subtle bugs.addrlen to connect() can make the kernel read past or short of your struct. Use sizeof addr where addr is the actual variable.read(fd, buf, sizeof buf - 1) must reserve one byte for the '\0'. Reading sizeof buf then writing buf[n] = '\0' when n == sizeof buf is an off-by-one overflow.read() does not null-terminate. Never treat the buffer as a string until you have added the terminator within bounds.close(fd). A leaked fd is a resource leak; long-running clients can hit the open-file limit.socket(), connect(), write(), and read() all return values that signal failure (-1) or end-of-stream (0 for read). Ignoring them is a common source of undefined behavior downstream.connect() shows upcurl, wget) call connect() to open a TCP connection to a web server before sending the HTTP request.libpq, MySQL connectors) connect() to the DB server's port (5432, 3306) at the start of every session.connect()ed socket to their server for real-time delivery.connect() to a port just to test "is this service up?" — success means the port is open.connect() to mirrors and registries to download artifacts.Beginner rules:
perror()/log the failure with the operation name.htons the port and htonl numeric addresses.close() the socket on every exit path.Advanced habits:
connect() hang indefinitely — set a connect timeout via non-blocking mode + poll(), or SO_SNDTIMEO.getaddrinfo() to resolve hostnames and to write code that works for both IPv4 and IPv6, instead of hardcoding AF_INET.ECONNREFUSED, ETIMEDOUT) rather than hammering the server in a tight loop.errno, but never log credentials sent over the connection.Objective: practice reading what connect() reports.
Requirements: Write int connect_succeeded(int rc) that returns 1 when rc == 0 (handshake completed) and 0 otherwise. Example: connect_succeeded(0) → 1; connect_succeeded(-1) → 0. Hint: a blocking connect() returns 0 only on success. Concepts: return-value checking.
Objective: open a TCP connection to a local listener.
Requirements: Write int connect_local(int port) that creates an AF_INET/SOCK_STREAM socket, fills a sockaddr_in for INADDR_LOOPBACK and the given port (remember htons/htonl), calls connect(), and returns the connected fd or -1 on failure (closing the fd on failure). Hint: test it by running nc -l <port> first. Concepts: socket creation, address filling, connect(), cleanup.
Objective: prove a full client round-trip.
Requirements: Extend Beginner 2: after connecting, write() the string "hello\n", then read() up to 127 bytes of reply into a buffer, null-terminate it within bounds, and return the number of bytes read (or -1 on any error). I/O example: against an echo server, sending hello\n yields a reply of 6 bytes. Hint: reserve one byte for '\0'. Concepts: write, read, buffer bounds.
Objective: turn a failed connect() into a meaningful message.
Requirements: Write a function that attempts a connect and, on failure, returns a code: 1 for ECONNREFUSED, 2 for ETIMEDOUT, 3 for any other error, and 0 for success. Read errno immediately after the failed call. Hint: include <errno.h>; do not call anything between the failed connect() and reading errno. Concepts: errno, error classification.
Objective: make connect() give up after a deadline instead of hanging.
Requirements: Set the socket non-blocking with fcntl(fd, F_SETFL, O_NONBLOCK), call connect() (expect -1 with EINPROGRESS), then use poll() to wait at most, say, 2000 ms for the socket to become writable. After poll() returns, check the real outcome with getsockopt(fd, SOL_SOCKET, SO_ERROR, ...). Return: connected fd on success, -1 and a clear message on timeout or error. Constraints: no busy-waiting; respect the timeout. Hint: SO_ERROR of 0 means the handshake succeeded. Concepts: non-blocking sockets, EINPROGRESS, poll(), getsockopt.
connect() is the client's single step to open a TCP connection: it drives the three-way handshake to a server you name with a filled struct sockaddr_in.connect(fd, (struct sockaddr *)&addr, sizeof addr) — cast to the generic type, pass the actual struct size; remember htons(port) and htonl(addr).0 = success, -1 = failure (read errno). ECONNREFUSED = nothing listening on that port; ETIMEDOUT = host unreachable; EADDRNOTAVAIL = out of source ports.poll() and EINPROGRESS for responsive clients.htons, not null-terminating after read(), and leaking the descriptor.connect(), the socket is an ordinary file descriptor — read()/write() it, then close() it on every exit path. Next up: send() and recv() for the actual byte exchange.