Published on

Network Programming in C using Sockets Part 2 of 6 Advanced C Topics

Authors

Ever wondered how applications communicate across the internet or a local network? The magic often happens through sockets. This article dives into the fundamentals of network programming in C using the standard Berkeley Sockets API, empowering you to build applications that can send and receive data across networks. Get ready to create your first client-server application!

Table of Contents

What is Socket Programming?

At its core, network programming involves writing programs that communicate with other programs over a network (like the internet or a local LAN). A socket is one endpoint of a two-way communication link between two programs running on the network. The Sockets API provides a set of functions and data structures that allow programmers to create and manage these communication endpoints.

The most common model used in network programming is the client-server model:

  • Server: A program that typically runs continuously, listens for incoming connection requests from clients on a specific network address and port, and provides some service (e.g., serving web pages, managing a database, running a game).
  • Client: A program that initiates a connection to a server to request a service or exchange data. Your web browser is a client connecting to web servers.

Core Concepts

Before diving into code, let's grasp some essential concepts:

  1. IP Addresses & Ports:
    • An IP Address (Internet Protocol Address) uniquely identifies a device (computer, server) on a network (e.g., 192.168.1.100 for IPv4 or 2001:0db8:85a3::8a2e:0370:7334 for IPv6).
    • A Port Number is a 16-bit number (0-65535) that identifies a specific application or service running on that device. Think of the IP address as the building address and the port number as the specific apartment number within that building. Standard services have well-known ports (e.g., HTTP uses port 80, HTTPS uses 443, SSH uses 22).
  2. Protocols (TCP vs. UDP):
    • TCP (Transmission Control Protocol): Connection-oriented, reliable, stream-based. It guarantees that data arrives in order and without errors (or it notifies you of failure). It establishes a connection before data transfer begins. Think of it like a phone call. We'll focus on TCP (SOCK_STREAM) in this guide.
    • UDP (User Datagram Protocol): Connectionless, unreliable, datagram-based. It's faster but doesn't guarantee delivery or order. Think of it like sending postcards – they might get lost or arrive out of sequence. Used for things like video streaming or DNS lookups where speed is critical and occasional loss is acceptable (SOCK_DGRAM).
  3. Byte Order (Endianness): Computers store multi-byte numbers differently (Little-Endian vs. Big-Endian). Networks typically use Big-Endian ("Network Byte Order"). The Sockets API provides functions to convert between host byte order and network byte order:
    • htons(): Host to Network Short (16-bit, for ports)
    • htonl(): Host to Network Long (32-bit, for IPv4 addresses)
    • ntohs(): Network to Host Short
    • ntohl(): Network to Host Long
    • Always use these when putting addresses and ports into socket structures or retrieving them!
  4. Socket Descriptors: When you create a socket, the operating system returns an integer, similar to a file descriptor (int socket_fd). You use this descriptor in subsequent socket function calls to refer to that specific socket.

The TCP Server Workflow

A typical TCP server performs the following steps:

  1. socket() - Create a Socket: Get a socket descriptor from the OS.
  2. bind() - Assign Address: Assign a specific IP address and port number to the created socket so clients know where to connect.
  3. listen() - Listen for Connections: Mark the socket as a passive socket that will be used to accept incoming connection requests. Define a queue limit (backlog) for pending connections.
  4. accept() - Accept Connections: Wait (block) until a client tries to connect. When a connection occurs, accept() creates a new socket descriptor dedicated solely to communicating with that specific client. The original listening socket remains open to accept further connections.
  5. read()/recv() & write()/send() - Communicate: Use the new socket descriptor returned by accept() to exchange data with the connected client.
  6. close() - Close Connection: Close the client-specific socket descriptor when communication is finished. The server might then loop back to accept() another connection or close the main listening socket when shutting down.

The TCP Client Workflow

A typical TCP client performs these steps:

  1. socket() - Create a Socket: Get a socket descriptor from the OS.
  2. connect() - Establish Connection: Actively try to connect to the server's IP address and port number. The client needs to know these beforehand.
  3. write()/send() & read()/recv() - Communicate: Exchange data with the server using the socket descriptor.
  4. close() - Close Connection: Close the socket descriptor when communication is finished.

Essential Data Structures and Headers

You'll primarily need these headers:

#include <stdio.h>      // Standard I/O
#include <stdlib.h>     // Standard library (exit)
#include <string.h>     // String manipulation (bzero, strlen)
#include <unistd.h>     // Unix standard functions (read, write, close)
#include <sys/socket.h> // Core socket functions and data structures
#include <netinet/in.h> // Structures for internet addresses (sockaddr_in)
#include <arpa/inet.h>  // Functions for manipulating IP addresses (inet_pton)
#include <errno.h>      // For error number variables (like errno)

The key structure for IPv4 addresses is struct sockaddr_in:

struct sockaddr_in {
    short            sin_family;   // Address family, AF_INET for IPv4
    unsigned short   sin_port;     // Port number (needs to be in Network Byte Order!)
    struct in_addr   sin_addr;     // IP address (needs to be in Network Byte Order!)
    char             sin_zero[8];  // Padding, set to zero
};

struct in_addr {
    unsigned long s_addr;          // 32-bit IPv4 address
};

Simple TCP Server Example (Iterative)

This server listens on port 8080, accepts one connection at a time, reads a message, sends a reply, and closes the connection.

// server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>

#define PORT 8080
#define BUFFER_SIZE 1024

void error_exit(const char *msg) {
    perror(msg);
    exit(EXIT_FAILURE);
}

int main() {
    int listen_fd, conn_fd; // Listening socket, connection socket
    struct sockaddr_in serv_addr, cli_addr;
    socklen_t cli_len;
    char buffer[BUFFER_SIZE];
    int n;

    // 1. Create socket
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        error_exit("ERROR opening socket");
    }
    printf("Socket created successfully.\n");

    // Initialize server address structure
    memset(&serv_addr, 0, sizeof(serv_addr)); // Use memset for portability
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // Listen on any available interface
    serv_addr.sin_port = htons(PORT);          // Set port (Network Byte Order)

    // 2. Bind socket to address and port
    if (bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        close(listen_fd);
        error_exit("ERROR on binding");
    }
    printf("Binding successful on port %d.\n", PORT);

    // 3. Listen for incoming connections
    if (listen(listen_fd, 5) < 0) { // 5 is the backlog queue size
         close(listen_fd);
        error_exit("ERROR on listen");
    }
    printf("Server listening...\n");

    while (1) { // Loop to accept multiple connections sequentially
        cli_len = sizeof(cli_addr);

        // 4. Accept a connection
        // accept() blocks until a client connects
        conn_fd = accept(listen_fd, (struct sockaddr *)&cli_addr, &cli_len);
        if (conn_fd < 0) {
            perror("ERROR on accept"); // Don't exit, just report and try again
            continue; // Continue to next iteration to accept another connection
        }

        // Get client IP address and port for logging
        char client_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &cli_addr.sin_addr, client_ip, sizeof(client_ip));
        printf("Connection accepted from %s:%d\n", client_ip, ntohs(cli_addr.sin_port));

        // --- Communication with the connected client ---
        memset(buffer, 0, BUFFER_SIZE);

        // 5. Read data from client
        n = read(conn_fd, buffer, BUFFER_SIZE - 1); // Read up to BUFFER_SIZE-1 bytes
        if (n < 0) {
            perror("ERROR reading from socket");
        } else if (n == 0) {
             printf("Client disconnected.\n");
        }
        else {
            buffer[n] = '\0'; // Null-terminate the received string
            printf("Received message: %s\n", buffer);

            // 5. Write response to client
            const char *response = "Message received by server.";
            n = write(conn_fd, response, strlen(response));
            if (n < 0) {
                perror("ERROR writing to socket");
            } else {
                printf("Response sent.\n");
            }
        }

        // 6. Close the connection socket for this client
        close(conn_fd);
        printf("Connection closed.\n\n");
    } // End of while loop

    // Close the listening socket (optional here as we loop forever, but good practice)
    // close(listen_fd);
    // return 0;
}

Simple TCP Client Example

This client connects to 127.0.0.1 (localhost) on port 8080, sends a message, reads the reply, and exits.

// client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>

#define SERVER_IP "127.0.0.1" // Loopback address for testing on the same machine
#define PORT 8080
#define BUFFER_SIZE 1024

void error_exit(const char *msg) {
    perror(msg);
    exit(EXIT_FAILURE);
}

int main() {
    int sock_fd;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_SIZE];
    int n;

    // 1. Create socket
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd < 0) {
        error_exit("ERROR opening socket");
    }
    printf("Socket created successfully.\n");

    // Initialize server address structure
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT); // Server port (Network Byte Order)

    // Convert IPv4 address from text to binary form
    if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
        close(sock_fd);
        error_exit("Invalid address/ Address not supported");
    }

    // 2. Connect to the server
    printf("Connecting to server %s:%d...\n", SERVER_IP, PORT);
    if (connect(sock_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        close(sock_fd);
        error_exit("ERROR connecting");
    }
    printf("Connected successfully.\n");

    // --- Communication ---
    // 3. Send message to server
    const char *message = "Hello from C client!";
    printf("Sending message: %s\n", message);
    n = write(sock_fd, message, strlen(message));
    if (n < 0) {
         close(sock_fd);
         error_exit("ERROR writing to socket");
    }

    // 3. Read response from server
    memset(buffer, 0, BUFFER_SIZE);
    printf("Waiting for server response...\n");
    n = read(sock_fd, buffer, BUFFER_SIZE - 1);
    if (n < 0) {
         close(sock_fd);
         error_exit("ERROR reading from socket");
    } else if (n == 0) {
        printf("Server closed connection unexpectedly.\n");
    }
    else {
        buffer[n] = '\0'; // Null-terminate
        printf("Server replied: %s\n", buffer);
    }


    // 4. Close the socket
    close(sock_fd);
    printf("Connection closed.\n");

    return 0;
}

Compiling and Running

Save the code above as server.c and client.c. Compile them using gcc (no special libraries needed for basic sockets on Linux/macOS beyond the standard C library):

gcc server.c -o server
gcc client.c -o client

Now, run them:

  1. Open a terminal window and run the server:

    ./server
    

    You should see output indicating it's listening.

  2. Open a second terminal window and run the client:

    ./client
    

    The client will connect, send its message, receive the reply, print it, and exit.

  3. Look back at the server terminal. You should see messages indicating a connection was accepted, the message received, the response sent, and the connection closed. The server will then go back to listening for the next connection. You can run the client again.

Error Handling

Network programming is prone to errors (network down, server busy, host unreachable, etc.). Always check the return values of socket functions.

  • Most return -1 on error and set the global errno variable.
  • Use perror("Descriptive message") to print the system error message corresponding to errno.
  • Handle errors gracefully – close opened sockets, free resources, and decide whether to retry, abort, or log the error.

Beyond the Basics: Next Steps

This iterative server can only handle one client at a time. Real-world servers need to handle multiple clients concurrently. This typically involves:

  1. Multi-threading: Create a new thread (using pthreads, as discussed in our previous article) for each accepted client connection.
  2. Multi-processing: Create a new process (fork()) for each client connection.
  3. Non-blocking I/O and Multiplexing: Use functions like select(), poll(), or epoll() (on Linux) to manage multiple connections within a single thread without blocking.

Other advanced topics include using UDP (SOCK_DGRAM), implementing secure communication using SSL/TLS (OpenSSL), handling partial reads/writes, and designing robust application-level protocols.

Conclusion

You've taken your first steps into the exciting world of network programming in C! You now understand the client-server model, the roles of IP addresses and ports, the basics of TCP communication, and the core socket API functions (socket, bind, listen, accept, connect, read, write, close). While our examples were simple, they form the foundation for building complex networked applications, from web servers and chat applications to distributed computing systems. Remember to handle errors diligently and consider concurrency models for real-world applications.



Suggested Reading: