Linux network programming - close OR shutdown?

We know that a TCP connection needs to go through three handshakes to enter the data transmission stage, and finally to the connection closing stage. In the final connection closure phase, we need to focus on the "half connected" state. Because TCP is bidirectional, the direction here refers to the write read direction of the data stream. For example, the direction from the client to the server refers to that the client sends TCP messages to the server through the socket interface, while the direction from the server to the client is another transmission direction. In most cases, TCP connections are closed in one direction first. At this time, the other direction can still transmit data normally.

When the client initiatively initiates the interruption of connection and closes the direction of data flow from itself to the server, the client will no longer write data to the server, and no new message will arrive after the server reads the client's data. But that doesn't mean that TCP The connection has been completely closed. It is possible that the server is processing the last message of the client, such as accessing the database and storing some data, or calculating the value required by a client. After these operations are completed, the server writes the result to the client through the socket. We say that the socket is "half closed" at this time . Finally, the server orderly closes the remaining half of the connection, ending the mission of this TCP connection.

What is described here is that the server side "gracefully" closes the connection. If the server is not handled well, the final shutdown process will be "rough" and fail to achieve the goal of "elegant" shutdown described above. The result is probably that the information processed by the server cannot be transmitted to the client normally, which destroys the user side's use scenario.

close function

int close(int sockfd)  //It is OK to close the connected socket, 0 if successful, and - 1 if there is an error.

What does socket reference count mean? Because sockets can be shared by multiple processes, you can understand that we have set an integral for each socket. If we generate subprocesses through fork, the socket will integrate + 1. If we call the close function once, the socket will integrate - 1. This is what socket reference counting means.

How does the close function close data flow in two directions?

In the input direction, the system kernel will set the socket to be unreadable, and any read operation will return an exception.

In the output direction, the system kernel tries to send the data of sending buffer to the opposite end, and finally sends a FIN message to the opposite end. If the socket is written again, an exception will be returned.

If the opposite end does not detect that the socket has been closed and continues to send messages, it will receive an RST message, telling the opposite end: "Hi, I have been closed, don't send me any more data."

We will find that the close function does not help us close one direction of the connection, so how to close one direction when necessary? Fortunately, the people who designed the TCP protocol helped us figure out a solution, which is the shutdown function.

shutdown function

int shutdown(int sockfd, int howto)   // Perform a shutdown operation on the connected socket, 0 if successful and - 1 if there is an error.

howto is the setting option of this function, which has three main options:

  • Shut [Rd (0): close the "read" direction of the connection, and read the socket to return to EOF directly. From the data point of view, the existing data in the receive buffer on the socket will be discarded. If a new data stream arrives, the data will be acked and then discarded quietly. In other words, the opposite end will still receive an ACK, in which case it is not known that the data has been discarded.
  • SHUT_WR(1): close the "write" direction of the connection, which is often called "semi closed" connection. At this point, regardless of the value of the socket reference count, the write direction of the connection is closed directly. The existing data in the transmit buffer on the socket will be sent out immediately and a FIN message will be sent to the opposite end. If the application writes to the socket, it will report an error.
  • Shut rdwr (2): it is equivalent to the operation of shut RD and shut WR once respectively, closing the read and write directions of socket.

Speaking of this, I don't know if you have the same confusion as I did at the beginning. Isn't it basically the same as close to call shutdown by using shut [rdwr], which is both the read and write directions of closing the connection.

The difference between close and shutdown

The first difference: close will close the connection and release all resources corresponding to the connection, while shutdown will not release the socket and all resources.

The second difference: close has the concept of reference count, which does not necessarily cause the socket to be unavailable; shutdown directly makes the socket unavailable regardless of the reference count. If other processes attempt to use the socket, it will be affected.

The third difference: the reference count of close causes that the FIN end message may not be sent, while the shutdown always sends the FIN end message, which is very important when we intend to close the connection to notify the opposite end.

The difference between close and shutdown

Next, we build a set of client and server programs to experiment with close and shutdown.

The client program receives the user input from the standard input, sends the input string to the server through the socket, and displays the response of the server to the standard output. In this case, we will be exposed to select multiplexing for the first time. It will not be expanded here. You just need to remember that using select enables us to handle the connection socket and standard input I/O objects at the same time.

/*client code*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include    <sys/select.h>
#include <unistd.h>

#define    MESSAGE_SIZE 10240000
#define    MAXLINE     4096

int main() {
    int sockfd;
    int connect_rt;
    struct sockaddr_in serv_addr;

    sockfd = socket(PF_INET, SOCK_STREAM, 0);
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(7878);
    inet_pton(AF_INET, "", &serv_addr.sin_addr);

    connect_rt = connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    if (connect_rt < 0)
        fprintf(stderr, "Connect failed !\n");

    char send_line[MAXLINE], recv_line[MAXLINE + 1];
    int n;
    fd_set readmask;
    fd_set allreads;
    FD_SET(0, &allreads);       // add  stdin->0 to allreads
    FD_SET(sockfd, &allreads);   // add sockfd to  addreads
    for (;;) {
        readmask = allreads;
        int rc = select(sock_fd + 1, &readmask, NULL, NULL, NULL);
        if (rc <= 0)
            error(1, errno, "select failed");
        if (FD_ISSET(socket_fd, &readmask)) {
            n = read(socket_fd, recv_line, MAXLINE);
            if (n < 0) {               //If there is an exception, exit by error
                fprintf(stderr, "read error\n");
            } else if (n == 0) {       //If the EOF sent by the server is read, it will exit normally
                fprintf(stderr, "server terminated\n");
            recv_line[n] = 0;
            fputs(recv_line, stdout);
            fputs("\n", stdout);
        if (FD_ISSET(0, &readmask)) {
            if (fgets(send_line, MAXLINE, stdin) != NULL) {
          When there is data readable on the standard input, judge after reading. If "shutdown" is entered, I/O of standard input is turned off 
          Event awareness, and call the shutdown function to close the write direction; if the input is "close", then call the close function to close 
                if (strncmp(send_line, "shutdown", 8) == 0) {
                    FD_CLR(0, &allreads);
                    if (shutdown(socket_fd, 1)) {
                        printf("shutdown failed");
                } else if (strncmp(send_line, "close", 5) == 0) {
                    FD_CLR(0, &allreads);
                    if (close(socket_fd)) {
                        printf("shutdown failed");
                } else {
                    int i = strlen(send_line);
                    if (send_line[i - 1] == '\n') {
                        send_line[i - 1] = 0;
                    printf("now sending %s\n", send_line);
                    size_t rt = write(socket_fd, send_line, strlen(send_line));
                    printf("send bytes: %zu \n", rt);
/*server  code*/
#include    <sys/types.h>    /* basic system data types */
#include    <sys/socket.h>    /* basic socket definitions */
#include    <netinet/in.h>    /* sockaddr_in{} and other Internet defns */
#include    <arpa/inet.h>    /* inet(3) functions */
#include    <errno.h>
#include    <signal.h>
#include    <stdio.h>
#include    <stdlib.h>
#include    <string.h>
#include    <unistd.h>
#include    <string.h>        /* for convenience */

static int count;
static void sig_int(int signo)
        printf("\nreceived %d datagrams\n", count);

int main()
        int listenfd, connfd;
        socklen_t  cli_addr_len;
        struct sockaddr_in serv_addr, cli_addr;

        listenfd = socket(PF_INET, SOCK_STREAM, 0);
        bzero(&serv_addr, sizeof(serv_addr));
        bzero(&cli_addr, sizeof(cli_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_port = htons(7878);
        serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        bind(listenfd, (struct sockaddr*) &serv_addr, sizeof(serv_addr));

        listen(listenfd, SOMAXCONN);
        signal(SIGINT, sig_int);
        signal(SIGPIPE, SIG_DFL);
        connfd = accept(listenfd, (struct sockaddr* )&cli_addr, &cli_addr_len);
        char message[4096];
        count = 0;
                int n = read(connfd, message, 4096);
                if(n < 0){
                        printf("error read\n");
                }else if(n == 0){
                        printf("client closed\n");

                message[n] = 0;
                printf("received %d bytes: %s\n", n, message);
                count ++;
                char send_line[4096];
                sprintf(send_line, "Hi, %s", message);

                int write_nc = send(connfd, send_line, strlen(send_line), 0);
                printf("send bytes: %d \n", write_nc);
        return 0;

We start the server, then the client, and input data1, data2 and close on the standard input in turn. After a period of observation, we can see:


# ./tcp_client 
now sending data1
send bytes: 5 
now sending data2
send bytes: 5 
# ./tcp_server 
received 5 bytes: data1
send bytes: 9 
received 5 bytes: data2
send bytes: 9 
client closed


# ./tcp_client 
now sending data1
send bytes: 5 
now sending data2
send bytes: 5 
Hi, data1
Hi, data2
server terminated
# ./tcp_server 
received 5 bytes: data1
send bytes: 9 
received 5 bytes: data2
send bytes: 9 
client closed

The following shows the sequence diagram of close and shutdown scenarios respectively:


This diagram explains in detail the sequence diagram of the interaction between the client and the server.

The left side shows that the client calls the close function to close the whole connection. When the "Hi, data1" packet sent by the server ends, the client sends back an RST packet; when the server tries to send the second "Hi, data2" reply packet again, the system kernel notifies SIGPIPE signal. This is because writing on the socket of RST will directly trigger SIGPIPE signal.

On the right side, data1 and data2 are output from the server, and "Hi,data1" and "Hi,data2" are output from the client. After the client and server have completed their own work, they exit normally. Because the client calls the shutdown function only to close the connection, and the server can continue to send and receive data from the client, so "Hi,data1" and "Hi,data2" can be transmitted normally. When the server reads the EOF, it immediately sends FIN message to the client, and the client senses the EOF in the read function, and also carries out the process Exit normally.

Note: in today's server-side program, exit(0) is called directly to complete the sending of FIN message. Why? Why not call the close or shutdown function?

Because after calling exit, the process will exit, and all resources, files, memory, signals and other resources allocated by the kernel related to the process will be released. In linux, everything is a file. Socket itself is a file type. The kernel will create a file structure for each open file and maintain the reference count to the modified structure, and each process structure will maintain this For the file array opened by the process, the index of the array is fd, and the content points to the file structure above. Close itself can be used to operate all files. The thing to do is to delete the fd item specified in the file array opened by the process, and reduce the reference count in the file structure pointed to by one. When the reference count is 0, the internal file operation close will be called For socket, its internal implementation should be to call shutdown, but the parameter is to close the read-write end, so as to close the connection roughly.


Consider the past you shall know the future!

42 original articles published, 25 praised, 10000 visitors+
Private letter follow

Tags: socket Database Linux

Posted on Mon, 10 Feb 2020 08:41:41 -0500 by Kane250