WinSock2:使用recv和send在单独的线程中处理接受的传入连接

WinSock2: handling accepted incoming connections in separate threads with recv and send

本文关键字:处理 连接 线程 WinSock2 recv 使用 send 单独      更新时间:2023-10-16

我正在实现一个基于windows的web服务器,使用WinSock2处理来自客户端的多个特定HTTP请求。我有一个类来启动和停止我的服务器。它看起来像这样:

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个错误比前面提到的其他错误发生得更频繁。

它看起来像是一个套接字被关闭了,但我只在ProcessRequest中调用::recv::send之后才关闭它。我还认为这可能是一个与使用::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 OKnContent-Type: text/plainnnHello 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;
}

将指向REQUEST_CONTEXT的指针传递给每个新创建的线程。然而,这是一个自动变量,在堆栈上分配。因此,它的生命周期受限于它的范围。在你调用ResumeThread之后,它就结束了。

实际上发生的是REQUEST_CONTEXT在每次循环迭代中使用相同的内存。现在想象一下你在短时间内接受了2个连接。很可能在第一个线程开始执行时,它的REQUEST_CONTEXT已经被覆盖了。这样你实际上有两个线程服务于同一个套接字。

最简单的修复是动态分配REQUEST_CONTEXT。也就是说,在新接受时分配它,将它的指针传递给新线程。然后在线程终止时不要忘记delete

当创建线程来处理请求时,您将地址作为线程的参数提供给本地变量。一旦局部变量超出作用域,该指针的数据将不再有效。在线程中使用newdelete动态创建它