因此,我一直在加强对传统Java非阻塞API的理解。但是API的某些方面让我有点困惑,这些方面似乎迫使我手动处理背压。
例如,WritableByteChannel.write(ByteBuffer) 的文档如下:
除非另有说明,否则写入操作仅在写入所有请求的字节后才返回。根据其状态,某些类型的通道可能只写入部分字节或可能根本不写入。例如,非阻塞模式下的套接字通道无法写入超出套接字输出缓冲区可用字节数的任何其他字节。
现在,请考虑来自Ron Hitchens书籍《Java NIO》的以下示例。
在下面的代码片段中,Ron试图演示如何在非阻塞套接字应用程序中实现回声响应(为了上下文,这里有一个gist,包含完整的示例)。//Use the same byte buffer for all channels. A single thread is
//servicing all the channels, so no danger of concurrent access.
private ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
protected void readDataFromSocket(SelectionKey key) throws Exception {
var channel = (SocketChannel) key.channel();
buffer.clear(); //empty buffer
int count;
while((count = channel.read(buffer)) > 0) {
buffer.flip(); //make buffer readable
//Send data; don't assume it goes all at once
while(buffer.hasRemaining()) {
channel.write(buffer);
}
//WARNING: the above loop is evil. Because
//it's writing back to the same nonblocking
//channel it read the data from, this code
//can potentially spin in a busy loop. In real life
//you'd do something more useful than this.
buffer.clear(); //Empty buffer
}
if(count < 0) {
//Close channel on EOF, invalidates the key
channel.close();
}
}
我对while循环向输出通道流写入的操作感到困惑:
//Send data; don't assume it goes all at once
while(buffer.hasRemaining()) {
channel.write(buffer);
}
这让我很困惑,NIO如何帮助我?根据
WriteableByteChannel.write(ByteBuffer)
的描述,代码可能不会阻塞,因为如果输出通道的缓冲区已满,则此写操作不会阻塞,只是不写入任何内容并返回,缓冲区保持不变。但是在这个例子中,至少没有一种简单的方法可以在等待客户端处理这些字节的同时使用当前线程进行更有用的事情。就所有的事情而言,如果我只有一个线程,那么其他请求将在选择器中堆积,而这个while循环将“等待”客户端缓冲区打开一些空间时浪费宝贵的CPU周期。在输出通道中注册准备好状态似乎没有明显的方法。或者说有吗?所以,假设我不是在实现一个回声服务器,而是试图实现需要向客户端发送大量字节的响应(例如文件下载),并且假设客户端的带宽非常低或输出缓冲区与服务器缓冲区相比非常小,发送此文件可能需要很长时间。似乎我们需要在我们的慢速客户端正在消耗我们的文件下载字节时使用宝贵的CPU周期来服务其他客户端。
如果输入通道已准备好,但输出通道没有准备好,似乎该线程可能会浪费宝贵的CPU周期。虽然该线程没有被阻塞,但由于执行无关紧要的CPU密集型工作而变得毫无意义,就像被阻塞一样。
为了解决这个问题,Hitchens的解决方案是将此代码移动到一个新线程--这只是将问题转移到另一个地方--。然后我想知道,如果每次需要处理长时间运行的请求时都必须打开一个线程,那么在处理此类请求时,Java NIO如何比常规IO更好?
我还不清楚如何使用传统的Java NIO来处理这些情况。就像在这种情况下承诺用更少的资源做更多工作的承诺会被打破一样。如果我正在实现一个HTTP服务器,并且我无法知道为客户端提供响应需要多长时间呢?
看起来这个例子存在严重缺陷,解决方案的良好设计应考虑在输出通道上监听可读性,例如:
registerChannel(selector, channel, SelectionKey.OP_WRITE);
但是这个解决方案会是什么样子呢?我一直在尝试想出这个解决方案,但我不知道如何适当地实现它。
我不想寻找像Netty这样的其他框架,我的目的是了解核心Java API。我感激任何人能分享的见解,任何关于只使用传统的Java NIO来处理这种背压情况的正确方式的想法。