30-Day Challenge: In-Depth Study of Linux Server Program Development

In the previous article, “30-Day Challenge: Linux Server Program Development: Starting from Sockets,” we implemented a client that initiates a socket connection and a server that accepts a socket connection. However, for functions like socket, bind, listen, accept, and connect, we assume the program runs perfectly without any exceptions, which is clearly impossible. No matter how skilled you are at coding, even Linus Torvalds would write bugs in his programs. In “Effective C++”, Item 08 states, “Don’t let exceptions escape from destructors.” Here, I would like to expand on this: we should not overlook any exceptions; otherwise, we will encounter hard-to-locate bugs in large project developments!

1. Error Handling in Linux Systems

For Linux system calls, the common way to indicate error types is through return values and setting errno. Generally, when a system call returns -1, it indicates an error has occurred. Let’s look at the most common error handling template in socket programming:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd == -1){    print("socket create error");    exit(-1);}

Handling an error requires at least five lines of code, making programming cumbersome and the code less readable, with error handling taking up more space than the program itself.

To facilitate coding and improve code readability, we can encapsulate an error handling function:

void errif(bool condition, const char *errmsg){    if(condition){        perror(errmsg);        exit(EXIT_FAILURE);    }}

The first parameter indicates whether an error has occurred; if true, it means an error has occurred, and it will call perror, which prints the actual meaning of errno and the string we passed in as the second parameter, making it easy to locate where the error occurred in the program. Then, we use exit from stdlib.h to exit the program and return a predefined constant EXIT_FAILURE.

When using it:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);errif(sockfd == -1, "socket create error");

This way, we only need one line for error handling, making it convenient and simple to write, while also outputting custom information for bug localization.

We use this method to handle errors for all functions:

errif(bind(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr)) == -1, "socket bind error");errif(listen(sockfd, SOMAXCONN) == -1, "socket listen error");int clnt_sockfd = accept(sockfd, (sockaddr*)&clnt_addr, &clnt_addr_len);errif(clnt_sockfd == -1, "socket accept error");errif(connect(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr)) == -1, "socket connect error");

Now, the simplest error handling function is encapsulated, but this is only for the development in this tutorial. In real server development, error handling is not such a simple topic.

Once we establish a socket connection, we can use read and write from the unistd.h header file for data read and write operations on the network interface.

Note that these two functions are used for TCP connections. For UDP, you need to use sendto and recvfrom. For detailed usage of these functions, refer to Chapter 5, Section 8 of You Shuang’s “Linux High-Performance Server Programming”.Next, we will implement the client and server code, starting with the server.Server code example:

while (true) {    char buf[1024];     // Define buffer    bzero(&buf, sizeof(buf));       // Clear buffer    ssize_t read_bytes = read(clnt_sockfd, buf, sizeof(buf)); // Read from client socket into buffer, return size of data read    if(read_bytes > 0){        printf("message from client fd %d: %s\n", clnt_sockfd, buf);          write(clnt_sockfd, buf, sizeof(buf));           // Write the same data back to the client    } else if(read_bytes == 0){             // read returns 0, indicating EOF        printf("client fd %d disconnected\n", clnt_sockfd);        close(clnt_sockfd);        break;    } else if(read_bytes == -1){        // read returns -1, indicating an error, handle error as described above        close(clnt_sockfd);        errif(true, "socket read error");    }}

The client code logic is the same:

while(true){    char buf[1024];     // Define buffer    bzero(&buf, sizeof(buf));       // Clear buffer    scanf("%s", buf);             // Input data to send to the server from the keyboard    ssize_t write_bytes = write(sockfd, buf, sizeof(buf));      // Send data from buffer to server socket, return size of data sent    if(write_bytes == -1){          // write returns -1, indicating an error        printf("socket already disconnected, can't write any more!\n");        break;    }    bzero(&buf, sizeof(buf));       // Clear buffer     ssize_t read_bytes = read(sockfd, buf, sizeof(buf));    // Read from server socket into buffer, return size of data read    if(read_bytes > 0){        printf("message from server: %s\n", buf);    }else if(read_bytes == 0){      // read returns 0, indicating EOF, usually means the server has disconnected, will test later        printf("server socket disconnected!\n");        break;    }else if(read_bytes == -1){     // read returns -1, indicating an error, handle error as described above        close(sockfd);        errif(true, "socket read error");    }}

Note that the file descriptor in Linux systems is theoretically limited, so after using an fd, you need to close it using the close function from the unistd.h header file. For more kernel-related knowledge, refer to the third edition of Robert Love’s “Linux Kernel Design and Implementation”.

2. Complete Code Example

Makefile:

server:    g++ util.cpp client.cpp -o client && 	g++ util.cpp server.cpp -o serverclean:rm server && rm client

Client Code:

#include <iostream>#include <sys/socket.h>#include <arpa/inet.h>#include <string.h>#include <unistd.h>#include "util.h"int main() {    int sockfd = socket(AF_INET, SOCK_STREAM, 0);    errif(sockfd == -1, "socket create error");    struct sockaddr_in serv_addr;    bzero(&serv_addr, sizeof(serv_addr));    serv_addr.sin_family = AF_INET;    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");    serv_addr.sin_port = htons(8888);    errif(connect(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr)) == -1, "socket connect error");    while(true){        char buf[1024];        bzero(&buf, sizeof(buf));        scanf("%s", buf);        ssize_t write_bytes = write(sockfd, buf, sizeof(buf));        if(write_bytes == -1){            printf("socket already disconnected, can't write any more!\n");            break;        }        bzero(&buf, sizeof(buf));        ssize_t read_bytes = read(sockfd, buf, sizeof(buf));        if(read_bytes > 0){            printf("message from server: %s\n", buf);        }else if(read_bytes == 0){            printf("server socket disconnected!\n");            break;        }else if(read_bytes == -1){            close(sockfd);            errif(true, "socket read error");        }    }    close(sockfd);    return 0;}

Server Code:

#include <stdio.h>#include <sys/socket.h>#include <arpa/inet.h>#include <string.h>#include <unistd.h>#include "util.h"int main() {    int sockfd = socket(AF_INET, SOCK_STREAM, 0);    errif(sockfd == -1, "socket create error");    struct sockaddr_in serv_addr;    bzero(&serv_addr, sizeof(serv_addr));    serv_addr.sin_family = AF_INET;    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");    serv_addr.sin_port = htons(8888);    errif(bind(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr)) == -1, "socket bind error");    errif(listen(sockfd, SOMAXCONN) == -1, "socket listen error");    struct sockaddr_in clnt_addr;    socklen_t clnt_addr_len = sizeof(clnt_addr);    bzero(&clnt_addr, sizeof(clnt_addr));    int clnt_sockfd = accept(sockfd, (sockaddr*)&clnt_addr, &clnt_addr_len);    errif(clnt_sockfd == -1, "socket accept error");    printf("new client fd %d! IP: %s Port: %d\n", clnt_sockfd, inet_ntoa(clnt_addr.sin_addr), ntohs(clnt_addr.sin_port));    while (true) {        char buf[1024];        bzero(&buf, sizeof(buf));        ssize_t read_bytes = read(clnt_sockfd, buf, sizeof(buf));        if(read_bytes > 0){            printf("message from client fd %d: %s\n", clnt_sockfd, buf);            write(clnt_sockfd, buf, sizeof(buf));        } else if(read_bytes == 0){            printf("client fd %d disconnected\n", clnt_sockfd);            close(clnt_sockfd);            break;        } else if(read_bytes == -1){            close(clnt_sockfd);            errif(true, "socket read error");        }    }    close(sockfd);    return 0;}

Util Code:

#ifndef UTIL_H#define UTIL_Hvoid errif(bool, const char*);#endif
#include "util.h"#include <stdio.h>#include <stdlib.h>void errif(bool condition, const char *errmsg){    if(condition){        perror(errmsg);        exit(EXIT_FAILURE);    }}

Leave a Comment