[ Team LiB ] Previous Section Next Section

14.2 Socket Timeouts

There are three ways to place a timeout on an I/O operation involving a socket:

  1. Call alarm, which generates the SIGALRM signal when the specified time has expired. This involves signal handling, which can differ from one implementation to the next, and it may interfere with other existing calls to alarm in the process.

  2. Block waiting for I/O in select, which has a time limit built-in, instead of blocking in a call to read or write.

  3. Use the newer SO_RCVTIMEO and SO_SNDTIMEO socket options. The problem with this approach is that not all implementations support these two socket options.

All three techniques work with input and output operations (e.g., read, write, and other variations such as recvfrom and sendto), but we would also like a technique that we can use with connect, since a TCP connect can take a long time to time out (typically 75 seconds). select can be used to place a timeout on connect only when the socket is in a nonblocking mode (which we show in Section 16.3), and the two socket options do not work with connect. We also note that the first two techniques work with any descriptor, while the third technique works only with socket descriptors.

We now show examples of all three techniques.

connect with a Timeout Using SIGALRM

Figure 14.1 shows our function connect_timeo that calls connect with an upper limit specified by the caller. The first three arguments are the three required by connect and the fourth argument is the number of seconds to wait.

Figure 14.1 connect with a timeout.

lib/connect_timeo.c

 1 #include    "unp.h"

 2 static void connect_alarm(int);

 3 int
 4 connect_timeo(int sockfd, const SA *saptr, socklen_t salen, int nsec)
 5 {
 6     Sigfunc *sigfunc;
 7     int     n;

 8     sigfunc = Signal(SIGALRM, connect_alarm);
 9     if (alarm(nsec) != 0)
10         err_msg("connect_timeo: alarm was already set");

11     if ( (n = connect(sockfd, saptr, salen)) < 0) {
12         close(sockfd);
13         if(errno == EINTR)
14            errno = ETIMEDOUT;
15     }
16     alarm(0);                   /* turn off the alarm */
17     Signal(SIGALRM, sigfunc);   /* restore previous signal handler */

18     return (n);
19 }

20 static void
21 connect_alarm(int signo)
22 {
23     return;                     /* just interrupt the connect() */
24 }
Establish signal handler

8 A signal handler is established for SIGALRM. The current signal handler (if any) is saved, so we can restore it at the end of the function.

Set alarm

9–10 The alarm clock for the process is set to the number of seconds specified by the caller. The return value from alarm is the number of seconds currently remaining in the alarm clock for the process (if one has already been set by the process) or 0 (if there is no current alarm). In the former case we print a warning message since we are wiping out that previously set alarm (see Exercise 14.2).

Call connect

11–15 connect is called and if the function is interrupted (EINTR), we set the errno value to ETIMEDOUT instead. The socket is closed to prevent the three-way handshake from continuing.

Turn off alarm and restore any previous signal handler

16–18 The alarm is turned off by setting it to 0 and the previous signal handler (if any) is restored.

Handle SIGALRM

20–24 The signal handler just returns, assuming this return will interrupt the pending connect, causing connect to return an error of EINTR. Recall our signal function (Figure 5.6) that does not set the SA_RESTART flag when the signal being caught is SIGALRM.

One point to make with this example is that we can always reduce the timeout period for a connect using this technique, but we cannot extend the kernel's existing timeout. That is, on a Berkeley-derived kernel the timeout for a connect is normally 75 seconds. We can specify a smaller value for our function, say 10, but if we specify a larger value, say 80, the connect itself will still time out after 75 seconds.

Another point with this example is that we use the interruptibility of the system call (connect) to return before the kernel's time limit expires. This is fine when we perform the system call and can handle the EINTR error return. But in Section 29.7, we will encounter a library function that performs the system call, and the library function reissues the system call when EINTR is returned. We can still use SIGALRM in this scenario, but we will see in Figure 29.10 that we also have to use sigsetjmp and siglongjmp to get around the library's ignoring of EINTR.

Although this example is fairly simple, signals are quite difficult to use correctly with multithreaded programs (see Chapter 26). So, the technique shown here is only recommended for single-threaded programs.

recvfrom with a Timeout Using SIGALRM

Figure 14.2 is a redo of our dg_cli function from Figure 8.8, but with a call to alarm to interrupt the recvfrom if a reply is not received within five seconds.

Figure 14.2 dg_cli function with alarm to timeout recvfrom.

advio/dgclitimeo3.c

 1 #include    "unp.h"

 2 static void sig_alrm(int);

 3 void
 4 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
 5 {
 6     int     n;
 7     char    sendline[MAXLINE], recvline[MAXLINE + 1];

 8     Signal(SIGALRM, sig_alrm);

 9     while (Fgets(sendline, MAXLINE, fp) != NULL) {

10         Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

11         alarm(5);
12         if ( (n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL)) < 0) {
13             if (errno == EINTR)
14                 fprintf(stderr, "socket timeout\n");
15             else
16                 err_sys("recvfrom error");
17         } else {
18             alarm(0);
19             recvline[n] = 0;    /* null terminate */
20             Fputs(recvline, stdout);
21         }
22     }
23 }

24 static void
25 sig_alrm(int signo)
26 {
27     return;                     /* just interrupt the recvfrom() */
28 }
Handle timeout from recvfrom

8–22 We establish a signal handler for SIGALRM and then call alarm for a five-second timeout before each call to recvfrom. If recvfrom is interrupted by our signal handler, we print a message and continue. If a line is read from the server, we turn off the pending alarm and print the reply.

SIGALRM signal handler

24–28 Our signal handler just returns, to interrupt the blocked recvfrom.

This example works correctly because we are reading only one reply each time we establish an alarm. In Section 20.4, we will use the same technique, but since we are reading multiple replies for a given alarm, a race condition exists that we must handle.

recvfrom with a Timeout Using select

We demonstrate the second technique for setting a timeout (using select) in Figure 14.3. It shows our function named readable_timeo which waits up to a specified number of seconds for a descriptor to become readable.

Figure 14.3 readable_timeo function: waits for a descriptor to become readable.

lib/readable_timeo.c

 1 #include     "unp.h"

 2 int
 3 readable_timeo(int fd, int sec)
 4 {
 5     fd_set rset;
 6     struct timeval tv;

 7     FD_ZERO(&rset);
 8     FD_SET(fd, &rset);

 9     tv.tv_sec = sec;
10     tv.tv_usec = 0;

11     return (select(fd + 1, &rset, NULL, NULL, &tv));
12         /* > 0 if descriptor is readable */
13 }
Prepare arguments for select

7–10 The bit corresponding to the descriptor is turned on in the read descriptor set. A timeval structure is set to the number of seconds that the caller wants to wait.

Block in select

11–12 select waits for the descriptor to become readable, or for the timeout to expire. The return value of this function is the return value of select: –1 on an error, 0 if a timeout occurs, or a positive value specifying the number of ready descriptors.

This function does not perform the read operation; it just waits for the descriptor to be ready for reading. Therefore, this function can be used with any type of socket, TCP or UDP.

It is trivial to create a similar function named writable_timeo that waits for a descriptor to become writable.

We use this function in Figure 14.4, which is a redo of our dg_cli function from Figure 8.8. This new version calls recvfrom only when our readable_timeo function returns a positive value.

We do not call recvfrom until the function readable_timeo tells us that the descriptor is readable. This guarantees that recvfrom will not block.

Figure 14.4 dg_cli function that calls readable_timeo to set a timeout.

advio/dgclitimeo1.c

 1 #include     "unp.h"

 2 void
 3 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
 4 {
 5     int     n;
 6     char    sendline[MAXLINE], recvline[MAXLINE + 1];

 7     while (Fgets(sendline, MAXLINE, fp) != NULL) {

 8         Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

 9         if (Readable_timeo(sockfd, 5) == 0) {
10             fprintf(stderr, "socket timeout\n");
11         } else {
12             n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
13             recvline[n] = 0;    /* null terminate */
14             Fputs(recvline, stdout);
15         }
16     }
17 }

recvfrom with a Timeout Using the SO_RCVTIMEO Socket Option

Our final example demonstrates the SO_RCVTIMEO socket option. We set this option once for a descriptor, specifying the timeout value, and this timeout then applies to all read operations on that descriptor. The nice thing about this method is that we set the option only once, compared to the previous two methods, which required doing something before every operation on which we wanted to place a time limit. But this socket option applies only to read operations, and the similar option SO_SNDTIMEO applies only to write operations; neither socket option can be used to set a timeout for a connect.

Figure 14.5 shows another version of our dg_cli function that uses the SO_RCVTIMEO socket option.

Set socket option

8–10 The fourth argument to setsockopt is a pointer to a timeval structure that is filled in with the desired timeout.

Test for timeout

15–17 If the I/O operation times out, the function (recvfrom, in this case) returns EWOULDBLOCK.

Figure 14.5 dg_cli function that uses the SO_RCVTIMEO socket option to set a timeout.

advio/dgclitimeo2.c

 1 #include     "unp.h"

 2 void
 3 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
 4 {
 5     int     n;
 6     char     sendline[MAXLINE], recvline[MAXLINE + 1];
 7     struct timeval tv;

 8     tv.tv_sec = 5;
 9     tv.tv_usec = 0;
10     Setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

11     while (Fgets(sendline, MAXLINE, fp) != NULL) {

12         Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

13         n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
14         if (n < 0) {
15             if (errno == EWOULDBLOCK) {
16                 fprintf(stderr, "socket timeout\n");
17                 continue;
18             } else
19                 err_sys("recvfrom error");
20         }

21         recvline[n] = 0;        /* null terminate */
22         Fputs(recvline, stdout);
23     }
24 }
    [ Team LiB ] Previous Section Next Section