c - 如何在单线程服务器上处理多个客户端(使用套接字)

标签 c performance sockets server client

开始之前

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

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

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


问题

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


已有的内容

我找到了一个很好的 PDF 教程,介绍如何创建处理多个客户端的 WinSock 服务器,可以在此处找到: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 分钟,具体取决于客户端连接的速度,因此当服务器处理一个连接时,其他客户端将阻塞对服务器的 connect() 调用客户端等待 5 分钟。这似乎不是很有效,但也许最好的方法是实现一个等待队列,客户端被连接并被告知等待一段时间?我不确定,但这让我好奇有多大的服务器同时向数千个客户端发送更新下载,以及我是否应该以同样的方式操作。

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


我要什么

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

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

提前致谢。

最佳答案

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

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

关于c - 如何在单线程服务器上处理多个客户端(使用套接字),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51587316/

相关文章:

将 ASCII 数字转换为其 ASCII 字符代码 - C(不打印!)

java - ANTLR3 C 目标错误生成 TreeParser : ( ASTTreeParser. stg 321:25:匿名模板有 0 个参数但映射到 1 个值)

java - 高效地遍历 HashMap 中的所有匹配键?

performance - 从工作表中删除命令按钮和行非常慢

c - 如何在C HTTP客户端程序中向文件写入HTTP请求

java - 将消息从 Java 服务器推送到 AIR 应用程序

php - 通过socket.io为广播 channel 授权laravel通行证

无法理解 sortedlinklist 函数

c - 如何在 asm 内联语句中请求通用寄存器?

ruby-on-rails - 寻找更快的 ActiveRecord 查询 (Ruby on Rails)