天天看點

(轉)非阻塞Connect對于select時應注意問題

    對于面向連接配接的socket類型(sock_stream,sock_seqpacket)在讀寫資料之前必須建立連接配接,首先伺服器端socket必須在一個用戶端知道的位址進行監聽,也就是建立socket之後必須調用bind綁定到一個指定的位址,然後調用int

listen(int sockfd, int

backlog);進行監聽。此時伺服器socket允許用戶端進行連接配接,backlog提示沒被accept的客戶連接配接請求隊列的大小,系統決定實際的值,最大值定義為somaxconn在頭檔案<sys/socket.h>裡面。如果某種原因導緻伺服器端程序未及時accpet客戶連接配接而導緻此隊列滿了的話則新的用戶端連接配接請求被拒絕(在工作中遇到過此情況,iona

orbix(corba中間件)由于沒有配置逾時時間結果在wifi網絡中傳輸資料出現異常情況一直阻塞而無機會調用accept接受新的客戶請求,于是最終隊列滿導緻新的客戶連接配接被拒絕)。

  調用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搭配使用)。

  用戶端調用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)

  /* 這裡我測試了一下,按照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

  case einprogress: // is connecting, need to check again

  connect_ok = -1

  default: 

  printf("connect fail err=%d \n",errno);

  connect_ok = -1;

  /*如程式所示,根據再次調用的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_set(sockfd,&rset);//如果前面select使用了rset,最好重新指派

  if( ( n = select(sockfd+1,&rset,null, null,&tval)) <= 0 )

  } 

  if ((recvbytes=recv(sockfd, buf, 1024, 0)) ==-1)

  perror("recv error!");

  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時會遇到很多可移植性問題;

處理非阻塞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在連接配接建立成功(使套接口描述符可寫)或連接配接建立失敗(使套接口描述符既可讀又可寫)時傳回;

在一個 client/server模型的網絡應用中,用戶端的調用序列大緻如下:

        socket -> connect ->

recv/send -> close

其中socket沒有什麼可疑問的,主要是建立一個套接字用于與服務端交換資料,并且通常它會迅速傳回,此時并沒有資料通過網卡發送出去,而緊随其後的

connect函數則會産生網絡資料的發送,tcp的三次握手也正是在此時開始,connect會先發送一個syn包給服務端,并從最初始的closed

狀态進入到syn_sent狀态,在此狀态等待服務端的确認包,通常情況下這個确認包會很快到達,以緻于我們根本無法使用netstat指令看到

syn_sent狀态的存在,不過我們可以做一個極端情況的模拟,讓用戶端去連接配接一個随意指定伺服器(如ip位址為88.88.88.88),因為該服務

器很明顯不會回報給我們syn包的确認包(syn

ack),用戶端就會在一定時間内處于syn_sent狀态,并在預定的逾時時間(比如3分鐘)之後從connect函數傳回,connect調用一旦失

敗(沒能到達established狀态)這個套接字便不可用,若要再次調用connect函數則必須要重新使用socket函數建立新的套接字。

下面結合執行個體分析,用戶端代碼如下:

[cpp] 

/** 

 * client.c 

 * 

 * tcp client program, it is a simple example only. 

 * writen by: zhou jianchun 

 * date: 2011.08.11 

 * 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>  

#include <errno.h>  

#define server_port 20000  

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);  

    if(argc < 2)  

    {  

        usage(argv[0]);  

        exit(1);  

    }  

    if((client_fd = socket(af_inet, sock_stream, 0)) < 0)  

        printf("create socket error, exit!\n");  

    srand(time(null));  

    bzero(&client_addr, sizeof(client_addr));  

    client_addr.sin_family = af_inet;  

    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(connect(client_fd, (struct sockaddr*)&server_addr, socklen) < 0)  

        printf("can not connect to %s, exit!\n", argv[1]);  

        printf("%s\n", strerror(errno));  

    return 0;  

編譯完成之後執行:

zhou@neptune:~/data/source$ ./client 88.88.88.88  

此時程式會在connect函數中阻塞等待,約180秒之後輸出:

can not connect to 88.88.88.88, exit!  

connection timed out  

此刻connect的傳回值為etimeout。

在此過程中我們可以用netstat指令查詢連接配接狀态:

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連接配接狀态為syn_sent,也就意味着發送了syn包之後一直未得到服務端回饋syn

ack包。

接下來我們使用這個用戶端程式來連接配接自己的機器,測試時我的ip位址是192.168.0.4,是一個無線區域網路,結果如下:

zhou@neptune:~/data/source$ ./client 192.168.0.4  

can not connect to 192.168.0.4, exit!  

connection refused  

因為我的機器上并沒有跑在指定端口(20000)上監聽的服務端程式,是以這個連接配接直接被協定棧拒絕(通過發送rst類型的tcp包),connect立刻傳回,傳回值為econnrefused。

再來看看去連接配接同一區域網路中一台不存在的主機時的情形,比如這台想象的主機的ip位址為192.168.0.188:

zhou@neptune:~/data/source$ ./client 192.168.0.188  

can not connect to 192.168.0.188, exit!  

no route to host  

因為本地區域網路中的該主機并不存在,arp請求得不到回應,網關會回應主機不可達的icmp封包,connect傳回ehostunreach。

至此connect函數的分析就結束了,由于本人水準有限,部落格中的不妥或錯誤之處在所難免,殷切希望讀者批評指正。同時也歡迎讀者共同探讨相關的内容,如果樂意交流的話請留下您寶貴的意見,謝謝。

原來我們實作connect()逾時基本上都使用unix網絡程式設計一書的非阻塞方式(connect_nonb),今天在網上看到一篇文章,覺得很有意思,轉載如下:

讀linux核心源碼的時候偶然發現其connect的逾時參數竟然和用so_sndtimo操作的參數一緻:

  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              

    569      

}

  這意味着:在linux平台下,可以通過在connect之前設定so_sndtimo來達到控制連接配接逾時的目的。簡單的寫了份測試代碼:

<code>#include &lt;stdlib.h&gt; #include &lt;stdio.h&gt; #include &lt;sys/types.h&gt; #include &lt;sys/socket.h&gt; #include &lt;netinet/in.h&gt; #include &lt;errno.h&gt; 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, &amp;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*)&amp;addr, sizeof(addr)) == -1) {                 if (errno == einprogress) {                         fprintf(stderr, "timeout/n");                         return -1;                 }                 perror("connect");                 return 0;         }         printf("connected/n");         return 0; }</code>

使用tcp協定進行網絡通訊時,通信的兩端首先需要建立起一條連接配接鍊路,當然這并不表示使用udp通信不需要“連接配接鍊路”,這裡說的連接配接鍊路指的是通信協

議範疇的東東,并不是實體媒體或者電磁波信号,隻是以說tcp是面向連接配接的網絡通信協定,主要是指雙方在通信時都會保持一些連接配接相關的資訊,比如已收到的

分組的序列号,下一次需要收到的分組的序号,對方的滑動視窗資訊等等。

ok,閑話少扯,我們進入主題,下面結合一個簡單的tcp服務端與用戶端代碼,借助tcpdump指令來分析一下tcp建立連接配接時的三次握手過程(three-way

handshake process)。

服務端代碼如下:

 * server.c 

 * tcp server program, it is a simple example only. 

 * date: 2011.08.12 

#include &lt;time.h&gt;  

#include &lt;strings.h&gt;  

#define length_of_listen_queue 10  

#define buffer_size 255  

#define welcome_message "welcome to our server."  

    int server_fd, client_fd;  

    if((server_fd = socket(af_inet, sock_stream, 0)) &lt; 0)  

    server_addr.sin_addr.s_addr = htons(inaddr_any);  

    if(bind(server_fd, (struct sockaddr*)&amp;server_addr, sizeof(server_addr)) &lt; 0)  

        printf("bind to port %d failed, exit!\n", server_port);  

    if(listen(server_fd, length_of_listen_queue) &lt; 0)  

        printf("failed to listen, exit!\n");  

    while(1)  

        char buf[buffer_size];  

        long timestamp;  

        socklen_t length = sizeof(client_addr);  

        client_fd = accept(server_fd, (struct sockaddr*)&amp;client_addr, &amp;length);  

        if(client_fd &lt;0)  

        {  

            printf("call accept error, break from while loop!\n");  

            break;  

        }  

        strcpy(buf, welcome_message);  

        printf("connect from client: ip: %s, port: %d\n", (char *)inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));  

        timestamp = time(null);  

        strcat(buf, "timestamp on server:");  

        strcat(buf, ctime(×tamp));  

        send(client_fd, buf, buffer_size, 0);  

        close(client_fd);  

        close(server_fd);  

        return 0;  

用戶端代碼:

#define client_port ((20001 + rand()) % 65536)  

#define request_message "welcome to connect the server.\n"  

    char buf[buffer_size];  

    //client_addr.sin_port = htons(client_port);  

    client_addr.sin_port = htons(40000);  

    /*if(bind(client_fd, (struct sockaddr*)&amp;client_addr, sizeof(client_addr)) &lt; 0) 

    { 

        printf("bind to port %d failed, exit!\n", client_port); 

        exit(1); 

    }*/  

    /*length = recv(client_fd, buf, buffer_size, 0); 

    if(length &lt; 0) 

        printf("recieve data from %s error, exit!\n", argv[1]); 

    } 

    */  

    char *tmp = buf;  

    while((length = read(client_fd, tmp, buffer_size)) &gt; 0)  

        tmp += length;  

    printf("frome server %s:\n\t%s", argv[1], buf);  

    close(client_fd);  

代碼邏輯十分簡單,服務端程式啟動後監聽在20000端口,等待外部連接配接,用戶端啟動後連接配接過來,服務端發送一串字元串資訊給用戶端,然後退出,用戶端在讀取完資訊後也退出。

運作程式之前先在另一個終端下輸入如下指令:

tcpdump ‘port 20000‘ -i lo -s

待兩端程式退出後可以看到該指令輸出如下資訊:

17:05:35.358403 ip neptune.local.49493 &gt; 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 &gt; 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 &gt; 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 &gt; 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 &gt; 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 &gt; 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 &gt; 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 &gt; neptune.local.49493: flags [.], ack 1317094745, win 512, options [nop,nop,ts val 7083694 ecr 7083694], length 0  

下面我們逐條進行分析:

1.用戶端通過49493端口向服務端的20000端口發送一個syn同步請求包,展開第一次握手,其中flags [s]表求資料包的類型為syn,

即同步請求包,seq字段辨別資料包序列号。

2.服務端發送ack确認包,同時附代一個syn請求包,在确認用戶端同步請求的同時 向用戶端發送同步請求,其中flags

[s.]中的點号表示這是個确認包(ack),s表示它同時又是一個syn請求包。因為tcp是雙工通信協定,連接配接建立之後雙方可以同時收發資料,是以雙

方都發送了syn包請求同步。

3.用戶端發送ack包确認服務端的syn同步請求,可以看到此時flags中隻有一個小數點,表示這個包隻是用來做确認的。

到此為止,三次握手過程就結束了,雙方如果都收到了ack包,則都進入到established狀态,表明此時可以進行資料發送了。

4.服務端向用戶端發送一個資料包,包中的内容就是一個字元串,可以看到此時的flags辨別中有個字母p,意為push

data,就是發送資料的意思。

至此tcp三次握手過程的分析就結束了,由于本人水準有限,部落格中的不妥或錯誤之處在所難免,殷切希望讀者批評指正。同時也歡迎讀者共同探讨相關的内容,如果樂意交流的話請留下您寶貴的意見,謝謝。

1、建立連接配接協定(三次握手)

(1)用戶端發送一個帶syn标志的tcp封包到伺服器。這是三次握手過程中的封包1。

(2)

伺服器端回應用戶端的,這是三次握手中的第2個封包,這個封包同時帶ack标志和syn标志。是以它表示對剛才用戶端syn封包的回應;同時又标志syn給用戶端,詢問用戶端是否準備好進行資料通訊。

(3)

客戶必須再次回應服務段一個ack封包,這是封包段3。

2、連接配接終止協定(四次握手)

 

 由于tcp連接配接是全雙工的,是以每個方向都必須單獨進行關閉。這原則是當一方完成它的資料發送任務後就能發送一個fin來終止這個方向的連接配接。收到一個 fin隻意味着這一方向上沒有資料流動,一個tcp連接配接在收到一個fin後仍能發送資料。首先進行關閉的一方将執行主動關閉,而另一方執行被動關閉。

 (1) tcp用戶端發送一個fin,用來關閉客戶到伺服器的資料傳送(封包段4)。

 (2)

伺服器收到這個fin,它發回一個ack,确認序号為收到的序号加1(封包段5)。和syn一樣,一個fin将占用一個序号。

 (3)

伺服器關閉用戶端的連接配接,發送一個fin給用戶端(封包段6)。

 (4)

客戶段發回ack封包确認,并将确認序号設定為收到序号加1(封包段7)。

closed: 這個沒什麼好說的了,表示初始狀态。

listen: 這個也是非常容易了解的一個狀态,表示伺服器端的某個socket處于監聽狀态,可以接受連接配接了。

syn_rcvd: 這個狀态表示接受到了syn封包,在正常情況下,這個狀态是伺服器端的socket在建立tcp連接配接時的三次握手會話過程中的一個中間狀态,很短暫,基本上用netstat你是很難看到這種狀态的,除非你特意寫了一個用戶端測試程式,故意将三次tcp握手過程中最後一個ack封包不予發送。是以這種狀态時,當收到用戶端的ack封包後,它會進入到established狀态。

syn_sent: 這個狀态與syn_rcvd遙想呼應,當用戶端socket執行connect連接配接時,它首先發送syn封包,是以也随即它會進入到了syn_sent狀态,并等待服務端的發送三次握手中的第2個封包。syn_sent狀态表示用戶端已發送syn封包。

established:這個容易了解了,表示連接配接已經建立了。

fin_wait_1: 這個狀态要好好解釋一下,其實fin_wait_1和fin_wait_2狀态的真正含義都是表示等待對方的fin封包。而這兩種狀态的差別是:fin_wait_1狀态實際上是當socket在established狀态時,它想主動關閉連接配接,向對方發送了fin封包,此時該socket即進入到fin_wait_1狀态。而當對方回應ack封包後,則進入到fin_wait_2狀态,當然在實際的正常情況下,無論對方何種情況下,都應該馬上回應ack封包,是以fin_wait_1狀态一般是比較難見到的,而fin_wait_2狀态還有時常常可以用netstat看到。

fin_wait_2:上面已經詳細解釋了這種狀态,實際上fin_wait_2狀态下的socket,表示半連接配接,也即有一方要求close連接配接,但另外還告訴對方,我暫時還有點資料需要傳送給你,稍後再關閉連接配接。

time_wait: 表示收到了對方的fin封包,并發送出了ack封包,就等2msl後即可回到closed可用狀态了。如果fin_wait_1狀态下,收到了對方同時帶fin标志和ack标志的封包時,可以直接進入到time_wait狀态,而無須經過fin_wait_2狀态。

closing: 這種狀态比較特殊,實際情況中應該是很少見,屬于一種比較罕見的例外狀态。正常情況下,當你發送fin封包後,按理來說是應該先收到(或同時收到)對方的ack封包,再收到對方的fin封包。但是closing狀态表示你發送fin封包後,并沒有收到對方的ack封包,反而卻也收到了對方的fin封包。什麼情況下會出現此種情況呢?其實細想一下,也不難得出結論:那就是如果雙方幾乎在同時close一個socket的話,那麼就出現了雙方同時發送fin封包的情況,也即會出現closing狀态,表示雙方都正在關閉socket連接配接。

close_wait: 這種狀态的含義其實是表示在等待關閉。怎麼了解呢?當對方close一個socket後發送fin封包給自己,你系統毫無疑問地會回應一個ack封包給對方,此時則進入到close_wait狀态。接下來呢,實際上你真正需要考慮的事情是察看你是否還有資料發送給對方,如果沒有的話,那麼你也就可以close這個socket,發送fin封包給對方,也即關閉連接配接。是以你在close_wait狀态下,需要完成的事情是等待你去關閉連接配接。

last_ack: 這個狀态還是比較容易好了解的,它是被動關閉一方在發送fin封包後,最後等待對方的ack封包。當收到ack封包後,也即可以進入到closed可用狀态了。

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

1、 為什麼建立連接配接協定是三次握手,而關閉連接配接卻是四次握手呢?

這是因為服務端的listen狀态下的socket當收到syn封包的建連請求後,它可以把ack和syn(ack起應答作用,而syn起同步作用)放在一個封包裡來發送。但關閉連接配接時,當收到對方的fin封包通知時,它僅僅表示對方沒有資料發送給你了;但未必你所有的資料都全部發送給對方了,是以你可以未必會馬上會關閉socket,也即你可能還需要發送一些資料給對方之後,再發送fin封包給對方來表示你同意現在可以關閉連接配接了,是以它這裡的ack封包和fin封包多數情況下都是分開發送的。

2、 為什麼time_wait狀态還需要等2msl後才能傳回到closed狀态?

這是因為:雖然雙方都同意關閉連接配接了,而且握手的4個封包也都協調和發送完畢,按理可以直接回到closed狀态(就好比從syn_send狀态到establish狀态那樣);但是因為我們必須要假想網絡是不可靠的,你無法保證你最後發送的ack封包會一定被對方收到,是以對方處于last_ack狀态下的socket可能會因為逾時未收到ack封包,而重發fin封包,是以這個time_wait狀态的作用就是用來重發可能丢失的ack封包,并保證于此。

繼續閱讀