0

I am trying to write a basic TCP server that streams serial data to a client. The server would connect to a serial device, read data from said device, and then transmit it as a byte stream to the client. Writing the TCP server is no problem. The issue is that the server will crash when a client disconnects. In other languages, like Python, I can simply wrap the write() statement in a try-catch block. The program will try to write to the socket, but if the client has disconnected then an exception will be thrown. In another project, this code snippet worked for me:

try:
  client_socket.send(bytes(buf, encoding='utf8'))
except Exception as e:
  logger.info("Client disconnected: %s", client_id)

I can handle client disconnects in my C code, but only by first reading from the socket and checking if the read is equal to 0. If it is, then my client has disconnected and I can carry on as usual. The problem with this solution is that my client has to ping back to the server after every write, which is less than ideal.

Does anyone know how to gracefully handle TCP client disconnects in C? My example code is shown below. Thank you!

// Define a TCP socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

// Allow for the backlog of 100 connections to the socket  
int backlog = 100;  

// Supply a port to bind the TCP server to
short port = 9527;

// Set up server attributes
struct sockaddr_in servaddr;  
servaddr.sin_family = AF_INET;  
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  
servaddr.sin_port = htons(port);  
    
// Set the socket so that we can bind to the same port when we exit the program
int flag = 1;  
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)) == -1) {  
    perror("setsockopt fail");  
}  
    
// Bind the socket to the specified port
int res = bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));  
if (res < 0) {
    perror("bind fail");  
    exit(1); 
}  
    
// Listen for incoming connections
if (listen(sockfd, backlog) == -1) {  
    perror("listen fail");  
    exit(1); 
} else {
    printf("Server listening on port\n", port); 
}

for(;;) {
    // Wait for incoming connection  
    struct sockaddr_in cliaddr;  
    socklen_t len = sizeof(cliaddr);  
    int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);  
    if (-1 == connfd) {  
        perror("Could not accept incoming client");
        continue;   
    }  
        
    //Resolving Client Address  
    char buff[INET_ADDRSTRLEN + 1] = {0};  
    inet_ntop(AF_INET, &cliaddr.sin_addr, buff, INET_ADDRSTRLEN);  
    uint16_t cli_port = ntohs(cliaddr.sin_port);  
    printf("connection from %s, port %d\n", buff, cli_port);

    for(;;) {
        // Read from serial device into variable here, then send
        if(send(connfd, "Data...Data...Data\n", 19, 0) < 0) {
            printf("Client disconnected...\n"); 
            break; 
        }
    }
}
  • 2
    Try-catch? C? C doesn't have exceptions. – Andrew Henle Dec 14 '20 at 15:32
  • 1
    Right. I'm saying that in another language like Python, I can use try-catch to handle client disconnects. C does not have this mechanism, so I'm wondering if there's an alternative that doesn't require the client having to ping back to the server all the time. – Brent Redmon Dec 14 '20 at 15:37
  • Without continuously ongoing communication there is no way of knowing that the connection is still alive. – Sven Nilsson Dec 14 '20 at 15:45
  • 1
    Normally a server would use `select` or `poll` to see which clients have sent new data. Disconnecting counts as sending new data, but when you try to read the data `read` will return 0. – user253751 Dec 14 '20 at 15:55
  • @user253751 -- I see. So does this mean that the only way to handle a client disconnecting is by having the client continually send new data, and then have the server read from the socket? It's just strange to me that most languages are built with C under the hood, and they can handle client disconnects without reading from the socket. But C itself cannot handle it unless the client constantly pings the server – Brent Redmon Dec 14 '20 at 16:13
  • If a function call returns a value that indicates an error (or special condition) you should always check `errno` to find out what exactly happened and handle the error/situation. If the client closed the connection, an attempt to `write` will return -1 and `errno==EPIPE` and the process will also receive a signal `SIGPIPE`. A call to `read` will probably return 0 to indicate EOF. Read the documentation of the functions you use. – Bodo Dec 14 '20 at 16:55
  • @BrentRedmon No, you ask the OS whether there is anything to read, and if the client has disconnected, the OS says "yes", but you can't read anything. That's how the OS tells you the socket was disconnected. They do read from the socket. Have you learned about non-blocking sockets yet? – user253751 Dec 14 '20 at 17:24

2 Answers2

0

Looks like a duplicate of this, this and this.

Long story short you can't detect the disconnection until you perform some write (or read) on that connection. More exactly, even if it seems there is no error returned by send, this is not a guarantee that this operation was really sent and received by the client. The reason is that the socket operations are buffered and the payload of send is just queued so that the kernel will dispatch it later on.

Depending on the context, the requirements and the assumptions you can do something more. For example, if you are under the hypothesys that you will send periodic message at constant frequency, you can use select and a timeout approach to detect an anomaly. In other words if you have not received anything in the last 3 minutes you assume that there is an issue.

As you can easily found, this and this are a good read on the topic. Look at that for a far more detailed explanation and other ideas.

What you call the ping (intended as a message that is sent for every received packet) is more similar to what is usually known as an ACK.

You only need something like that (ACK/NACK) if you also want to be sure that the client received and processed that message.

emmunaf
  • 426
  • 2
  • 9
  • Thank you for your response and for all of the links! I am normally a Golang/Python developer, so this is all very new to me. I will read over the links. Thanks again! – Brent Redmon Dec 14 '20 at 17:20
0

Thanks to @emmanuaf, this is the solution that fits my project criteria. The thing that I was missing was the MSG_NOSIGNAL flag, referenced here.

I use Mashpoe's C Vector Library to create a new vector, which will hold all of my incoming client connections.

int* client_array = vector_create(); 

I then spawn a pthread that continually reads from a serial device, stores that data in a variable, and then sends it to each client in the client list

void* serve_clients(int *vargp) {

    for(;;) {

        // Perform a microsleep
        sleep(0.1);

        // Read from the Serial device

        // Get the size of the client array vector
        int client_vector_size = vector_size(vargp); 

        for(int i = 0 ; i < client_vector_size ; i++) {
            
            // Make a reference to the socket
            int* conn_fd = &vargp[i]; 

            /*
                In order to properly handle client disconnects, we supply a MSG_NOSIGNAL 
                flag to the send() call. That way, if the client disconnects, we will 
                be able to detect this, and properly remove them from the client list. 

                Referenced from: https://beej.us/guide/bgnet/html//index.html#sendman
            */
            if (send(*conn_fd, "Reply from server\n", 18, MSG_NOSIGNAL) < 0) {
                printf("Client disconnected...\n"); 

                // Close the client connection
                close(*conn_fd); 

                // Remove client socket from the vector
                vector_remove(vargp, i); 

                // Decrement index and client_server_size by 1
                i--; 
                client_vector_size--; 

            }             
        }
    }
}

To spawn the pthread:

// Spawn the thread that serves clients
pthread_t serving_thread; 
pthread_create(&serving_thread, NULL, serve_clients, client_array);

When a new connection comes in, I simply add the new connection to the client vector

while(1) {
    // Wait for incoming connection  
    struct sockaddr_in cliaddr;  
    socklen_t len = sizeof(cliaddr);  
    int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);  
    if (-1 == connfd) {  
        perror("Could not accept incoming client");
        continue;   
    }  
        
    //Resolving Client Address  
    char buff[INET_ADDRSTRLEN + 1] = {0};  
    inet_ntop(AF_INET, &cliaddr.sin_addr, buff, INET_ADDRSTRLEN);  
    uint16_t cli_port = ntohs(cliaddr.sin_port);  
    printf("connection from %s:%d -- Connfd: %d\n", buff, cli_port, connfd); 

    // Add client to vector list
    vector_add(&client_array, connfd);
}

In the end, we have a TCP server that can multiplex data to many clients, and handle when those clients disconnect.