JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Writing a TCP Client

Guides 1 and 2 gave you the idea of a socket and the addresses it speaks to; now we write a real client that dials a server, sends a line, and reads the reply. Four calls do almost all the work — and one of them, recv(), hides a trap that bites nearly every beginner.

Four calls, in order

From guide 1 you carry the picture: a TCP socket is a two-way pipe between two programs, and once it is connected you read and write it almost exactly like a file. From guide 2 you carry the other half: a server is named by an IP address and a port, and the numbers travel the wire in network byte order. A client is the side that starts the conversation — it reaches out to a server that is already waiting. Writing one is mostly about doing four client-side calls in the right order: socket(), connect(), send(), recv(), and then close().

Walk the order once, plainly. socket() asks the kernel for a fresh, unconnected socket and hands you back a file descriptor — a small int, the same kind of handle open() gives you for a file. connect() takes that descriptor plus a filled-in address and performs the three-way handshake with the server; when it returns success, you have a live connection. After that, send() pushes bytes out and recv() pulls bytes in, in any pattern your protocol needs. close() tears the connection down when you are done. The whole life of a client fits in those five steps.

Filling in the address

Before connect() can dial anyone, you must hand it an address in the exact shape the kernel expects: a struct sockaddr_in for IPv4. Guide 2 taught the field rules; here is the whole job in one place. You zero the struct, set the family to AF_INET, set the port with htons() so it lands in network byte order, and set the IP with inet_pton(), which parses a text address like "127.0.0.1" into the packed binary the struct wants. Using "127.0.0.1" — the loopback address — means "talk to a server on this very machine", which is exactly what you want while learning, before any real network is involved.

One honest caveat: inet_pton() only parses a numeric IP, not a name like "example.com". Turning a hostname into an address is the job of DNS, done with getaddrinfo() — the modern, IPv4-and-IPv6-friendly way real clients resolve names. We use a literal loopback IP here to keep the first client to a single new idea at a time; getaddrinfo() is a small, worthwhile next step once the bare connect() works.

The whole client, error-checked

Here is a complete client that connects to a server on port 8080 of this machine, sends one line, prints whatever comes back, and quits. Every call that can fail is checked — a socket program that ignores errors will appear to work on your laptop and fall over the first time a port is busy or a server is down. Read it top to bottom; it is exactly the five-step skeleton from the first section, made real.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main(void) {
    int fd = socket(AF_INET, SOCK_STREAM, 0);   /* 1. unconnected TCP socket */
    if (fd < 0) { perror("socket"); return 1; }

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof addr);              /* zero every byte first */
    addr.sin_family = AF_INET;
    addr.sin_port   = htons(8080);              /* port -> network order */
    if (inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr) != 1) {
        fprintf(stderr, "bad address\n");
        close(fd);
        return 1;
    }

    /* 2. connect: performs the three-way handshake */
    if (connect(fd, (struct sockaddr *)&addr, sizeof addr) < 0) {
        perror("connect");
        close(fd);
        return 1;
    }

    const char *msg = "hello\n";               /* 3. send one line */
    if (send(fd, msg, strlen(msg), 0) < 0) {
        perror("send");
        close(fd);
        return 1;
    }

    char buf[256];                             /* 4. read one reply chunk */
    ssize_t n = recv(fd, buf, sizeof buf - 1, 0);
    if (n < 0)      { perror("recv"); close(fd); return 1; }
    else if (n == 0) printf("server closed the connection\n");
    else { buf[n] = '\0'; printf("got: %s", buf); }

    close(fd);                                 /* 5. tear it down */
    return 0;
}

/* build:  gcc -O2 -Wall client.c -o client  &&  ./client */
A complete TCP client. SOCK_STREAM in socket() is what makes it TCP rather than UDP. The cast (struct sockaddr *) on the connect() call is required because the API is older than per-protocol address structs. perror() prints a human-readable reason from errno, which every one of these calls sets on failure.

Two details earn their keep. First, send() and recv() return an ssize_t — a signed size — precisely so they can return -1 for an error; treat the count as a number that might be negative, never as an unsigned that wrapped. Second, that buf[n] = '\0' line matters: recv() gives you raw bytes, not a C string, so it does not place a terminating zero for you. If you printf("%s", buf) without writing that '\0', you print past the real data into whatever garbage follows — a small bug with an ugly payoff.

The trap: recv() is not 'receive one message'

Now the single most important idea in this guide, and the one almost every beginner gets wrong. The client above calls recv() once and assumes it got the whole reply. On a tiny loopback test it usually does — which is exactly why the bug hides. But TCP does not deliver messages; it delivers a byte stream. There are no message boundaries inside that stream. If the server sends "hello world", a single recv() may hand you "hello wor" now and "ld" on the next call. Two send() calls of "abc" and "def" may arrive as one recv() of "abcdef". The stream preserves the order of bytes and nothing about where one send ended.

So the count recv() returns is the count it gave you this time — anywhere from 1 up to your buffer size, and you must be ready for less than you hoped for. This is the network cousin of the short read you met with files: a single call can do part of the work. The honest way to read a known amount, or to read until the server closes, is a loop that keeps calling recv() and accumulating bytes until you have what your protocol says is a complete reply.

  1. Decide what 'a complete reply' means for your protocol — a fixed number of bytes, a line ending in '\n', or 'everything until the server closes'.
  2. Call recv() into the next free slot of your buffer; it returns n, the number of bytes it actually delivered this time.
  3. If n > 0, advance your write position by n and check whether you now hold a complete reply; if not, loop and call recv() again.
  4. If n == 0 the server closed the connection cleanly (end of stream); if n < 0 it is a real error — check errno and stop.

send() can be short too, and other honest edges

The mirror image is just as real: send() can be short too. It returns the number of bytes it actually queued, which may be fewer than you asked for — especially for a large write when the kernel's send buffer is nearly full. The robust pattern is to loop, sending the slice you have not yet sent, advancing a pointer by the returned count each time, until everything is out. For a six-byte "hello\n" on loopback you will essentially never see a short send, which is precisely why beginners write code that breaks only under load. Model the loop now and you never get bitten.

Two more edges worth naming honestly. First, send() and recv() on a TCP socket are very close to write() and read() — in fact write(fd, ...) and read(fd, ...) work on a connected socket — but the named pair carries a useful flags argument (we pass 0 here) and is the conventional choice for network code. Second, every call so far is a blocking call: connect() waits for the handshake, recv() waits for data to arrive, send() can wait for buffer room. That is the simplest, most teachable model, and it is exactly right for a first client. The final guide in this rung shows how to make these calls not wait — useful once you must juggle many connections at once.

What you can now do, and what comes next

Take stock of a genuine skill. You can now build a TCP socket, fill a sockaddr_in with a port in network order and an IP parsed by inet_pton(), connect() across the handshake, send a request and receive a reply, check every return value, and close cleanly. Just as importantly, you understand the one thing that separates working network code from fragile demos: TCP is a byte stream with no message boundaries, so you read and write in loops, never in a single hopeful call.

There is one obvious thing missing: nothing was listening on port 8080, so run this client now and connect() fails with "connection refused". That is the cue for the other half. Guide 4, Writing a TCP Server, builds the program your client has been trying to reach — the side that does bind(), listen(), and accept(), and waits for clients to arrive. Together the two halves form the classic echo client and server you will assemble and run end to end. Then Guide 5 revisits send() and recv() to make them non-blocking, so one program can serve many clients without freezing on any single one.