通过 SocketChannel 读取(写入)的正确方法

Proper way to read (write) through a SocketChannel

我的问题比下面的场景更通用,尽管这涵盖了所有需要的问题。 是为了Java和套接字编程的正确做法。

场景:

我找到的所有非阻塞示例 I/O 都是这样的:

InetAddress host = InetAddress.getByName("localhost");
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(host, 1234));
serverSocketChannel.register(selector, SelectionKey. OP_ACCEPT);
while (true) {
   if (selector.select() <= 0)
       continue;
   Set<SelectionKey> selectedKeys = selector.selectedKeys();
   Iterator<SelectionKey> iterator = selectedKeys.iterator();
   while (iterator.hasNext()) {
       key = (SelectionKey) iterator.next();
       iterator.remove();
       if (key.isAcceptable()) {
           SocketChannel socketChannel = serverSocketChannel.accept();
           socketChannel.configureBlocking(false);
           socketChannel.register(selector, SelectionKey.OP_READ);
           // Do something or do nothing
       }
       if (key.isReadable()) {
           SocketChannel socketChannel = (SocketChannel) key.channel();
           ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
           socketChannel.read(buffer);
           // Something something dark side
           if (result.length() <= 0) {
               sc.close();
               // Something else
           }
        }
    }

如果缓冲区足够大,read 是否会读取来自该特定客户端和该特定请求的所有传入数据,或者我是否需要将其放在 while 循环中?如果缓冲区不够大?

write 的情况下,我是否也只做 socketChannel.write(buffer) 就可以了(至少从程序的角度来看)?

文档 here 没有指定所有传入数据都适合缓冲区的情况。当我遇到阻塞 I/O:

时,它也会让人有点困惑

It is guaranteed, however, that if a channel is in blocking mode and there is at least one byte remaining in the buffer then this method will block until at least one byte is read.

这是否意味着在这里(阻塞 I/O)我需要 read 通过 while 循环(我发现的大多数例子都是这样做的)? write 操作怎么样?

所以,总结一下,我的问题是,从中间服务器(客户端到第二个服务器)的角度来看,在我的场景中读取和写入数据的正确方法是什么?

如果您没有调用 configureBlocking(false),那么是的,您将使用循环来填充缓冲区。

但是……非阻塞套接字的要点是不要在任何一个套接字上挂起等待,因为这会延迟所有剩余套接字的读取,这些套接字的选定键尚未被您的迭代器处理.实际上,如果有 10 个客户端连接,而其中一个恰好连接速度较慢,则其他部分或所有客户端可能会遇到同样的速度缓慢问题。

(未指定所选键集的确切顺序。查看 Selector 实现的源代码是不明智的 class,因为缺乏任何顺序保证意味着 JavaSE可以更改顺序。)

为了避免等待任何一个套接字,你不要试图一次性填满缓冲区;相反,您可以通过每次 select() 调用只读取一次来读取套接字可以为您提供的任何内容而不会阻塞。

由于每个 ByteBuffer 可能包含部分数据序列,因此您需要记住每个 ByteBuffer 对每个 Socket 的进度。幸运的是,SelectionKey 有一种方便的方法来做到这一点:attachment.

您还想记住从每个套接字读取了多少字节。所以,现在您需要为每个套接字记住两件事:字节数和 ByteBuffer。

class ReadState {
    final ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
    long count;
}

while (true) {

    // ...

        if (key.isAcceptable()) {
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(false);

            // Attach the read state for this socket
            // to its corresponding key.
            socketChannel.register(selector, SelectionKey.OP_READ,
                new ReadState());
        }

        if (key.isReadable()) {
            SocketChannel socketChannel = (SocketChannel) key.channel();
            ReadState state = (ReadState) key.attachment();
            ByteBuffer buffer = state.buffer;
            state.count += socketChannel.read(buffer);

            if (state.count >= DATA_LENGTH) {
                socketChannel.close();
            }

            buffer.flip();

            // Caution: The speed of this connection will limit your ability
            // to process the remaining selected keys!
            anotherServerChannel.write(buffer);
        }

对于阻塞通道,您可以只使用一个 write(buffer) 调用,但是如您所见,使用阻塞通道可能会限制主服务器使用非阻塞通道的优势。将与其他服务器的连接也设置为非阻塞通道可能是值得的。这会让事情变得更复杂,所以我不会在这里解决这个问题,除非你希望我这样做。