2

我最近一直在编写带有非阻塞套接字的基于 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 个字节。在写入尝试失败后,我唯一的办法仍然是倒带缓冲区。

4

4 回答 4

2

当使用非阻塞 IO 时,通常是在低延迟或高吞吐量之后。

不要通过压缩来移动缓冲区内的数据,而是分配所需长度的缓冲区(通常适合一条消息,单独的消息头)。并在将内容完全写入套接字后处理它们。如果您不能接受 GC,您可以使用 2 倍增长大小的缓冲区池。

如果吞吐量是您主要关心的问题,那么不要尝试直接写入套接字,而是使用 Selector 注册套接字可写性,并在套接字可写时尝试写入。为此,您需要维护等待发送的缓冲区队列。保持注册套接字可写性,直到此队列为空。在这种情况下,您最好使用分散/收集类型的 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();
于 2013-01-10T08:13:07.653 回答
0

这与缓冲区的写入方式有关。如果您在多线程并发配置中写入缓冲区,则此问题非常常见。

如果你做类似的事情

ByteBuffer bb = HashMap.get(ByteBufferForXParameter);

假设有一个 100 字节的源 ByteBuffer,线程 1 正在写入 bb(来自同一个源),线程 2 也在尝试写入另一个缓冲区 bb1(但来自同一个源缓冲区),它们可能与位置冲突,所以当 thread1遍历循环一次,它写入了 0 到 10 个字节。Thread2 开始写入它可能会从 10 到 20 字节写入,依此类推..

如果缓冲区大小很小,则线程 1 可能会在线程 2 开始写入之前写入整个缓冲区。因此,当它尝试写入时,它将 bb1.hasRemaining() 设为 false 并退出循环!

于 2016-07-17T13:35:56.020 回答
-1

当输出套接字缓冲区中没有空间时,write(bb) 返回 0。您必须等待该缓冲区空闲时。第一个想法是 make Thread.sleep(little time),但这肯定比使用阻塞套接字更糟糕。

如果你不能使用阻塞套接字,那么你必须重构你的程序,使它由异步部分组成:一个方法只开始写入,然后当套接字输出缓冲区中有空闲空间并且可以写入更多数据时调用另一个方法,当你所有的缓冲区数据都发送完毕后,我们需要调用一个方法来写入另一个缓冲区等。

Java 提供了在可以写入(和读取)时获得通知的方法—— java.nio.channels.Selector(nio 1)和asynchronous channels(nio2,仅从 Java 7 开始可用)。异步编程很复杂,因为它还需要线程操作和同步,所以从头开始编写异步服务器不是初学者的任务。选择一个准备好的库开始。例子有:Netty——一个复杂的,有很多特性的;df4j - 一个用于异步计算的库,以 nio1 和 nio2 的接口为例(由我开发)。

于 2013-01-10T07:43:08.810 回答
-1

我们也面临同样的问题。在我们的例子中,看起来客户端很慢并且我们的缓冲区已满,而 SocketChannel.write 最终导致 EAGAIN。

接下来是因为上面提到的while循环,它进入忙等待,不断返回EAGAIN,并且作为一个副作用,给CPU增加了高负载。

Thread.sleep 是一个临时的解决方法,现在我们必须重构完整的代码来处理这种情况。

于 2016-03-03T16:58:42.707 回答