對於面向鏈接的socket類型(SOCK_STREAM,SOCK_SEQPACKET)在讀寫數據以前必須創建鏈接。
首先服務器端socket必須在一個客戶端知道的地址進行監聽,也就是建立socket以後必須調用bind綁定到一個指定的地址,而後調用int listen(int sockfd, int backlog)進行監聽。此時服務器socket容許客戶端進行鏈接,backlog提示沒被accept的客戶鏈接請求隊列的大小,系統決定實際的值,最大值定義爲SOMAXCONN在頭文件<sys/socket.h>裏面。若是某種緣由致使服務器端進程未及時accpet客戶鏈接而致使此隊列滿了的話則新的客戶端鏈接請求被拒絕(在工做中遇到過此狀況,IONAORBIX(CORBA中間件)因爲沒有配置超時時間結果在WIFI網絡中傳輸數據出現異常狀況一直阻塞而無機會調用accept接受新的客戶請求,因而最終隊列滿致使新的客戶鏈接被拒絕)。 linux
調用listen以後當有客戶端鏈接到達的時候調用int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len)接受客戶端鏈接創建起鏈接返回用於鏈接數據傳送的socket描述符,進行監聽的socket能夠用於繼續監聽客戶端的鏈接請求,返回的socket描述符跟監聽的socket類型一致。若是addr不爲NULL,則客戶端發起鏈接請求的socket地址信息會經過addr進行返回。若是監聽的socket描述符爲阻塞模式則accept一直會阻塞直到有客戶發起鏈接請求,若是監聽的socket描述符爲非阻塞模式則若是當前沒有可用的客戶鏈接請求,則返回-1(errno設置爲EAGAIN)。可使用select函數對監聽的socket描述符進行多路分離,若是有客戶鏈接請求則select將監聽的socket描述符設置爲可讀(注意,若是監聽的socket爲阻塞模式而使用select進行多路分離則可能形成select返回可讀可是調用accept會被阻塞住的狀況,緣由是在調用accept以前客戶端可能主動關閉鏈接或者發送RST異常關閉鏈接,所以select最好跟非阻塞socket搭配使用)。 web
客戶端調用int connect(int sockfd, const struct sockaddr *addr, socklen_t len);發起對服務器的socket的鏈接請求,若是客戶端socket描述符爲阻塞模式則會一直阻塞到鏈接創建或者鏈接失敗(注意阻塞模式的超時時間可能爲75秒到幾分鐘之間),而若是爲非阻塞模式,則調用connect以後若是鏈接不能立刻創建則返回-1(errno設置爲EINPROGRESS,注意鏈接也可能立刻創建成功好比鏈接本機的服務器進程),若是沒有立刻創建返回,此時TCP的三路握手動做在背後繼續,而程序能夠作其餘的東西,而後調用select檢測非阻塞connect是否完成(此時能夠指定select的超時時間,這個超時時間能夠設置爲比connect的超時時間短),若是select超時則關閉socket,而後能夠嘗試建立新的socket從新鏈接,若是select返回非阻塞socket描述符可寫則代表鏈接創建成功,若是select返回非阻塞socket描述符既可讀又可寫則代表鏈接出錯(注意:這兒必須跟另一種鏈接正常的狀況區分開來,就是鏈接創建好了以後,服務器端發送了數據給客戶端,此時select一樣會返回非阻塞socket描述符既可讀又可寫,這時能夠經過如下方法區分:
1.調用getpeername獲取對端的socket地址.若是getpeername返回ENOTCONN,表示鏈接創建失敗,而後用SO_ERROR調用getsockopt獲得套接口描述符上的待處理錯誤;
2.調用read,讀取長度爲0字節的數據.若是read調用失敗,則表示鏈接創建失敗,並且read返回的errno指明瞭鏈接失敗的緣由.若是鏈接創建成功,read應該返回0;
3.再調用一次connect.它應該失敗,若是錯誤errno是EISCONN,就表示套接口已經創建,並且第一次鏈接是成功的;不然,鏈接就是失敗的;
對於無鏈接的socket類型(SOCK_DGRAM),客戶端也能夠調用connect進行鏈接,此鏈接實際上並不創建相似SOCK_STREAM的鏈接,而僅僅是在本地保存了對端的地址,這樣後續的讀寫操做能夠默認以鏈接的對端爲操做對象。
當對端機器crash或者網絡鏈接被斷開(好比路由器不工做,網線斷開等),此時發送數據給對端而後讀取本端socket會返回ETIMEDOUT或者EHOSTUNREACH 或者ENETUNREACH(後兩個是中間路由器判斷服務器主機不可達的狀況)。
當對端機器crash以後又從新啓動,而後客戶端再向原來的鏈接發送數據,由於服務器端已經沒有原來的鏈接信息,此時服務器端回送RST給客戶端,此時客戶端讀本地端口返回ECONNRESET錯誤。
當服務器所在的進程正常或者異常關閉時,會對全部打開的文件描述符進行close,所以對於鏈接的socket描述符則會向對端發送FIN分節進行正常關閉流程。對端在收到FIN以後端口變得可讀,此時讀取端口會返回0表示到了文件結尾(對端不會再發送數據)。
當一端收到RST致使讀取socket返回ECONNRESET,此時若是再次調用write發送數據給對端則觸發SIGPIPE信號,信號默認終止進程,若是忽略此信號或者從SIGPIPE的信號處理程序返回則write出錯返回EPIPE。
能夠看出只有當本地端口主動發送消息給對端才能檢測出鏈接異常中斷的狀況,搭配select進行多路分離的時候,socket收到RST或者FIN時候,select返回可讀(心跳消息就是用於檢測鏈接的狀態)。也可使用socket的KEEPLIVE選項,依賴socket自己偵測socket鏈接異常中斷的狀況。
發送socket數據有如下方法:
調用ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);,只能用於創建好了鏈接的socket(面向鏈接的SOCK_STREAM或者調用了connect的SOCK_DGRAM)。flags取值以下:
MSG_DONTROUTE 對數據不進行路由
MSG_DONTWAIT 不等待數據發送完成
MSG_EOR 數據包結尾
MSG_OOB 帶外數據
注意send函數成功返回並不表明對端必定收到了發送的消息,另外對於數據報協議若是發送的數據大於一個數據報長度則發送失敗(errno設置爲EMSGSIZE)。
linux 客戶端 Socket 非阻塞connect編程(正文)
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,通訊端口以下: */
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);
*/
非阻塞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調用標記爲既可讀又可寫; 編程
非阻塞connect有這麼多好處,可是處理非阻塞connect時會遇到不少可移植性問題; windows
處理非阻塞connect的步驟:
第一步:建立socket,返回套接口描述符;
第二步:調用fcntl把套接口描述符設置成非阻塞;
第三步:調用connect開始創建鏈接;
第四步:判斷鏈接是否成功創建;
A:若是connect返回0,表示鏈接簡稱成功(服務器可客戶端在同一臺機器上時就有可能發生這種狀況);
B:調用select來等待鏈接創建成功完成;
若是select返回0,則表示創建鏈接超時;咱們返回超時錯誤給用戶,同時關閉鏈接,以防止三路握手操做繼續進行下去;
若是select返回大於0的值,則須要檢查套接口描述符是否可讀或可寫;若是套接口描述符可讀或可寫,則咱們能夠經過調用getsockopt來獲得套接口上待處理的錯誤(SO_ERROR),若是鏈接創建成功,這個錯誤值將是0,若是創建鏈接時遇到錯誤,則這個值是鏈接錯誤所對應的errno值(好比:ECONNREFUSED,ETIMEDOUT等).
"讀取套接口上的錯誤"是遇到的第一個可移植性問題;若是出現問題,getsockopt源自Berkeley的實現是返回0,等待處理的錯誤在變量errno中返回;可是Solaris會讓getsockopt返回-1,errno置爲待處理的錯誤;咱們對這兩種狀況都要處理; 後端
這樣,在處理非阻塞connect時,在不一樣的套接口實現的平臺中存在的移植性問題,首先,有可能在調用select以前,鏈接就已經創建成功,並且對方的數據已經到來.在這種狀況下,鏈接成功時套接口將既可讀又可寫.這和鏈接失敗時是同樣的.這個時候咱們還得經過getsockopt來讀取錯誤值;這是第二個可移植性問題;
移植性問題總結:
1.對於出錯的套接口描述符,getsockopt的返回值源自Berkeley的實現是返回0,待處理的錯誤值存儲在errno中;而源自Solaris的實現是返回0,待處理的錯誤存儲在errno中;(套接口描述符出錯時調用getsockopt的返回值不可移植)
2.有可能在調用select以前,鏈接就已經創建成功,並且對方的數據已經到來,在這種狀況下,套接口描述符是既可讀又可寫;這與套接口描述符出錯時是同樣的;(怎樣判斷鏈接是否創建成功的條件不可移植) 瀏覽器
這樣的話,在咱們判斷鏈接是否創建成功的條件不惟一時,咱們能夠有如下的方法來解決這個問題:
1.調用getpeername代替getsockopt.若是調用getpeername失敗,getpeername返回ENOTCONN,表示鏈接創建失敗,咱們必須以SO_ERROR調用getsockopt獲得套接口描述符上的待處理錯誤;
2.調用read,讀取長度爲0字節的數據.若是read調用失敗,則表示鏈接創建失敗,並且read返回的errno指明瞭鏈接失敗的緣由.若是鏈接創建成功,read應該返回0;
3.再調用一次connect.它應該失敗,若是錯誤errno是EISCONN,就表示套接口已經創建,並且第一次鏈接是成功的;不然,鏈接就是失敗的; 服務器
被中斷的connect:
若是在一個阻塞式套接口上調用connect,在TCP的三路握手操做完成以前被中斷了,好比說,被捕獲的信號中斷,將會發生什麼呢?假定connect不會自動重啓,它將返回EINTR.那麼,這個時候,咱們就不能再調用connect等待鏈接創建完成了,若是再次調用connect來等待鏈接創建完成的話,connect將會返回錯誤值EADDRINUSE.在這種狀況下,應該作的是調用select,就像在非阻塞式connect中所作的同樣.而後,select在鏈接創建成功(使套接口描述符可寫)或鏈接創建失敗(使套接口描述符既可讀又可寫)時返回; 網絡