linux 客戶端 Socket 非阻塞connect編程(正文)/*開(kāi)發(fā)過(guò)程與源碼解析
開(kāi)發(fā)測(cè)試環(huán)境:虛擬機(jī)CentOS,windows網(wǎng)絡(luò)調(diào)試助手
非阻塞模式有3種用途
1.三次握手同時(shí)做其他的處理。connect要花一個(gè)往返時(shí)間完成,從幾毫秒的局域網(wǎng)到幾百毫秒或幾秒的廣域網(wǎng)。這段時(shí)間可能有一些其他的處理要執(zhí)行,比如數(shù)據(jù)準(zhǔn)備,預(yù)處理等。
2.用這種技術(shù)建立多個(gè)連接。這在web瀏覽器中很普遍.
3.由于程序用select等待連接完成,可以設(shè)置一個(gè)select等待時(shí)間限制,從而縮短connect超時(shí)時(shí)間。多數(shù)實(shí)現(xiàn)中,connect的超時(shí)時(shí)間在75秒到幾分鐘之間。有時(shí)程序希望在等待一定時(shí)間內(nèi)結(jié)束,使用非阻塞connect可以防止阻塞75秒,在多線程網(wǎng)絡(luò)編程中,尤其必要。 例如有一個(gè)通過(guò)建立線程與其他主機(jī)進(jìn)行socket通信的應(yīng)用程序,如果建立的線程使用阻塞connect與遠(yuǎn)程通信,當(dāng)有幾百個(gè)線程并發(fā)的時(shí)候,由于網(wǎng)絡(luò)延遲而全部阻塞,阻塞的線程不會(huì)釋放系統(tǒng)的資源,同一時(shí)刻阻塞線程超過(guò)一定數(shù)量時(shí)候,系統(tǒng)就不再允許建立新的線程(每個(gè)進(jìn)程由于進(jìn)程空間的原因能產(chǎn)生的線程有限),如果使用非阻塞的connect,連接失敗使用select等待很短時(shí)間,如果還沒(méi)有連接后,線程立刻結(jié)束釋放資源,防止大量線程阻塞而使程序崩潰。
目前connect非阻塞編程的普遍思路是:
在一個(gè)TCP套接口設(shè)置為非阻塞后,調(diào)用connect,connect會(huì)在系統(tǒng)提供的errno變量中返回一個(gè)EINRPOCESS錯(cuò)誤,此時(shí)TCP的三路握手繼續(xù)進(jìn)行。之后可以用select函數(shù)檢查這個(gè)連接是否建立成功。以下實(shí)驗(yàn)基于unix網(wǎng)絡(luò)編程和網(wǎng)絡(luò)上給出的普遍示例,在經(jīng)過(guò)大量測(cè)試之后,發(fā)現(xiàn)其中有很多方法,在linux中,并不適用。
我先給出了重要源碼的逐步分析,在最后給出完整的connect非阻塞源碼。
1.首先填寫(xiě)套接字結(jié)構(gòu),包括遠(yuǎn)程的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轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序
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建立為非阻塞,此時(shí)socket被設(shè)置為非阻塞模式
flags = fcntl(sockfd,F_GETFL,0);//獲取建立的sockfd的當(dāng)前狀態(tài)(非阻塞)
fcntl(sockfd,F_SETFL,flags|O_NONBLOCK);//將當(dāng)前sockfd設(shè)置為非阻塞
/*4. 建立connect連接,此時(shí)socket設(shè)置為非阻塞,connect調(diào)用后,無(wú)論連接是否建立立即返回-1,同時(shí)將errno(包含errno.h就可以直接使用)設(shè)置為EINPROGRESS, 表示此時(shí)tcp三次握手仍舊進(jìn)行,如果errno不是EINPROGRESS,則說(shuō)明連接錯(cuò)誤,程序結(jié)束。
當(dāng)客戶端和服務(wù)器端在同一臺(tái)主機(jī)上的時(shí)候,connect回馬上結(jié)束,并返回0;無(wú)需等待,所以使用goto函數(shù)跳過(guò)select等待函數(shù),直接進(jìn)入連接后的處理部分。*/
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.設(shè)置等待時(shí)間,使用select函數(shù)等待正在后臺(tái)連接的connect函數(shù),這里需要說(shuō)明的是使用select監(jiān)聽(tīng)socket描述符是否可讀或者可寫(xiě),如果只可寫(xiě),說(shuō)明連接成功,可以進(jìn)行下面的操作。如果描述符既可讀又可寫(xiě),分為兩種情況,第一種情況是socket連接出現(xiàn)錯(cuò)誤(不要問(wèn)為什么,這是系統(tǒng)規(guī)定的,可讀可寫(xiě)時(shí)候有可能是connect連接成功后遠(yuǎn)程主機(jī)斷開(kāi)了連接close(socket)),第二種情況是connect連接成功,socket讀緩沖區(qū)得到了遠(yuǎn)程主機(jī)發(fā)送的數(shù)據(jù)。需要通過(guò)connect連接后返回給errno的值來(lái)進(jìn)行判定,或者通過(guò)調(diào)用 getsockopt(sockfd,SOL_SOCKET,SO_ERROR,&error,&len); 函數(shù)返回值來(lái)判斷是否發(fā)生錯(cuò)誤,這里存在一個(gè)可移植性問(wèn)題,在solaris中發(fā)生錯(cuò)誤返回-1,但在其他系統(tǒng)中可能返回0.我首先按unix網(wǎng)絡(luò)編程的源碼進(jìn)行實(shí)現(xiàn)。如下:*/
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;
}
/* 這里我測(cè)試了一下,按照unix網(wǎng)絡(luò)編程的描述,當(dāng)網(wǎng)絡(luò)發(fā)生錯(cuò)誤的時(shí)候,getsockopt返回-1,return -1,程序結(jié)束。網(wǎng)絡(luò)正常時(shí)候返回0,程序繼續(xù)執(zhí)行。
可是我在linux下,無(wú)論網(wǎng)絡(luò)是否發(fā)生錯(cuò)誤,getsockopt始終返回0,不返回-1,說(shuō)明linux與unix網(wǎng)絡(luò)編程還是有些細(xì)微的差別。就是說(shuō)當(dāng)socket描述符可讀可寫(xiě)的時(shí)候,這段代碼不起作用。不能檢測(cè)出網(wǎng)絡(luò)是否出現(xiàn)故障。
我測(cè)試的方法是,當(dāng)調(diào)用connect后,sleep(2)休眠2秒,借助這兩秒時(shí)間將網(wǎng)絡(luò)助手?jǐn)嚅_(kāi)連接,這時(shí)候select返回2,說(shuō)明套接口可讀又可寫(xiě),應(yīng)該是網(wǎng)絡(luò)連接的出錯(cuò)情況。
此時(shí),getsockopt返回0,不起作用。獲取errno的值,指示為EINPROGRESS,沒(méi)有返回unix網(wǎng)絡(luò)編程中說(shuō)的ENOTCONN,EINPROGRESS表示正在試圖連接,不能表示網(wǎng)絡(luò)已經(jīng)連接失敗。
針對(duì)這種情況,unix網(wǎng)絡(luò)編程中提出了另外3種方法,這3種方法,也是網(wǎng)絡(luò)上給出的常用的非阻塞connect示例:
a.再調(diào)用connect一次。失敗返回errno是EISCONN說(shuō)明連接成功,表示剛才的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;
}
/*如程序所示,根據(jù)再次調(diào)用的errno返回值將connect_ok的值,來(lái)進(jìn)行下面的處理,connect_ok為1繼續(xù)執(zhí)行其他操作,否則程序結(jié)束。
但這種方法我在linux下測(cè)試了,當(dāng)發(fā)生錯(cuò)誤的時(shí)候,socket描述符(我的程序里是sockfd)變成可讀且可寫(xiě),但第二次調(diào)用connect 后,errno并沒(méi)有返回EISCONN,,也沒(méi)有返回連接失敗的錯(cuò)誤,仍舊是EINPROGRESS,而當(dāng)網(wǎng)絡(luò)不發(fā)生故障的時(shí)候,第二次使用 connect連接也返回EINPROGRESS,因此也無(wú)法通過(guò)再次connect來(lái)判斷連接是否成功。
b.unix網(wǎng)絡(luò)編程中說(shuō)使用read函數(shù),如果失敗,表示connect失敗,返回的errno指明了失敗原因,但這種方法在linux上行不通,linux在socket描述符為可讀可寫(xiě)的時(shí)候,read返回0,并不會(huì)置errno為錯(cuò)誤。
c.unix網(wǎng)絡(luò)編程中說(shuō)使用getpeername函數(shù),如果連接失敗,調(diào)用該函數(shù)后,通過(guò)errno來(lái)判斷第一次連接是否成功,但我試過(guò)了,無(wú)論網(wǎng)絡(luò)連接是否成功,errno都沒(méi)變化,都為EINPROGRESS,無(wú)法判斷。
悲哀啊,即使調(diào)用getpeername函數(shù),getsockopt函數(shù)仍舊不行。
綜上方法,既然都不能確切知道非阻塞connect是否成功,所以我直接當(dāng)描述符可讀可寫(xiě)的情況下進(jìn)行發(fā)送,通過(guò)能否獲取服務(wù)器的返回值來(lái)判斷是否成功。(如果服務(wù)器端的設(shè)計(jì)不發(fā)送數(shù)據(jù),那就悲哀了。)
程序的書(shū)寫(xiě)形式出于可移植性考慮,按照unix網(wǎng)絡(luò)編程推薦寫(xiě)法,使用getsocketopt進(jìn)行判斷,但不通過(guò)返回值來(lái)判斷,而通過(guò)函數(shù)的返回參數(shù)來(lái)判斷。
6. 用select查看接收描述符,如果可讀,就讀出數(shù)據(jù),程序結(jié)束。在接收數(shù)據(jù)的時(shí)候注意要先對(duì)先前的rset重新賦值為描述符,因?yàn)閟elect會(huì)對(duì) rset清零,當(dāng)調(diào)用select后,如果socket沒(méi)有變?yōu)榭勺x,則rset在select會(huì)被置零。所以如果在程序中使用了rset,最好在使用時(shí)候重新對(duì)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
在一個(gè)TCP套接口被設(shè)置為非阻塞之后調(diào)用connect,connect會(huì)立即返回EINPROGRESS錯(cuò)誤,表示連接操作正在進(jìn)行中,但是仍未完成;同時(shí)TCP的三路握手操作繼續(xù)進(jìn)行;在這之后,我們可以調(diào)用select來(lái)檢查這個(gè)鏈接是否建立成功;非阻塞connect有三種用途:
1.我們可以在三路握手的同時(shí)做一些其它的處理.connect操作要花一個(gè)往返時(shí)間完成,而且可以是在任何地方,從幾個(gè)毫秒的局域網(wǎng)到幾百毫秒或幾秒的廣域網(wǎng).在這段時(shí)間內(nèi)我們可能有一些其他的處理想要執(zhí)行;
2.可以用這種技術(shù)同時(shí)建立多個(gè)連接.在Web瀏覽器中很普遍;
3.由于我們使用select來(lái)等待連接的完成,因此我們可以給select設(shè)置一個(gè)時(shí)間限制,從而縮短connect的超時(shí)時(shí)間.在大多數(shù)實(shí)現(xiàn)中,connect的超時(shí)時(shí)間在75秒到幾分鐘之間.有時(shí)候應(yīng)用程序想要一個(gè)更短的超時(shí)時(shí)間,使用非阻塞connect就是一種方法;
非阻塞connect聽(tīng)起來(lái)雖然簡(jiǎn)單,但是仍然有一些細(xì)節(jié)問(wèn)題要處理:
1.即使套接口是非阻塞的,如果連接的服務(wù)器在同一臺(tái)主機(jī)上,那么在調(diào)用connect建立連接時(shí),連接通常會(huì)立即建立成功.我們必須處理這種情況;
2.源自Berkeley的實(shí)現(xiàn)(和Posix.1g)有兩條與select和非阻塞IO相關(guān)的規(guī)則:
A:當(dāng)連接建立成功時(shí),套接口描述符變成可寫(xiě);
B:當(dāng)連接出錯(cuò)時(shí),套接口描述符變成既可讀又可寫(xiě);
注意:當(dāng)一個(gè)套接口出錯(cuò)時(shí),它會(huì)被select調(diào)用標(biāo)記為既可讀又可寫(xiě);
非阻塞connect有這么多好處,但是處理非阻塞connect時(shí)會(huì)遇到很多可移植性問(wèn)題;
處理非阻塞connect的步驟:
第一步:創(chuàng)建socket,返回套接口描述符;
第二步:調(diào)用fcntl把套接口描述符設(shè)置成非阻塞;
第三步:調(diào)用connect開(kāi)始建立連接;
第四步:判斷連接是否成功建立;
A:如果connect返回0,表示連接簡(jiǎn)稱成功(服務(wù)器可客戶端在同一臺(tái)機(jī)器上時(shí)就有可能發(fā)生這種情況);
B:調(diào)用select來(lái)等待連接建立成功完成;
如果select返回0,則表示建立連接超時(shí);我們返回超時(shí)錯(cuò)誤給用戶,同時(shí)關(guān)閉連接,以防止三路握手操作繼續(xù)進(jìn)行下去;
如果select返回大于0的值,則需要檢查套接口描述符是否可讀或可寫(xiě);如果套接口描述符可讀或可寫(xiě),則我們可以通過(guò)調(diào)用getsockopt來(lái)得到套接口上待處理的錯(cuò)誤(SO_ERROR),如果連接建立成功,這個(gè)錯(cuò)誤值將是0,如果建立連接時(shí)遇到錯(cuò)誤,則這個(gè)值是連接錯(cuò)誤所對(duì)應(yīng)的errno值(比如:ECONNREFUSED,ETIMEDOUT等).
"讀取套接口上的錯(cuò)誤"是遇到的第一個(gè)可移植性問(wèn)題;如果出現(xiàn)問(wèn)題,getsockopt源自Berkeley的實(shí)現(xiàn)是返回0,等待處理的錯(cuò)誤在變量errno中返回;但是Solaris會(huì)讓getsockopt返回-1,errno置為待處理的錯(cuò)誤;我們對(duì)這兩種情況都要處理;
這樣,在處理非阻塞connect時(shí),在不同的套接口實(shí)現(xiàn)的平臺(tái)中存在的移植性問(wèn)題,首先,有可能在調(diào)用select之前,連接就已經(jīng)建立成功,而且對(duì)方的數(shù)據(jù)已經(jīng)到來(lái).在這種情況下,連接成功時(shí)套接口將既可讀又可寫(xiě).這和連接失敗時(shí)是一樣的.這個(gè)時(shí)候我們還得通過(guò)getsockopt來(lái)讀取錯(cuò)誤值;這是第二個(gè)可移植性問(wèn)題;
移植性問(wèn)題總結(jié):
1.對(duì)于出錯(cuò)的套接口描述符,getsockopt的返回值源自Berkeley的實(shí)現(xiàn)是返回0,待處理的錯(cuò)誤值存儲(chǔ)在errno中;而源自Solaris的實(shí)現(xiàn)是返回0,待處理的錯(cuò)誤存儲(chǔ)在errno中;(套接口描述符出錯(cuò)時(shí)調(diào)用getsockopt的返回值不可移植)
2.有可能在調(diào)用select之前,連接就已經(jīng)建立成功,而且對(duì)方的數(shù)據(jù)已經(jīng)到來(lái),在這種情況下,套接口描述符是既可讀又可寫(xiě);這與套接口描述符出錯(cuò)時(shí)是一樣的;(怎樣判斷連接是否建立成功的條件不可移植)
這樣的話,在我們判斷連接是否建立成功的條件不唯一時(shí),我們可以有以下的方法來(lái)解決這個(gè)問(wèn)題:
1.調(diào)用getpeername代替getsockopt.如果調(diào)用getpeername失敗,getpeername返回ENOTCONN,表示連接建立失敗,我們必須以SO_ERROR調(diào)用getsockopt得到套接口描述符上的待處理錯(cuò)誤;
2.調(diào)用read,讀取長(zhǎng)度為0字節(jié)的數(shù)據(jù).如果read調(diào)用失敗,則表示連接建立失敗,而且read返回的errno指明了連接失敗的原因.如果連接建立成功,read應(yīng)該返回0;
3.再調(diào)用一次connect.它應(yīng)該失敗,如果錯(cuò)誤errno是EISCONN,就表示套接口已經(jīng)建立,而且第一次連接是成功的;否則,連接就是失敗的;
被中斷的connect:
如果在一個(gè)阻塞式套接口上調(diào)用connect,在TCP的三路握手操作完成之前被中斷了,比如說(shuō),被捕獲的信號(hào)中斷,將會(huì)發(fā)生什么呢?假定connect不會(huì)自動(dòng)重啟,它將返回EINTR.那么,這個(gè)時(shí)候,我們就不能再調(diào)用connect等待連接建立完成了,如果再次調(diào)用connect來(lái)等待連接建立完成的話,connect將會(huì)返回錯(cuò)誤值EADDRINUSE.在這種情況下,應(yīng)該做的是調(diào)用select,就像在非阻塞式connect中所做的一樣.然后,select在連接建立成功(使套接口描述符可寫(xiě))或連接建立失敗(使套接口描述符既可讀又可寫(xiě))時(shí)返回;