Networking in C · beginner · ~8 min

connect() — the client side

## 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()`.

Overview

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.

Why it matters

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:

  • Correctness. A connection that you think succeeded but actually failed leads to writing bytes into a dead socket and confusing errors later. Checking the return value of connect() is the difference between a clear "connection refused" message and a mysterious crash three functions down.
  • Responsiveness. A blocking 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.
  • Reliability and diagnostics. The 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.

Core concepts

1. The client socket lifecycle

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?

2. The TCP three-way handshake

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?

3. Blocking vs. non-blocking connect

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.

4. Reading errors with errno

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?

Syntax notes

The 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.
  • Pass sizeof a (the specific struct's size), not sizeof(struct sockaddr).
  • Return value is 0 on success, -1 on error (then read errno).

Lesson

What connect() does

connect(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:

  • The connection succeeds, and it returns 0.
  • The connection fails, and it returns -1 with errno set.

Common connect errors

  • 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).

After a successful connect

Once connect() returns 0, treat fd like any other file descriptor:

  • Use write() to send data.
  • Use read() to receive data.

Code examples

#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.

Line by line

Walking through the example in execution order:

  1. 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).

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. close(fd) sends a FIN, tears down the connection, and frees the descriptor so the program leaks nothing.

Common mistakes

Mistake 1: Not checking the return value of 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.

Mistake 2: Forgetting htons on the port

addr.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.

Mistake 3: Passing the wrong size or forgetting the cast

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.

Mistake 4: Treating a blocking connect as instant

/* 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.

Mistake 5: Not null-terminating after 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.

Debugging tips

Compiler errors

  • "implicit declaration of function 'connect'" — you forgot #include <sys/socket.h>.
  • "'INADDR_LOOPBACK' undeclared" / "htonl undeclared" — add #include <netinet/in.h> and #include <arpa/inet.h>.
  • "incompatible pointer type" on the connect call — you forgot the (struct sockaddr *) cast on your sockaddr_in.

Runtime errors (read 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.
  • Program hangs forever on connect() — blocking connect to an unresponsive host. Add a timeout or use non-blocking mode.

Logic errors

  • You connect but read garbage / extra characters — you likely printed buf without null-terminating after read().
  • Connect succeeds but write does nothing visible — the server may not be echoing; test with a simple listener like nc -l 8080 on the server side.

Questions to ask when it does not work

  1. Is a server actually listening on that exact IP and port right now?
  2. Did I htons() the port and htonl() the address?
  3. Did I check the return value and perror it to see the real errno?
  4. Am I on loopback (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.

Memory safety

Although connect() itself is about the network, the surrounding code has the usual C safety concerns:

  • Initialize the address struct. Always memset(&addr, 0, sizeof addr) before filling fields. Uninitialized padding bytes are undefined behavior to rely on and can cause subtle bugs.
  • Correct length argument. Passing a wrong addrlen to connect() can make the kernel read past or short of your struct. Use sizeof addr where addr is the actual variable.
  • Buffer bounds on read. 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.
  • No leaked descriptors. Every exit path (including error paths) must close(fd). A leaked fd is a resource leak; long-running clients can hit the open-file limit.
  • Validate return values. 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.
  • Robustness habit: treat all external input (bytes from the network) as untrusted — never assume length, content, or that the peer follows your protocol.

Real-world uses

Where client-side connect() shows up

  • Web browsers and HTTP clients (curl, wget) call connect() to open a TCP connection to a web server before sending the HTTP request.
  • Database drivers (PostgreSQL libpq, MySQL connectors) connect() to the DB server's port (5432, 3306) at the start of every session.
  • Messaging and chat apps keep a long-lived connect()ed socket to their server for real-time delivery.
  • Monitoring and health checks do a bare connect() to a port just to test "is this service up?" — success means the port is open.
  • Package managers and CI runners connect() to mirrors and registries to download artifacts.

Professional best practices

Beginner rules:

  • Always check the return value and perror()/log the failure with the operation name.
  • Always htons the port and htonl numeric addresses.
  • Always close() the socket on every exit path.
  • Reserve room for the null terminator before printing received bytes.

Advanced habits:

  • Use timeouts. Never let a blocking connect() hang indefinitely — set a connect timeout via non-blocking mode + poll(), or SO_SNDTIMEO.
  • Prefer getaddrinfo() to resolve hostnames and to write code that works for both IPv4 and IPv6, instead of hardcoding AF_INET.
  • Retry with backoff on transient failures (ECONNREFUSED, ETIMEDOUT) rather than hammering the server in a tight loop.
  • Log diagnostics, not secrets. Record the destination host/port and the errno, but never log credentials sent over the connection.
  • Handle partial reads/writes — TCP is a byte stream, so loop until you have all the bytes (covered in the send/recv lesson).

Practice tasks

Beginner 1 — Interpret the return value

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.

Beginner 2 — Connect to loopback

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.

Intermediate 1 — Send and receive once

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.

Intermediate 2 — Distinguish the failure reason

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.

Challenge — Non-blocking connect with a timeout

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.

Summary

  • 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.
  • Most important syntax: connect(fd, (struct sockaddr *)&addr, sizeof addr) — cast to the generic type, pass the actual struct size; remember htons(port) and htonl(addr).
  • Return value: 0 = success, -1 = failure (read errno). ECONNREFUSED = nothing listening on that port; ETIMEDOUT = host unreachable; EADDRNOTAVAIL = out of source ports.
  • Blocking by default: the call pauses until the handshake finishes, so an unreachable host can freeze your program — use non-blocking + poll() and EINPROGRESS for responsive clients.
  • Common mistakes: ignoring the return value, forgetting htons, not null-terminating after read(), and leaking the descriptor.
  • Remember: after a successful 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.

Practice with these exercises