Java NIO中使用SocketChannel.write()时的线程问题

3

有时候,通过SocketChannel.write()发送大量数据时,底层TCP缓冲区会被填满,我必须不断重试write(),直到所有数据都被发送。

因此,我可能会有以下代码:

public void send(ByteBuffer bb, SocketChannel sc){
   sc.write(bb);
   while (bb.remaining()>0){
      Thread.sleep(10);
      sc.write(bb);          
   }
}

问题在于大型ByteBuffer和溢出的底层TCP缓冲区偶尔会导致此send()调用阻塞时间超出预期。在我的项目中,有数百个客户端同时连接,一个套接字连接引起的延迟可能会使整个系统陷入瘫痪,直到解决这个SocketChannel的一个延迟。当发生延迟时,它可能会引起其他项目领域的减速连锁反应,因此低延迟非常重要。
我需要一个解决方案,可以在需要多次调用SocketChannel.write()时,透明地处理这个TCP缓冲区溢出问题,而不会造成所有内容都被阻塞。我考虑将send()放入一个扩展线程的单独类中,以便它作为自己的线程运行,并且不会阻塞调用代码。但是,我担心为我维护的每个套接字连接创建一个线程所需的开销,尤其是当99%的情况下,SocketChannel.write()第一次就成功了,这意味着没有必要存在该线程。(换句话说,只有在使用while()循环的情况下,将send()放入单独的线程中才是必要的——仅在存在缓冲区问题的情况下,例如1%的时间)如果只有1%的缓冲区问题,我不需要为其他99%的send()调用产生线程开销。
希望这样解释清楚了……我真的需要一些建议。谢谢!
6个回答

1

我越是阅读关于Java NIO的内容,就越感到它让我毛骨悚然。不管怎样,我认为这篇文章可以解决你的问题...

http://weblogs.java.net/blog/2006/05/30/tricks-and-tips-nio-part-i-why-you-must-handle-opwrite

听起来这个人有比睡眠循环更优雅的解决方案。

而且我迅速得出结论,仅使用Java NIO可能太危险了。在我能做到的范围内,我想我可能会使用Apache MINA,它在Java NIO之上提供了一个不错的抽象层,并避免了一些小“惊喜”。


连接现在已经损坏。 - user207421

1
在Java NIO之前,您必须使用一个线程来处理每个套接字以获得良好的性能。这是所有基于套接字的应用程序的问题,不仅仅是Java。为了解决这个问题,所有操作系统都添加了对非阻塞IO的支持。Java NIO实现基于选择器。
请参阅权威的Java NIO书籍On Java文章以开始学习。但请注意,这是一个复杂的主题,它仍然会在您的代码中引入一些多线程问题。搜索“非阻塞NIO”以获取更多信息。

1

你不需要使用sleep(),因为write()方法要么立即返回,要么阻塞。 如果第一次写入失败,你可以将write()方法传递给一个executor。 另一个选择是创建一个小线程池来执行写操作。

然而,对于你来说最好的选择可能是使用Selector(正如之前建议的那样),这样你就知道何时套接字已准备好执行另一次写操作。


如果套接字是非阻塞的,且缓冲区已满,则 sleep 函数确实有一定作用,但原帖并未说明。 - Stefan L
它假设缓冲区至少需要10毫秒才能清除。个人认为,如果缓冲区不能立即清除,那么要么缓冲区太小,要么读取器太慢。我会关闭连接。 - Peter Lawrey
如果示例中的bytebuffer大于内核的套接字输出缓冲区,则代码效率低下,因为它可能比网络适配器写入更快,从而超过并休眠。在10ms的休眠期间,网络适配器可以向1Gb网络写入约1MB,远远超过典型的套接字输出缓冲区大小。如果bytebuffer只有几KB,那么读取器可能太慢了,这时稍微暂停一下是有意义的,使用OP_WRITE更复杂,但可以避免占用线程睡眠以及缓冲区不足。 - Stefan L
1
@StefanL 不,sleep没有任何意义。当select()可以准确告诉你的时候,代码猜测缓冲区清除需要多长时间是毫无意义的。例如,如果接收者没有读取,缓冲区将需要无限长的时间才能清除。 - user207421

0

对于数百个连接,您可能不需要使用NIO。老式的阻塞式套接字和线程也能胜任。

NIO使您可以为选择键注册关心OP_WRITE,并在有更多可写数据的空间时得到通知。


0

假设您已经使用Selector.select()创建循环来确定哪些套接字准备好进行I/O操作,那么有一些事情需要您完成。

  • 创建完套接字后,将其设置为非阻塞模式,即sc.configureBlocking(false);
  • 写入缓冲区(可能只是部分数据)并检查是否还有剩余内容。缓冲区本身会负责当前位置和剩余可用空间的管理。

类似于以下代码:

sc.write(bb);
if(sc.remaining() == 0)
   //we're done with this buffer, remove it from the select set if there's nothing else to send.
else
    //do other stuff/return to select loop
  • 摒弃你的睡眠循环

0

我现在也面临着同样的问题:
- 如果您有少量连接,但传输数据量很大,则只需创建一个线程池,并让写入操作阻塞等待写入线程。
- 如果您有很多连接,则可以使用完整的Java NIO,在接受(accept())套接字上注册OP_WRITE,然后等待选择器(selector)。

Orielly Java NIO书籍中有所有这些内容。
此外: http://www.exampledepot.com/egs/java.nio/NbServer.html?l=rel

在线研究表明,除非您有大量传入连接,否则NIO可能会过度杀伤。否则,如果只是几个大型传输,则只需使用写线程。它的响应速度可能更快。许多人遇到了NIO响应速度不够快的问题。由于您的写线程处于自己的阻塞状态,因此不会对您造成影响。


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