【问题标题】:How to handle Multiple Clients on Single Thread Server (with Sockets)如何在单线程服务器上处理多个客户端(使用套接字)
【发布时间】:2019-01-06 07:28:39
【问题描述】:

开始之前

请不要将此问题标记为重复问题。 我已经看过 SO 上关于使用套接字编程处理多个客户端的大量帖子。 大多数人只推荐多线程,但我试图避免这条路径,因为我读过它有一些问题:

  • 可扩展性差
  • 开销大/效率低/内存不足
  • 难以调试

我读过的任何专门讨论使用单线程的帖子要么有错误/没有答案,要么解释不清楚,比如人们说“只需使用select()!”


问题

我正在为服务器编写代码以处理多个(约 1000 个)客户端,但我无法弄清楚如何创建有效的解决方案。现在,我已经有了能够一次处理 1 个客户端的服务器代码。两者都是用 C 编写的;服务器在使用 WinSock 的 Windows 上,客户端在 Linux 上。 服务器和客户端使用send() 和阻止recv() 调用来回发送多个通信。编写这段代码非常简单,我不会在这里发布它,因为它很长,我怀疑有人会真正阅读所有这些。确切的实现也不重要,我只想谈谈高级伪代码。真正的困难是改变服务器来处理多个客户端。


已经有什么

我找到了一个关于如何创建处理多个客户端的 WinSock 服务器的不错的 PDF 教程,可以在此处找到它:WinSock Multiple Client Support。它在 C++ 中,但很容易转移到 C 中。

据我了解,服务器的运行方式如下:

while (running) {
    Sleep(1000);
    /* Accept all incoming clients and add to clientArray. */

    for (client in clientArray) {
        /* Interact with client */

        if (recv(...) == "disconnect") {
            /* Disconnect from client */
        }
    }
}
/* Close all connections. */

我看到使用这种方法的问题是您基本上一次只处理一个客户端(这很明显,因为您不是多线程),但是如果与每个客户端的交互呢?只需要发生一次?意思是,如果我只想来回发送一些数据并关闭连接怎么办? 此操作可能需要 5 秒到 5 分钟,具体取决于客户端连接的速度,因此其他客户端会在服务器处理客户端 5 分钟时阻止对服务器的 connect() 调用。 这似乎不是很有效,但也许最好的方法是实现一个等待队列,客户端被连接并被告知等待一段时间?我不确定,但这让我很好奇大型服务器如何同时向数千个客户端发送更新下载,以及我是否应该以同样的方式操作。

另外,如果服务器和客户端之间的send()recv() 需要一段时间(约1 分钟),是否有理由在主服务器循环中添加Sleep(1000) 调用?


我的要求

我想要的是一种在单线程服务器上处理多个客户端的解决方案,该解决方案对于大约 1000 个客户端来说足够高效。如果您告诉我 PDF 中的解决方案很好,那对我来说已经足够了(也许我只是太专注于效率。)

如果你觉得虐待狂,请给出答案,包括对实现的口头解释、服务器/客户端伪代码,甚至是服务器的小示例代码。)

提前致谢。

【问题讨论】:

  • Windows 特定:I/O 完成端口
  • 关于您的第一个要点,我很难相信单线程解决方案会比多线程解决方案更好地扩展。在您使用多线程的那一刻,您会立即从一个扩展到系统中的核心数量。如果您谈论的是可扩展性和数千个客户端,您可能会考虑像this 这样的服务器。在其中之一上,多线程将使您的性能提高接近 64 比 1。
  • P.S. 不要犯我几年前工作的公司所犯的错误。他们的扩展解决方案是使用单线程并为每个内核启动一个服务器实例。由于很多原因,这根本无法很好地扩展。
  • 我必须同意亚历克斯。如果您的服务器 (a) 基于 Windows,并且 (b) 无意偏离 (a),那么 I/O 完成端口就是猫须。坦率地说,它们是令人难以置信的,关于 SO 的几个答案,以及网络上记录的使用示例,都试图公正地传达它们真正的 omfg 有多棒。在谷歌上搜索“io 完成端口示例”值得您花半小时的时间。你不会后悔的;我保证。
  • 谢谢大家,我会查找 I/O 完成端口

标签: c performance sockets server client


【解决方案1】:

我已经编写了单线程套接字池处理。我使用非阻塞套接字并选择调用来处理所有发送、接收和错误。 我的班级将所有套接字保存在数组中,并为选择调用构建 3 个 fd 集。当发生某些事情时,它会检查读取或写入或错误列表并处理这些事件。 例如,连接期间的非阻塞客户端套接字可以触发写入或错误事件。如果发生错误事件,则连接失败。如果发生写入,则建立连接。 所有套接字都在读取 fd 集中。如果您创建服务器套接字(使用绑定和监听),新连接将触发读取事件。然后检查套接字是否是服务器套接字,然后调用接受新连接。如果读取操作是由常规套接字触发的,那么有一些字节要读取.. 只需使用缓冲区 arge 调用 recv 即可从该套接字中吸取所有数据。

SOCKET maxset=0;
fd_set rset, wset, eset;
FD_ZERO(&rset);
FD_ZERO(&wset);
FD_ZERO(&eset);

for (size_t i=0; i<readsockets.size(); i++)
{
    SOCKET s = readsockets[i]->s->GetSocket();
    FD_SET(s, &rset);
    if (s > maxset) maxset = s;
}
for (size_t i=0; i<writesockets.size(); i++)
{
    SOCKET s = writesockets[i]->s->GetSocket();
    FD_SET(s, &wset);
    if (s > maxset) maxset = s;
}
for (size_t i=0; i<errorsockets.size(); i++)
{
    SOCKET s = errorsockets[i]->s->GetSocket();
    FD_SET(s, &eset);
    if (s > maxset) maxset = s;
}


int ret = 0;
if (bBlocking)
    ret = select(maxset + 1, &rset, &wset, &eset, NULL/*&tv*/);
else
{
    timeval tv= {0, timeout*1000};
    ret = select(maxset + 1, &rset, &wset, &eset, &tv);
}

if (ret < 0)
{
    //int err = errno;
    NetworkCheckError();
    return false;
}
if (ret > 0) 
{
    // loop through eset and check each with FD_ISSET. if you find some socket it means connect failed
    // loop through wset and check each with FD_ISSET. If you find some socket check is there any pending connectin on that socket. If there is pending connection then that socket just got connected. Otherwise select just reported that some data has been sent and you can send more.
    // finally, loop through rset and check each with FD_ISSET. If you find some socket then check is this socket your server socket (bind and listen). If its server socket then this is signal new client want to connect.. just call accept and new connection is established. If this is not server socket, then just do recv on that socket to collect new data.
}

还有一些事情要处理...所有套接字都必须处于非阻塞模式。每个 send 或 recv 调用都将返回 -1(错误)但错误代码是 EWOULDBLOCK。那是正常的,忽略错误。如果 recv 返回 0,则断开此连接。如果发送返回 0 个字节,则内部缓冲区已满。 您需要编写额外的代码来序列化和解析数据。例如,在 recv 之后,消息可能不完整(取决于消息大小),因此可能需要多次调用 recv 才能接收到完整消息。有时,如果消息很短,recv 调用可以在缓冲区中传递多条消息。所以,你需要编写好的解析器或设计好的协议,易于解析。

【讨论】:

    【解决方案2】:

    首先,关于单线程方法:我认为这是个坏主意,因为您的服务器处理能力受到单处理器内核性能的限制。但除此之外,它会在一定程度上起作用。

    现在关于多客户端问题。我建议在他们的编译例程中使用WSASendWSARecv。如果需要,它也可以扩展到多个线程。

    服务器核心看起来像这样:

    struct SocketData {
        ::SOCKET socket;
        ::WSAOVERLAPPED overlapped;
        ::WSABUF bufferRef;
        char buf [1024];
        // other client-related data
        
        SocketData (void) {
            overlapped->hEvent = (HANDLE) this;
            bufferRef->buf = buf;
            bufferRef->len = sizeof (buf);
            // ...
            }
        };
    
    void OnRecv (
        DWORD dwError,
        DWORD cbTransferred,
        LPWSAOVERLAPPED lpOverlapped,
        DWORD dwFlags) {
        auto data = (SocketData*) lpOverlapped->hEvent;
        if (dwError || !cbTransferred) {
             ::closesocket (data->socket);
             delete data;
             return;
             }
        // process received data
        // ...
        }
    
    // same for OnSend
    
    void main (void) {
        // init and start async listener
        ::SOCKET serverSocket = ::socket (...);
        HANDLE hAccept = ::CreateEvent (nullptr, 0, 0, nullptr);
        ::WSAEventSelect (serverSocket, FD_ACCEPT, hAccept);
        ::bind (serverSocket, ...);
        ::listen (serverSocket, ...);
        // main loop
        for (;;) {
            int r = ::WaitForSingleObjectEx (hAccept, INFINITE, 1);
            if (r == WAIT_IO_COMPLETION)
                 continue;
            // accept processing
            auto data = new SocketData ();
            data->socket = ::accept (serverSocket, ...);
            // detach new socket from hAccept event
            ::WSAEventSelect (data->socket, 0, nullptr);
            // recv first data from client
            ::WSARecv (
                data->socket,
                &data->bufferRef,
                1,
                nullptr,
                0,
                &data->overlapped,
                &OnRecv);
            }
        }
    

    要点:

    • 在主循环中等待(WaitForSingleObjectExWaitForMultipleObjectsEx 等)必须是可报警的;
    • 大部分数据处理在OnSend/OnRecv完成;
    • 所有处理都必须在不阻塞OnSend/OnRecv 中的 API 的情况下完成;
    • 对于基于事件的处理事件,必须在主循环中等待。

    OnRecv 将为每个处理的传入数据包调用。 OnSend 将为每个已处理的传出数据包调用。请记住:您要求发送/接收的数据量与数据包中实际处理的数据量不同。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-04-28
      • 2012-09-17
      • 2023-04-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-07-28
      相关资源
      最近更新 更多