UNIX網路程式設計--非阻塞connect的實現
一、《UNIX網路程式設計》-非阻塞connect
在一個TCP套介面被設定為非阻塞之後呼叫connect,connect會立即返回EINPROGRESS錯誤,表示連線操作正在進行中,但是仍未完成;同時TCP的三路握手操作繼續進行;在這之後,我們可以呼叫select來檢查這個連結是否建立成功;
非阻塞connect有三種用途:
1.我們可以在三路握手的同時做一些其它的處理.connect操作要花一個往返時間完成,而且可以是在任何地方,從幾個毫秒的區域網到幾百毫秒或幾秒的廣域網.在這段時間內我們可能有一些其他的處理想要執行;
2.可以用這種技術同時建立多個連線.在Web瀏覽器中很普遍;
3.由於我們使用select來等待連線的完成,因此我們可以給select設定一個時間限制,從而縮短connect的超時時間.在大多數實現中,connect的超時時間在75秒到幾分鐘之間.有時候應用程式想要一個更短的超時時間,使用非阻塞connect就是一種方法;
非阻塞connect聽起來雖然簡單,但是仍然有一些細節問題要處理:
1.即使套介面是非阻塞的,如果連線的伺服器在同一臺主機上,那麼在呼叫connect建立連線時,連線通常會立即建立成功.我們必須處理這種情況;
2.源自Berkeley的實現(和Posix.1g)有兩條與select和非阻塞IO相關的規則:
A:當連線建立成功時,套介面描述符變成可寫;
B:當連接出錯時,套介面描述符變成既可讀又可寫;
注意:當一個套接口出錯時,它會被select呼叫標記為既可讀又可寫;
當發現套介面描述符可讀或可寫時,可進一步判斷是連線成功還是出錯。這裡必須將B)和另外一種連線正常的情況區分開,就是連線建立好了之後,伺服器端傳送了資料給客戶端,此時select同樣會返回非阻塞socket描述符既可讀又可寫。
因此,僅從socket可讀或可寫無法判斷socket連線的狀態。
非阻塞connect有這麼多好處,但是處理非阻塞connect時會遇到很多可移植性問題;
在處理非阻塞connect時,在不同的套介面實現的平臺中存在的移植性問題,首先,有可能在呼叫select之前,連線就已經建立成功,而且對方的資料已經到來.在這種情況下,連線成功時套介面將既可讀又可寫.這和連線失敗時是一樣的.這個時候我們還得通過getsockopt來讀取錯誤值;這是第二個可移植性問題;移植性問題總結:
1.對於出錯的套介面描述符,getsockopt的返回值源自Berkeley的實現是返回0,待處理的錯誤值儲存在errno中;而源自Solaris的實現是返回0,待處理的錯誤儲存在errno中;(套介面描述符出錯時呼叫getsockopt的返回值不可移植)
2.有可能在呼叫select之前,連線就已經建立成功,而且對方的資料已經到來,在這種情況下,套介面描述符是既可讀又可寫;這與套介面描述符出錯時是一樣的;(怎樣判斷連線是否建立成功的條件不可移植)
這樣的話,在我們判斷連線是否建立成功的條件不唯一時,我們可以有以下的方法來解決這個問題:
1.呼叫getpeername代替getsockopt.如果呼叫getpeername失敗,getpeername返回ENOTCONN,表示連線建立失敗,我們必須以SO_ERROR呼叫getsockopt得到套介面描述符上的待處理錯誤; 此為《Unix Network Programming》一書中提供的方法,該方法在Linux環境上測試,發現是無效的
2.呼叫read,讀取長度為0位元組的資料.如果read呼叫失敗,則表示連線建立失敗,而且read返回的errno指明瞭連線失敗的原因.如果連線建立成功,read應該返回0;
3.再呼叫一次connect.它應該失敗,如果錯誤errno是EISCONN,就表示套介面已經建立,而且第一次連線是成功的;否則,連線就是失敗的;
被中斷的connect(這是說明使用非阻塞connect的必要性):
如果在一個阻塞式套介面上呼叫connect,在TCP的三路握手操作完成之前被中斷了,比如說,被捕獲的訊號中斷,將會發生什麼呢?假定connect不會自動重啟,它將返回EINTR.那麼,這個時候,我們就不能再呼叫connect等待連線建立完成了,如果再次呼叫connect來等待連線建立完成的話,connect將會返回錯誤值EADDRINUSE.在這種情況下,應該做的是呼叫select,就像在非阻塞式connect中所做的一樣.然後,select在連線建立成功(使套介面描述符可寫)或連線建立失敗(使套介面描述符既可讀又可寫)時返回。
上述是書中的內容。
二、非阻塞connect的實現
在一個TCP套介面設定為非阻塞後,呼叫connect,connect會在系統提供的errno變數中返回一個EINRPOCESS錯誤,此時TCP的三路握手繼續進行。之後可以用select函式檢查這個連線是否建立成功。以下實驗基於unix網路程式設計和網路上給出的普遍示例,在經過大量測試之後,發現其中有很多方法,在linux中,並不適用。
我先給出了重要原始碼的逐步分析,在最後給出完整的connect非阻塞原始碼。
1.首先填寫套接字結構,包括遠端的ip,通訊埠如下: */
struct sockaddr_in serv_addr;
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(9999);
serv_addr.sin_addr.s_addr = inet_addr("58.31.231.255"); //inet_addr轉換為網路位元組序
bzero(&(serv_addr.sin_zero),8);
// 2.建立socket套接字:if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket creat error");
return 1;
}
// 3.將socket建立為非阻塞,此時socket被設定為非阻塞模式
flags = fcntl(sockfd,F_GETFL,0);//獲取建立的sockfd的當前狀態(非阻塞)
fcntl(sockfd,F_SETFL,flags|O_NONBLOCK);//將當前sockfd設定為非阻塞
4. 建立connect連線,此時socket設定為非阻塞,connect呼叫後,無論連線是否建立立即返回-1,同時將errno(包含errno.h就可以直接使用)設定為EINPROGRESS, 表示此時tcp三次握手仍舊進行,如果errno不是EINPROGRESS,則說明連線錯誤,程式結束。
當客戶端和伺服器端在同一臺主機上的時候,connect回馬上結束,並返回0;無需等待,所以使用goto函式跳過select等待函式,直接進入連線後的處理部分。
if ( ( n = connect( sockfd, ( struct sockaddr *)&serv_addr , sizeof(struct sockaddr)) ) < 0 )
{
if(errno != EINPROGRESS) return 1;
}
if(n==0)
{
printf("connect completed immediately");
goto done;
}
/5.設定等待時間,使用select函式等待正在後臺連線的connect函式,這裡需要說明的是使用select監聽socket描述符是否可讀或者可寫,如果只可寫,說明連線成功,可以進行下面的操作。如果描述符既可讀又可寫,分為兩種情況,第一種情況是socket連接出現錯誤(不要問為什麼,這是系統規定的,可讀可寫時候有可能是connect連線成功後遠端主機斷開了連線close(socket)),第二種情況是connect連線成功,socket讀緩衝區得到了遠端主機發送的資料。需要通過connect連線後返回給errno的值來進行判定,或者通過呼叫
getsockopt(sockfd,SOL_SOCKET,SO_ERROR,&error,&len); 函式返回值來判斷是否發生錯誤,這裡存在一個可移植性問題,在solaris中發生錯誤返回-1,但在其他系統中可能返回0.我首先按unix網路程式設計的原始碼進行實現。如下:FD_ZERO(&rset);
FD_SET(sockfd,&rset);
wset = rset;
tval.tv_sec = 0;
tval.tv_usec = 300000;
int error;
socklen_t len;
if(( n = select(sockfd+1, &rset, &wset, NULL,&tval)) <= 0)
{
printf("time out connect error");
close(sockfd);
return -1;
}
If ( FD_ISSET(sockfd,&rset) || FD_ISSET(sockfd,&west) )
{
len = sizeof(error);
if( getsockopt(sockfd,SOL_SOCKET,SO_ERROR,&error,&len) <0)
return 1;
}
對錯誤的處理:
這裡我測試了一下,按照unix網路程式設計的描述,當網路發生錯誤的時候,getsockopt返回-1,return -1,程式結束。網路正常時候返回0,程式繼續執行。
可是我在linux下,無論網路是否發生錯誤,getsockopt始終返回0,不返回-1,說明linux與unix網路程式設計還是有些細微的差別。就是說當socket描述符可讀可寫的時候,這段程式碼不起作用。不能檢測出網路是否出現故障。
我測試的方法是,當呼叫connect後,sleep(2)休眠2秒,藉助這兩秒時間將網路助手斷開連線,這時候select返回2,說明套介面可讀又可寫,應該是網路連線的出錯情況。
此時,getsockopt返回0,不起作用。獲取errno的值,指示為EINPROGRESS,沒有返回unix網路程式設計中說的ENOTCONN,EINPROGRESS表示正在試圖連線,不能表示網路已經連線失敗。
針對這種情況,unix網路程式設計中提出了另外3種方法,這3種方法,也是網路上給出的常用的非阻塞connect示例:
a.再呼叫connect一次。失敗返回errno是EISCONN說明連線成功,表示剛才的connect成功,否則返回失敗。 程式碼如下:
int connect_ok;
connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr) );
switch (errno)
{
case EISCONN: //connect ok
printf("connect OK \n");
connect_ok = 1;
break;
case EALREADY:
connect_0k = -1
break;
case EINPROGRESS: // is connecting, need to check again
connect_ok = -1
break;
default:
printf("connect fail err=%d \n",errno);
connect_ok = -1;
break;
}
如程式所示,根據再次呼叫的errno返回值將connect_ok的值,來進行下面的處理,connect_ok為1繼續執行其他操作,否則程式結束。
但這種方法我在linux下測試了,當發生錯誤的時候,socket描述符(我的程式裡是sockfd)變成可讀且可寫,但第二次呼叫connect 後,errno並沒有返回EISCONN,,也沒有返回連線失敗的錯誤,仍舊是EINPROGRESS,而當網路不發生故障的時候,第二次使用 connect連線也返回EINPROGRESS,因此也無法通過再次connect來判斷連線是否成功。
在這種情況下的測試還有一個問題需要提出:
一次select之後,發現此時套介面描述字可讀或可寫,再次執行connect,此時errno始終不變,仍未EINPROGRESS,增加select的超時時間結果也一樣。
之後嘗試在select返回值為0,或返回值為1,且connect後errno仍為EINPROGRESS(115)時,再次執行select+connect,即再次檢測連線狀態。此時errno被置為EISCONN(106),connect成功。(也就是說使用這種判斷可以解決移植問題)
b.unix網路程式設計中說使用read函式,如果失敗,表示connect失敗,返回的errno指明瞭失敗原因,但這種方法在linux上行不通,linux在socket描述符為可讀可寫的時候,read返回0,並不會置errno為錯誤。
c.unix網路程式設計中說使用getpeername函式,如果連線失敗,呼叫該函式後,通過errno來判斷第一次連線是否成功,但我試過了,無論網路連線是否成功,errno都沒變化,都為EINPROGRESS,無法判斷。
即使呼叫getpeername函式,getsockopt函式仍舊不行。
綜上方法,既然都不能確切知道非阻塞connect是否成功,所以我直接當描述符可讀可寫的情況下進行傳送,通過能否獲取伺服器的返回值來判斷是否成功。(如果伺服器端的設計不傳送資料,那就悲哀了。)
程式的書寫形式出於可移植性考慮,按照unix網路程式設計推薦寫法,使用getsocketopt進行判斷,但不通過返回值來判斷,而通過函式的返回引數來判斷。
6. 用select檢視接收描述符,如果可讀,就讀出資料,程式結束。在接收資料的時候注意要先對先前的rset重新賦值為描述符,因為select會對 rset清零,當呼叫select後,如果socket沒有變為可讀,則rset在select會被置零。所以如果在程式中使用了rset,最好在使用時候重新對rset賦值。
程式如下:
FD_ZERO(&rset);
FD_SET(sockfd,&rset);//如果前面select使用了rset,最好重新賦值
if( ( n = select(sockfd+1,&rset,NULL, NULL,&tval)) <= 0 )
{
close(sockfd);
return -1;
}
if ((recvbytes=recv(sockfd, buf, 1024, 0)) ==-1)
{
perror("recv error!");
close(sockfd);
return 1;
}
printf("receive num %d\n",recvbytes);
printf("%s\n",buf);
*/
綜上所述一種更有效的判斷方法,經測試驗證,在Linux環境下是有效的:
再次呼叫connect,相應返回失敗,如果錯誤errno是EISCONN,表示socket連線已經建立,否則認為連線失敗。
三、非阻塞connect的實現
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
#include<sys/ioctl.h>//inet_addr()
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PEER_IP "192.254.1.1"
#define PEER_PORT 7008
int main(int argc, char **argv)
{
int ret = 0;
int sock_fd;
int flags;
struct sockaddr_in addr;
/* obtain a socket */
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
/* set non-blocking mode on socket*/
#if 1
flags = fcntl(sock_fd, F_GETFL, 0);
fcntl(sock_fd, F_SETFL, flags|O_NONBLOCK);
#else
int imode = 1;
ioctl(sock_fd, FIONBIO, &imode);
#endif
/* connect to server */
addr.sin_family = AF_INET;
addr.sin_port = htons(PEER_PORT);
addr.sin_addr.s_addr = inet_addr(PEER_IP);
int res = connect(sock_fd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in));
if (0 == res)
{
printf("socket connect succeed immediately.\n");
ret = 0;
}
else
{
printf("get the connect result by select().\n");
if (errno == EINPROGRESS)
{
int times = 0;
while (times++ < 5)
{
fd_set rfds, wfds;
struct timeval tv;
printf("errno = %d\n", errno);
FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_SET(sock_fd, &rfds);
FD_SET(sock_fd, &wfds);
/* set select() time out */
tv.tv_sec = 10;
tv.tv_usec = 0;
int selres = select(sock_fd + 1, &rfds, &wfds, NULL, &tv);
switch (selres)
{
case -1:
printf("select error\n");
ret = -1;
break;
case 0:
printf("select time out\n");
ret = -1;
break;
default:
if (FD_ISSET(sock_fd, &rfds) || FD_ISSET(sock_fd, &wfds))
{
#if 0 // not useable in linux environment, suggested in <<Unix network programming>>
int errinfo, errlen;
if (-1 == getsockopt(sock_fd, SOL_SOCKET, SO_ERROR, &errinfo, &errlen))
{
printf("getsockopt return -1.\n");
ret = -1;
break;
}
else if (0 != errinfo)
{
printf("getsockopt return errinfo = %d.\n", errinfo);
ret = -1;
break;
}
ret = 0;
printf("connect ok?\n");
#else
#if 1
connect(sock_fd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in));
int err = errno;
if (err == EISCONN)
{
printf("connect finished 111.\n");
ret = 0;
}
else
{
printf("connect failed. errno = %d\n", errno);
printf("FD_ISSET(sock_fd, &rfds): %d\n FD_ISSET(sock_fd, &wfds): %d\n", FD_ISSET(sock_fd, &rfds) , FD_ISSET(sock_fd, &wfds));
ret = errno;
}
#else
char buff[2];
if (read(sock_fd, buff, 0) < 0)
{
printf("connect failed. errno = %d\n", errno);
ret = errno;
}
else
{
printf("connect finished.\n");
ret = 0;
}
#endif
#endif
}
else
{
printf("haha\n");
}
}
if (-1 != selres && (ret != 0))
{
printf("check connect result again... %d\n", times);
continue;
}
else
{
break;
}
}
}
else
{
printf("connect to host %s:%d failed.\n", PEER_IP, PEER_PORT);
ret = errno;
}
}
if (0 == ret)
{
send(sock_fd, "12345", sizeof("12345"), 0);
}
else
{
printf("connect to host %s:%d failed.\n", PEER_IP, PEER_PORT);
}
close(sock_fd);
return ret;
}
上述程式碼可以解決問題了Linux下常見的socket錯誤碼:
EACCES, EPERM:使用者試圖在套接字廣播標誌沒有設定的情況下連線廣播地址或由於防火牆策略導致連線失敗。
EADDRINUSE 98:Address already in use(本地地址處於使用狀態)
EAFNOSUPPORT 97:Address family not supported by protocol(引數serv_add中的地址非合法地址)
EAGAIN:沒有足夠空閒的本地埠。
EALREADY 114:Operation already in progress(套接字為非阻塞套接字,並且原來的連線請求還未完成)
EBADF 77:File descriptor in bad state(非法的檔案描述符)
ECONNREFUSED 111:Connection refused(遠端地址並沒有處於監聽狀態)
EFAULT:指向套接字結構體的地址非法。
EINPROGRESS 115:Operation now in progress(套接字為非阻塞套接字,且連線請求沒有立即完成)
EINTR:系統呼叫的執行由於捕獲中斷而中止。
EISCONN 106:Transport endpoint is already connected(已經連線到該套接字)
ENETUNREACH 101:Network is unreachable(網路不可到達)
ENOTSOCK 88:Socket operation on non-socket(檔案描述符不與套接字相關)
ETIMEDOUT 110:Connection timed out(連線超時)
(總結:其實只需要知道覅阻塞connect的方法即可,因為在不同的平臺不同的作業系統需要不同的使用方法)