Socket service example: TCP (stream oriented socket) implements ECHO server and client

  • The so-called ECHO service is to print relevant parameters on the screen. The relevant application process is as follows:

Server logic

  • 1. Create a server socket after the server is started, and always call accept after making corresponding settings (2) to wait for the connection of the client. After the client is connected normally, create a child process as a business process to serve the client, and the parent process always acts as a listening process to wait for the connection of the next client;
  • 2. In order to prevent the emergence of zombie processes, the server needs to have the function of processing the exit of sub processes. For simplicity, a signal processing program is directly installed in the program to process SIGCHLD signals. This process is completely asynchronous and is not reflected in the flowchart.
    The figure shows the program flow chart

    However, the logic of the server is relatively simple. It can only realize a single connection to a client, send certain information to the client after connection, and then disconnect
    Sort out the function prototype of socket related operations:
int socket(int domain, int type, int protocol);//Create socket
//sock_fd = socket(AF_INET, SOCK_STREAM, 0);
//sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
int bind(int socket, const struct sockaddr *address, socklent address_len);//Bind the address port. If the binding succeeds, the binding fails
int connect(int socket, const struct sockaddr *address, socklent address_len);//connect
//It can be seen that the parameters are exactly the same,
//bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr))
int listen(int socket, int backlog);//The listening mode backlog refers to the length of the queue waiting for connection, but the actual queue may be greater than this number, usually taken as 5.
int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);//Accept connection

Code parsing:

  • 1. Server related code:
  • Create socket: server_sock = socket(AF_INET, SOCK_STREAM, 0);
int server_sock,conn_sock;
server_sock =  socket(AF_INET,SOCK_STREAM,0);  //Functional correlation
if(server_sock<0){    //Processing error
	perror("socket(2) erros");
	goto create_error;
	//Define create in main_ Error content
}
  • 2. Bind to port: bind (server_sock, (struct SOCKADDR *) & server_addr, sizeof (server_add))
struct sockaddr_in server_addr,client_addr;
//The address description data structure used by the IP protocol is defined in the header file < netinet / in. H >.
socklen_t sock_len  = sizeof(client_addr);
(void)memset(&server_addr,0,sock_len);//The function replaces the memory unit of the first n bytes pointed to by the pointer variable s with an "integer" c. note that c is of type int. S is a pointer variable of void *, so it can be initialized for any type of data.
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(LISTEN_PORT);
if(bind(server_sock,(struct sockaddr *)&server_addr,sizeof(server_add))){
	perror("bind(2) error");
	goto err;
}

The bound address and port are mainly populated with sturct SOCKADDR in lines 57 to 59_ The in structure is completed, and the server does not
In case of special requirements, the binding address is INADDR_ANY can listen to all addresses. In addition, pay attention to the conversion of byte order
In other words, this must be paid attention to for programs, especially those requiring portability.

  • Set to passive listening:
if(listen(server_sock,5)){
	perror("listen(2) error");
	goto err;
}
  • Accept new connections:
while(true){
	sock_len = sizeof(client_addr);
	conn_sock = accept(server_sock,(struct sockaddr *)&client_addr,&sock_len);
	
	if (conn_sock < 0) {
            if (errno == EINTR) {
                /* restart accept(2) when EINTR */
                continue;
            }
            goto end;
        }
	printf("client from %s:%hu connected\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port));
        fflush(stdout);//Clear all I / O file contents
  • Child process connection client:
chld_pid	= fork();   //Creating child processes through fork
        if (chld_pid < 0) {//Less than 0 indicates creation failure
            /* fork(2) error */
            perror("fork(2) error");
            close(conn_sock);
            goto err;
        } else if (chld_pid == 0){//0 means that the child process is returned
            /* child process */
            int ret_code;

            close(server_sock);
            ret_code	= tcp_echo(conn_sock);
            close(conn_sock);

            /* Is usage of inet_ntoa(2) right? why? */
            printf("client from %s:%hu disconnected\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port));

            exit(ret_code); //exit(x) (x is not 0) indicates abnormal exit. exit(0) indicates normal exit. The parameters of exit() will be passed to some operating systems, including UNIX,Linux, and MS DOS, for use by other programs.
        } else {
            /* parent process */
            continue;
        }

After the call is executed, the following code
It is divided into father and son processes to continue running. Here we let the subprocess provide services to the newly connected client, and the service is completed
Quit when you're done. The parent process continues to listen and wait for the next client to connect. In this way, the server can be concurrent
Service response to multiple clients.
The sub process first calls close(2) to turn off its own listener Socket, and then calls tcp_. Echo() function
Service. After the service is completed, close the Socket, print the client disconnection information and exit the process.

  • Service function
int tcp_echo(int client_fd)
{
    char				buff[BUFF_SIZE]	= {0};
    ssize_t				len				= 0;

    len	= read(client_fd, buff, sizeof(buff));
    if (len < 1) {
        goto err;
    }

    (void)write(client_fd, buff, (size_t)len);

    return EXIT_SUCCESS;
 err:
    return EXIT_FAILURE;
    /*
    EXIT_SUCCESS Is a symbolic constant defined in the C language header file library. return EXIT_SUCCESS is equivalent to return 0; return EXIT_FAILURE is equivalent to return 1; What's the advantage of writing like this? Is the program easy to read?
    Header file stdlib.h: #include < stdlib.h >
    Definition of the argument values for the exit() function 
    #define EXIT_SUCCESS 0
    #define EXIT_FAILURE 1 
    */
}
  • SIGCHLD signal processing function to prevent zombie processes:
    After processing the service, all child processes will directly call exit(3) to terminate themselves. In this
    At the same time, the system will keep the termination status they returned, send SIGCHLD signal to the parent process, and the child process will enter
    Zombie state. Only after the parent process has processed the resource can it really be fully recycled. So it can be implemented with the following function
    Now the zombie sub process is recycled.
void zombie_cleaning(int signo)
{
    int status;
    (void)signo;
    while (waitpid(-1, &status, WNOHANG) > 0);
}
  • Installing signal processing functions
struct sigaction clean_zombie_act;
    (void)memset(&clean_zombie_act, 0, sizeof(clean_zombie_act));
    clean_zombie_act.sa_handler	= zombie_cleaning;
    if (sigaction(SIGCHLD, &clean_zombie_act, NULL) < 0) {
        perror("sigaction(2) error");
        goto err;
    }

Client program:

  • Create the Socket immediately after startup, and directly call connect(2) to connect to the server, eliminating the bind(2) call. The system will implicitly bind the Socket just created to a random port. connect(2) and directly send the data to the server. After sending, directly read the data sent back by the server and print the received data, and then end
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>

#define SERVER_IP	"192.168.1.133"
#define SERVER_PORT	((uint16_t)7007)
#define BUFF_SIZE	(1024 * 4)

int main(int argc, char *argv[])
{
    int conn_sock;
    char test_str[BUFF_SIZE]	= "tcp echo test";
    struct sockaddr_in	server_addr;

    conn_sock	= socket(AF_INET, SOCK_STREAM, 0);
    if (conn_sock < 0) {
        perror("socket(2) error");
        goto create_err;
    }

    (void)memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family		= AF_INET;
    server_addr.sin_port		= htons(SERVER_PORT);
    if (argc != 3) {
        server_addr.sin_addr.s_addr	= inet_addr(SERVER_IP);//inet_addr() is used to convert the network address string referred to by the parameter cp into a binary number used by the network. The network address string is a string composed of numbers and points, for example: "163.13.132.68"
    } else {
        server_addr.sin_addr.s_addr	= inet_addr(argv[1]);
        snprintf(test_str, BUFF_SIZE, "%s", argv[2]);
    }

    if (connect(conn_sock,
                (struct sockaddr *)&server_addr,
                sizeof(server_addr)) < 0) {
        perror("connect(2) error");
        goto err;
    }

    if (write(conn_sock, test_str, strlen(test_str)) < 0) {
        perror("send data error");
        goto err;
    }
    (void)memset(test_str, 0, BUFF_SIZE);
    if (read(conn_sock, test_str, BUFF_SIZE) < 0) {
        perror("receive data error");
        goto err;
    }
    printf("%s\n", test_str);
    fflush(stdout);//Empty file buffer or standard input / output buffer

    return EXIT_SUCCESS;

 err:
    close(conn_sock);
 create_err:
    fprintf(stderr, "client error");
    return EXIT_FAILURE;
}

Server code:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <signal.h>

#define LISTEN_PORT	((uint16_t)7007)
#define BUFF_SIZE	(1024 * 4)

void zombie_cleaning(int signo)
{
    int status;
    (void)signo;
    while (waitpid(-1, &status, WNOHANG) > 0);
}

int tcp_echo(int client_fd)
{
    char				buff[BUFF_SIZE]	= {0};
    ssize_t				len				= 0;

    len	= read(client_fd, buff, sizeof(buff));
    if (len < 1) {
        goto err;
    }

    (void)write(client_fd, buff, (size_t)len);

    return EXIT_SUCCESS;
 err:
    return EXIT_FAILURE;
    /*
    EXIT_SUCCESS Is a symbolic constant defined in the C language header file library. return EXIT_SUCCESS is equivalent to return 0; return EXIT_FAILURE is equivalent to return 1; What's the advantage of writing like this? Is the program easy to read?
    Header file stdlib.h: #include < stdlib.h >
    Definition of the argument values for the exit() function 
    #define EXIT_SUCCESS 0
    #define EXIT_FAILURE 1 
    */
}

int main(void)
{
    int server_sock, conn_sock;  //Definition of connection
    struct sockaddr_in server_addr, client_addr;
    socklen_t	sock_len	= sizeof(client_addr);
    pid_t	chld_pid;
    struct sigaction clean_zombie_act;

    server_sock	= socket(AF_INET, SOCK_STREAM, 0);
    if (server_sock < 0) {
        perror("socket(2) error");  //The C library function void perror(const char *str) outputs a descriptive error message to the standard error stderr. The string str is output first, followed by a colon, followed by a space.
        goto create_err;
    }

    (void)memset(&server_addr, 0, sock_len);
    server_addr.sin_family		= AF_INET;
    server_addr.sin_addr.s_addr	= htonl(INADDR_ANY);
    server_addr.sin_port		= htons(LISTEN_PORT);

    if (bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr))) {
        perror("bind(2) error");
        goto err;
    }

    if (listen(server_sock, 5)) {
        perror("listen(2) error");
        goto err;
    }

    (void)memset(&clean_zombie_act, 0, sizeof(clean_zombie_act));
    clean_zombie_act.sa_handler	= zombie_cleaning;
    if (sigaction(SIGCHLD, &clean_zombie_act, NULL) < 0) {
        perror("sigaction(2) error");
        goto err;
    }

    while (true) {
        sock_len	= sizeof(client_addr);
        conn_sock	= accept(server_sock, (struct sockaddr *)&client_addr, &sock_len);
        if (conn_sock < 0) {
            if (errno == EINTR) {
                /* restart accept(2) when EINTR */
                continue;
            }
            goto end;
        }

        printf("client from %s:%hu connected\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port));
        fflush(stdout);

        chld_pid	= fork();   //Creating child processes through fork
        if (chld_pid < 0) {//Less than 0 indicates creation failure
            /* fork(2) error */
            perror("fork(2) error");
            close(conn_sock);
            goto err;
        } else if (chld_pid == 0){//0 means that the child process is returned
            /* child process */
            int ret_code;

            close(server_sock);
            ret_code	= tcp_echo(conn_sock);
            close(conn_sock);

            /* Is usage of inet_ntoa(2) right? why? */
            printf("client from %s:%hu disconnected\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port));

            exit(ret_code); //exit(x) (x is not 0) indicates abnormal exit. exit(0) indicates normal exit. The parameters of exit() will be passed to some operating systems, including UNIX,Linux, and MS DOS, for use by other programs.
        } else {
            /* parent process */
            continue;
        }
    }

 end:
    perror("exit with:");
    close(server_sock);
    return EXIT_SUCCESS;
 err:
    close(server_sock);
 create_err:
    fprintf(stderr, "server error");
    return EXIT_FAILURE;
}

Tags: Linux Ubuntu UI

Posted on Tue, 30 Nov 2021 02:03:22 -0500 by sarabjit