1. 程式人生 > >socket程式設計中的超時設定示例詳解之一

socket程式設計中的超時設定示例詳解之一

簡介:
在網路通訊中,如果socket處於阻塞模式執行時,就需要考慮處理socket操作超時的問題。

所謂阻塞模式,是指其完成指定的操作之前阻塞當前的程序或執行緒,直到操作有結果返回.
在我們直接呼叫socket操作函式時,如果不進行特意宣告的話,它們都是工作在阻塞模式的,
如 connect, send, recv等.

更多關於阻塞/非阻塞,同步/非同步的講解可以參見我總結的相關專題文章:
http://blog.chinaunix.net/uid-26000296-id-3754118.html
http://blog.chinaunix.net/uid-26000296-id-3755264.html
http://blog.chinaunix.net/uid-26000296-id-3755268.html




簡單分類的話,可以將超時處理分成兩類:
連線(connect)超時;
傳送(send), 接收(recv)超時;
下面對這兩類超時一一做示例講解

一、連線(connect)超時
基本實現流程如下:
1.建立socket;
2.將該socket設定為非阻塞(Non-blocking)模式;
3.呼叫connect();
   正常情況下,因為TCP三次握手需要一些時間;
   而非阻塞呼叫只要不能立即完成就會返回錯誤,
   所以這裡會返回EINPROGRESS,表示在建立連線但還沒有完成。
4. 在讀套介面描述符集(fd_set readset)和寫套介面描述符集(fd_set writeset)中
   將當前套介面置位(用FD_ZERO()、FD_SET()巨集);

   並設定好超時時間(struct timeval *timeout);

   如果你設定的超時時間大於75秒就沒有必要這樣做了,因為核心中對connect有超時限制就是75秒

5.使用select()檢查該socket描述符是否可寫(注意,是可寫);
6.根據select()返回的結果判斷connect()結果
     返回0表示connect超時;

7.將socket設定為阻塞模式;
   如果你的程式不需要用阻塞模式的,這步就省了,
   不過一般情況下都是用阻塞模式的,這樣也容易管理;

下面是示例程式碼的實現:
/*
 * \brief
 * tcp client
 */

#include 
#include 
#include 

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define TRUE  1
#define FALSE 0


#define SERVPORT 8080
#define MAXDATASIZE 100


#define TIMEOUT_TIME 10


int main(int argc, char *argv[])
{
  int sockfd, recvbytes;
  char rcv_buf[MAXDATASIZE]; /*./client 127.0.0.1 hello */
  char snd_buf[MAXDATASIZE];
  struct hostent *host;             /* struct hostent
                                     * {
                                     * char *h_name; // general hostname
                                     * char **h_aliases; // hostname's alias
                                     * int h_addrtype; // AF_INET
                                     * int h_length; 
                                     * char **h_addr_list;
                                     * };
                                     */
  struct sockaddr_in server_addr;


  /* */
  fd_set readset, writeset;
  struct timeval timeout;
  unsigned long ul = 1;
  int error = -1, len = sizeof(int);
  int bTimeoutFlag = FALSE;
  int ret;

  if (argc < 3)
  {
    printf("Usage:%s [ip address] [any string]\n", argv[0]);
    return 1;
  }
  *snd_buf = '\0';
  strcat(snd_buf, argv[2]);


  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
  {
    perror("socket:");
    exit(1);
  }


  server_addr.sin_family = AF_INET;
  server_addr.sin_port = htons(SERVPORT);
  inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
  memset(&(server_addr.sin_zero), 0, 8);


  /*Setting socket to non-blocking mode */
  ioctl(sockfd, FIONBIO, &ul);


  /* create the connection by socket 
   * means that connect "sockfd" to "server_addr"
   */
  if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1)
  {
    timeout.tv_sec  = TIMEOUT_TIME;
    timeout.tv_usec = 0;
    FD_ZERO(&writeset);
    FD_SET(sockfd, &writeset);


    ret = select(sockfd+1, NULL, &writeset, NULL, &timeout);
    if (ret == 0)              //返回0,代表在描述詞狀態改變已超過timeout時間
    {
      getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, (socklen_t *)&len);
      if (error == 0)          // 超時,可以做更進一步的處理,如重試等
      {
        bTimeoutFlag = TRUE;
        printf("Connect timeout!\n");
      }
      else
      {
        printf("Cann't connect to server!\n");
      }
      goto end;
    }
    else if ( ret == -1)      // 返回-1, 有錯誤發生,錯誤原因存在於errno
    {
      printf("Cann't connect to server!\n");
      goto end;
    }
    else                      // 成功,返回描述詞狀態已改變的個數
    {
      printf("Connect success!\n");
    }
  }
  else
  {
    printf("Connect success!\n");
    ret = TRUE;
  }


  ul = 0;
  ioctl(sockfd, FIONBIO, &ul); //重新將socket設定成阻塞模式


  /* 同步阻塞模式,未設定超時 */
  if (send(sockfd, snd_buf, sizeof(snd_buf), 0) == -1)
  {
    perror("send:");
    exit(1);
  }
  printf("send:%s\n", snd_buf);


  if ((recvbytes = recv(sockfd, rcv_buf, MAXDATASIZE, 0)) == -1)
  {
    perror("recv:");
    exit(1);
  }


  rcv_buf[recvbytes] = '\0';
  printf("recv:%s\n", rcv_buf);


end:
  close(sockfd);
  return 0;
}

以上程式碼,僅供參考,也是為初學者提供一些提示;
主要用到的幾個函式,select, ioctl, getsockopt都可以找到相關資料;

需要再說明的是:
1. 雖然用ioctl把套介面設定為非阻塞模式,但select本身是阻塞的,
   阻塞的時間就是其超時的時間,
   由呼叫select 的時候的最後一個引數timeval型別的變數指標指向的timeval結構變數來決定的,
   timeval結構由一個表示秒數的和一個表示微秒數(long型別)的成員組成,
   一般我們設定了秒數就行了,把微妙數設為0(注:1秒等於100萬微秒)。

2. select函式中另一個值得一提的引數就是上面我們用到的fd_set型別的變數指標。
   呼叫之前,這個變數裡面存了要用select來檢查的描述符,
   呼叫之後,針對上面的程式這裡面是可寫的描述符,我們可以用巨集FD_ISSET來檢查某個描述符是否在其中。
   由於這裡只有一個套介面描述符,就沒有使用FD_ISSET巨集來檢查呼叫select之後這個sockfd是否在set裡面,
   其實是需要加上這個判斷的。

   不過這裡用了getsockopt來檢查,這樣才可以判斷出這個套介面是否是真的連線上了,
   因為我們只是變相的用select來檢查它是否連線上了,

實際上select檢查的是它是否可寫,而對於可寫,是針對以下三種條件任一條件滿足時都表示可寫的:
1) 套介面傳送緩衝區中的可用控制元件位元組數大於等於套介面傳送緩衝區低潮限度的當前值,
    且或者
    i) 套介面已連線,或者
    ii)套介面不要求連線(UDP方式的)
2) 連線的寫這一半關閉。
3) 有一個套介面錯誤待處理。


這樣,我們就需要用getsockopt函式來獲取套介面目前的一些資訊來判斷是否真的是連線上了,
沒有連線上的時候還能給出發生了什麼錯誤,
當然我程式中並沒有標出那麼多狀態,只是簡單的表示可連線/不可連線。


下面談談對這個程式測試的結果。這裡針對3種情形做了測試:
1. 目標機器網路正常的情況
   可以連線到目標主機,並能成功以阻塞方式進行發包收包作業。
2.目標機器網路斷開的情況
   在等待設定的超時時間(上面的程式中為10秒)後,顯示目標主機不能連線。
3.程式執行前斷開目標機器網路,超時時間內,恢復目標機器的網路
   在恢復目標主機網路連線之前,程式一隻等待;
   恢復目標主機後,程式顯示連線目標主機成功,並能成功以阻塞方式進行發包收包作業。
   以上各種情況的測試結果表明,這種設定connect超時的方法是完全可行的。

九五,飛龍在天,利見大人。

【白話】九五,龍飛上了高空,利於出現德高勢隆的大人物。

《象》曰:“飛龍在天”,大人造也。

【白話】《象辭》說:“龍飛上了高空”,象徵德高勢隆的大人物一定會有所作為。