0

我正在用 c++ 构建一个聊天服务器(允许用户之间的私人消息)......这对我来说是一个挑战,而且我已经遇到了一个死点......我不知道什么可能会更好。

顺便说一句:我对 C++ 并不陌生;这就是我想要挑战的原因......所以如果有其他最佳方式,多线程等......请告诉我。

选项 A

我有一个 c++ 应用程序正在运行,它有一个套接字数组,在每个循环(我猜是 1 秒循环)中读取所有输入(循环通过所有套接字)并将其存储到 DB(需要一个日志),然后,再次循环遍历所有套接字,发送每个套接字中需要的内容。

优点:一个单一的过程,包含。易于开发。 缺点:我认为它几乎没有可扩展性,并且只有一个故障焦点……我的意思是,20k 套接字的性能如何?

选项 B

我有一个监听连接的 C++ 应用程序。当接收到连接时,它会派生一个处理该套接字的子进程......读取用户的所有输入并将其保存到数据库中。并在每个循环中检查数据库中所有必需的输出以写入套接字。

优点:如果守护进程足够小,每个套接字都有一个进程可能更具可扩展性。同时,如果一个进程失败,所有其他进程都保持在线。 缺点:更难开发。可能是为每个连接维护一个进程消耗了太多资源。

你认为最好的选择是什么?欢迎任何其他想法或建议:)

4

1 回答 1

2

正如评论中所提到的,还有一个额外的选择是使用select()or poll()(或者,如果您不介意使您的应用程序平台特定,类似epoll())。我个人建议poll(),因为我觉得它更方便,但我认为select()至少在某些版本的 Windows 上可用 - 我不知道在 Windows 上运行对你来说是否重要。

这里的基本方法是首先将所有套接字(包括一个监听套接字,如果你正在监听连接)添加到一个结构中,然后根据需要调用select()poll()。此调用将阻塞您的应用程序,直到至少一个套接字有一些数据要读取,然后您被唤醒并通过准备好读取的套接字,处理数据,然后再次跳回阻塞状态. 您通常在循环中执行此操作,例如:

while (running) {
    int rc = poll(...);
    // Handle active file descriptors here.
}

这是编写主要受 IO 限制的应用程序的好方法 - 即它花费更多时间处理网络(或磁盘)流量,而不是实际使用 CPU 处理数据。

正如评论中提到的,另一种方法是为每个连接分叉一个线程。这非常有效,您可以在每个线程中使用简单的阻塞 IO 来读取和写入该连接。出于几个原因,我个人建议不要使用这种方法,其中大部分主要是个人喜好。

首先,处理需要一次写入大量数据的连接很繁琐。套接字不能保证一次写入所有待处理的数据(即它发送的数量可能不是您请求的全部数量)。在这种情况下,您必须在本地缓冲待处理的数据并等待套接字中有空间发送它。这意味着在任何给定时间,您可能正在等待两个条件 - 套接字已准备好发送,或者套接字已准备好读取。当然,您可以避免从套接字读取,直到所有待处理的数据都发送完毕,但这会在处理数据时引入延迟。或者,您可以使用select()poll()就在那个连接上 - 但如果是这样,为什么还要使用线程,只需以这种方式处理所有连接。您还可以为每个连接使用两个线程,一个用于读取,一个用于写入,如果您不确定是否始终可以在一次调用中发送所有消息,这可能是最好的方法,尽管这会使您需要的线程数量翻倍这可能会使您的代码更复杂并略微增加资源使用量。

其次,如果您计划处理许多连接或高连接周转率,那么线程对系统的负载要比使用select()或朋友多一些。在大多数情况下,这并不是什么大问题,但对于大型应用程序来说却是一个因素。这可能不是一个实际问题,除非您正在编写类似于每秒处理数百个请求的网络服务器,但我认为值得提及以供参考。如果您正在编写这种规模的东西,那么您最终可能会使用混合方法,在这种方法中,您将进程、线程和非阻塞 IO 的某些组合相互叠加。

第三,一些程序员发现线程处理起来很复杂。您需要非常小心地使所有共享数据结构成为线程安全的,无论是使用排他锁定(互斥锁)还是使用为您执行此操作的其他库代码。有很多示例和库可以帮助您解决这个问题,但我只是指出需要注意 - 多线程编码是否适合您是一个品味问题。忘记锁定某些东西并让您的代码在测试中正常工作相对容易,因为线程不会碰巧与该数据结构竞争,然后在现实世界中较高负载下发生这种情况时会发现难以诊断的问题。小心谨慎,编写健壮的多线程代码并不难,我不反对(尽管意见不一)),但您应该注意所需的护理。在某种程度上,这适用于编写任何软件,当然,这只是程度问题。

除了这些问题,线程对于许多应用程序来说是一种相当合理的方法,而且有些人似乎发现它们比非阻塞 IO 更容易处理select()

至于您的方法,A 可以工作,但会浪费 CPU,因为无论是否有实际有用的工作要做,您都必须每秒醒来。此外,您在处理消息时会延迟一秒钟,这可能会让聊天服务器感到恼火。一般来说,我会建议类似select()的方法比这更好。

选项 B 可以工作,尽管当您想在连接之间发送消息时,您将不得不使用管道之类的东西在进程之间进行通信,这有点痛苦。您最终将不得不等待传入管道(用于发送数据)和套接字(用于接收数据),因此您最终会遇到同样的问题,不得不等待两个文件句柄,例如select()或线程。真的,正如其他人所说,线程是分别处理每个连接的正确方法。单独的进程也比线程更昂贵的资源(尽管在 Linux 等平台上,写时复制方法fork()意味着它实际上并不算太糟糕)。

对于只有几十个连接的小型应用程序,在线程和进程之间进行技术选择并不多,这在很大程度上取决于哪种风格更能吸引你。我个人会使用非阻塞 IO(有些人称之为异步 IO,但这不是我使用术语的方式)并且我已经编写了很多执行此操作的代码以及许多多线程代码,但它仍然是真的只是我个人的意见。

最后,如果您想编写可移植的非阻塞 IO 循环,我强烈建议您研究libev(或者可能是libevent,但我个人认为前者更易于使用且性能更高)。这些库在不同的平台上使用不同的原语,select()因此poll()您的代码可以保持不变,并且它们还倾向于提供更方便的接口。

如果您对此还有任何疑问,请随时提问。

于 2013-04-27T11:12:19.453 回答