日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

深度好文:select与非阻塞IO

發(fā)布時間:2025/3/21 编程问答 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深度好文:select与非阻塞IO 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

對于面向連接的socket類型(SOCK_STREAM,SOCK_SEQPACKET)在讀數(shù)據(jù)之前必須建立連接,首先服務器端socket必須在一個客戶端知道的地址進行監(jiān)聽,也就是創(chuàng)建socket之后必須調(diào)用bind綁定到一個指定的地址,然后調(diào)用int listen(int sockfd, int backlog);進行監(jiān)聽。此時服務器socket允許客戶端進行連接,backlog提示沒被accept的客戶連接請求隊列的大小,系統(tǒng)決定實際的值,最大值定義為SOMAXCONN在頭文件<sys/socket.h>里面。如果某種原因?qū)е路掌鞫诉M程未及時accpet客戶連接而導致此隊列滿了的話則新的客戶端連接請求被拒絕(在工作中遇到過此情況,IONA ORBIX(CORBA中間件)由于沒有配置超時時間結(jié)果在WIFI網(wǎng)絡中傳輸數(shù)據(jù)出現(xiàn)異常情況一直阻塞而無機會調(diào)用accept接受新的客戶請求,于是最終隊列滿導致新的客戶連接被拒絕)。

  調(diào)用listen之后當有客戶端連接到達的時候調(diào)用int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);接受客戶端連接建立起連接返回用于連接數(shù)據(jù)傳送的socket描述符,進行監(jiān)聽的socket可以用于繼續(xù)監(jiān)聽客戶端的連接請求,返回的socket描述符跟監(jiān)聽的socket類型一致。如果addr不為NULL,則客戶端發(fā)起連接請求的socket地址信息會通過addr進行返回。如果監(jiān)聽的socket描述符為阻塞模式則accept一直會阻塞直到有客戶發(fā)起連接請求,如果監(jiān)聽的socket描述符為非阻塞模式則如果當前沒有可用的客戶連接請求,則返回-1(errno設(shè)置為EAGAIN)。可以使用select函數(shù)對監(jiān)聽的socket描述符進行多路分離,如果有客戶連接請求則select將監(jiān)聽的socket描述符設(shè)置為可讀(注意,如果監(jiān)聽的socket為阻塞模式而使用select進行多路分離則可能造成select返回可讀但是調(diào)用accept會被阻塞住的情況,原因是在調(diào)用accept之前客戶端可能主動關(guān)閉連接或者發(fā)送RST異常關(guān)閉連接,因此select最好跟非阻塞socket搭配使用)。

  客戶端調(diào)用int connect(int sockfd, const struct sockaddr *addr, socklen_t len);發(fā)起對服務器的socket的連接請求,如果客戶端socket描述符為阻塞模式則會一直阻塞到連接建立或者連接失敗(注意阻塞模式的超時時間可能為75秒到幾分鐘之間),而如果為非阻塞模式,則調(diào)用connect之后如果連接不能馬上建立則返回-1(errno設(shè)置為EINPROGRESS,注意連接也可能馬上建立成功比如連接本機的服務器進程),如果沒有馬上建立返回,此時TCP的三路握手動作在背后繼續(xù),而程序可以做其他的東西,然后調(diào)用select檢測非阻塞connect是否完成(此時可以指定select的超時時間,這個超時時間可以設(shè)置為比connect的超時時間短),如果select超時則關(guān)閉socket,然后可以嘗試創(chuàng)建新的socket重新連接,如果select返回非阻塞socket描述符可寫則表明連接建立成功,如果select返回非阻塞socket描述符既可讀又可寫則表明連接出錯(注意:這兒必須跟另外一種連接正常的情況區(qū)分開來,就是連接建立好了之后,服務器端發(fā)送了數(shù)據(jù)給客戶端,此時select同樣會返回非阻塞socket描述符既可讀又可寫,這時可以通過以下方法區(qū)分:
  1.調(diào)用getpeername獲取對端的socket地址.如果getpeername返回ENOTCONN,表示連接建立失敗,然后用SO_ERROR調(diào)用getsockopt得到套接口描述符上的待處理錯誤;
  2.調(diào)用read,讀取長度為0字節(jié)的數(shù)據(jù).如果read調(diào)用失敗,則表示連接建立失敗,而且read返回的errno指明了連接失敗的原因.如果連接建立成功,read應該返回0;
  3.再調(diào)用一次connect.它應該失敗,如果錯誤errno是EISCONN,就表示套接口已經(jīng)建立,而且第一次連接是成功的;否則,連接就是失敗的;
  對于無連接的socket類型(SOCK_DGRAM),客戶端也可以調(diào)用connect進行連接,此連接實際上并不建立類似SOCK_STREAM的連接,而僅僅是在本地保存了對端的地址,這樣后續(xù)的讀寫操作可以默認以連接的對端為操作對象。

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


  當對端機器crash之后重新啟動,然后客戶端再向原來的連接發(fā)送數(shù)據(jù),因為服務器端已經(jīng)沒有原來的連接信息,此時服務器端回送RST給客戶端,此時客戶端讀本地端口返回ECONNRESET錯誤。

  當服務器所在的進程正常或者異常關(guān)閉時,會對所有打開的文件描述符進行close,因此對于連接的socket描述符則會向?qū)Χ税l(fā)送FIN分節(jié)進行正常關(guān)閉流程。對端在收到FIN之后端口變得可讀,此時讀取端口會返回0表示到了文件結(jié)尾(對端不會再發(fā)送數(shù)據(jù))。 

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

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


  發(fā)送socket數(shù)據(jù)有以下方法:

  調(diào)用ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);,只能用于建立好了連接的socket(面向連接的SOCK_STREAM或者調(diào)用了connect的SOCK_DGRAM)。flags取值如下:

  MSG_DONTROUTE 對數(shù)據(jù)不進行路由

  MSG_DONTWAIT 不等待數(shù)據(jù)發(fā)送完成

  MSG_EOR 數(shù)據(jù)包結(jié)尾

  MSG_OOB 帶外數(shù)據(jù)

  注意send函數(shù)成功返回并不代表對端一定收到了發(fā)送的消息,另外對于數(shù)據(jù)報協(xié)議如果發(fā)送的數(shù)據(jù)大于一個數(shù)據(jù)報長度則發(fā)送失敗(errno設(shè)置為EMSGSIZE)。
?

linux 客戶端 Socket 非阻塞connect編程(正文)

linux 客戶端 Socket 非阻塞connect編程(正文)/*開發(fā)過程與源碼解析

  開發(fā)測試環(huán)境:虛擬機CentOS,windows網(wǎng)絡調(diào)試助手
  非阻塞模式有3種用途

  1.三次握手同時做其他的處理。connect要花一個往返時間完成,從幾毫秒的局域網(wǎng)到幾百毫秒或幾秒的廣域網(wǎng)。這段時間可能有一些其他的處理要執(zhí)行,比如數(shù)據(jù)準備,預處理等。
  2.用這種技術(shù)建立多個連接。這在web瀏覽器中很普遍.
  3.由于程序用select等待連接完成,可以設(shè)置一個select等待時間限制,從而縮短connect超時時間。多數(shù)實現(xiàn)中,connect的超時時間在75秒到幾分鐘之間。有時程序希望在等待一定時間內(nèi)結(jié)束,使用非阻塞connect可以防止阻塞75秒,在多線程網(wǎng)絡編程中,尤其必要。 例如有一個通過建立線程與其他主機進行socket通信的應用程序,如果建立的線程使用阻塞connect與遠程通信,當有幾百個線程并發(fā)的時候,由于網(wǎng)絡延遲而全部阻塞,阻塞的線程不會釋放系統(tǒng)的資源,同一時刻阻塞線程超過一定數(shù)量時候,系統(tǒng)就不再允許建立新的線程(每個進程由于進程空間的原因能產(chǎn)生的線程有限),如果使用非阻塞的connect,連接失敗使用select等待很短時間,如果還沒有連接后,線程立刻結(jié)束釋放資源,防止大量線程阻塞而使程序崩潰。

  目前connect非阻塞編程的普遍思路是:
  在一個TCP套接口設(shè)置為非阻塞后,調(diào)用connect,connect會在系統(tǒng)提供的errno變量中返回一個EINRPOCESS錯誤,此時TCP的三路握手繼續(xù)進行。之后可以用select函數(shù)檢查這個連接是否建立成功。以下實驗基于unix網(wǎng)絡編程和網(wǎng)絡上給出的普遍示例,在經(jīng)過大量測試之后,發(fā)現(xiàn)其中有很多方法,在linux中,并不適用。

  我先給出了重要源碼的逐步分析,在最后給出完整的connect非阻塞源碼。
  1.首先填寫套接字結(jié)構(gòu),包括遠程的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)絡字節(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建立為非阻塞,此時socket被設(shè)置為非阻塞模式
  flags = fcntl(sockfd,F_GETFL,0);//獲取建立的sockfd的當前狀態(tài)(非阻塞)
  fcntl(sockfd,F_SETFL,flags|O_NONBLOCK);//將當前sockfd設(shè)置為非阻塞
  /*4. 建立connect連接,此時socket設(shè)置為非阻塞,connect調(diào)用后,無論連接是否建立立即返回-1,同時將errno(包含errno.h就可以直接使用)設(shè)置為EINPROGRESS, 表示此時tcp三次握手仍舊進行,如果errno不是EINPROGRESS,則說明連接錯誤,程序結(jié)束。
  當客戶端和服務器端在同一臺主機上的時候,connect回馬上結(jié)束,并返回0;無需等待,所以使用goto函數(shù)跳過select等待函數(shù),直接進入連接后的處理部分。*/

  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è)置等待時間,使用select函數(shù)等待正在后臺連接的connect函數(shù),這里需要說明的是使用select監(jiān)聽socket描述符是否可讀或者可寫,如果只可寫,說明連接成功,可以進行下面的操作。如果描述符既可讀又可寫,分為兩種情況,第一種情況是socket連接出現(xiàn)錯誤(不要問為什么,這是系統(tǒng)規(guī)定的,可讀可寫時候有可能是connect連接成功后遠程主機斷開了連接close(socket)),第二種情況是connect連接成功,socket讀緩沖區(qū)得到了遠程主機發(fā)送的數(shù)據(jù)。需要通過connect連接后返回給errno的值來進行判定,或者通過調(diào)用 getsockopt(sockfd,SOL_SOCKET,SO_ERROR,&error,&len); 函數(shù)返回值來判斷是否發(fā)生錯誤,這里存在一個可移植性問題,在solaris中發(fā)生錯誤返回-1,但在其他系統(tǒng)中可能返回0.我首先按unix網(wǎng)絡編程的源碼進行實現(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;
  }

  /* 這里我測試了一下,按照unix網(wǎng)絡編程的描述,當網(wǎng)絡發(fā)生錯誤的時候,getsockopt返回-1,return -1,程序結(jié)束。網(wǎng)絡正常時候返回0,程序繼續(xù)執(zhí)行。
  可是我在linux下,無論網(wǎng)絡是否發(fā)生錯誤,getsockopt始終返回0,不返回-1,說明linux與unix網(wǎng)絡編程還是有些細微的差別。就是說當socket描述符可讀可寫的時候,這段代碼不起作用。不能檢測出網(wǎng)絡是否出現(xiàn)故障。
  我測試的方法是,當調(diào)用connect后,sleep(2)休眠2秒,借助這兩秒時間將網(wǎng)絡助手斷開連接,這時候select返回2,說明套接口可讀又可寫,應該是網(wǎng)絡連接的出錯情況。
  此時,getsockopt返回0,不起作用。獲取errno的值,指示為EINPROGRESS,沒有返回unix網(wǎng)絡編程中說的ENOTCONN,EINPROGRESS表示正在試圖連接,不能表示網(wǎng)絡已經(jīng)連接失敗。
針對這種情況,unix網(wǎng)絡編程中提出了另外3種方法,這3種方法,也是網(wǎng)絡上給出的常用的非阻塞connect示例:
  a.再調(diào)用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;
  }

  /*如程序所示,根據(jù)再次調(diào)用的errno返回值將connect_ok的值,來進行下面的處理,connect_ok為1繼續(xù)執(zhí)行其他操作,否則程序結(jié)束。
  但這種方法我在linux下測試了,當發(fā)生錯誤的時候,socket描述符(我的程序里是sockfd)變成可讀且可寫,但第二次調(diào)用connect 后,errno并沒有返回EISCONN,,也沒有返回連接失敗的錯誤,仍舊是EINPROGRESS,而當網(wǎng)絡不發(fā)生故障的時候,第二次使用 connect連接也返回EINPROGRESS,因此也無法通過再次connect來判斷連接是否成功。
  b.unix網(wǎng)絡編程中說使用read函數(shù),如果失敗,表示connect失敗,返回的errno指明了失敗原因,但這種方法在linux上行不通,linux在socket描述符為可讀可寫的時候,read返回0,并不會置errno為錯誤。
 ? c.unix網(wǎng)絡編程中說使用getpeername函數(shù),如果連接失敗,調(diào)用該函數(shù)后,通過errno來判斷第一次連接是否成功,但我試過了,無論網(wǎng)絡連接是否成功,errno都沒變化,都為EINPROGRESS,無法判斷。
  悲哀啊,即使調(diào)用getpeername函數(shù),getsockopt函數(shù)仍舊不行。
  綜上方法,既然都不能確切知道非阻塞connect是否成功,所以我直接當描述符可讀可寫的情況下進行發(fā)送,通過能否獲取服務器的返回值來判斷是否成功。(如果服務器端的設(shè)計不發(fā)送數(shù)據(jù),那就悲哀了。)
  程序的書寫形式出于可移植性考慮,按照unix網(wǎng)絡編程推薦寫法,使用getsocketopt進行判斷,但不通過返回值來判斷,而通過函數(shù)的返回參數(shù)來判斷。
  6. 用select查看接收描述符,如果可讀,就讀出數(shù)據(jù),程序結(jié)束。在接收數(shù)據(jù)的時候注意要先對先前的rset重新賦值為描述符,因為select會對 rset清零,當調(diào)用select后,如果socket沒有變?yōu)榭勺x,則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套接口被設(shè)置為非阻塞之后調(diào)用connect,connect會立即返回EINPROGRESS錯誤,表示連接操作正在進行中,但是仍未完成;同時TCP的三路握手操作繼續(xù)進行;在這之后,我們可以調(diào)用select來檢查這個鏈接是否建立成功;非阻塞connect有三種用途:
1.我們可以在三路握手的同時做一些其它的處理.connect操作要花一個往返時間完成,而且可以是在任何地方,從幾個毫秒的局域網(wǎng)到幾百毫秒或幾秒的廣域網(wǎng).在這段時間內(nèi)我們可能有一些其他的處理想要執(zhí)行;
2.可以用這種技術(shù)同時建立多個連接.在Web瀏覽器中很普遍;
3.由于我們使用select來等待連接的完成,因此我們可以給select設(shè)置一個時間限制,從而縮短connect的超時時間.在大多數(shù)實現(xiàn)中,connect的超時時間在75秒到幾分鐘之間.有時候應用程序想要一個更短的超時時間,使用非阻塞connect就是一種方法;
非阻塞connect聽起來雖然簡單,但是仍然有一些細節(jié)問題要處理:
1.即使套接口是非阻塞的,如果連接的服務器在同一臺主機上,那么在調(diào)用connect建立連接時,連接通常會立即建立成功.我們必須處理這種情況;
2.源自Berkeley的實現(xiàn)(和Posix.1g)有兩條與select和非阻塞IO相關(guān)的規(guī)則:
? A:當連接建立成功時,套接口描述符變成可寫;
? B:當連接出錯時,套接口描述符變成既可讀又可寫;
? 注意:當一個套接口出錯時,它會被select調(diào)用標記為既可讀又可寫;

非阻塞connect有這么多好處,但是處理非阻塞connect時會遇到很多可移植性問題;

處理非阻塞connect的步驟:
第一步:創(chuàng)建socket,返回套接口描述符;
第二步:調(diào)用fcntl把套接口描述符設(shè)置成非阻塞;
第三步:調(diào)用connect開始建立連接;
第四步:判斷連接是否成功建立;
?????? A:如果connect返回0,表示連接簡稱成功(服務器可客戶端在同一臺機器上時就有可能發(fā)生這種情況);
?????? B:調(diào)用select來等待連接建立成功完成;
???????? 如果select返回0,則表示建立連接超時;我們返回超時錯誤給用戶,同時關(guān)閉連接,以防止三路握手操作繼續(xù)進行下去;
???????? 如果select返回大于0的值,則需要檢查套接口描述符是否可讀或可寫;如果套接口描述符可讀或可寫,則我們可以通過調(diào)用getsockopt來得到套接口上待處理的錯誤(SO_ERROR),如果連接建立成功,這個錯誤值將是0,如果建立連接時遇到錯誤,則這個值是連接錯誤所對應的errno值(比如:ECONNREFUSED,ETIMEDOUT等).
"讀取套接口上的錯誤"是遇到的第一個可移植性問題;如果出現(xiàn)問題,getsockopt源自Berkeley的實現(xiàn)是返回0,等待處理的錯誤在變量errno中返回;但是Solaris會讓getsockopt返回-1,errno置為待處理的錯誤;我們對這兩種情況都要處理;

這樣,在處理非阻塞connect時,在不同的套接口實現(xiàn)的平臺中存在的移植性問題,首先,有可能在調(diào)用select之前,連接就已經(jīng)建立成功,而且對方的數(shù)據(jù)已經(jīng)到來.在這種情況下,連接成功時套接口將既可讀又可寫.這和連接失敗時是一樣的.這個時候我們還得通過getsockopt來讀取錯誤值;這是第二個可移植性問題;
移植性問題總結(jié):
1.對于出錯的套接口描述符,getsockopt的返回值源自Berkeley的實現(xiàn)是返回0,待處理的錯誤值存儲在errno中;而源自Solaris的實現(xiàn)是返回0,待處理的錯誤存儲在errno中;(套接口描述符出錯時調(diào)用getsockopt的返回值不可移植)
2.有可能在調(diào)用select之前,連接就已經(jīng)建立成功,而且對方的數(shù)據(jù)已經(jīng)到來,在這種情況下,套接口描述符是既可讀又可寫;這與套接口描述符出錯時是一樣的;(怎樣判斷連接是否建立成功的條件不可移植)

這樣的話,在我們判斷連接是否建立成功的條件不唯一時,我們可以有以下的方法來解決這個問題:
1.調(diào)用getpeername代替getsockopt.如果調(diào)用getpeername失敗,getpeername返回ENOTCONN,表示連接建立失敗,我們必須以SO_ERROR調(diào)用getsockopt得到套接口描述符上的待處理錯誤;
2.調(diào)用read,讀取長度為0字節(jié)的數(shù)據(jù).如果read調(diào)用失敗,則表示連接建立失敗,而且read返回的errno指明了連接失敗的原因.如果連接建立成功,read應該返回0;
3.再調(diào)用一次connect.它應該失敗,如果錯誤errno是EISCONN,就表示套接口已經(jīng)建立,而且第一次連接是成功的;否則,連接就是失敗的;

被中斷的connect:
如果在一個阻塞式套接口上調(diào)用connect,在TCP的三路握手操作完成之前被中斷了,比如說,被捕獲的信號中斷,將會發(fā)生什么呢?假定connect不會自動重啟,它將返回EINTR.那么,這個時候,我們就不能再調(diào)用connect等待連接建立完成了,如果再次調(diào)用connect來等待連接建立完成的話,connect將會返回錯誤值EADDRINUSE.在這種情況下,應該做的是調(diào)用select,就像在非阻塞式connect中所做的一樣.然后,select在連接建立成功(使套接口描述符可寫)或連接建立失敗(使套接口描述符既可讀又可寫)時返回;

?

?

Linux網(wǎng)絡編程之connect函數(shù)分析

在一個 CLIENT/SERVER模型的網(wǎng)絡應用中,客戶端的調(diào)用序列大致如下:

??????? socket -> connect -> recv/send -> close

??????? 其中socket沒有什么可疑問的,主要是創(chuàng)建一個套接字用于與服務端交換數(shù)據(jù),并且通常它會迅速返回,此時并沒有數(shù)據(jù)通過網(wǎng)卡發(fā)送出去,而緊隨其后的 connect函數(shù)則會產(chǎn)生網(wǎng)絡數(shù)據(jù)的發(fā)送,TCP的三次握手也正是在此時開始,connect會先發(fā)送一個SYN包給服務端,并從最初始的CLOSED 狀態(tài)進入到SYN_SENT狀態(tài),在此狀態(tài)等待服務端的確認包,通常情況下這個確認包會很快到達,以致于我們根本無法使用netstat命令看到 SYN_SENT狀態(tài)的存在,不過我們可以做一個極端情況的模擬,讓客戶端去連接一個隨意指定服務器(如IP地址為88.88.88.88),因為該服務 器很明顯不會反饋給我們SYN包的確認包(SYN ACK),客戶端就會在一定時間內(nèi)處于SYN_SENT狀態(tài),并在預定的超時時間(比如3分鐘)之后從connect函數(shù)返回,connect調(diào)用一旦失 敗(沒能到達ESTABLISHED狀態(tài))這個套接字便不可用,若要再次調(diào)用connect函數(shù)則必須要重新使用socket函數(shù)創(chuàng)建新的套接字。

?

下面結(jié)合實例分析,客戶端代碼如下:

[cpp]?view plaincopy

  • zhou@neptune:~/data/source$?./client?88.88.88.88??
  • 此時程序會在connect函數(shù)中阻塞等待,約180秒之后輸出:

    [cpp]?view plaincopy

  • zhou@neptune:~/data/source$?sudo?netstat?-natp?|grep?20000??
  • tcp????????0??????1?192.168.0.4:44203???????88.88.88.88:20000???????SYN_SENT????5954/client??????

  • 可以看到此時的TCP連接狀態(tài)為SYN_SENT,也就意味著發(fā)送了SYN包之后一直未得到服務端回饋SYN ACK包。

    接下來我們使用這個客戶端程序來連接自己的機器,測試時我的IP地址是192.168.0.4,是一個無線局域網(wǎng),結(jié)果如下:

    [cpp]?view plaincopy

  • zhou@neptune:~/data/source$?./client?192.168.0.188??
  • can?not?connect?to?192.168.0.188,?exit!??
  • No?route?to?host??
  • 因為本地局域網(wǎng)中的該主機并不存在,ARP請求得不到回應,網(wǎng)關(guān)會回應主機不可達的ICMP報文,connect返回EHOSTUNREACH。

    至此connect函數(shù)的分析就結(jié)束了,由于本人水平有限,博客中的不妥或錯誤之處在所難免,殷切希望讀者批評指正。同時也歡迎讀者共同探討相關(guān)的內(nèi)容,如果樂意交流的話請留下您寶貴的意見,謝謝。

    ?

    ?

    ?

    linux connect 設(shè)置連接超時

    原來我們實現(xiàn)connect()超時基本上都使用unix網(wǎng)絡編程一書的非阻塞方式(connect_nonb),今天在網(wǎng)上看到一篇文章,覺得很有意思,轉(zhuǎn)載如下:

    讀Linux內(nèi)核源碼的時候偶然發(fā)現(xiàn)其connect的超時參數(shù)竟然和用SO_SNDTIMO操作的參數(shù)一致:

      File: net/ipv4/af_inet.c

    ??? 559?????? timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
    ??? 560
    ??? 561?????? if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
    ??? 562?????????? /* Error code is set above */
    ??? 563?????????? if (!timeo || !inet_wait_for_connect(sk, timeo))
    ??? 564?????????????? goto out;
    ??? 565
    ??? 566?????????? err = sock_intr_errno(timeo);
    ??? 567?????????? if (signal_pending(current))
    ??? 568?????????????? goto out;
    ??? 569?????? }


      這意味著:在Linux平臺下,可以通過在connect之前設(shè)置SO_SNDTIMO來達到控制連接超時的目的。簡單的寫了份測試代碼:


    #include?<stdlib.h>
    #include?<stdio.h>
    #include?<sys/types.h>
    #include?<sys/socket.h>
    #include?<netinet/in.h>
    #include?<errno.h>

    int?main(int?argc,?char?*argv[])
    {
    ????????int?fd;
    ????????struct?sockaddr_in?addr;
    ????????struct?timeval?timeo?=?{3,?0};
    ????????socklen_t?len?=?sizeof(timeo);

    ???????? fd?=?socket(AF_INET,?SOCK_STREAM,?0);
    ????????if?(argc?==?4)
    ???????????????? timeo.tv_sec?=?atoi(argv[3]);
    ????????setsockopt(fd,?SOL_SOCKET,?SO_SNDTIMEO,?&timeo,?len);
    ???????? addr.sin_family?=?AF_INET;
    ???????? addr.sin_addr.s_addr?=?inet_addr(argv[1]);
    ???????? addr.sin_port?=?htons(atoi(argv[2]));
    ????????if?(connect(fd,?(struct?sockaddr*)&addr,?sizeof(addr))?==?-1)?{
    ????????????????if?(errno?==?EINPROGRESS)?{
    ????????????????????????fprintf(stderr,?"timeout/n");
    ????????????????????????return?-1;
    ????????????????}
    ????????????????perror("connect");
    ????????????????return?0;
    ????????}
    ????????printf("connected/n");

    ????????return?0;
    }

    ?

    Linux網(wǎng)絡編程之tcpdump抓包分析TCP三次握手過程

    使用TCP協(xié)議進行網(wǎng)絡通訊時,通信的兩端首先需要建立起一條連接鏈路,當然這并不表示使用UDP通信不需要“連接鏈路”,這里說的連接鏈路指的是通信協(xié) 議范疇的東東,并不是物理介質(zhì)或者電磁波信號,只所以說TCP是面向連接的網(wǎng)絡通信協(xié)議,主要是指雙方在通信時都會保持一些連接相關(guān)的信息,比如已收到的 分組的序列號,下一次需要收到的分組的序號,對方的滑動窗口信息等等。

    ??????? OK,閑話少扯,我們進入主題,下面結(jié)合一個簡單的TCP服務端與客戶端代碼,借助tcpdump命令來分析一下TCP建立連接時的三次握手過程(Three-way handshake process)。

    服務端代碼如下:

    [cpp]?view plaincopy

  • /**?
  • ?*?client.c?
  • ?*?
  • ?*?TCP?client?program,?it?is?a?simple?example?only.?
  • ?*?
  • ?*?Writen?By:?Zhou?Jianchun?
  • ?*?Date:?2011.08.12?
  • ?*?
  • ?*?Compiled?With:?gcc?-o?client?client.c?
  • ?*?Tested?On:?Ubuntu?11.04?LTS?
  • ?*?
  • ?*?gcc?version:?4.5.2?
  • ?*?
  • ?*/??
  • ??
  • #include?<stdio.h>??
  • #include?<sys/socket.h>??
  • #include?<unistd.h>??
  • #include?<sys/types.h>??
  • #include?<netinet/in.h>??
  • #include?<stdlib.h>??
  • #include?<string.h>??
  • ??
  • #define?SERVER_PORT?20000??
  • #define?CLIENT_PORT?((20001?+?rand())?%?65536)??
  • #define?BUFFER_SIZE?255??
  • #define?REQUEST_MESSAGE?"welcome?to?connect?the?server.\n"??
  • ??
  • void?usage(char?*name)??
  • {??
  • ????printf("usage:?%s?IP\n",?name);??
  • }??
  • int?main(int?argc,?char?**argv)??
  • {??
  • ????int?server_fd,?client_fd,?length?=?0;??
  • ????struct?sockaddr_in?server_addr,?client_addr;??
  • ????socklen_t?socklen?=?sizeof(server_addr);??
  • ????char?buf[BUFFER_SIZE];??
  • ??
  • ????if(argc?<?2)??
  • ????{??
  • ????????usage(argv[0]);??
  • ????????exit(1);??
  • ????}??
  • ????if((client_fd?=?socket(AF_INET,?SOCK_STREAM,?0))?<?0)??
  • ????{??
  • ????????printf("create?socket?error,?exit!\n");??
  • ????????exit(1);??
  • ????}??
  • ????srand(time(NULL));??
  • ????bzero(&client_addr,?sizeof(client_addr));??
  • ????client_addr.sin_family?=?AF_INET;??
  • ????//client_addr.sin_port?=?htons(CLIENT_PORT);??
  • ????client_addr.sin_port?=?htons(40000);??
  • ????client_addr.sin_addr.s_addr?=?htons(INADDR_ANY);??
  • ??
  • ????bzero(&server_addr,?sizeof(server_addr));??
  • ????server_addr.sin_family?=?AF_INET;??
  • ????inet_aton(argv[1],?&server_addr.sin_addr);??
  • ????server_addr.sin_port?=?htons(SERVER_PORT);??
  • ??
  • ????/*if(bind(client_fd,?(struct?sockaddr*)&client_addr,?sizeof(client_addr))?<?0)?
  • ????{?
  • ????????printf("bind?to?port?%d?failed,?exit!\n",?CLIENT_PORT);?
  • ????????exit(1);?
  • ????}*/??
  • ??
  • ????if(connect(client_fd,?(struct?sockaddr*)&server_addr,?socklen)?<?0)??
  • ????{??
  • ????????printf("can?not?connect?to?%s,?exit!\n",?argv[1]);??
  • ????????exit(1);??
  • ????}??
  • ??
  • ????/*length?=?recv(client_fd,?buf,?BUFFER_SIZE,?0);?
  • ????if(length?<?0)?
  • ????{?
  • ????????printf("recieve?data?from?%s?error,?exit!\n",?argv[1]);?
  • ????????exit(1);?
  • ????}?
  • ????*/??
  • ????char?*tmp?=?buf;??
  • ????while((length?=?read(client_fd,?tmp,?BUFFER_SIZE))?>?0)??
  • ????{??
  • ????????tmp?+=?length;??
  • ????}??
  • ????printf("frome?server?%s:\n\t%s",?argv[1],?buf);??
  • ????close(client_fd);??
  • ????return?0;??
  • }??
  • 代碼邏輯十分簡單,服務端程序啟動后監(jiān)聽在20000端口,等待外部連接,客戶端啟動后連接過來,服務端發(fā)送一串字符串信息給客戶端,然后退出,客戶端在讀取完信息后也退出。

    運行程序之前先在另一個終端下輸入如下命令:

    tcpdump 'port 20000' -i lo -S

    待兩端程序退出后可以看到該命令輸出如下信息:

    <div bg_cpp"="">

    [cpp]?view plaincopy

  • 17:05:35.358403?IP?neptune.local.49493?>?neptune.local.20000:?Flags?[S],?seq?1317094743,?win?32792,?options?[mss?16396,sackOK,TS?val?7083694?ecr?0,nop,wscale?6],?length?0??
  • 17:05:35.358439?IP?neptune.local.20000?>?neptune.local.49493:?Flags?[S.],?seq?1311370954,?ack?1317094744,?win?32768,?options?[mss?16396,sackOK,TS?val?7083694?ecr?7083694,nop,wscale?6],?length?0??
  • 17:05:35.358468?IP?neptune.local.49493?>?neptune.local.20000:?Flags?[.],?ack?1311370955,?win?513,?options?[nop,nop,TS?val?7083694?ecr?7083694],?length?0??
  • 17:05:35.358871?IP?neptune.local.20000?>?neptune.local.49493:?Flags?[P.],?seq?1311370955:1311371210,?ack?1317094744,?win?512,?options?[nop,nop,TS?val?7083694?ecr?7083694],?length?255??
  • 17:05:35.358890?IP?neptune.local.49493?>?neptune.local.20000:?Flags?[.],?ack?1311371210,?win?530,?options?[nop,nop,TS?val?7083694?ecr?7083694],?length?0??
  • 17:05:35.358913?IP?neptune.local.20000?>?neptune.local.49493:?Flags?[F.],?seq?1311371210,?ack?1317094744,?win?512,?options?[nop,nop,TS?val?7083694?ecr?7083694],?length?0??
  • 17:05:35.359419?IP?neptune.local.49493?>?neptune.local.20000:?Flags?[F.],?seq?1317094744,?ack?1311371211,?win?530,?options?[nop,nop,TS?val?7083694?ecr?7083694],?length?0??
  • 17:05:35.359441?IP?neptune.local.20000?>?neptune.local.49493:?Flags?[.],?ack?1317094745,?win?512,?options?[nop,nop,TS?val?7083694?ecr?7083694],?length?0??
  • ?

    下面我們逐條進行分析:

    1.客戶端通過49493端口向服務端的20000端口發(fā)送一個SYN同步請求包,展開第一次握手,其中Flags [S]表求數(shù)據(jù)包的類型為SYN, 即同步請求包,seq字段標識數(shù)據(jù)包序列號。

    2.服務端發(fā)送ACK確認包,同時附代一個SYN請求包,在確認客戶端同步請求的同時 向客戶端發(fā)送同步請求,其中Flags [S.]中的點號表示這是個確認包(ACK),S表示它同時又是一個SYN請求包。因為TCP是雙工通信協(xié)議,連接建立之后雙方可以同時收發(fā)數(shù)據(jù),所以雙 方都發(fā)送了SYN包請求同步。

    3.客戶端發(fā)送ACK包確認服務端的SYN同步請求,可以看到此時Flags中只有一個小數(shù)點,表示這個包只是用來做確認的。

    到此為止,三次握手過程就結(jié)束了,雙方如果都收到了ACK包,則都進入到ESTABLISHED狀態(tài),表明此時可以進行數(shù)據(jù)發(fā)送了。

    4.服務端向客戶端發(fā)送一個數(shù)據(jù)包,包中的內(nèi)容就是一個字符串,可以看到此時的Flags標識中有個字母P,意為PUSH DATA,就是發(fā)送數(shù)據(jù)的意思。

    至此TCP三次握手過程的分析就結(jié)束了,由于本人水平有限,博客中的不妥或錯誤之處在所難免,殷切希望讀者批評指正。同時也歡迎讀者共同探討相關(guān)的內(nèi)容,如果樂意交流的話請留下您寶貴的意見,謝謝。

    TCP連接狀態(tài)轉(zhuǎn)換詳細描述

    1、建立連接協(xié)議(三次握手)

    (1)客戶端發(fā)送一個帶SYN標志的TCP報文到服務器。這是三次握手過程中的報文1。

    (2) 服務器端回應客戶端的,這是三次握手中的第2個報文,這個報文同時帶ACK標志和SYN標志。因此它表示對剛才客戶端SYN報文的回應;同時又標志SYN給客戶端,詢問客戶端是否準備好進行數(shù)據(jù)通訊。

    (3) 客戶必須再次回應服務段一個ACK報文,這是報文段3。

    2、連接終止協(xié)議(四次握手)
       由于TCP連接是全雙工的,因此每個方向都必須單獨進行關(guān)閉。這原則是當一方完成它的數(shù)據(jù)發(fā)送任務后就能發(fā)送一個FIN來終止這個方向的連接。收到一個?FIN只意味著這一方向上沒有數(shù)據(jù)流動,一個TCP連接在收到一個FIN后仍能發(fā)送數(shù)據(jù)。首先進行關(guān)閉的一方將執(zhí)行主動關(guān)閉,而另一方執(zhí)行被動關(guān)閉。

     (1)?TCP客戶端發(fā)送一個FIN,用來關(guān)閉客戶到服務器的數(shù)據(jù)傳送(報文段4)。
     (2) 服務器收到這個FIN,它發(fā)回一個ACK,確認序號為收到的序號加1(報文段5)。和SYN一樣,一個FIN將占用一個序號。
     (3) 服務器關(guān)閉客戶端的連接,發(fā)送一個FIN給客戶端(報文段6)。
     (4) 客戶段發(fā)回ACK報文確認,并將確認序號設(shè)置為收到序號加1(報文段7)。

    CLOSED:?這個沒什么好說的了,表示初始狀態(tài)。

    LISTEN:?這個也是非常容易理解的一個狀態(tài),表示服務器端的某個SOCKET處于監(jiān)聽狀態(tài),可以接受連接了。

    SYN_RCVD:?這個狀態(tài)表示接受到了SYN報文,在正常情況下,這個狀態(tài)是服務器端的SOCKET在建立TCP連接時的三次握手會話過程中的一個中間狀態(tài),很短暫,基本上用netstat你是很難看到這種狀態(tài)的,除非你特意寫了一個客戶端測試程序,故意將三次TCP握手過程中最后一個ACK報文不予發(fā)送。因此這種狀態(tài)時,當收到客戶端的ACK報文后,它會進入到ESTABLISHED狀態(tài)。

    SYN_SENT:?這個狀態(tài)與SYN_RCVD遙想呼應,當客戶端SOCKET執(zhí)行CONNECT連接時,它首先發(fā)送SYN報文,因此也隨即它會進入到了SYN_SENT狀態(tài),并等待服務端的發(fā)送三次握手中的第2個報文。SYN_SENT狀態(tài)表示客戶端已發(fā)送SYN報文。

    ESTABLISHED:這個容易理解了,表示連接已經(jīng)建立了。

    FIN_WAIT_1:?這個狀態(tài)要好好解釋一下,其實FIN_WAIT_1和FIN_WAIT_2狀態(tài)的真正含義都是表示等待對方的FIN報文。而這兩種狀態(tài)的區(qū)別是:FIN_WAIT_1狀態(tài)實際上是當SOCKET在ESTABLISHED狀態(tài)時,它想主動關(guān)閉連接,向?qū)Ψ桨l(fā)送了FIN報文,此時該SOCKET即進入到FIN_WAIT_1狀態(tài)。而當對方回應ACK報文后,則進入到FIN_WAIT_2狀態(tài),當然在實際的正常情況下,無論對方何種情況下,都應該馬上回應ACK報文,所以FIN_WAIT_1狀態(tài)一般是比較難見到的,而FIN_WAIT_2狀態(tài)還有時常常可以用netstat看到。

    FIN_WAIT_2:上面已經(jīng)詳細解釋了這種狀態(tài),實際上FIN_WAIT_2狀態(tài)下的SOCKET,表示半連接,也即有一方要求close連接,但另外還告訴對方,我暫時還有點數(shù)據(jù)需要傳送給你,稍后再關(guān)閉連接。

    TIME_WAIT:?表示收到了對方的FIN報文,并發(fā)送出了ACK報文,就等2MSL后即可回到CLOSED可用狀態(tài)了。如果FIN_WAIT_1狀態(tài)下,收到了對方同時帶FIN標志和ACK標志的報文時,可以直接進入到TIME_WAIT狀態(tài),而無須經(jīng)過FIN_WAIT_2狀態(tài)。

    CLOSING:?這種狀態(tài)比較特殊,實際情況中應該是很少見,屬于一種比較罕見的例外狀態(tài)。正常情況下,當你發(fā)送FIN報文后,按理來說是應該先收到(或同時收到)對方的ACK報文,再收到對方的FIN報文。但是CLOSING狀態(tài)表示你發(fā)送FIN報文后,并沒有收到對方的ACK報文,反而卻也收到了對方的FIN報文。什么情況下會出現(xiàn)此種情況呢?其實細想一下,也不難得出結(jié)論:那就是如果雙方幾乎在同時close一個SOCKET的話,那么就出現(xiàn)了雙方同時發(fā)送FIN報文的情況,也即會出現(xiàn)CLOSING狀態(tài),表示雙方都正在關(guān)閉SOCKET連接。

    CLOSE_WAIT:?這種狀態(tài)的含義其實是表示在等待關(guān)閉。怎么理解呢?當對方close一個SOCKET后發(fā)送FIN報文給自己,你系統(tǒng)毫無疑問地會回應一個ACK報文給對方,此時則進入到CLOSE_WAIT狀態(tài)。接下來呢,實際上你真正需要考慮的事情是察看你是否還有數(shù)據(jù)發(fā)送給對方,如果沒有的話,那么你也就可以close這個SOCKET,發(fā)送FIN報文給對方,也即關(guān)閉連接。所以你在CLOSE_WAIT狀態(tài)下,需要完成的事情是等待你去關(guān)閉連接。

    LAST_ACK:?這個狀態(tài)還是比較容易好理解的,它是被動關(guān)閉一方在發(fā)送FIN報文后,最后等待對方的ACK報文。當收到ACK報文后,也即可以進入到CLOSED可用狀態(tài)了。

    最后有2個問題的回答,我自己分析后的結(jié)論(不一定保證100%正確)

    1、?為什么建立連接協(xié)議是三次握手,而關(guān)閉連接卻是四次握手呢?

    這是因為服務端的LISTEN狀態(tài)下的SOCKET當收到SYN報文的建連請求后,它可以把ACK和SYN(ACK起應答作用,而SYN起同步作用)放在一個報文里來發(fā)送。但關(guān)閉連接時,當收到對方的FIN報文通知時,它僅僅表示對方?jīng)]有數(shù)據(jù)發(fā)送給你了;但未必你所有的數(shù)據(jù)都全部發(fā)送給對方了,所以你可以未必會馬上會關(guān)閉SOCKET,也即你可能還需要發(fā)送一些數(shù)據(jù)給對方之后,再發(fā)送FIN報文給對方來表示你同意現(xiàn)在可以關(guān)閉連接了,所以它這里的ACK報文和FIN報文多數(shù)情況下都是分開發(fā)送的。

    2、?為什么TIME_WAIT狀態(tài)還需要等2MSL后才能返回到CLOSED狀態(tài)?

    這是因為:雖然雙方都同意關(guān)閉連接了,而且握手的4個報文也都協(xié)調(diào)和發(fā)送完畢,按理可以直接回到CLOSED狀態(tài)(就好比從SYN_SEND狀態(tài)到ESTABLISH狀態(tài)那樣);但是因為我們必須要假想網(wǎng)絡是不可靠的,你無法保證你最后發(fā)送的ACK報文會一定被對方收到,因此對方處于LAST_ACK狀態(tài)下的SOCKET可能會因為超時未收到ACK報文,而重發(fā)FIN報文,所以這個TIME_WAIT狀態(tài)的作用就是用來重發(fā)可能丟失的ACK報文,并保證于此。

    《新程序員》:云原生和全面數(shù)字化實踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀

    總結(jié)

    以上是生活随笔為你收集整理的深度好文:select与非阻塞IO的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。