1、前言
最近在寫一個測試工具,要求快速的高效率的掃描出各個服務器開放了哪些端口。當時想了一下,ping只能檢測ip,判斷服務器的網絡是連通的,而不能判斷是否開放了端口。我們知道端口屬於網絡的傳輸層,因此需要用ip和端口來探測,這個時候就可以用connect來探測一下,針對TCP協議,connect函數要進行TCP三次握手,如果connect成功,則說明服務器開放了某個端口,如果connect失敗,則說明服務器沒有開放某個端口。而connect失敗是通過超時來控制的,在規定的時間內,connect會發起多次連接,一直執行到超時,才返回錯誤。默認情況下,connect是阻塞的,而且默認的超時時間為75s,正常情況下,檢測網絡的連通性都是毫秒級,如果要判斷10萬台服務器的,用阻塞的默認的connect去做,效率非常低下。因此采用非阻塞的connect,而且需要自定義超時間(我自定義超時時間為5s)。
2、非阻塞connect
對於阻塞式套接字,調用connect函數將激發TCP的三次握手過程,而且僅在連接建立成功或者出錯時才返回;對於非阻塞式套接字,如果調用connect函數會之間返回-1(表示出錯),且錯誤為EINPROGRESS,表示連接建立,建立啟動但是尚未完成;如果返回0,則表示連接已經建立,這通常是在服務器和客戶在同一台主機上時發生。
select是一種IO多路復用機制,它允許進程指示內核等待多個事件的任何一個發生,並且在有一個或者多個事件發生或者經歷一段指定的時間后才喚醒它。connect本身並不具有設置超時功能,如果想對套接字的IO操作設置超時,可使用select函數。
對於select和非阻塞connect,注意兩點:[1] 當連接成功建立時,描述符變成可寫; [2] 當連接建立遇到錯誤時,描述符變為即可讀,也可寫,遇到這種情況,可調用getsockopt函數。
3、實現步驟
(1) 創建socket,並利用fcntl將其設置為非阻塞
(2) 調用connect函數,如果返回0,則連接建立;如果返回-1,檢查errno ,如果值為 EINPROGRESS,則連接正在建立。
(3) 為了控制連接建立時間,將該socket描述符加入到select的可讀可寫集合中,采用select函數設定超時。
(4) 如果規定時間內成功建立,則描述符變為可寫;否則,采用getsockopt函數捕獲錯誤信息
(5) 恢復套接字的文件狀態並返回。
測試代碼如下所示:
1 #include <stdio.h>
2 #include <stdlib.h> 3 #include <string.h> 4 #include <unistd.h> 5 #include <sys/types.h> /* See NOTES */ 6 #include <sys/socket.h> 7 #include <netinet/in.h> 8 #include <fcntl.h> 9 #include <errno.h> 10 11 int main(int argc, char **argv) 12 { 13 if (argc < 3) { 14 printf("please input ip and port, for example ./main 120.12.34.56 80.\n"); 15 return -1; 16 } 17 18 19 char *ipaddr = argv[1]; 20 unsigned int port = atoi(argv[2]); 21 22 int fd = 0; 23 struct sockaddr_in addr; 24 fd_set fdr, fdw; 25 struct timeval timeout; 26 int err = 0; 27 int errlen = sizeof(err); 28 29 fd = socket(AF_INET,SOCK_STREAM,0); 30 if (fd < 0) { 31 fprintf(stderr, "create socket failed,error:%s.\n", strerror(errno)); 32 return -1; 33 } 34 35 bzero(&addr, sizeof(addr)); 36 addr.sin_family = AF_INET; 37 addr.sin_port = htons(port); 38 inet_pton(AF_INET, ipaddr, &addr.sin_addr); 39 40 /*設置套接字為非阻塞*/ 41 int flags = fcntl(fd, F_GETFL, 0); 42 if (flags < 0) { 43 fprintf(stderr, "Get flags error:%s\n", strerror(errno)); 44 close(fd); 45 return -1; 46 } 47 flags |= O_NONBLOCK; 48 if (fcntl(fd, F_SETFL, flags) < 0) { 49 fprintf(stderr, "Set flags error:%s\n", strerror(errno)); 50 close(fd); 51 return -1; 52 } 53 54 /*阻塞情況下linux系統默認超時時間為75s*/ 55 int rc = connect(fd, (struct sockaddr*)&addr, sizeof(addr)); 56 if (rc != 0) { 57 if (errno == EINPROGRESS) { 58 printf("Doing connection.\n"); 59 /*正在處理連接*/ 60 FD_ZERO(&fdr); 61 FD_ZERO(&fdw); 62 FD_SET(fd, &fdr); 63 FD_SET(fd, &fdw); 64 timeout.tv_sec = 10; 65 timeout.tv_usec = 0; 66 rc = select(fd + 1, &fdr, &fdw, NULL, &timeout); 67 printf("rc is: %d\n", rc); 68 /*select調用失敗*/ 69 if (rc < 0) { 70 fprintf(stderr, "connect error:%s\n", strerror(errno)); 71 close(fd); 72 return -1; 73 } 74 75 /*連接超時*/ 76 if (rc == 0) { 77 fprintf(stderr, "Connect timeout.\n"); 78 close(fd); 79 return -1; 80 } 81 /*[1] 當連接成功建立時,描述符變成可寫,rc=1*/ 82 if (rc == 1 && FD_ISSET(fd, &fdw)) { 83 printf("Connect success\n"); 84 close(fd); 85 return 0; 86 } 87 /*[2] 當連接建立遇到錯誤時,描述符變為即可讀,也可寫,rc=2 遇到這種情況,可調用getsockopt函數*/ 88 if (rc == 2) { 89 if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errlen) == -1) { 90 fprintf(stderr, "getsockopt(SO_ERROR): %s", strerror(errno)); 91 close(fd); 92 return -1; 93 94 } 95 96 if (err) { 97 errno = err; 98 fprintf(stderr, "connect error:%s\n", strerror(errno)); 99 close(fd); 100 return -1; 101 102 } 103 } 104 105 } 106 fprintf(stderr, "connect failed, error:%s.\n", strerror(errno)); 107 return -1; 108 } 109 return 0; 110 }
4、參考資料
http://dongxicheng.org/network/non-block-connect-implemention/
http://www.cnblogs.com/flyxiang2010/archive/2010/12/17/1909051.html
非阻塞connect編寫方法介紹
TCP連接的建立涉及到一個三次握手的過程,且SOCKET中connect函數需要一直等到客戶接收到對於自己的SYN的ACK為止才返回,這意味着每個connect函數總會阻塞其調用進程至少一個到服務器的RTT時間,而RTT波動范圍很大,從局域網的幾個毫秒到幾百個毫秒甚至廣域網上的幾秒。這段時間內,我們可以執行其他處理工作,以便做到並行。在此,需要用到非阻塞connect。本文主要介紹了非阻塞connect的編寫方法以及應用場景。
1. 基礎知識
(1) fcntl函數
fcntl函數可執行各種描述符的控制操作,對於socket描述符,常用應用是將其設置為阻塞式IO,代碼如下:
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,則表示連接已經建立,這通常是在服務器和客戶在同一台主機上時發生。
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函數。
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的出錯信息:
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實現。
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;
}
Linux網絡編程 — 設置connect函數連接超時
最近在做一個在開發板啟動后,程序運行前首先檢查一下網絡是否已經連通的東西。參考了網上的一些方法,現在整理一下。(使用system(ping IPaddress)也可以辦到)
(2)connect();
(3)判斷connect()的返回值,一般情況會返回-1,這時你還必須判斷錯誤碼如果是EINPROGRESS,那說明connect還在繼續;如果錯誤碼不是前者那么就是有問題了,不必往下執行,必須關掉socket;待下次重聯;
(4)select();設置好函數中的超時時間,將select()中的read和write置位,在超時時間內,如果select返回1,即描述字變為了可寫,那么連接成功。 如果返回2,即描述字變為即可讀又可寫,那么出錯。 如果返回0,那么超時。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <errno.h>
#define TIMEOUT 5
#define SERV_PORT 53
int main(int argc, char *argv[])
{
if(argc != 2)
{
printf("Usage: client xxx.xxx.xxx.xxx\nExample: client 192.168.0.1\n");
exit(1);
}
int sockfd, flags, res;
struct sockaddr_in servaddr;
fd_set fdr, fdw;
struct timeval timeout;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0) {
perror("Netwrok test...\n");
return -1;
}
/* set socket fd noblock */
if((flags = fcntl(sockfd, F_GETFL, 0)) < 0) {
perror("Netwrok test...\n");
close(sockfd);
return -1;
}
if(fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) < 0) {
perror("Network test...\n");
close(sockfd);
return -1;
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
if(connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0) {if(errno != EINPROGRESS) { // EINPROGRESS
perror("Network test...\n");
close(sockfd);
return -1;
}
}
else {
printf("Connected\n");
return 0;
}
FD_ZERO(&fdr);
FD_ZERO(&fdw);
FD_SET(sockfd, &fdr);
FD_SET(sockfd, &fdw);
timeout.tv_sec = TIMEOUT;
timeout.tv_usec = 0;
res = select(sockfd + 1, &fdr, &fdw, NULL, &timeout);
if(res < 0) {
perror("Network test...\n");
close(sockfd);
return -1;
}
if(res == 0) {
printf("Connect server timeout")
close(sockfd);
return -1;
}
if(res == 1) {
if(FD_ISSET(sockfd, &fdw))
{
printf("Connected\n");
close(sockfd);
return 0;
}
}
/* Not necessary */
if(res == 2) {
printf("Connect server timeout");
close(sockfd);
return -1;
}
printf("Connect server timeout");
close(sockfd);
return -1;
}