linux 客戶端 Socket 非阻塞connect編程

開發測試環境:虛擬機CentOS,windows網絡調試助手
        非阻塞模式有3種用途
        1.三次握手同時作其餘的處理。connect要花一個往返時間完成,從幾毫秒的局域網到幾百毫秒或幾秒的廣域網。這段時間可能有一些其餘的處理要執行,好比數據準備,預處理等。
        2.用這種技術創建多個鏈接。這在web瀏覽器中很廣泛.
        3.因爲程序用select等待鏈接完成,能夠設置一個select等待時間限制,從而縮短connect超時時間。多數實現中,connect的超時時間在75秒到幾分鐘之間。有時程序但願在等待必定時間內結束,使用非阻塞connect能夠防止阻塞75秒,在多線程網絡編程中,尤爲必要。   例若有一個經過創建線程與其餘主機進行socket通訊的應用程序,若是創建的線程使用阻塞connect與遠程通訊,當有幾百個線程併發的時候,因爲網絡延遲而所有阻塞,阻塞的線程不會釋放系統的資源,同一時刻阻塞線程超過必定數量時候,系統就再也不容許創建新的線程(每一個進程因爲進程空間的緣由能產生的線程有限),若是使用非阻塞的connect,鏈接失敗使用select等待很短期,若是尚未鏈接後,線程馬上結束釋放資源,防止大量線程阻塞而使程序崩潰。
目前connect非阻塞編程的廣泛思路是:
在一個TCP套接口設置爲非阻塞後,調用connect,connect會在系統提供的errno變量中返回一個EINRPOCESS錯誤,此時TCP的三路握手繼續進行。以後能夠用select函數檢查這個鏈接是否創建成功。如下實驗基於unix網絡編程和網絡上給出的廣泛示例,在通過大量測試以後,發現其中有不少方法,在linux中,並不適用。
我先給出了重要源碼的逐步分析,在最後給出完整的connect非阻塞源碼。
        1.首先填寫套接字結構,包括遠程的ip,通訊端口以下: */linux

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套接字:web

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等待函數,直接進入鏈接後的處理部分。*/windows

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來判斷鏈接是否成功。
     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);

非阻塞connect完整代碼綜合以下:多線程

 

intmain(int argc, char** argv)
{
intsockfd,recvbytes,res,flags,error,n;
socklen_tlen;
fd_setrset,wset;
structtimevaltval;
tval.tv_sec=0;
tval.tv_usec=300000;
structsockaddr_inserv_addr;
char*sendData="1234567890";//發送字符串
charbuf[1024]="/0"; //接收buffer
//建立socket描述符
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket create failed");
return1;
}

serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(9999);
serv_addr.sin_addr.s_addr=inet_addr("58.31.231.255");
bzero(&(serv_addr.sin_zero),8);
flags=fcntl(sockfd,F_GETFL,0);
fcntl(sockfd,F_SETFL,flags|O_NONBLOCK);//設置爲非阻塞

if( (res = connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr)) )< 0)
{
if(errno != EINPROGRESS)
{
return1;
}

}
//若是server與client在同一主機上,有些環境socket設爲非阻塞會返回 0
if(0 == res) goto done;
FD_ZERO(&rset);
FD_SET(sockfd,&rset);
wset=rset;
if( ( res = select(sockfd+1, NULL, &wset, NULL,&tval) ) <= 0)
{
perror("connect time out/n");
close(sockfd);
return1;
}
else
{
len=sizeof(error);
getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len);
if(error)
{
fprintf(stderr, "Error in connection() %d - %s/n", error, strerror(error));
return1;
}
}
done:
if( (n = send(sockfd, sendData, strlen(sendData),0) ) ==-1 )
{

perror("send error!");
close(sockfd);
return1;
}
if( ( n = select(sockfd+1,&rset,NULL, NULL,&tval)) <= 0 )//rset沒有使用過,不用從新置爲sockfd
{
perror("receive time out or connect error");
close(sockfd);
return-1;
}
if((recvbytes=recv(sockfd, buf, 1024, 0)) ==-1)
{
perror("recv error!");
close(sockfd);
return1;
}
printf("receive num %d/n",recvbytes);
printf("%s/n",buf);
}
相關文章
相關標籤/搜索