Считывание данных из сокета c таймаутом

Те, кто работал с tcp-ip сокетами, знают про всякие подводные камни, связаные с этим.
Приведу несколько из них.

Первый: данные в tcp-ip стеке могут появляться не все сразу, а кусками. Если клиент послал
нам данные с помощью одной функции send(), это совсем не значит, что они могут быть приняты одной
функцией recv().
Второй: если с отправителем данных что либо случилось, и он не смог данные полностью, то возможны зависоны на функции recv() (при использовании блокирующих сокетов).Аналогично для фукнции send().

Вашему вниманию представлены функции SendNBytes() и RecvNBytes(), которые избавлены от данных недостатков: используется функция select() для установки величины таймаута на однократный прием данных, и используется тот факт, что функция recv() в случае удачного приема возвращает количество байт, которые были реально приняты.

Вот, собственно, функции:
#define     _SOCKET_TIMEOUT    5000000     //величина таймаута в микросекундах

int SendNBytes(int nSocket, char *pBuffer, int iMessageLength)
{
  int     iRC         = 0;
  int     iSendStatus = 0;
  timeval SendTimeout;

  //установка величины таймаута
  SendTimeout.tv_sec  = 0;
  SendTimeout.tv_usec = _SOCKET_TIMEOUT;              

  fd_set fds;
  FD_ZERO(&fds);
  FD_SET(nSocket, &fds);


  //..до тех пор, пока нам нужно посылать данные...
  while(iMessageLength > 0)
  {
    iRC = select(0, NULL, &fds, NULL, &SendTimeout);

    //истек таймаут, возврат ошибки
    if(!iRC)
      return -1;

    //произошла ошибка
    if(iRC < 0)
      return WSAGetLastError();

    //отправить несколько байт
    iSendStatus = send(nSocket, pBuffer, iMessageLength, 0);   
                                                         
    //произошла ошибка в момент отправки данных
    if(iSendStatus < 0)
      return WSAGetLastError();
    else
    {
      //обновить буфер и счетчик
      iMessageLength -= iSendStatus;
      pBuffer += iSendStatus;
    }
  }

  return 0;
}


//функция приема данных
int ReceiveNBytes(int nSocket, char *pBuffer, int iStillToReceive)
{
  int     iRC               = 0;
  int     iReceiveStatus    = 0;
  timeval ReceiveTimeout;

  //установка величины таймаута
  fd_set fds;
  FD_ZERO(&fds);
  FD_SET(nSocket, &fds);

  ReceiveTimeout.tv_sec  = 0;
  ReceiveTimeout.tv_usec = _SOCKET_TIMEOUT;             // 500 ms

  //..пока данные не посланы..
  while(iStillToReceive > 0)
  {
    iRC = select(0, &fds, NULL, NULL, &ReceiveTimeout);
    
    //выход по таймауту
    if(!iRC)
      return -1;

    //произошла какая то ошибка
    if(iRC < 0)
      return WSAGetLastError();

    //прием нескольких байт
    iReceiveStatus = recv(nSocket, pBuffer, iStillToReceive, 0);

    //произошла ошибка в момент функции recv()
    if(iReceiveStatus < 0)
    {
      return WSAGetLastError();
    }
    else
    {
      //изменили величину счетчика и буфер
      iStillToReceive -= iReceiveStatus;
      pBuffer += iReceiveStatus;
    }
  }

  return 0;
}

как пользоваться:

Хорошим тоном считается посылка в начале основного сообщения информацию о длине сообщения.
Нужно договориться (разработчикам клиента и сервера), что, например, первые 2 или 4 байта в начале
сообщения будут длиной сообщения. Поэтому, полное сообщение из сокета можно получить примерно так:
TCHAR   *szMessage;
TCHAR   szLength[4];
int nLength;

if(RecvNBytes(nSocket, szLength, 2) != 0)   //приняли первые 2 байта - длину
{
  return FALSE;
}

nLength = atoi(szLength);       //преобразовали 2 байта в длину
szMessage = new TCHAR[szLength];

if(RecvNBytes(nSocket, szMessage, nLength) !=0) //приняли полное сообщение
{
  return FALSE;
}
 
Следующая статья »