TheChosenOne
TheChosenOne

Reputation: 105

What's the ideal way to write a code for non-blocking connect()?

What is the ideal way to write code for non-blocking connect? I saw a reference from the other thread in stackoverflow(Linux, sockets, non-blocking connect) where it mentions checking status of the socket by getsockopt(fd, SOL_SOCKET, SO_ERROR, ...) in the end and I cannot find any reference for why this is needed? Also, it mentions about handling ECONNREFUSED. How and why it needs to be handled? Can anyone comment? Thanks.

int nonblocking_connect() {
  int flags, ret, res;
  struct sockaddr_in serv_addr;
  int fd = socket(AF_INET, SOCK_STREAM, 0);
  if (fd == -1) {
    return -1;
  }

  flags = fcntl(fd, F_GETFL, 0);
  if (flags == -1) {
    goto end;
  }

  if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
    goto end;
  }

  serv_addr.sin_family = AF_INET;
  serv_addr.sin_port = htons(8080);
  serv_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

  ret = connect(fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
  if (ret == -1) {
    fd_set wfd, efd;
    struct timeval tv;
    if (errno != EINPROGRESS) {
      goto end;
    }

    FD_ZERO(&wfd);
    FD_SET(fd, &wfd);

    FD_ZERO(&efd);
    FD_SET(fd, &efd);

    // Set 1 second timeout for successfull connect
    tv.tv_sec = 1;

    res = select(fd + 1, NULL, &wfd, &efd, &tv);
    if (res == -1) {
      goto end;
    }

    // timed-out
    if (res == 0) {
      goto end;
    }

    if (FD_ISSET(fd, &efd)) {
      goto end;
    }
  }
  return fd;
end:
  close(fd);
  return -1;
}

Upvotes: 1

Views: 951

Answers (1)

Jeremy Friesner
Jeremy Friesner

Reputation: 73061

The code shown in the example is a bit misleading, in that it's not really implementing a non-blocking connect; rather it is implementing a blocking connect with a one-second timeout. (That is, if the code is working as intended, the nonblocking_connect() function might not return for up to one second when it is called).

That's fine, if that's what you want to do, but the real use-case for a non-blocking connect() is when your event-loop needs to make a TCP connection but also wants to be able to do other things while the TCP connection-setup is in progress.

For example, the program below will echo back any text you type in to stdin; however if you type in a command of the form connect 172.217.9.4 it will start a non-blocking TCP connection to port 443 of the IP address you entered. The interesting thing to note is that while the TCP connection is in progress you are still able to enter text into stdin and the program can still respond (it can even abort the TCP-connection-in-progress and start a new one if you tell it to) -- that can be useful, especially when the TCP connection is taking a long time to set up (e.g. because the server is slow, or because there is a firewall between you and the server that is blocking your client's TCP packets, in which case the TCP connection attempt might take several minutes before it times out and fails)

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char ** argv)
{
   printf("Type something and press return to have your text echoed back to you\n");
   printf("Or type e.g. connect 172.217.9.4 and press return to start a non-blocking TCP connection.\n");
   printf("Note that the text-echoing functionality still works, even when the TCP connection setup is still in progress!\n");

   int tcpSocket = -1;  // this will be set non-negative only when we have a TCP connection in progress
   while(1)
   {
      fd_set readFDs, writeFDs;
      FD_ZERO(&readFDs);
      FD_ZERO(&writeFDs);

      FD_SET(STDIN_FILENO, &readFDs);
      if (tcpSocket >= 0) FD_SET(tcpSocket, &writeFDs); 
   
      int maxFD = STDIN_FILENO;
      if (tcpSocket > maxFD) maxFD = tcpSocket;
       
      if (select(maxFD+1, &readFDs, &writeFDs, NULL, NULL) < 0) {perror("select"); exit(10);}
   
      if (FD_ISSET(STDIN_FILENO, &readFDs))
      {
         char buf[256] = "\0";
         fgets(buf, sizeof(buf), stdin);
      
         if (strncmp(buf, "connect ", 8) == 0)
         {  
            if (tcpSocket >= 0)
            {
               printf("Closing existing TCP socket %i before starting a new connection attempt\n", tcpSocket);
               close(tcpSocket);
               tcpSocket = -1;
            }
       
            tcpSocket = socket(AF_INET, SOCK_STREAM, 0);
            if (tcpSocket < 0) {perror("socket"); exit(10);}
            
            const char * connectDest = &buf[8];
            printf("Starting new TCP connection using tcpSocket=%i to: %s\n", tcpSocket, connectDest);
            
            int flags = fcntl(tcpSocket, F_GETFL, 0);
            if (flags == -1) {perror("fcntl"); exit(10);}
            if (fcntl(tcpSocket, F_SETFL, flags | O_NONBLOCK) == -1) {perror("fcntl"); exit(10);}
               
            struct sockaddr_in serv_addr; memset(&serv_addr, 0, sizeof(serv_addr));
            serv_addr.sin_family = AF_INET;
            serv_addr.sin_port = htons(443);  // https port
            if (inet_aton(connectDest, &serv_addr.sin_addr) != 1) printf("Unable to parse IP address %s\n", connectDest);
            int ret = connect(tcpSocket, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
            if (ret == 0)
            {
               printf("connect() succeeded immediately!  We can just use tcpSocket now\n");
               close(tcpSocket);  // but for the sake of this demo, I won't
               tcpSocket = -1;
            }
            else if (ret == -1)
            {
               if (errno == EINPROGRESS)
               {
                  printf("connect() returned -1/EINPROGRESS: the TCP connection attempt is now happening, but in the background.\n");
                  printf("while that's going on, you can still enter text here.\n");
               } 
               else
               {
                  perror("connect"); 
                  exit(10);
               }
            }
         }  
         else printf("You typed:  %s\n", buf);
      }        
                  
      if ((tcpSocket >= 0)&&(FD_ISSET(tcpSocket, &writeFDs)))
      {
         // Aha, the TCP setup has completed!  Now let's see if it succeeded or failed
         int setupResult;
         socklen_t resultLength = sizeof(setupResult);
         if (getsockopt(tcpSocket, SOL_SOCKET, SO_ERROR, &setupResult, &resultLength) < 0) {perror("getsocketopt"); exit(10);}

         if (setupResult == 0)
         {
            printf("\nTCP connection setup complete!  The TCP socket can now be used to communicate with the server\n");
         }
         else
         {
            printf("\nTCP connection setup failed because [%s]\n", strerror(setupResult));
         }

         // Close the socket, since for the purposes of this demo we don't need it any longer
         // A real program would probably keep it around and select()/send()/recv() on it as appropriate
         close(tcpSocket);
         tcpSocket = -1;
      }
   }
}

As for why you would want to call getsockopt(fd, SOL_SOCKET, SO_ERROR, ...), it's simply to determine whether select() returned ready-for-write on the TCP socket because the TCP-connection-setup succeeded, or because it failed (and in the latter case why it failed, if you care about why)

Upvotes: 4

Related Questions