Java.nio与为每个套接字创建新线程的区别

8

我正在开发一个一对多的服务器客户端应用程序,这是一个小项目。

由于socket IO是阻塞的。我正在寻找解决方案。

有人能告诉我每个解决方案的优缺点吗?

  1. 使用java.nio
  2. 为每个连接的客户端新建一个单独的线程。

谢谢


为每个客户端使用一个线程?这不是每个客户端两个线程吗,一个用于读取,另一个用于写入?否则,在read()中线程被阻塞的情况下,您将无法执行任何操作。 - Sergei Tachenov
@Sergey,写入阻塞是真正的罪魁祸首,因为它无法被中断,在出现IO错误的情况下,线程会一直阻塞,直到操作系统认为它已经完成。读取线程可以通过SO_TIMEOUT进行管理。因此,每个套接字需要2个线程。 - bestsss
@bestsss,SO_TIMEOUT是否也影响write()?如果存在不良IO,则通常线程没有更好的事情可做,只能尝试写入。 - Sergei Tachenov
@Sergey,不,它没有。即使有中断写操作,您仍然无法知道写入了多少。这种设计真的很糟糕,因为会面对任意“坏”客户端每秒读取几个字节的情况。您甚至不能在写入期间停止线程。使用读线程时,您甚至可以使用available(但这可能会降低一个JNI调用的性能)。 - bestsss
5个回答

5

这两种方法都没有问题。如果您的客户数量有限,则第二种选择就足够了(并且可能会基于多核架构而繁荣),否则让java.nio管理您的资源可能会更有益。

请参见此问题,以及另一篇文章,或者考虑阅读这篇博客文章,它反对在大多数情况下使用java.nio。


4

单独的线程

  • 您可以使用简单的InputStream/OutputStream(或Reader/Writer)API,并将流包装在一起。在我的当前项目中,我正在使用一个堆栈:
  • MessageFormatter,一个自定义格式化类
  • PrintWriter
  • OutputStreamWriter
  • DebugOutputStream(自定义类,用于调试目的)
  • DeflatorOutputStream(实际上是一个支持刷新的自定义子类)
  • SSLSocket的OutputStream

在接收端也是相同的方式进行处理。这很舒适,因为您只需要处理程序逻辑中的顶层(其余大多是每个构造函数调用一次)。

  • 对于每个连接,您需要一个新的Thread(甚至是一对线程,具体取决于架构)。

(在我的项目中,我为每个连接都有一个MessageParser-Thread,然后将单独的工作分配给一个ThreadPool,这些工作可能会写入一个或多个打开的连接,而不仅仅是产生它们的那个连接。当然,写入是同步的。)

  • 每个线程需要相当多的堆栈空间,如果您在资源受限的机器上,则可能会出现问题。

对于短暂的连接,您实际上不希望每个连接都有一个新线程,而只需在ThreadPool上执行一个新的Runnable,因为构造新线程需要一点时间。(注意:使用JDK 19,我们可以获得廉价的虚拟线程。请使用这些线程,线程池由平台在后台隐藏。)

非阻塞IO

  • 如果您具有多层架构和多个转换,则必须自行安排。这是可能的:
    • 我的MessageFormatter可以写入CharBuffer以及Writer。
    • 对于Char-to-Byte格式化,请使用CharsetEncoder/CharsetDecoder(在CharBuffer和ByteBuffer之间传输数据)。
    • 对于压缩,我使用了Deflater/Inflater的包装类,这些类在两个ByteBuffer之间传输数据。
    • 对于加密,我使用了SSLEngine(每个连接一个),该引擎还使用ByteBuffer进行输入和输出。
    • 然后,写入SocketChannel(或从其读取)。

尽管如此,管理开销仍然相当麻烦,因为您必须跟踪数据所在的位置。(实际上,每个连接都有两个管道,还有一两个线程管理所有管道中的数据,等待选择器上的新数据(或在传出情况下等待新空间)。 (消息解析后的实际处理仍然在ThreadPool中产生的Runnables中进行。)

  • 您只需要少量的线程。这实际上是我尝试异步处理的原因。它可以工作(使用相同的同步客户端),但比我的多线程解决方案慢得多,因此我将其放回,直到我们的线程太多而内存不足为止。(到目前为止,同时连接的连接不是很多。)

1
NIO 胜出,我可以随时停止线程。学习 NIO 并不难,但正确使用缓冲区几乎从未有人解释过。我相信这是人们无法充分利用 NIO 的原因之一。
NIO 的其他部分是大多数开发者无法编写和使用状态机的普遍无能,所以他们最终会过度复制缓冲区。

0

NIO非常适合高性能应用,建议不要超过#cores个线程,乘以您的应用程序的IO因子。其中,IO因子是您的应用程序等待磁盘IO完成的百分比。

原因很简单,当您有#cores个worker时,每个worker可能会绑定到一个单独的CPU核心,并将其最大化利用。 而且,工作人员越多,则上下文切换就会越多,这正是您使用NIO的原因所在。

如果工人们需要等待IO,则可以处理其他请求,因此请使用比核心更多的工人以实现完全的CPU利用率。

如果您使用线程,您将获得以下优势:

  • 您可以在ThreadLocals中存储会话信息。
  • 您无需以其他方式管理会话信息。

1
java.nio并不一定更高效。线程是廉价的,而且您没有考虑SMT。 - Johan Sjöberg

0

我尝试过Apache MINA,它真的非常好。我强烈推荐它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接