我最近一直在编写一个带有非阻塞套接字的基于Java NIO的服务器,而且我在编写数据方面遇到了一些问题。我现在知道,有些情况下,非阻塞写入无法写入ByteBuffer中的一些或所有字节。
我目前处理这种情况的方法是重写或压缩缓冲区,然后尝试在下一次选择迭代中再次发送它。这会导致显着的性能损失,我必须快速发送数据。
我尝试过使用类似的东西:
ByteBuffer bb = ...;
SocketChannel sc = ...;
while(bb.remaining() > 0) {
sc.write(bb);
}
但问题是它可能写入0个字节并仍然退出while循环。我不确定为什么,但似乎write() - 方法将达到ByteBuffer的限制,无论它是否实际发送了所有字节。
我使用这种写入方法的另一个问题是,即使我没有尝试阻塞写入,有时在高负载下也会导致缓冲区溢出异常。
我迫切需要一些关于如何正确执行阻塞写入的建议以及可能导致SocketChannel.write(ByteBuffer)溢出缓冲区的条件(如果在达到限制时它不会停止?)。
提前致谢。
编辑:我还没有找到sc.write(bb)将缓冲区中的位置设置为bb.limit()的原因,即使它写了0个字节。写入尝试失败后,我唯一的办法仍然是倒带缓冲区。
答案 0 :(得分:2)
使用非阻塞IO时,通常是在低延迟或高吞吐量之后。
不要通过压缩缓冲区来移动缓冲区内的数据,而是分配所需长度的缓冲区(通常为一条消息,分别为消息头)。在将内容完全写入套接字后将其丢弃。如果您不能接受GC,则可以使用2倍增长大小的缓冲池。
如果吞吐量是您主要关心的问题,那么不要尝试直接写入套接字,而是使用Selector注册套接字可写性,并尝试在套接字可写时写入。要实现此目的,您需要维护待发送的缓冲区队列。保持注册的套接字可写性,直到此队列变空。在这种情况下,您最好使用scatter/gather类型IO,因为它可以最大限度地减少应用程序中系统调用的数量。
write requester thread:
ioloop.submit(buffer[]{msgheaderbuf, msgbodybuf}, sock);
selector thread (ioloop):
submit(buffer[] bufs, socket sock):
queue.enqueue(bufs);
selector.register(sock, WRITABLE);
selector.wakeup();
如果延迟很重要,那么首先尝试直接写入套接字,并且只有在写入失败时才将数据排入队列并注册套接字可写性,如上所述。这将需要额外的互斥锁来保护套接字不被直接和从选择器线程中写入(由于可写事件触发)。
write requester thread:
ioloop.submit(buffer[]{msgheaderbuf, msgbodybuf}, sock);
selector thread (ioloop):
submit(buffer[] bufs, socket sock):
size = sock.write(bufs);
while (!bufs.empty() && !bufs[0].remaining()): bufs.pop_front();
if (bufs.empty()) return;
queue.enqueue(bufs);
selector.register(sock, WRITABLE);
selector.wakeup();
答案 1 :(得分:0)
write(bb)返回0。当缓冲区空闲时你必须等待。第一个想法是制作Thread.sleep(little time)
,但这肯定比使用阻塞套接字更糟糕。
如果你不能使用阻塞套接字,那么你必须重构你的程序,使它由异步部分组成:一个方法只开始写,然后当套接字输出缓冲区中有空闲空间和更多数据时调用其他方法可以写入,当发送所有缓冲区数据时,我们需要调用一个写入另一个缓冲区等的方法。
Java提供了在写入(和读取)时可以获得通知的方法 - java.nio.channels.Selector
(nio 1)和asynchronous channels
(nio2,仅在Java 7之后可用)。异步编程很复杂,因为它还需要线程操作和同步,因此从头开始编写异步服务器不是初学者的任务。选择一个准备好的库来开始。示例是:Netty - 复杂的,具有许多功能; df4j - 用于异步计算的库,以nio1和nio2为例(由我开发)。
答案 2 :(得分:0)
我们也面临同样的问题。在我们的例子中,看起来客户端很慢并且我们的缓冲区已满,而SocketChannel.write最终会导致EAGAIN。
接下来的事情是因为如上所述的while循环,它进入繁忙等待,并且不断地继续返回EAGAIN,并且作为副作用,对CPU施加高负荷。
Thread.sleep是一个临时的解决方法,现在我们必须重构完整的代码来处理这种情况。
答案 3 :(得分:0)
这与缓冲区的编写方式有关。如果要在多线程并发配置中编写缓冲区,则此问题非常常见。
如果您执行类似
的操作ByteBuffer bb = HashMap.get(ByteBufferForXParameter);
假设有一个100字节的源ByteBuffer,线程1正在写bb(来自同一个源),而线程2也试图写另一个缓冲区bb1(但来自相同的源缓冲区) 它们可能与位置冲突,因此当thread1迭代循环一次时,它会写入0到10个字节。 Thread2开始写入它可能写入10到20个字节,依此类推..
如果缓冲区大小很小,则在thread2可以开始写入之前,thread1可能会写入整个缓冲区。因此,当它尝试写入时,它会将bb1.hasRemaining()视为false并从循环中退出!