To do a simple back-firing server:
The client reads a line of text from the standard input, writes it to the server, reads the text from the network input, and returns it to the customer, and the customer reads the text back from the network input and displays it on the standard output
Here is my code (some. h files are renamed by. c Files in the unpv13e folder)
#include". /unpv13e/unp.h "#include". /unpv13e/apueerror.h "#include". /unpv13e/wrapsock.h "#include". /unpv13e/wrapunix.h "#include". /unpv13e/wraplib.h "#include". /unpv13e/writen.h "#include". /unpv13e/str_echo.h "Intmain (int argc, char **argv) {INTLISTENFD, connfd;pid_tchildpid;socklen_tclilen;struct Sockaddr_in cliaddr, servaddr;listenfd = Socket (af_inet, sock_stream, 0); Bzero (&servaddr, sizeof (SERVADDR)); Serva ddr.sin_family = Af_inet;servaddr.sin_addr.s_addr = htonl (inaddr_any); servaddr.sin_port = Htons (SERV_PORT); Bind (LISTENFD, (SA *) &servaddr, sizeof (SERVADDR)); Listen (LISTENFD, Listenq); for (;;) {Clilen = sizeof (CLIADDR); connfd = Accept (LISTENFD, (SA *) &cliaddr, &clilen); if ((Childpid = Fork ()) = = 0) {/* C Hild process */close (LISTENFD);/* Close listening Socket */str_echo (CONNFD);/* Process the request */exit (0);} Close (CONNFD);/* Parent Closes connected socket */}}
#include ". /unpv13e/unp.h "#include ". /unpv13e/apueerror.h "#include ". /unpv13e/wrapsock.h "#include ". /unpv13e/wrapunix.h "#include ". /unpv13e/wraplib.h "#include ". /unpv13e/writen.h "#include ". /unpv13e/str_cli.h "#include ". /unpv13e/readline.h "#include ". /unpv13e/wrapstdio.h "Intmain (int argc, char **argv) {intsockfd;struct sockaddr_inservaddr;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); Inet_pton (Af_inet, argv[1], &servaddr.sin_ addr); Connect (SOCKFD, (SA *) &servaddr, sizeof (SERVADDR)), Str_cli (stdin, SOCKFD);/* Do it All */exit (0);}
The server code and the client code are shown above.
Among them, Str_echo reads the data from the customer and shoots them back to the client; Str_cli reads a line of text from the standard input, writes it to the server, reads back the server to the row, and writes the return line to the standard output.
Start the server program normally, and then use Netstat-a to view the status of the server listening sockets:
You can see that the *:9877 line is the above program, after calling Socket,bind,listen, because the backlog queue is empty, blocked by the accept call.
When the client program runs (./tcpcli01 127.0.0.1), use the command again NETSTAT-A view:
You can see that the child process has successfully connected with the client and is in the established state.
Use the PS command to check the status of the process:
You can see that the parent process of the last line of the process is the first row. The s in stat are sleep states, and Wchan indicate that they are in stat condition. (The display is different from the book, but the meaning should be the same)
Normal termination: Press Ctrl+d and then immediately execute netstat-a, as shown below (due to the test, the temporary port is different):
The steps to terminate the client and server normally are:
1) Type the EOF character, fgets returns a null pointer, and the STR_CLI function returns
2) When Str_cli returns to the client's main function, main terminates by calling exit
3) The part of the process that terminates processing is to close all open descriptors, so that the client opens a socket that is closed by the kernel. This causes the client TCP to send a fin to the server, and the server TCP responds with an ACK, which is the first half of the TCP connection termination sequence.
4) When the server TCP receives fin, the server child process is blocked by the ReadLine call, so ReadLine returns 0, causing the Str_echo function to return the main function of the server child process
5) The server child process terminates by calling exit
6) All descriptors opened in the server subprocess are closed. Having a child process to close a connected socket raises the last two bytes of the TCP connection termination sequence: A fin from the server to the client and an ACK from the client to the server. At this point, the connection is completely terminated and the client socket enters the TIME_WAIT state
7) Another part of process termination processing is to send a SIGCHLD signal to the parent process when the server child process terminates. This happens in this case, but we do not capture the signal in the code, and the default behavior of the signal is ignored. Since the parent process has not been processed, the child process goes into a zombie state.
Zombie Process: When a child process exits, the parent process does not properly handle the SIGCHLD signal it emits, causing the child process to stay in a zombie state waiting for the parent process to bury it.
POSIX signal processing
POSIX represents a portable operating system interface (Portable Operating system Interface, abbreviated as POSIX), and the POSIX standard defines the interface standards that the operating system should provide for applications
Signal: Informs a process that an event has occurred, also known as a software interrupt (software interrupt). The signal is usually asynchronous, that is, the process does not know the exact time of the signal to occur.
asynchronous parties do not need a common clock, that is, the receiver does not know when the sender is sent, so in the message sent to the receiver will be prompted to start receiving information, such as the start bit, and at the end of the stop bit. Another implication of asynchrony is the asynchronous processing of computer multithreading. In contrast to synchronous processing, asynchronous processing does not block the current thread from waiting for processing to complete, but allows subsequent operations until the other thread finishes processing and notifies the thread of the callback.
A signal can be sent from one process to another (or to itself) or to a process by the kernel.
The SIGCHLD signal mentioned above is a signal that the kernel sends to its parent process at the end of any process.
Each signal has a disposition associated with it (disposition), also called behavior (action), by invoking the Sigaction function to set a signal disposition, three options:
1) provides a function that is called whenever a particular signal occurs, and such a function becomes a signal processing function (signal handler), which acts as a capture (catching) signal. Two of these signals cannot be captured (SIGKILL and SIGSTOP). The signal processing function is called by the integer parameter of the signal value, and there is no return value, and the function prototype is void handler (int signo). For most signals, calling the Sigaction function and specifying the function called when the signal occurs is all you need to do to capture the signal, but the individual signals like Sigio,sigpoll,sigurg also require the process of capturing them to do some extra work.
2) set the signal to sig_ign to ignore it . But the two signals of Sigkill and sigstop cannot be ignored.
3) set the signal to SIG_DFL to enable its default disposition . The default disposition is usually to terminate the process after the signal is received, some of which are still in the current working directory to produce a process core image (core image), and a separate signal is ignored by default, SIGCHLD and Sigurg.
Signal function: a function for establishing signal disposition. This function requires its own encapsulation (because the POSIX-provided method sigaction functions are too complex, and the system provides simple signal functions that are incompatible with different signals before POSIX appears.) = = Define your own signal function, in which the Sigaction function is called)
The following functions are common in the book (final version):
/* include signal */#include "unp.h" Sigfunc *signal (int signo, sigfunc *func) {struct sigactionact, Oact;act.sa_handler = f Unc;sigemptyset (&act.sa_mask); act.sa_flags = 0;if (Signo = sigalrm) {#ifdefSA_INTERRUPTact. sa_flags |= Sa_ interrupt;/* SunOS 4.x */#endif} else {#ifdefSA_RESTARTact. sa_flags |= sa_restart;/* SVR4, 44BSD */#endif}if (Sigaction (s Igno, &act, &oact) < 0) return (SIG_ERR); return (Oact.sa_handler);} /* End signal */sigfunc *signal (int signo, Sigfunc *func)/* for our signal () function */{sigfunc*sigfunc;if ((Sigfunc = s Ignal (Signo, func)) = = Sig_err) Err_sys ("Signal error"); return (SIGFUNC);}
The prototype of the system-provided signal function is this: void (*signal (int signo, void (*func) (int))) (int);
To understand this function prototype you need to know the function pointer first
A function pointer is a pointer variable that points to a function. Thus the function pointer itself should first be a pointer variable, except that the pointer variable points to the function. A function pointer has two uses: calling a function and making a function argument.
Disassembled to understand, this is a function like void (*) (int), and a function such as signal (int signo, void (*func) (int)) Returns a function pointer to the function that the function pointer points to is void (*) (int)
Thus, the function prototype means: declare a signal function, the parameter is the semaphore (int) and signal processing function (only 1 int parameters, no return value), the return value is a function pointer (pointing to a 1 int parameter, no return value)
Simplified Way of understanding:
First, define a sigfunc type, typedef void Sigfunc (int);
How to understand typedef? Find a reference article.
The function of a typedef in a statement is simply to turn the function of the statement definition variable into a function of defining the type. For example: typedef int *apple, don't look at typedef first. int *apple is a pointer to an integer variable, so Apple is the type that points to the integer variable pointer.
Therefore,Sigfunc is a function type that has an integer parameter and does not return a value. The prototype became Sigfunc *signal (int signo, Sigfunc *func) at this time; The second and return values of the function are pointers to the signal handler function.
Handling SIGCHLD Signals
The purpose of setting the zombie (Zoombie) state is to maintain the information of the child process so that the parent process obtains the information at a later time, including the process ID of the child process, the terminating state, and the resource utilization information.
If the parent process terminates, the parent process ID of the zombie child process is reset to 1 (the init process), and the Init process cleans them (wait)
Whenever you fork a child process, you have to wait for them in case they become zombie processes.
So, to create a signal processing function that captures SIGCHLD signals and calls wait in the function body.
After calling listen, add signal (SIGCHLD, SIG_CHLD); To be completed before the first child process is fork, and only once. SIG_CHLD needs to define itself.
(Note: The following function is not the final version, which needs to be improved later)
#include "unp.h" voidsig_chld (int signo) {pid_tpid;intstat;pid = Wait (&stat);p rintf ("Child%d terminated\n", PID); return;}
The portable way to handle a zombie process is to capture sigchld and call wait or waitpid.
If the signal function is from the system's own library of functions and not the custom signal function, the following occurs:
Even if the SIGCHLD signal is processed, the slow system call (accept) is interrupted (a eintr error is returned), and the parent process does not handle the error, so the parent process aborts. While some cores will automatically reboot some of the interrupted system calls, we must be prepared to be compatible with the slow system call return EINTR. The signal function provided in the standard C function library does not cause the kernel to automatically restart the interrupted system call. So you need to set the Sa_restart flag . But even if it is set up, some kernels or some system calls cannot be restarted, so they need to be processed after the accept.
The term applies to system calls that may be blocked for ever. A system call that is never blocked means that a call can never be returned, and most network support functions fall into this category. For example, if no client is connected to the server, the server's accept call will be blocked.
When testing in my machine, the code and the corresponding display are as follows:
#include ". /unpv13e/unp.h "#include". /unpv13e/apueerror.h "#include". /unpv13e/wrapsock.h "#include". /unpv13e/wrapunix.h "#include". /unpv13e/wraplib.h "#include". /unpv13e/writen.h "#include". /unpv13e/str_echo.h "//#include". /unpv13e/signal.h "#include". /unpv13e/sigchldwait.h "//Use wait to handle zombie process Intmain (int argc, char **argv) {INTLISTENFD, Connfd;pid_tchildpid;socklen_ Tclilen;struct sockaddr_incliaddr, servaddr;voidsig_chld (int); LISTENFD = Socket (af_inet, sock_stream, 0); Bzero (& SERVADDR, sizeof (SERVADDR)); servaddr.sin_family = Af_inet;servaddr.sin_addr.s_addr = htonl (inaddr_any); servaddr.sin _port = htons (Serv_port); Bind (LISTENFD, (SA *) &servaddr, sizeof (SERVADDR)); Listen (LISTENFD, Listenq); signal (SIGCHLD, SIG_CHLD); for (;;) {Clilen = sizeof (CLIADDR); connfd = Accept (LISTENFD, (SA *) &cliaddr, &clilen); if ((Childpid = Fork ()) = = 0) {/* C Hild process */close (LISTENFD);/* Close listening Socket */str_echo (CONNFD);/* Process the request */exiT (0);} Close (CONNFD);/* Parent Closes connected socket */}}
The Accept error:interrupted system call does not appear, which is the type of the kernel that automatically restarts some interrupted system calls. For portability, however, the Sa_restart flag is set, and the EINTR error is ignored in the Accept function, which is:
1) Use the custom signal function (the final version is shown in the signal.h above)
2) Ignore errors in accept
for (;;) {Clilen = sizeof (CLIADDR), if ((CONNFD = Accept (LISTENFD, (SA *) &cliaddr, &clilen)) < 0) {if (errno = = Eintr) c ontinue;/* back to For () */elseerr_sys ("Accept error"); // ... ...}
In this case, why does the accept this parcel function do not directly deal with EINTR errors? because the Connect function returns EINTR, it cannot be called again, or an error is returned immediately!
After modifying the processing of the EINTR and using the custom signal function, the final version of the server program is implemented. (The signal processing function is not in the Tcpserv program, so the tcpserv03 and tcpserv04 programs on the book are the same)
Wait and WAITPID functions: Handling terminated child processes
#include <sys/wait.h>pid_t wait (int *statloc);p id_t waitpid (pid_t pid, int *statloc, int options); The process ID was successfully returned with an error of 0 or-1
The above two functions return the process ID number (PID), and the child process termination state (STATLOC)
If the process that calls wait does not have a child process that has been terminated, but one or more of the sub-processes are still executing, wait will block until the first termination of the existing child process.
The Waitpid function has more control options, and the PID parameter allows the specified waiting process id,-1 to represent a child process waiting for the first termination. The most common option in the options is Wnohang, which tells the kernel not to block when there are no terminated child processes.
The difference between these two functions is that:
Establishing a signal processing function and calling wait in it is not sufficient to prevent a zombie process from appearing. When multiple signals are terminated simultaneously, triggering multiple fin and generating sigchld signals, the signal processing function is not guaranteed to execute several times because the UNIX signal is not queued, so it is possible to leave a zombie process.
In the case of a test using wait, the following is the appropriate display:
(The signal processing function is shown above, mainly PID = Wait (&stat);)
Having done two tests, the serious problem is not only to leave a zombie process, but also to be unsure of the outcome. The above two examples, one is left 4 zombie process, one is left one.
The correct approach is to call Waitpid: Call Waitpid within a loop to get the state of all terminated child processes, and you must specify the Wnohang option to tell Waitpid not to block when there are pending child processes running.
final version of the SIG_CHLD function:
#include "unp.h" voidsig_chld (int signo) {pid_tpid;intstat;while (PID = Waitpid ( -1, &stat, Wnohang)) > 0) printf (" Child%d terminated\n ", PID); return;}
The final version of the server program is as follows (you can correctly handle the EINTR returned by the accept and establish a signal handler to call Waitpid for all terminated subroutines)
#include "unp.h" intmain (int argc, char **argv) {INTLISTENFD, connfd;pid_tchildpid;socklen_tclilen;struct sockaddr_ INCLIADDR, servaddr;voidsig_chld (int), LISTENFD = Socket (af_inet, sock_stream, 0); Bzero (&servaddr, sizeof ( SERVADDR)); servaddr.sin_family = Af_inet;servaddr.sin_addr.s_addr = htonl (inaddr_any); Servaddr.sin_port = Htons (Serv_port); Bind (LISTENFD, (SA *) &servaddr, sizeof (SERVADDR)); Listen (LISTENFD, Listenq); Signal (SIGCHLD, SIG_CHLD);/* Must call Waitpid () */for (;;) {Clilen = sizeof (CLIADDR), if ((CONNFD = Accept (LISTENFD, (SA *) &cliaddr, &clilen)) < 0) {if (errno = = Eintr) c ontinue;/* back to For () */elseerr_sys ("Accept error"); if ((Childpid = Fork ()) = = 0) {/* Child process */close (LISTENFD);/* Close listening Socket */str_echo (CONNFD);/* Process The request */exit (0);} Close (CONNFD);/* Parent Closes connected socket */}}
Use the diagram after Waitpid:
What the final version can handle:
1) SIGCHLD signal must be captured when fork sub-process
2) The interrupted system call must be processed when the signal is captured
3) SIGCHLD signal processing function must be correctly written, should use the WAITPID function to avoid leaving the zombie process
Some unusual cases:
1) Accept return before connection abort: three-way handshake completed thereby establishing the connection, the client TCP sends a RST. That is, the connection is queued, waiting for the server process to call accept when the RST arrives.
How this aborted connection is handled depends on the different implementations. Some implementations completely handle aborted connections in the kernel, the server process is not visible at all, and some return the errno value of Eproto, POSIX returns econnaborted (because some fatal protocol related events return Eproto, so instead of a econnaborted error, So that the server can ignore it). general server processing only need to call the accept on the line again .
2) The server process terminates: The processing client's child process terminates unexpectedly, all open descriptors of the subprocess are closed, which leads to a fin being sent to the customer, while the client TCP responds with an ACK, completes the first half of the four waves, and the SIGCHLD signal is sent to the server parent process. and be handled correctly. At this point, however, the client process blocks on the fgets call, waiting for a line of text to be received from the terminal.
Where tcpcli01 is turned on, the child process is killed in another terminal, so the server returns a fin to the client, the customer responds with an ACK, and then when the customer sends a row of data again (at which point the data can be sent to the server, because after the first half of the four waves is over, The client needs to tell the server itself that it's closed, and the server process is closed, so it returns an RST. At this point, there are two possible scenarios in the client process: 1) The call ReadLine occurs before the RST is received (as shown in this example), the received fin causes ReadLine to return 0 immediately (EOF), and the client does not expect to receive EOF at this time and then exits with an error message. 2) If readline occurs after receiving the RST, a econnreset is returned, indicating that the other side resets the connection error.
The problem with this example is that when fin arrives at the socket, the customer is blocking on the fgets call, and the customer is actually dealing with two descriptors--sockets and user input--it cannot simply block one of the inputs, but should block the input of either of the sources , which is the Select and Poll one of the purposes of these two functions.
The STR_CLI function is rewritten later, and once the server sub-process is killed, the customer is immediately informed that fin has been received.
3) sigpipe signal: Ignore the error returned by the ReadLine function, write more data to the server. (for example, a client performs two write (writen) operations before reading back any data (called ReadLine), and the RST is the first time it is written)
It is no problem to write a socket that has received the fin, but it is an error to write a socket that has received the RST. So according to the example of server process termination, after killing the child process, the customer writes the data for the first time, and the second write will raise the sigpipe signal, the default behavior is to terminate the process, and whether the process is capturing the signal and returning from its signal processing function, or ignoring the signal, The write operation will return a epipe error.
The broken pipe was not shown in my test. The method of handling sigpipe depends on what the application process wants to do when it occurs. If there is no special thing to do, set the signal processing method directly to sig_ign and assume that subsequent output operations will catch the Epipe error and terminate. If you have operations such as logging into a log file, you must capture the signal, and if you need to know which socket is wrong, you need to process the epipe from write after the signal handler function or ignore it.
4) Server host crashes: (Impersonation process: normal connection and test text send normal, then disconnect server host no longer restarts) the client is blocked in ReadLine after sending the text, waiting for the reply to be shot back. Depending on the retransmission mechanism, client TCP attempts to retransmit multiple times and fails to return Etimedout (server host crashes, no response to Customer Data sub-section) or Ehostunreach/enetunreach (intermediate router determines that the server host is unreachable). However, it is time-consuming to detect multiple passes, in order to detect this situation more quickly and not to proactively send data to the server host to confirm that the so_keepalive socket option is required.
5) Server host crashes after reboot: (Simulation process: normal connection and Test text sent normal, then immediately restart, in the process of restarting, the customer did not send text) after restarting, the customer sends the text, but in the server TCP has lost the previous connection information, so in response to an RST, The call returns Econnreset.
6) Server main machine: When the normal shutdown, the INIT process usually first send sigterm signal to all processes (can be captured, the default action is to terminate the process, only ignored or processed after no termination, will be terminated by Sigkill), and wait for a fixed time, so that the process has time to clear and terminate , when the time is up, the sigkill signal (which cannot be captured) is sent to all processes that are still running, as shown in 2 when the process shuts down, so a select or poll function must be used in the client, so that the termination of the server process can be detected by the client once it has occurred.
Traversing sockets it is unwise to pass a binary structure between the client and the server, and the following issues may occur:
1) Big endian byte sequence and small end byte sequence
2) 32-bit and 64-bit, varying sizes of data types
3) Different structure packing method
Workaround:
1) All numeric data is transmitted as a text string on the premise that the client and the server host have the same character set
2) Displays the binary format (number of bits, size end byte order) that defines the supported data types
"UNIX Network Programming" TCP client/Server program example