非阻塞connect編寫方法介紹

TCP連接的建立涉及到一個三次握手的過程,且SOCKET中connect函數需要一直等到客戶接收到對於自己的SYN的ACK爲止才返回,這意味着每個connect函數總會阻塞其調用進程至少一個到服務器的RTT時間,而RTT波動範圍很大,從局域網的幾個毫秒到幾百個毫秒甚至廣域網上的幾秒。這段時間內,我們可以執行其他處理工作,以便做到並行。在此,需要用到非阻塞connect。本文主要介紹了非阻塞connect的編寫方法以及應用場景。

1. 基礎知識

(1) fcntl函數

fcntl函數可執行各種描述符的控制操作,對於socket描述符,常用應用是將其設置爲阻塞式IO,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
int flags;
  
if((flags = fcntl(fd, F_GETFL)) < 0) //獲取當前的flags標誌
  
  err_sys(“F_GETFL error!”);
  
flags |= O_NONBLOCK; //修改非阻塞標誌位
  
if(fcntl(fd, F_SETFL, flags) < 0)
  
  err_sys(“F_SETFL error!”);

(2) connect函數

對於阻塞式套接字,調用connect函數將激發TCP的三次握手過程,而且僅在連接建立成功或者出錯時才返回;對於非阻塞式套接字,如果調用connect函數會之間返回-1(表示出錯),且錯誤爲EINPROGRESS,表示連接建立,建立啓動但是尚未完成;如果返回0,則表示連接已經建立,這通常是在服務器和客戶在同一臺主機上時發生。

1
2
3
4
5
6
7
8
9
10
11
12
if (connect(fd, (struct sockaddr*)&sa, sizeof(sa)) == -1)
  
  if (errno != EINPROGRESS) {
  
   return -1;
  
  }
  
  if(n == 0)
  
    goto done;

(3) select函數

select是一種IO多路複用機制,它允許進程指示內核等待多個事件的任何一個發生,並且在有一個或者多個事件發生或者經歷一段指定的時間後才喚醒它。

connect本身並不具有設置超時功能,如果想對套接字的IO操作設置超時,可使用select函數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fd_set wfd;
  
FD_ZERO(&wfd);
  
FD_SET(fd, &wfd);
  
if (select(FD_SETSIZE, NULL, &wfd, NULL, toptr) == -1) {
  
  __redisSetError(c,REDIS_ERR_IO,
  
    sdscatprintf(sdsempty(), "select(2): %s", strerror(errno)));
  
  close(fd);
  
  return REDIS_ERR;
  
}

對於select和非阻塞connect,注意兩點:

[1] 當連接成功建立時,描述符變成可寫; [2] 當連接建立遇到錯誤時,描述符變爲即可讀,也可寫,遇到這種情況,可調用getsockopt函數。

(4) getsockopt函數

可獲取影響套接字的選項,比如SOCKET的出錯信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
err = 0;
  
errlen = sizeof(err);
  
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errlen) == -1) {
  
  sprintf("getsockopt(SO_ERROR): %s", strerror(errno)));
  
  close(fd);
  
  return ERR;
  
}
  
if (err) {
  
  errno = err;
  
  close(fd);
  
  return ERR;
  
}

2. 實現非阻塞式connect

分以下幾步:

(1) 創建socket,並利用fcntl將其設置爲非阻塞

(2) 調用connect函數,如果返回0,則連接建立;如果返回-1,檢查errno ,如果值爲 EINPROGRESS,則連接正在建立。

(3) 爲了控制連接建立時間,將該socket描述符加入到select的可寫集合中,採用select函數設定超時。

(4) 如果規定時間內成功建立,則描述符變爲可寫;否則,採用getsockopt函數捕獲錯誤信息

(5) 恢復套接字的文件狀態並返回。

3. 應用實例

(1)實例一

《unix網絡編程》卷1的16.5節有一個Netscape 的web客戶端的程序實例,客戶端先建立一個與某個web服務器的HTTP連接,然後獲取該網站的主頁。該主頁往往含有多個對於其他網頁的引用,客戶可以使用非阻塞connect同時獲取多個網頁,以此取代每次只獲取一個網頁的串行獲取手段。

(2)實例二

Redis客戶端CLI (command line interface),位於源代碼的src/deps/hiredis下面。實際上,不僅是Redis客戶端,其他類似的client/server架構中,client均可採用非阻塞式connect實現。

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
int redisContextConnectTcp(redisContext *c, const char *addr, int port, struct timeval *timeout) {
 int s;
 int blocking = (c->flags & REDIS_BLOCK);
 struct sockaddr_in sa;
  
 if ((s = redisCreateSocket(c,AF_INET)) < 0)
  return REDIS_ERR;
 if (redisSetBlocking(c,s,0) != REDIS_OK)
  return REDIS_ERR;
 sa.sin_family = AF_INET;
 sa.sin_port = htons(port);
 if (inet_aton(addr, &sa.sin_addr) == 0) {
  struct hostent *he;
  he = gethostbyname(addr);
  if (he == NULL) {
   __redisSetError(c,REDIS_ERR_OTHER,  sdscatprintf(sdsempty(),"Can't resolve: %s",addr));
   close(s);
   return REDIS_ERR;
}
memcpy(&sa.sin_addr, he->h_addr, sizeof(struct in_addr));
 }
  
 if (connect(s, (struct sockaddr*)&sa, sizeof(sa)) == -1) {
 if (errno == EINPROGRESS && !blocking) {
 /* This is ok. */
 } else {
 if (redisContextWaitReady(c,s,timeout) != REDIS_OK)
 return REDIS_ERR;
 }
 }
  
 /* Reset socket to be blocking after connect(2). */
 if (blocking && redisSetBlocking(c,s,1) != REDIS_OK)
 return REDIS_ERR;
  
 if (redisSetTcpNoDelay(c,s) != REDIS_OK)
 return REDIS_ERR;
  
 c->fd = s;
 c->flags |= REDIS_CONNECTED;
 return REDIS_OK;
}
  
static int redisContextWaitReady(redisContext *c, int fd, const struct timeval *timeout) {
 struct timeval to;
 struct timeval *toptr = NULL;
 fd_set wfd;
 int err;
 socklen_t errlen;
  
 /* Only use timeout when not NULL. */
 if (timeout != NULL) {
 to = *timeout;
 toptr = &to;
 }
  
 if (errno == EINPROGRESS) {
 FD_ZERO(&wfd);
 FD_SET(fd, &wfd);
  
 if (select(FD_SETSIZE, NULL, &wfd, NULL, toptr) == -1) {
 __redisSetError(c,REDIS_ERR_IO,
 sdscatprintf(sdsempty(), "select(2): %s", strerror(errno)));
 close(fd);
 return REDIS_ERR;
 }
  
 if (!FD_ISSET(fd, &wfd)) {
 errno = ETIMEDOUT;
 __redisSetError(c,REDIS_ERR_IO,NULL);
 close(fd);
 return REDIS_ERR;
 }
  
 err = 0;
 errlen = sizeof(err);
 if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errlen) == -1) {
 __redisSetError(c,REDIS_ERR_IO,
 sdscatprintf(sdsempty(), "getsockopt(SO_ERROR): %s", strerror(errno)));
 close(fd);
 return REDIS_ERR;
 }
  
 if (err) {
 errno = err;
 __redisSetError(c,REDIS_ERR_IO,NULL);
 close(fd);
 return REDIS_ERR;
 }
  
 return REDIS_OK;
 }
  
 __redisSetError(c,REDIS_ERR_IO,NULL);
 close(fd);
 return REDIS_ERR;
}

原創文章,轉載請註明: 轉載自董的博客

本文鏈接地址: http://dongxicheng.org/network/non-block-connect-implemention/

作者:Dong,作者介紹:http://dongxicheng.org/about/

本博客的文章集合:


發佈了100 篇原創文章 · 獲贊 6 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章