首页 > 解决方案 > 通过 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)并且我很高兴(至少从程序的角度来看)?

此处的文档未指定所有传入数据都适合缓冲区的情况。当我有阻塞 I/O 时,它也会让人有点困惑:

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

这是否意味着在这里(阻塞 I/O)我需要read通过while任何一种方式循环(我发现的大多数示例都是这样做的)?write手术呢?

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

标签: javaloopsnonblockingwritingsocketchannel

解决方案


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

但是……非阻塞套接字的要点是不要挂断等待任何一个套接字,因为这会延迟从您的迭代器尚未处理所选键的所有剩余套接字的读取。实际上,如果十个客户端连接,其中一个碰巧连接速度很慢,那么其他一些或所有客户端可能会遇到同样的速度。

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

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

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

您还想记住从每个套接字读取了多少字节。因此,现在您需要为每个套接字记住两件事:字节数和 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)调用,但是正如你所看到的,使用阻塞通道可能会限制主服务器使用非阻塞通道的优势。将与其他服务器的连接也设为非阻塞通道可能是值得的。这会使事情变得更复杂,所以我不会在这里解决它,除非你想让我这样做。


推荐阅读