Lesson 15-C Language Network Programming Basics

Network Programming Fundamentals

Core Elements of Network Programming

The essence of network programming is inter-process communication via network protocols. The core elements include:
• IP Address: Identifies a host in the network (e.g., 192.168.1.1).
• Port Number: Identifies a process on the host (e.g., HTTP default port 80).
• Socket: The endpoint of network communication, a network interface provided by the operating system.
• Protocol: Communication rules (e.g., TCP, UDP, HTTP).

Core Differences Between TCP and UDP

FeatureTCPUDP
Connection TypeConnection-oriented (three-way handshake)Connectionless (send directly)
ReliabilityReliable (ordered, no loss)Unreliable (possible loss, disorder)
Transmission UnitByte stream (no message boundaries)Datagram (with message boundaries)
Use CasesFile transfer, email, web browsingVideo streaming, games, real-time communication

Byte Order (Endianness)

In network communication, different hosts may use different byte orders (big-endian: high byte first; little-endian: low byte first). Use the following functions for conversion:
htons(): Host to network byte order (short integer).
htonl(): Host to network byte order (long integer).
ntohs(): Network to host byte order (short integer).
ntohl(): Network to host byte order (long integer).

Socket: The Endpoint of Network Communication

A socket is the network interface provided by the operating system, created via the socket function, and is the core object in network programming.

Creating a Socket (socket Function)

The socket function creates a socket descriptor (similar to a file descriptor). Its prototype is:

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

• Parameter Explanation:
domain: Protocol family (e.g., AF_INET for IPv4, AF_INET6 for IPv6).
type: Socket type (e.g., SOCK_STREAM for TCP, SOCK_DGRAM for UDP).
protocol: Specific protocol (usually 0, determined by type).

• Return Value: Returns a socket descriptor (non-negative integer) on success; -1 on failure.

Example: Creating a TCP Socket

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
    perror("socket creation failed");
    exit(EXIT_FAILURE);
}

Binding an Address (bind Function)

The bind function binds a socket to a local IP address and port number. Its prototype is:

int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);

• Parameter Explanation:
sockfd: Socket descriptor.
addr: Pointer to a sockaddr structure (must fill in IP and port).
addrlen: Size of the addr structure.

• Return Value: Returns 0 on success; -1 on failure.

Example: Binding a TCP Socket to Port 8080

#include <netinet/in.h>  // Includes sockaddr_in structure

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));  // Clear structure
server_addr.sin_family = AF_INET;              // IPv4
server_addr.sin_port = htons(8080);            // Port 8080 (network byte order)
server_addr.sin_addr.s_addr = INADDR_ANY;      // Listen on all local IPs

if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
    perror("bind failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}

Listening for Connections (listen Function, TCP Only)

The listen function puts the socket into listening state to wait for client connections. Its prototype is:

int listen(int sockfd, int backlog);

• Parameter Explanation:
sockfd: Bound socket descriptor.
backlog: Maximum number of pending connections (length of the incomplete connection queue).

• Return Value: Returns 0 on success; -1 on failure.

Example: TCP Socket Listening

if (listen(sockfd, 5) == -1) {  // Allow up to 5 pending connections
    perror("listen failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}

Accepting Connections (accept Function, TCP Only)

The accept function accepts a client connection request and returns a new socket descriptor (used for communication with the client). Its prototype is:

int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);

• Parameter Explanation:
sockfd: Listening socket descriptor.
addr: Stores client address information (optional).
addrlen: Size of the addr structure (input/output parameter).

• Return Value: Returns a new socket descriptor on success; -1 on failure.

Example: TCP Server Accepting Client Connection

struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd == -1) {
    perror("accept failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}
printf("Client connected successfully, IP: %s, Port: %d\n", 
       inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

Connecting to a Server (connect Function, Client Only)

The connect function is used by the client to connect to a server. Its prototype is:

int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);

• Parameter Explanation:
sockfd: Created socket descriptor.
addr: Server address information (IP and port).
addrlen: Size of the addr structure.

• Return Value: Returns 0 on success; -1 on failure.

Example: TCP Client Connecting to Server

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);  // Convert IP address

if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
    perror("connect failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}
printf("Connected to server successfully\n");

Sending and Receiving Data

Sending and receiving data are the core operations of network communication. The functions differ slightly between TCP and UDP.

TCP Data Send/Receive (send/recv)

// Send data (TCP)
ssize_t send(int sockfd, const void* buf, size_t len, int flags);

// Receive data (TCP)
ssize_t recv(int sockfd, void* buf, size_t len, int flags);

• Parameter Explanation:
sockfd: Socket descriptor (TCP uses the new descriptor returned by accept).
buf: Buffer for sending/receiving data.
len: Data length.
flags: Control flags (usually 0).

• Return Value: Returns the number of bytes sent/received on success; -1 on failure (TCP) or -1 with errno=EAGAIN (non-blocking mode).

Example: TCP Client Sending Data

char* msg = "Hello, Server!";
ssize_t sent = send(client_fd, msg, strlen(msg), 0);
if (sent == -1) {
    perror("send failed");
    close(client_fd);
    exit(EXIT_FAILURE);
}
printf("Sent %zd bytes\n", sent);

UDP Data Send/Receive (sendto/recvfrom)

// Send data (UDP)
ssize_t sendto(int sockfd, const void* buf, size_t len, int flags,
               const struct sockaddr* dest_addr, socklen_t addrlen);

// Receive data (UDP)
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags,
                 struct sockaddr* src_addr, socklen_t* addrlen);

• Parameter Explanation:
dest_addr (send): Target server address.
src_addr (receive): Stores sender address information.

Example: UDP Server Receiving Data

struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
char buffer[1024];

ssize_t received = recvfrom(sockfd, buffer, sizeof(buffer), 0,
                            (struct sockaddr*)&client_addr, &client_len);
if (received == -1) {
    perror("recvfrom failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}
buffer[received] = '\0';  // String terminator
printf("Received data: %s, from IP: %s, Port: %d\n", buffer,
       inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

TCP Server and Client Practice

TCP Server Implementation Steps

  1. Create socket (socket).
  2. Bind address (bind).
  3. Listen for connections (listen).
  4. Loop to accept client connections (accept).
  5. Communicate with client (recv/send).
  6. Close connection (close).

Example: TCP Echo Server

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) { perror("socket failed"); exit(EXIT_FAILURE); }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed"); close(sockfd); exit(EXIT_FAILURE);
    }

    if (listen(sockfd, 5) == -1) {
        perror("listen failed"); close(sockfd); exit(EXIT_FAILURE);
    }

    printf("TCP server started, listening on port %d...\n", PORT);

    while (1) {
        struct sockaddr_in client_addr;
        socklen_t client_len = sizeof(client_addr);
        int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
        if (client_fd == -1) {
            perror("accept failed"); continue;
        }

        char buffer[BUFFER_SIZE];
        ssize_t received = recv(client_fd, buffer, sizeof(buffer), 0);
        if (received == -1) {
            perror("recv failed"); close(client_fd); continue;
        }
        buffer[received] = '\0';

        printf("Received data: %s\n", buffer);
        send(client_fd, buffer, received, 0);  // Echo data

        close(client_fd);  // Close client connection
    }

    close(sockfd);
    return 0;
}

TCP Client Implementation Steps

  1. Create socket (socket).
  2. Connect to server (connect).
  3. Send data (send).
  4. Receive response (recv).
  5. Close connection (close).

Example: TCP Echo Client

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

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) { perror("socket failed"); exit(EXIT_FAILURE); }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

    if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect failed"); close(sockfd); exit(EXIT_FAILURE);
    }

    char* msg = "Hello, TCP Server!";
    send(sockfd, msg, strlen(msg), 0);
    printf("Sent: %s\n", msg);

    char buffer[BUFFER_SIZE];
    ssize_t received = recv(sockfd, buffer, sizeof(buffer), 0);
    if (received == -1) { perror("recv failed"); close(sockfd); exit(EXIT_FAILURE); }
    buffer[received] = '\0';
    printf("Received response: %s\n", buffer);

    close(sockfd);
    return 0;
}

UDP Server and Client Practice

UDP Server Implementation Steps

  1. Create socket (socket).
  2. Bind address (bind).
  3. Loop to receive data (recvfrom).
  4. Send response (sendto, optional).

Example: UDP Time Server

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <time.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) { perror("socket failed"); exit(EXIT_FAILURE); }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed"); close(sockfd); exit(EXIT_FAILURE);
    }

    printf("UDP time server started, listening on port %d...\n", PORT);

    while (1) {
        struct sockaddr_in client_addr;
        socklen_t client_len = sizeof(client_addr);
        char buffer[BUFFER_SIZE];

        ssize_t received = recvfrom(sockfd, buffer, sizeof(buffer), 0,
                                    (struct sockaddr*)&client_addr, &client_len);
        if (received == -1) {
            perror("recvfrom failed"); continue;
        }
        buffer[received] = '\0';

        time_t now = time(NULL);
        char* time_str = ctime(&now);
        sendto(sockfd, time_str, strlen(time_str), 0,
              (struct sockaddr*)&client_addr, client_len);
    }

    close(sockfd);
    return 0;
}

UDP Client Implementation Steps

  1. Create socket (socket).
  2. Send data (sendto).
  3. Receive response (recvfrom).
  4. Close socket (close).

Example: UDP Time Client

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

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) { perror("socket failed"); exit(EXIT_FAILURE); }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

    char* msg = "Request time";
    sendto(sockfd, msg, strlen(msg), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
    printf("Sent time request\n");

    struct sockaddr_in server_resp;
    socklen_t server_len = sizeof(server_resp);
    char buffer[BUFFER_SIZE];
    ssize_t received = recvfrom(sockfd, buffer, sizeof(buffer), 0,
                                (struct sockaddr*)&server_resp, &server_len);
    if (received == -1) { perror("recvfrom failed"); close(sockfd); exit(EXIT_FAILURE); }
    buffer[received] = '\0';

    printf("Server time: %s", buffer);

    close(sockfd);
    return 0;
}

Advanced Network Programming

Multithreaded TCP Server

A single-threaded TCP server can only handle one client connection at a time. A multithreaded server creates a separate thread for each client to achieve concurrency. A multithreaded server can handle multiple client connections simultaneously, improving server concurrency.

Example: Multithreaded TCP Server

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

#define PORT 8080
#define MAX_CLIENTS 5
#define BUFFER_SIZE 1024

void *handle_client(void *arg) {
    int client_socket = *(int *)arg;
    char buffer[BUFFER_SIZE];

    ssize_t bytes_received = recv(client_socket, buffer, BUFFER_SIZE - 1, 0);
    if (bytes_received <= 0) {
        perror("Receive failed");
        goto cleanup;
    }

    buffer[bytes_received] = '\0';
    printf("Received: %s\n", buffer);

    // Respond to client
    const char *response = "Hello from server";
    send(client_socket, response, strlen(response), 0);

cleanup:
    close(client_socket);
    free(arg);
    pthread_exit(NULL);
}

int main() {
    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(struct sockaddr_in);

    // Create socket
    if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // Set address structure
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(PORT);

    // Bind socket
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("Bind failed");
        exit(EXIT_FAILURE);
    }

    // Start listening
    if (listen(server_socket, MAX_CLIENTS) == -1) {
        perror("Listen failed");
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d\n", PORT);

    while (1) {
        // Accept client connection
        if ((client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &addr_len)) == -1) {
            perror("Accept failed");
            continue;
        }

        int *client_socket_ptr = malloc(sizeof(int));
        *client_socket_ptr = client_socket;

        pthread_t thread_id;
        if (pthread_create(&thread_id, NULL, handle_client, client_socket_ptr) != 0) {
            perror("Thread creation failed");
            free(client_socket_ptr);
            close(client_socket);
            continue;
        }

        pthread_detach(thread_id);
    }

    close(server_socket);

    return 0;
}

Pros and Cons:

• Advantages: Simple to implement, utilizes multi-core CPUs.
• Disadvantages: High overhead for thread creation/destruction, complex inter-thread synchronization (e.g., shared data requires locking).

Non-blocking I/O

Non-blocking I/O allows the server to continue performing other tasks while waiting for client connections or data. Non-blocking I/O sets the socket to non-blocking mode, causing functions like recv/send to return immediately (returning EAGAIN or EWOULDBLOCK if no data is available), avoiding process blocking.

Example: Setting a Socket to Non-blocking Mode

#include <fcntl.h>

int set_non_blocking(int sockfd) {
    int flags = fcntl(sockfd, F_GETFL, 0);
    if (flags == -1) return -1;
    flags |= O_NONBLOCK;  // Add non-blocking flag
    return fcntl(sockfd, F_SETFL, flags);
}

Use Cases:

• Polling multiple sockets (e.g., game servers).
• Avoiding long blocking (e.g., real-time communication).

Example: Non-blocking TCP Server

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

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(struct sockaddr_in);

    // Create socket
    if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // Set address structure
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(PORT);

    // Bind socket
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("Bind failed");
        exit(EXIT_FAILURE);
    }

    // Set socket to non-blocking mode
    if (fcntl(server_socket, F_SETFL, O_NONBLOCK) == -1) {
        perror("Set non-blocking mode failed");
        exit(EXIT_FAILURE);
    }

    // Start listening
    if (listen(server_socket, 5) == -1) {
        perror("Listen failed");
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d\n", PORT);

    while (1) {
        // Accept client connection
        client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &addr_len);
        if (client_socket == -1 && errno == EAGAIN) {
            continue; // No connection request
        } else if (client_socket == -1) {
            perror("Accept failed");
            continue;
        }

        char buffer[BUFFER_SIZE];
        ssize_t bytes_received = recv(client_socket, buffer, BUFFER_SIZE - 1, 0);
        if (bytes_received <= 0) {
            perror("Receive failed");
            close(client_socket);
            continue;
        }

        buffer[bytes_received] = '\0';
        printf("Received: %s\n", buffer);

        // Respond to client
        const char *response = "Hello from server";
        send(client_socket, response, strlen(response), 0);

        close(client_socket);
    }

    close(server_socket);

    return 0;
}

I/O Multiplexing

I/O multiplexing allows a server to monitor multiple sockets for activity simultaneously, improving efficiency.

I/O multiplexing uses a single system call to monitor multiple sockets, notifying the application when a socket becomes readable/writable, avoiding the need to create a thread per socket.

select System Call

select can monitor multiple file descriptors (sockets) for readability, writability, and exceptional conditions.

#include <sys/select.h>

int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);

• Parameter Explanation:
nfds: Maximum file descriptor value + 1.
readfds: Set of file descriptors to monitor for readability.
writefds: Set of file descriptors to monitor for writability.
exceptfds: Set of file descriptors to monitor for exceptions.
timeout: Timeout duration (NULL for blocking).

epoll (Linux-specific)

epoll is a high-performance I/O multiplexing mechanism provided by Linux, supporting event-driven operation (only notifying active sockets), outperforming select and poll.

#include <sys/epoll.h>

// Create epoll instance
int epoll_fd = epoll_create1(0);

// Add event to monitor
struct epoll_event event;
event.data.fd = sockfd;
event.events = EPOLLIN | EPOLLOUT;  // Monitor readable and writable
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &event);

// Wait for events
struct epoll_event events[MAX_EVENTS];
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);  // Block waiting
for (int i = 0; i < n; i++) {
    if (events[i].events & EPOLLIN) {
        // Handle readable event
    }
    if (events[i].events & EPOLLOUT) {
        // Handle writable event
    }
}

Advantages:

• Efficient: Event-driven, only processes active sockets.
• Scalable: Supports a large number of connections (e.g., 100,000+).

Example: I/O Multiplexing Server Using select

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

#define PORT 8080
#define BUFFER_SIZE 1024
#define MAX_CLIENTS 5

int main() {
    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(struct sockaddr_in);

    // Create socket
    if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // Set address structure
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(PORT);

    // Bind socket
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("Bind failed");
        exit(EXIT_FAILURE);
    }

    // Start listening
    if (listen(server_socket, MAX_CLIENTS) == -1) {
        perror("Listen failed");
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d\n", PORT);

    fd_set readfds;
    FD_ZERO(&readfds);
    FD_SET(server_socket, &readfds);

    int max_fd = server_socket;

    while (1) {
        fd_set tmp_fds = readfds;
        int ret = select(max_fd + 1, &tmp_fds, NULL, NULL, NULL);
        if (ret == -1) {
            perror("Select failed");
            continue;
        }

        if (FD_ISSET(server_socket, &tmp_fds)) {
            // Accept client connection
            if ((client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &addr_len)) == -1) {
                perror("Accept failed");
                continue;
            }

            FD_SET(client_socket, &readfds);
            if (client_socket > max_fd) {
                max_fd = client_socket;
            }
        }

        for (int i = 0; i <= max_fd; i++) {
            if (FD_ISSET(i, &tmp_fds)) {
                if (i == server_socket) {
                    continue;
                }

                char buffer[BUFFER_SIZE];
                ssize_t bytes_received = recv(i, buffer, BUFFER_SIZE - 1, 0);
                if (bytes_received <= 0) {
                    perror("Receive failed");
                    FD_CLR(i, &readfds);
                    close(i);
                    continue;
                }

                buffer[bytes_received] = '\0';
                printf("Received: %s\n", buffer);

                // Respond to client
                const char *response = "Hello from server";
                send(i, response, strlen(response), 0);
            }
        }
    }

    close(server_socket);

    return 0;
}

I/O Multiplexing Using epoll

epoll is a high-performance I/O multiplexing mechanism in Linux, especially suitable for handling a large number of concurrent connections.

Example: I/O Multiplexing Server Using epoll

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

#define PORT 8080
#define BUFFER_SIZE 1024
#define MAX_EVENTS 10

int main() {
    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(struct sockaddr_in);

    // Create socket
    if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // Set address structure
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(PORT);

    // Bind socket
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("Bind failed");
        exit(EXIT_FAILURE);
    }

    // Start listening
    if (listen(server_socket, 5) == -1) {
        perror("Listen failed");
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d\n", PORT);

    // Create epoll instance
    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        perror("Epoll create failed");
        exit(EXIT_FAILURE);
    }

    // Add server socket to epoll instance
    struct epoll_event event;
    event.events = EPOLLIN | EPOLLET;
    event.data.fd = server_socket;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_socket, &event) == -1) {
        perror("Epoll add failed");
        exit(EXIT_FAILURE);
    }

    while (1) {
        struct epoll_event events[MAX_EVENTS];
        int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (num_events == -1) {
            perror("Epoll wait failed");
            continue;
        }

        for (int i = 0; i < num_events; i++) {
            if (events[i].data.fd == server_socket) {
                // Accept client connection
                if ((client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &addr_len)) == -1) {
                    perror("Accept failed");
                    continue;
                }

                // Add client socket to epoll instance
                event.events = EPOLLIN | EPOLLET;
                event.data.fd = client_socket;
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_socket, &event) == -1) {
                    perror("Epoll add failed");
                    close(client_socket);
                    continue;
                }
            } else {
                char buffer[BUFFER_SIZE];
                ssize_t bytes_received = recv(events[i].data.fd, buffer, BUFFER_SIZE - 1, 0);
                if (bytes_received <= 0) {
                    perror("Receive failed");
                    // Remove client socket from epoll instance
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
                    close(events[i].data.fd);
                    continue;
                }

                buffer[bytes_received] = '\0';
                printf("Received: %s\n", buffer);

                // Respond to client
                const char *response = "Hello from server";
                send(events[i].data.fd, response, strlen(response), 0);
            }
        }
    }

    // Clean up resources
    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, server_socket, NULL);
    close(epoll_fd);
    close(server_socket);

    return 0;
}

Summary

  • Multithreaded Server: Suitable for handling a small number of high-load connections, each in a separate thread.
  • Non-blocking I/O: Allows the server to continue other tasks while waiting for I/O operations.
  • I/O Multiplexing:
    • select: Simple to use, but inefficient with many connections.
    • epoll: High performance and suitable for handling a large number of concurrent connections.
Share your love