linux 用戶端 Socket 非阻塞connect編程

來源:互聯網
上載者:User

開發測試環境:虛擬機器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,通訊連接埠如下: */
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來判斷串連是否成功。
     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 用戶端 Socket 非阻塞connect編程(源碼)
非阻塞connect完整代碼綜合如下:
int main(int argc, char** argv)
{
int sockfd, recvbytes,res,flags,error,n;
socklen_t len;
fd_set rset,wset;
struct timeval tval;
tval.tv_sec = 0;
tval.tv_usec = 300000;
struct sockaddr_in serv_addr;
char* sendData = "1234567890";//發送字串
char buf[1024] = "/0"; //接收buffer
//建立socket描述符
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket create failed");
return 1;
}
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)
{
return 1;
}
}
//如果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);
return 1;
}
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));
return 1;
}
}
done:
if ( (n = send(sockfd, sendData, strlen(sendData),0) ) ==-1 )
{
perror("send error!");
close(sockfd);
return 1;
}
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);
return 1;
}
printf("receive num %d/n",recvbytes);
printf("%s/n",buf);
}

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.