WinSock2: обработка принятых входящих соединений в отдельных потоках с помощью recv и send - PullRequest
0 голосов
/ 17 ноября 2011

Я реализую веб-сервер на базе Windows, обрабатывающий несколько конкретных HTTP-запросов от клиентов, использующих WinSock2 .У меня есть класс для запуска и остановки моего сервера.Это выглядит примерно так:

class CMyServer
{
  // Not related to this question methods and variables here
  // ...

public:

  SOCKET m_serverSocket;

  TLM_ERROR Start();
  TLM_ERROR Stop();
  static DWORD WINAPI ProcessRequest(LPVOID pInstance);
  static DWORD WINAPI Run(LPVOID pInstance);
}

, где TLM_ERROR - определение типа для перечисления ошибок моего сервера.

bool CMyServer::Start() метод запускает сервер, создавая прослушивание сокета на настроенном порту исоздание отдельного потока DWORD CMyServer::Run(LPVOID) для приема входящих соединений, как описано здесь:

  // Creating a socket
  m_serverSocket = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (m_serverSocket == INVALID_SOCKET)
    return TLM_ERROR_CANNOT_CREATE_SOCKET;

  // Socket address
  sockaddr_in serverSocketAddr;
  serverSocketAddr.sin_family = AF_INET;                                  // address format is host and port number  
  serverSocketAddr.sin_addr.S_un.S_addr = inet_addr(m_strHost.c_str());   // specifying host
  serverSocketAddr.sin_port = htons(m_nPort);                             // specifying port number

  // Binding the socket
  if (::bind(m_serverSocket, (SOCKADDR*)&serverSocketAddr, sizeof(serverSocketAddr)) == SOCKET_ERROR)
  {
    // Error during binding the socket
    ::closesocket(m_serverSocket);
    m_serverSocket = NULL;
    return TLM_ERROR_CANNOT_BIND_SOCKET;
  }

  // Starting to listen to requests
  int nBacklog = 20;
  if (::listen(m_serverSocket, nBacklog) == SOCKET_ERROR)
  {
    // Error listening on socket
    ::closesocket(m_serverSocket);
    m_serverSocket = NULL;
    return TLM_ERROR_CANNOT_LISTEN;
  }

  // Further initialization here...
  // ...

  // Creating server's main thread
  m_hManagerThread = ::CreateThread(NULL, 0, CTiledLayersManager::Run, (LPVOID)this, NULL, NULL);

Я использую ::accept(...) для ожидания входящих клиентских подключений в CMyServer::Run(LPVOID), и после принятия нового подключения я создаюотдельный поток CMyServer::ProcessRequest(LPVOID) для получения данных от клиента и отправки ответа, передавая сокет, возвращенный ::accept(...) как часть аргумента функции потока:

DWORD CMyServer::Run(LPVOID pInstance)
{
  CMyServer* pTLM = (CMyServer*)pInstance;

  // Initialization here...
  // ...

  bool bContinueRun = true;
  while (bContinueRun)
  {
    // Waiting for a client to connect
    SOCKADDR clientSocketAddr;                            // structure to store socket's address
    int nClientSocketSize = sizeof(clientSocketAddr);     // defining structure's length
    ZeroMemory(&clientSocketAddr, nClientSocketSize);     // cleaning the structure
    SOCKET connectionSocket = ::accept(pTLM->m_serverSocket, &clientSocketAddr, &nClientSocketSize);      // waiting for client's request
    if (connectionSocket != INVALID_SOCKET)
    {
      if (bContinueRun)
      {
        // Running a separate thread to handle this request
        REQUEST_CONTEXT rc;
        rc.pTLM = pTLM;
        rc.connectionSocket = connectionSocket;
        HANDLE hRequestThread = ::CreateThread(NULL, 0, CTiledLayersManager::ProcessRequest, (LPVOID)&rc, CREATE_SUSPENDED, NULL);

        // Storing created thread's handle to be able to close it later
        // ...

        // Starting suspended thread
        ::ResumeThread(hRequestThread);
      }
    }

    // Checking whether thread is signaled to stop...
    // ...
  }

  // Waiting for all child threads to over...
  // ...
}

Тестирование этой реализации вручную дает мне желаемые результаты.Но когда я отправляю несколько запросов, сгенерированных JMeter , я вижу, что некоторые из них не обрабатываются DWORD CMyServer::ProcessRequest(LPVOID) должным образом.Глядя на файл журнала, созданный с помощью ProcessRequest, я определяю 10038 код ошибки WinSock (что означает, что ::recv вызов был предпринят на не-сокете), код ошибки 10053 (программное обеспечение вызвало прерывание соединения) или даже код ошибки 10058 (невозможноотправить после отключения сокета).Но 10038-я ошибка возникает чаще, чем упоминалось другими.

Похоже, сокет как-то был закрыт, но я закрываю его только после вызова ::recv и ::send в ProcessRequest.Я также подумал, что это может быть проблема, связанная с использованием :: CreateThread вместо :: _ beginthreadex , но, как я понимаю, это может привести только к утечкам памяти.У меня не обнаружено утечек памяти описанным методом здесь , поэтому я сомневаюсь, что это причина.Более того, ::CreateThread возвращает дескриптор, который можно использовать в :: WaitForMultipleObjects для ожидания завершения потоков, и мне нужно, чтобы он правильно остановил мой сервер.

Могут ли ониошибки возникают из-за того, что клиент больше не хочет ждать ответа?У меня нет идей, и я буду вам благодарен, если вы скажете мне, что мне не хватает или я делаю / неправильно понимаю.Кстати, и мой сервер, и JMeter работают на локальном хосте.

Наконец, вот моя реализация метода ProcessRequest:

DWORD CMyServer::ProcessRequest(LPVOID pInstance)
{
  REQUEST_CONTEXT* pRC = (REQUEST_CONTEXT*)pInstance;
  CMyServer* pTLM = pRC->pTLM;
  SOCKET connectionSocket = pRC->connectionSocket;

  // Retrieving client's request
  const DWORD dwBuffLen = 1 << 15;
  char buffer[dwBuffLen];
  ZeroMemory(buffer, sizeof(buffer));
  if (::recv(connectionSocket, buffer, sizeof(buffer), NULL) == SOCKET_ERROR)
  {
    stringStream ss;
    ss << "Unable to receive client's request with the following error code " << ::WSAGetLastError() << ".";
    pTLM->Log(ss.str(), TLM_LOG_TYPE_ERROR);
    ::SetEvent(pTLM->m_hRequestCompleteEvent);
    return 0;
  }

  string str = "HTTP/1.1 200 OK\nContent-Type: text/plain\n\nHello World!";
  if (::send(connectionSocket, str.c_str(), str.length(), 0) == SOCKET_ERROR)
  {
    stringStream ss;
    ss << "Unable to send response to client with the following error code " << ::WSAGetLastError() << ".";
    pTLM->Log(ss.str(), TLM_LOG_TYPE_ERROR);
    ::SetEvent(pTLM->m_hRequestCompleteEvent);
    return 0;
  }

  ::closesocket(connectionSocket);
  connectionSocket = NULL;

  pTLM->Log(string("Request has been successfully handled."));
  ::SetEvent(pTLM->m_hRequestCompleteEvent);
  return 0;
}

Ответы [ 2 ]

3 голосов
/ 17 ноября 2011

При создании потока для обработки запросов вы указываете адрес локальной переменной в качестве аргумента потока.Данные этого указателя будут недействительными, как только локальная переменная выйдет из области видимости.Создайте его динамически с new и delete в потоке.

3 голосов
/ 17 ноября 2011

Вы передаете указатель на REQUEST_CONTEXT каждому вновь созданному потоку. Однако это автоматическая переменная, размещенная в стеке . Следовательно, его срок службы ограничен областью применения. Он заканчивается сразу после того, как вы позвоните ResumeThread.

Практически получается, что одна и та же память для REQUEST_CONTEXT используется в каждой итерации цикла. Теперь представьте, что вы принимаете 2 внутренних соединения за короткое время. Вполне вероятно, что в тот момент, когда первый поток начнет выполнение, его REQUEST_CONTEXT уже будет перезаписано. Таким образом, у вас есть два потока, обслуживающих один и тот же сокет.

Самым простым решением является динамическое выделение REQUEST_CONTEXT. То есть выделить его при новом принятии, передать его указатель новому потоку. Тогда во время завершения потока не забудьте delete it.

...