linux網路程式設計之socket(十一):套接字I/O超時設定方法和用select實現超時

weixin_34344677發表於2013-06-11

一、使用alarm 函式設定超時

 

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
 
void handler( int sig)
{
}
signal(SIGALRM, handler);

alarm( 5);
int ret = read(fd, buf,  sizeof(buf));
if (ret == - 1 && errno == EINTR)
    errno = ETIMEOUT;
else  if (ret >=  0)
    alarm( 0);
.................

 

 

程式大概框架如上所示,如果read在5s內被SIGALRM訊號中斷而返回,則表示超時,否則未超時已讀取到資料,取消鬧鐘。但這種方法不常用,因為有時可能在其他地方使用了alarm會造成混亂。


二、使用套接字選項SO_SNDTIMEO、SO_RCVTIMEO

 

 C++ Code 
1
2
3
4
5
6
 
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO,  5);

int ret = read(sock, buf,  sizeof(buf));
if (ret == - 1 && errno == EWOULDBLOCK)
    errno = ETIMEOUT;
..........

 

 

即使用setsockopt 函式進行設定,但這種方法可移植性比較差,不是每種系統實現都有這些選項。


三、使用select 實現超時

下面程式包含read_timeout、write_timeout、accept_timeout、connect_timeout 四個函式封裝

 

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
 
/*************************************************************************
    > File Name: sysutil.c
    > Author: Simba
    > Mail: dameng34@163.com
    > Created Time: Sat 02 Mar 2013 10:53:06 PM CST
 ************************************************************************/


#include  "sysutil.h"

/* read_timeout - 讀超時檢測函式,不含讀操作
 * fd:檔案描述符
 * wait_seconds:等待超時秒數, 如果為0表示不檢測超時;
 * 成功(未超時)返回0,失敗返回-1,超時返回-1並且errno = ETIMEDOUT
 */


int read_timeout( int fd,  unsigned  int wait_seconds)
{
     int ret =  0;
     if (wait_seconds >  0)
    {

        fd_set read_fdset;
         struct timeval timeout;

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

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec =  0;

         do
        {
            ret = select(fd +  1, &read_fdset,  NULLNULL, &timeout);  //select會阻塞直到檢測到事件或者超時
             // 如果select檢測到可讀事件傳送,則此時呼叫read不會阻塞
        }
         while (ret <  0 && errno == EINTR);

         if (ret ==  0)
        {
            ret = - 1;
            errno = ETIMEDOUT;
        }
         else  if (ret ==  1)
             return  0;

    }

     return ret;
}

/* write_timeout - 寫超時檢測函式,不含寫操作
 * fd:檔案描述符
 * wait_seconds:等待超時秒數, 如果為0表示不檢測超時;
 * 成功(未超時)返回0,失敗返回-1,超時返回-1並且errno = ETIMEDOUT
 */


int write_timeout( int fd,  unsigned  int wait_seconds)
{
     int ret =  0;
     if (wait_seconds >  0)
    {

        fd_set write_fdset;
         struct timeval timeout;

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

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec =  0;

         do
        {
            ret = select(fd +  1NULL, &write_fdset,  NULL, &timeout);
        }
         while (ret <  0 && errno == EINTR);

         if (ret ==  0)
        {
            ret = - 1;
            errno = ETIMEDOUT;
        }
         else  if (ret ==  1)
             return  0;

    }

     return ret;
}

/* accept_timeout - 帶超時的accept
 * fd: 套接字
 * addr: 輸出引數,返回對方地址
 * wait_seconds: 等待超時秒數,如果為0表示正常模式
 * 成功(未超時)返回已連線套接字,失敗返回-1,超時返回-1並且errno = ETIMEDOUT
 */


int accept_timeout( int fd,  struct sockaddr_in *addr,  unsigned  int wait_seconds)
{
     int ret;
    socklen_t addrlen =  sizeof( struct sockaddr_in);

     if (wait_seconds >  0)
    {

        fd_set accept_fdset;
         struct timeval timeout;
        FD_ZERO(&accept_fdset);
        FD_SET(fd, &accept_fdset);

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec =  0;

         do
        {
            ret = select(fd +  1, &accept_fdset,  NULLNULL, &timeout);
        }
         while (ret <  0 && errno == EINTR);

         if (ret == - 1)
             return - 1;
         else  if (ret ==  0)
        {
            errno = ETIMEDOUT;
             return - 1;
        }
    }

     if (addr !=  NULL)
        ret = accept(fd, ( struct sockaddr *)addr, &addrlen);
     else
        ret = accept(fd,  NULLNULL);
     if (ret == - 1)
        ERR_EXIT( "accpet error");

     return ret;
}

/* activate_nonblock - 設定IO為非阻塞模式
 * fd: 檔案描述符
 */

void activate_nonblock( int fd)
{
     int ret;
     int flags = fcntl(fd, F_GETFL);
     if (flags == - 1)
        ERR_EXIT( "fcntl error");

    flags |= O_NONBLOCK;
    ret = fcntl(fd, F_SETFL, flags);
     if (ret == - 1)
        ERR_EXIT( "fcntl error");
}

/* deactivate_nonblock - 設定IO為阻塞模式
 * fd: 檔案描述符
 */

void deactivate_nonblock( int fd)
{
     int ret;
     int flags = fcntl(fd, F_GETFL);
     if (flags == - 1)
        ERR_EXIT( "fcntl error");

    flags &= ~O_NONBLOCK;
    ret = fcntl(fd, F_SETFL, flags);
     if (ret == - 1)
        ERR_EXIT( "fcntl error");
}

/* connect_timeout - 帶超時的connect
 * fd: 套接字
 * addr: 輸出引數,返回對方地址
 * wait_seconds: 等待超時秒數,如果為0表示正常模式
 * 成功(未超時)返回0,失敗返回-1,超時返回-1並且errno = ETIMEDOUT
 */

int connect_timeout( int fd,  struct sockaddr_in *addr,  unsigned  int wait_seconds)
{
     int ret;
    socklen_t addrlen =  sizeof( struct sockaddr_in);

     if (wait_seconds >  0)
        activate_nonblock(fd);

    ret = connect(fd, ( struct sockaddr *)addr, addrlen);
     if (ret <  0 && errno == EINPROGRESS)
    {

        fd_set connect_fdset;
         struct timeval timeout;
        FD_ZERO(&connect_fdset);
        FD_SET(fd, &connect_fdset);

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec =  0;

         do
        {
             /* 一旦連線建立,套接字就可寫 */
            ret = select(fd +  1NULL, &connect_fdset,  NULL, &timeout);
        }
         while (ret <  0 && errno == EINTR);

         if (ret ==  0)
        {
            errno = ETIMEDOUT;
             return - 1;
        }
         else  if (ret <  0)
             return - 1;

         else  if (ret ==  1)
        {
             /* ret返回為1,可能有兩種情況,一種是連線建立成功,一種是套接字產生錯誤
             * 此時錯誤資訊不會儲存至errno變數中(select沒出錯),因此,需要呼叫
             * getsockopt來獲取 */

             int err;
            socklen_t socklen =  sizeof(err);
             int sockoptret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &socklen);
             if (sockoptret == - 1)
                 return - 1;
             if (err ==  0)
                ret =  0;
             else
            {
                errno = err;
                ret = - 1;
            }
        }
    }

     if (wait_seconds >  0)
        deactivate_nonblock(fd);


     return ret;
}
下面來解析一下這些函式的封裝:

1、read_timeout :如註釋所寫,這只是讀超時檢測函式,並不包含讀操作,如果從此函式成功返回,則此時呼叫read將不再阻塞,測試程式碼可以這樣寫:
 C++ Code 
1
2
3
4
5
6
7
8
 
int ret;
ret = read_timeout(fd,  5);
if (ret ==  0)
    read(fd, buf,  sizeof(buf));
else  if (ret == - 1 && errno == ETIMEOUT)
    printf( "timeout...\n");
else
    ERR_EXIT( "read_timeout");

 

如果 read_timeout(fd, 0); 則表示不檢測超時,函式直接返回為0,此時再呼叫read 將會阻塞。

當wait_seconds 引數大於0,則進入if 括號執行,將超時時間設定為select函式的超時時間結構體,select會阻塞直到檢測到事件發生或者超時。如果select返回-1且errno 為EINTR,說明是被訊號中斷,需要重啟select;如果select返回0表示超時;如果select返回1表示檢測到可讀事件;否則select返回-1 表示出錯。


2、write_timeout :此函式跟read_timeout 函式類似,只是select 關心的是可寫事件,不再贅述。


3、accept_timeout :此函式是帶超時的accept 函式,如果能從if (wait_seconds > 0) 括號執行後向下執行,說明select 返回為1,檢測到已連線佇列不為空,此時再呼叫accept 不再阻塞,當然如果wait_seconds == 0 則像正常模式一樣,accept 阻塞等待,注意,accept 返回的是已連線套接字。


4、connect_timeout :在呼叫connect前需要使用fcntl 函式將套接字標誌設定為非阻塞,如果網路環境很好,則connect立即返回0,不進入if 大括號執行;如果網路環境擁塞,則connect返回-1且errno == EINPROGRESS,表示正在處理。此後呼叫select與前面3個函式類似,但這裡關注的是可寫事件,因為一旦連線建立,套接字就可寫。還需要注意的是當select 返回1,可能有兩種情況,一種是連線成功,一種是套接字產生錯誤,由這裡可知,這兩種情況都會產生可寫事件,所以需要使用getsockopt來獲取一下。退出之前還需重新將套接字設定為阻塞。


我們可以寫個小程式測試一下connect_timeout 函式,客戶端程式如下:

 

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 
#include  "sysutil.h"

int main( void)
{
     int sock;
     if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) <  0)
        ERR_EXIT( "socket");

     struct sockaddr_in servaddr;
    memset(&servaddr,  0sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons( 5188);
    servaddr.sin_addr.s_addr = inet_addr( "127.0.0.1");


     int ret = connect_timeout(sock, &servaddr,  5);
     if (ret == - 1 && errno == ETIMEDOUT)
    {
        printf( "timeout...\n");
         return  1;
    }
     else  if (ret == - 1)
        ERR_EXIT( "connect_timeout");

     struct sockaddr_in localaddr;
    socklen_t addrlen =  sizeof(localaddr);
     if (getsockname(sock, ( struct sockaddr *)&localaddr, &addrlen) <  0)
        ERR_EXIT( "getsockname");

    printf( "ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));


     return  0;
}

 

 

因為是在本機上測試,所以不會出現超時的情況,但出錯的情況還是可以看到的,比如不要啟動伺服器端程式,而直接啟動客戶端程式,輸出如下:

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echocli_timeout 
connect_timeout: Connection refused

很明顯是connect_timeout 函式返回了-1,我們也可以推算出connect_timeout 函式中,select返回1,但卻是套接字發生錯誤的情況,errno = ECONNREFUSED,所以列印出Connection refused。


 

參考:

《Linux C 程式設計一站式學習》

《TCP/IP詳解 卷一》

《UNP》

相關文章