"UNP" Essay -- implementing a simple retroreflective server

Keywords: TCP/IP

1. What is a retroreflective server

Execution steps of echo server:
(1) The client reads a line of text from standard input and writes it to the server.
(2) The server reads this line of text from the network input and injects it back to the client;
(3) The customer reads this line of text from network input and displays it on standard output.

2. Server program

// 5.2 TCP echo server program 
#include	"unp.h"
void str_echo(int sockfd)
{
	ssize_t		n;
	char		buf[MAXLINE];

again:
	while ( (n = read(sockfd, buf, MAXLINE)) > 0)
		Writen(sockfd, buf, n);

	if (n < 0 && errno == EINTR)
		goto again;
	else if (n < 0)
		err_sys("str_echo: read error");
}

int main(int argc, char **argv){
    int listenfd, connfd;
    pid_t childpid;
    socklen_t clilen;
    struct sockaddr_in  cliaddr, servaddr;

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // Large endian byte order, INADDR_ANY indicates the general distribution address, 
    servaddr.sin_port        = htons(SERV_PORT); // Small endian byte order, the well-known port of the server, is 9877 in the unp

    Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    Listen(listenfd, LISTENQ); // LISTENQ indicates the maximum capacity in the listening queue, which is defined as 1024 in unp

    for( ; ; ){
        clilen = sizeof(cliaddr);
        connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen );
        
        if((childpid = Fork()) == 0){
            Close(listenfd); // The child process needs to close the listening socket
            str_echo(connfd);
            exit(0);
        }
        Close(connfd); //The parent process closes the connected socket
    }
}

Process: ① define the socket address. ② The Bind function binds the address to the socket. ③ Use the Listen function to Listen for socket signals. ④ The connection request of each client is processed by using dead loop and sub process.

Points to note:

  • INADDR_ANY is the address with the specified address of 0.0.0.0. In fact, this address represents an uncertain address, or "all addresses" and "any address". It is also called universal address. Its function is to tell the host that the socket can accept connections with the destination address as any local interface. (tested, unable to connect to other hosts using LAN)
  • The Accept function is used to return the next completed connection from the queue head of the completed connection queue. If the completed connection queue is empty, the process will be put into sleep state (generally, the nested word socket will default to blocking state).
  • Program outline of concurrent server. When the server may have to accept connection requests from multiple hosts, the simplest way is to fork a sub process to serve each client. The child process closes the listening socket, and the parent process closes the connected socket and waits for other connections. (the reasons are explained in Section 4)

3. Client program

#include	"unp.h"
void
str_cli(FILE *fp, int sockfd)
{
	char	sendline[MAXLINE], recvline[MAXLINE];
    // Fgets will call the fgets function to read the data entered from the keyboard
	while (Fgets(sendline, MAXLINE, fp) != NULL) {

        // The write function will be called to send the row data to the client
		Writen(sockfd, sendline, strlen(sendline));
        // Call the read function to accept the data returned by the server and display it with fputs
		if (Readline(sockfd, recvline, MAXLINE) == 0)
			err_quit("str_cli: server terminated prematurely");

		Fputs(recvline, stdout);
	}
}

int main(int argc, char **argv)
{
	int					sockfd;
	struct sockaddr_in	servaddr;

	if (argc != 2)
		err_quit("usage: tcpcli <IPaddress>");

	sockfd = Socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(SERV_PORT);  // Port uses the default port 9877 of unp
	Inet_pton(AF_INET, argv[1], &servaddr.sin_addr); // Put the specified ip argv[1] into sin_ In addr, 

	Connect(sockfd, (SA *) &servaddr, sizeof(servaddr)); // Initiate connection request

	str_cli(stdin, sockfd);		/* do it all */

	exit(0);
}

4. Outline of parallel server

Question: why close the listening socket listenfd in the child process? Why did connfd close the connected socket twice?

Explain graphically:

At this time, there is a connection request. The server accepts the request and creates a new socket connfd. This is a connected socket that can be used to read and write data across connections. As shown below:

The next step is to call fork to create a child process, as shown in the following figure

You can see that the child process copies both the listening socket listenfd and the connection socket connfd of the parent process. However, the purpose of creating a child process is to process connected sockets. If two listening sockets are kept all the time, redundant connection sockets will appear in the listening queue when a new connection is made. Similarly, we also want the parent process to only process listening sockets. Therefore, the parent process closes the connected socket and the child process closes the listening socket.

This is the final state of the two sockets. The parent process and the child process perform their respective functions. The child process processes the currently connected socket and performs cross connection read-write operations. The parent process continues to process the next client connection and creates a new child process to process the new connection.

  • Why has the connected socket been closed twice?
    Each file or socket has a reference count, which is the number of currently open descriptors that reference the file or socket. When listenfd and connfd are returned from the function, their reference counts are both 1, but fork is called. When the child process is created, the two descriptors are also shared in the child process of the parent process. Therefore, the reference counts of the file table entries associated with the two sockets are 2. In this way, when the child process executes close(), the reference count is reduced from 2 to 1 and will not be really cleaned up and released. (at the end of the subprocess, the descriptor whose reference count is not executed is automatically decremented by one, so the close(connfd) is often omitted.)

5. Expand (observe the connection status, etc.)

  1. Observe IP message

    The client inputs asd, and you can see the ASCII characters of asd in the message, that is, the data information contained. At the same time, it is also the process of TCP transmitting information. The sender sends an information message, and the receiver returns a confirmation message
  2. View port information
losf -i:Port number

  1. View relationships between processes
ps -t pts/5 -t pts/7 -o pid,ppid,tty,stat,args,wchan

  • pts indicates the open terminal
  • wait_woken is sleeping (usually in a blocked state) and waiting to wake up.
  • The status of the process with PID=5045 is Z, that is, the dead state. The reason is that when the server child process terminates, it will send a SIGCHLD signal to the parent process, but this program does not capture this signal in the code. This signal is ignored by default, resulting in the death of the child process. (the dead process will also consume system resources, so it needs to be processed.)

6. Summary

  1. How to design a server that can monitor LAN?

Posted by genom on Wed, 06 Oct 2021 07:38:09 -0700