Socket Connect問題


一、非阻塞Connect對於Select時應注意的問題

對於面向連接的socket(SOCK_STREAM、SOCK_SEQPACKET),在讀寫數據之前必須建立連接。

建立連接的過程

首先,服務器端socket必須在一個客戶端知曉的地址(IP和端口號)進行監聽,也就是說,創建socket之后,必須調用int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);將socket綁定到一個指定的地址struct sockaddr *addr,然后調用int listen(int sockfd, int backlog);進行監聽。此時,服務器socket允許客戶端進行連接,backlog表示套接字socket排隊的最大連接個數,系統決定實際的值,最大值定義為定義在頭文件 里的SOMAXCONN宏。如果由於某種原因,服務器端進程未及時accept客戶端連接導致此隊列滿,則新的客戶端連接請求將被拒絕。

服務器調用listen監聽之后,當有客戶端連接到達時,調用int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);接收客戶端連接請求,同時返回一個已連接socket描述符用於在客戶端和服務器連接間傳輸數據。同時,原監聽socket可以繼續監聽客戶端的連接請求。如果addr不為NULL,則客戶端發起連接請求的客戶端socket的地址信息會通過addr返回。如果監聽socket描述符為阻塞模式,則accept會一直阻塞到有客戶發起連接請求;如果監聽socket描述符為非阻塞模式,如果當前沒有可用的客戶端連接請求,則會返回-1(errno設置為EAGAIN)。可以使用select函數對監聽的socket描述符進行多路分離:如果有客戶端連接請求,select函數將監聽socket描述符設置為可讀。注意:如果監聽socket為阻塞模式,那么,當使用select進行多路分離時,可能造成select返回可讀但是調用accept會被阻塞住的情況,原因是在調用accept之前客戶端可能主動關閉連接或者發送RST異常關閉連接,因此,建議select與非阻塞select一起使用

對於客戶端,其調用int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);發起對服務器socket的連接請求,如果客戶端socket描述符為阻塞模式,則其會一直阻塞到連接建立或者連接失敗(注意,阻塞模式的超時時間可能為75秒到幾分鍾之間);如果客戶端socket描述符為非阻塞模式,則調用connect之后,如果連接未能立即建立,則返回-1(errno設置為EINPROGRESS,注意連接也可能馬上建立成功,如連接本機服務器進程),如果沒有立即建立返回,此時TCP的三路握手動作在后台進行,程序可執行其他操作,然后調用select檢測非阻塞connect是否完成(可以指定select的超時時間,該超時時間可以設置為比connect阻塞超時時間短),如果select超時,則可以直接關閉socket,然后可以嘗試創建新的socket重新連接;如果select返回非阻塞socket描述符可寫,則表明連接建立成功;如果select返回非阻塞socket描述符既可讀又可寫,則表明連接出錯。注意,此處必須與另外一種正常連接的情況區分開來,那就是,當連接建立完成之后,服務器發送了數據給客戶端。此時select會同時返回非阻塞socket描述符既可讀有可寫。此時,可以通過以下方法區分:

  • 調用getpeername獲取對端的socket地址。如果getpeername返回ENOTCONN,表示連接建立失敗,然后用SO_ERROR調用getsockopt得到套接字描述符上的待處理錯誤;

  • 調用read,讀取socket上長度為0字節的數據。如果read調用失敗,則表示連接失敗,而且read返回的errno指明了連接失敗的原因。如果連接建立成功,read應該返回0;

  • 再次調用connect,它應該返回失敗。如果錯誤errno是EISCONN,則表示套接口已經建立連接,而且第一次連接是成功的;否則,連接建立失敗。

另外,我們需要注意下面幾點:

  • 對於無連接的socket(SOCK_DGRAM),客戶端也可以調用connect進行連接,此連接實際上並不建立類似SOCK_STREAM的連接,而是僅僅在本地保存了對端的地址,這樣,后續的讀寫操作可以默認以該對端作為操作對象

  • 當對端機器Crash或者網絡連接被斷開(比如路由器不工作、網線斷開等),此時發送數據給對端然后讀取本端socket會返回ETIMEOUT或者EHOSTUNREACH或者ENETUNREACH(后兩個是中間路由器判斷服務器主機不可達的情況)。

  • 當對端機器Crash之后又重新啟動,然后客戶端再向原來的連接發送數據,因為服務器端已經沒有原來的連接信息,此時服務器端回送RST給客戶端,此時,客戶端讀本地端口返回ECONNRESET錯誤。

  • 當服務器所在的進程正常或異常關閉時,會對所有打開的文件描述符進行close,因此,對於連接的socket描述符,則會向對端發送FIN進行正常關閉流程。對端在收到FIN之后端口變得可讀,此時讀取端口會返回0,表示到了文件結尾(對端不會再發送數據)。

  • 當一端收到RST導致讀取socket返回ECONNRESET,此時如果再次調用write發送數據給對端則觸發SIGPIPE信號,信號默認終止進程,如果忽略此信號或者從SIGPIPE的信號處理程序返回,則write出錯返回EPIPE。

  • 只有當本地端口主動發送消息給對端才能檢測出連接異常中斷的情況,搭配select進行多路分離的時候,socket收到RST或者FIN的時候,select返回可讀(心跳消息就是用於檢測連接的狀態)。也可以使用socket的KEEPALIVE選項,依賴socket本身偵測socket連接異常中斷的情況。

非阻塞socket進行connect的過程

  • 將打開的socket設為非阻塞,可以用fcntl(socket, F_SETFL, O_NDELAY)完成;

  • 發起connect調用,此時返回-1,但是,errno被設為EINPROGRESS,意即connect仍舊在進行,還沒有完成;

  • 將打開的socket添加至select監控的可寫集合,如果可寫,用getsockopt(socket, SOL_SOCKET, SO_ERROR, &error, sizeof(int))來得到error的值,如果為零,則說明socket connect成功。

二、linux客戶端socket非阻塞connect編程

非阻塞模式有三種用途:

  • 三次握手的同時做其他的處理。connect要花一個往返的時間完成,從幾毫秒的局域網到幾百毫秒或幾秒的廣域網。這段時間可能有一些其他的處理要執行,比如數據准備,預處理等;

  • 用非阻塞技術建立多個連接,這在web瀏覽器中十分普遍;

  • 由於程序使用select等待連接完成,可以設置一個select超時時間,從而縮短connect超時時間。多數實現中,connect的超時時間在75秒到幾分鍾之間。有時程序希望在等待一定時間內結束,使用非阻塞connect可以防止阻塞75秒,在多線程網絡編程中,尤其必要。 例如有一個通過建立線程與其他主機進行socket通信的應用程序,如果建立的線程使用阻塞connect與遠程通信,當有幾百個線程並發的時候,由於網絡延遲而全部阻塞,阻塞的線程不會釋放系統的資源,同一時刻阻塞線程超過一定數量時候,系統就不再允許建立新的線程(每個進程由於進程空間的原因能產生的線程有限),如果使用非阻塞的connect,連接失敗使用select等待很短時間,如果還沒有連接后,線程立刻結束釋放資源,防止大量線程阻塞而使程序崩潰。

目前,connect非阻塞編程的普遍思路是:

在一個TCP套接口設置為非阻塞后,調用connect,connect會在系統提供的errno變量中返回一個EINPROGRESS錯誤,此時TCP的三路握手繼續進行。之后可以用select函數檢查這個連接是否建立成功。

以下實驗基於Unix網絡編程和網絡上給出的普遍示例,在經過大量測試后,發現其中有很多方法,在Linux中,並不適用。

  1. 首先填寫套接字結構,包括遠程IP和通信端口:
 
 
 
         
  1. struct sockaddr_in serv_addr;
  2. serv_addr.sin_family = AF_INET;
  3. serv_addr.sin_port = htons(12345);
  4. serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
  5. bzero(&(serv_addr.sin_zero), 8);
  1. 建立socket套接字:
 
 
 
         
  1. if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
  2. perror("Socket create failure.");
  3. return 1;
  4. }
  1. 將socket建立為非阻塞,此時socket被設置為非阻塞模式:
 
 
 
         
  1. flags = fcntl(sockfd, F_GETFL, 0);
  2. fcntl(sockfd, F_SETFL, flags|O_NONBLOCK);
  1. 建立connect連接,此時socket設置為非阻塞,connect調用后,無論連接是否建立,立即返回-1,同時將errno設置為EINPROGRESS,表示此時TCP三次握手仍舊進行,如果errno不是EINPROGRESS,則說明連接錯誤,程序結束:
 
 
 
         
  1. if ((n = connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr))) < 0) {
  2. if (errno != EINPROGRESS)
  3. return 1;
  4. if (n == 0) {
  5. printf("Connect completed immediately.");
  6. goto done;
  7. }
  8. }
  1. 設置等待時間,使用select函數等待正在后台連接的connect函數,這里需要說明的是使用select監聽socket描述符是否可讀或者可寫,如果只可寫,說明連接成功,可以進行下面的操作。如果描述符既可讀又可寫,分為兩種情況,第一種情況是socket連接錯誤(這是系統規定的,可讀可寫時可能是connect連接成功后遠程主機斷開了連接close(socket)),第二種情況是connect連接成功,socket讀緩沖區接收到了遠程主機發送的數據,需要通過connect連接后返回的errno值來判定,或者通過調用getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &errno, &len)函數返回值來判斷是否發生錯誤,這里存在一個可移植性問題,在Solaris系統中發生錯誤返回-1, 其他系統中可能返回0,Linux中如下:
 
 
 
         
  1. FD_ZERO(&rset);
  2. FD_SET(sockfd, &rset);
  3. wset = rset;
  4. tval.tv_sec = 0;
  5. tval.tv_usec = 300000;
  6. int error;
  7. sock_len_t len;
  8. if (( n = select(sockfd + 1, &rset, &wset, NULL, &tval)) <= 0) {
  9. printf("Time out connect error.");
  10. close(sockfd);
  11. return -1;
  12. }
  13. if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
  14. len = sizeof(error);
  15. if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0) {
  16. return 1;
  17. }
  18. }

這里我測試了一下,按照unix網絡編程的描述,當網絡發生錯誤的時候,getsockopt返回-1,程序結束。網絡正常時,返回0,程序繼續執行。

可是我在linux下,無論網絡是否發生錯誤,getsockopt始終返回0,不返回-1,說明linux與unix網絡編程還是有些細微的差別。就是說當socket描述符可讀可寫的時候,這段代碼不起作用。不能檢測出網絡是否出現故障。

我測試的方法是,當調用connect后,sleep(2)休眠2秒,在這兩秒時間內,將網絡斷開並連接,此時select返回2,說明套接口可讀又可寫,應該是網絡連接的出錯情況。

此時,getsockopt返回0,不起作用。獲取errno的值,指示為EINPROGRESS,沒有返回unix網絡編程中說的ENOTCONN,EINPROGRESS表示正在試圖連接,不能表示網絡已經連接失敗。

針對這種情況,Unix網絡編程中提出了另外三種方法:

  • 再次調用connect一次,失敗返回errno是EISCONN,則說明連接成功,表示剛才的connect成功,否則返回失敗。
 
 
 
         
  1. int connect_ok;
  2. connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr));
  3. switch(errno) {
  4. case EISCONN:
  5. printf("Connect OK.");
  6. connect_ok = 1;
  7. break;
  8. default:
  9. connect_ok = -1;
  10. break;
  11. }

如程序所示,根據再次調用的errno返回值將connect_ok的值,來進行下面的處理,connect_ok為1繼續執行其他操作,否則程序結束。

但這種方法我在linux下測試了,當發生錯誤的時候,socket描述符變成可讀且可寫,但第二次調用connect 后,errno並沒有返回EISCONN,,也沒有返回連接失敗的錯誤,仍舊是EINPROGRESS,而當網絡不發生故障的時候,第二次使用 connect連接也返回EINPROGRESS,因此也無法通過再次connect來判斷連接是否成功。

  • Unix網絡編程中說明使用read函數,如果失敗,表示connect失敗,返回的errno指明了失敗原因,但這種方法在Linux上行不通,linux在socket描述符為可讀可寫時,read返回0,並不會置errno為錯誤。

  • Unix網絡編程中說使用getpeername函數,如果連接失敗,調用該函數后,通過errno來判斷第一次連接是否成功,但我試過了,無論網絡連接是否成功,errno都沒變化,都為EINPROGRESS,無法判斷。

悲哀啊,即使調用getpeername函數,getsockopt函數仍舊不行。

綜上方法,既然都不能確切知道非阻塞connect是否成功,所以我直接當描述符可讀可寫的情況下進行發送,通過能否獲取服務器的返回值來判斷是否成功。(如果服務器端的設計不發送數據,那就悲哀了。)

程序的書寫形式出於可移植性考慮,按照unix網絡編程推薦寫法,使用getsocketopt進行判斷,但不通過返回值來判斷,而通過函數的返回參數來判斷。

  1. 用select查看接收描述符,如果可讀,就讀出數據,程序結束。在接收數據的時候注意要先對先前的rset重新賦值為描述符,因為select會對 rset清零,當調用select后,如果socket沒有變為可讀,則rset在select會被置零。所以如果在程序中使用了rset,最好在使用時候重新對rset賦值。
 
 
 
         
  1. FD_ZERO(&rset);
  2. FD_SET(sockfd,&rset);//如果前面select使用了rset,最好重新賦值
  3. if( ( n = select(sockfd+1,&rset,NULL, NULL,&tval)) <= 0 ) {
  4.   close(sockfd);
  5.   return -1;
  6. }
  7. if ((recvbytes=recv(sockfd, buf, 1024, 0)) ==-1){
  8.   perror("recv error!");
  9.   close(sockfd);
  10.   return 1;
  11. }
  12. printf("receive num %d\n",recvbytes);
  13. printf("%s\n",buf);





免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM