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

4
我的问题比以下场景更通用,尽管这个场景包含了所需的一切。它是关于Java和套接字编程的正确实践。
场景: - 一个服务器有多个客户端。使用非阻塞I/O。 - 该服务器是另一个服务器的客户端。使用阻塞I/O。 - 每种情况有两种情况:在一个情况下,所有数据都适合分配的字节缓冲区,而在第二种情况下,它们不适合(仅适用于一个迭代,而不是程序的生命周期)。
我找到的所有非阻塞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),程序就可以继续运行吗? 此处的文档没有说明当所有传入数据都适合缓冲区时的情况。当我使用阻塞I/O时,这也使得问题有点混乱:

但是,如果通道处于阻塞模式并且缓冲区中至少剩余一个字节,则保证此方法将阻塞,直到读取至少一个字节。

这是否意味着在这里(阻塞I/O)我无论如何都需要通过while循环进行read(大多数我找到的示例都是这样做的)?那写操作呢?

总之,我的问题是,从中间服务器(客户端到第二个服务器)的角度来看,在我的情况下读写数据的正确方法是什么?


如果(result.length() <= 0):这段代码甚至无法编译。没有“result”变量。此时正确的测试是if (socketChannel.read(buffer) < 0) - user207421
1个回答

1
如果您没有调用configureBlocking(false),那么是的,您需要使用循环来填充缓冲区。
然而...非阻塞套接字的重点不在于被任何一个套接字挂起等待,因为这会延迟读取所有其余选定键尚未由迭代器处理的套接字的时间。实际上,如果有十个客户端连接,其中一个连接速度较慢,其他人可能会经历相同的缓慢。
(选定键集的确切顺序未指定。查看选择器实现类的源代码是不明智的,因为缺乏任何顺序保证意味着Java SE的未来版本可以更改顺序。)
为了避免等待任何一个套接字,您不会尝试一次性填满缓冲区;相反,您只在每个select()调用中读取套接字能够给出的内容,而不会阻塞。

由于每个ByteBuffer可能包含部分数据序列,您需要为每个Socket记住每个ByteBuffer的进度。幸运的是,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)调用即可。但是,正如您所看到的,使用阻塞通道可能会限制主服务器使用非阻塞通道的优势。如果将与其他服务器的连接也设置为非阻塞通道,则可能值得一试。这会使事情变得更加复杂,因此我不会在这里讨论它,除非您想让我这样做。


我知道阻塞和非阻塞的区别和好处,我只是想知道在每种情况下读写的正确方式。缓冲区也可以很大并且适合所有内容,或者不行。我想要2(读/写)* 2(阻塞/非阻塞)* 2(缓冲区是否足够大)= 8种不同的情况。我需要循环吗(单个读取/单个写入,例如获取一个客户端请求/发送一个服务器请求)? - Spyridon Non Serviam
1
我试图回答那个问题。答案是否定的,你不应该使用循环。这样做会削弱非阻塞套接字通道的好处。阻塞写操作不需要循环,因为它将始终写入整个缓冲区。 - VGR

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