多用户聊天服务器c++

Multiuser chat server c++

本文关键字:c++ 服务器 聊天 多用户      更新时间:2023-10-16

我正在用c++构建一个聊天服务器(允许用户之间的私人消息)…对我来说是个挑战,我遇到瓶颈了…我不知道哪一个更好。

顺便说一下:我对c++几乎不陌生;这就是我想要挑战的原因……所以,如果有其他的最佳方式,多线程等…请让我知道。

选择

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

优点:一个单独的过程,包含。易于开发。缺点:我认为它很难扩展,而且失败的单一焦点…我的意思是,2万个插槽的性能如何?

选项B

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

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

你认为哪个选项是最好的?欢迎有任何其他想法或建议:)

正如评论中提到的,还有另外一种选择,那就是使用select()poll()(或者,如果您不介意让您的应用程序特定于平台,也可以使用epoll())。就我个人而言,我建议poll(),因为我发现它更方便,但我认为只有select()在至少一些版本的Windows上可用-我不知道在Windows上运行对你是否重要。

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

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

这是编写主要是io绑定的应用程序的好方法-也就是说,它花费更多的时间来处理网络(或磁盘)流量,而不是实际用CPU处理数据。

正如在评论中提到的,另一种方法是为每个连接派生一个线程。这是非常有效的,您可以在每个线程中使用简单的阻塞IO来读写该连接。就我个人而言,我反对这种方法有几个原因,其中大部分是个人偏好。

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

其次,如果您计划处理许多连接,或者高连接周转率,那么与使用select()或友线程相比,线程对系统的负载更大。在大多数情况下,这不是一个特别大的问题,但对于较大的应用程序来说,这是一个因素。这可能不是一个实际问题,除非你正在编写一个像web服务器一样的东西,每秒处理数百个请求,但我认为这是相关的,以供参考。如果你正在编写这种规模的东西,你可能最终会使用混合方法,在这种方法中,你将进程,线程和非阻塞IO的一些组合复用在彼此之上。

第三,一些程序员发现线程很难处理。您需要非常小心地使所有共享数据结构都是线程安全的,要么使用独占锁(互斥锁),要么使用其他人的库代码来为您实现这一点。有很多示例和库可以帮助您解决这个问题,但我只是指出需要注意——多线程编码是否适合您是一个个人喜好的问题。相对容易忘记锁定某些内容,并让代码在测试中正常工作,因为线程不会碰巧使用该数据结构,然后在现实世界中更高负载下发生这种情况时发现难以诊断的问题。只要谨慎和有纪律,编写健壮的多线程代码并不难,我对此并不反对(尽管意见不一),但是您应该意识到需要谨慎。在某种程度上,这适用于编写任何软件,当然,这只是一个程度的问题。

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

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

选项B可以工作,尽管当你想在连接之间发送消息时,你将不得不使用管道之类的东西在进程之间进行通信,这有点麻烦。您最终将不得不等待传入管道(用于发送数据)以及套接字(用于接收数据),因此您最终将有效地解决相同的问题,必须等待两个文件句柄,例如select()或线程。实际上,正如其他人所说,线程是单独处理每个连接的正确方法。单独的进程占用的资源也比线程多一点(尽管在Linux这样的平台上,对fork()采用的copy-on-write方法实际上并不是太糟糕)。

对于只有几十个连接的小型应用程序来说,在线程和进程之间没有太多的技术选择,这主要取决于哪种风格更吸引您。我个人会使用非阻塞IO(有些人称之为异步IO,但这不是我如何使用这个术语),我已经写了相当多的代码,以及许多多线程代码,但这仍然只是我的个人意见。

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

如果你有任何问题,请随时提问。