如何在NIO Server

时间:2015-06-19 10:37:06

标签: java nio

我有一个Java NIO服务器,它从客户端接收数据。

当信道准备好读取时,key.isReadable()返回true read(key)被调用来读取数据。

目前我正在为所有通道使用单个读缓冲区,在read()方法中,我清除缓冲区并读入它然后最终放入一个字节数组,假设我将一次性获取所有数据。

但是,假设我没有一次获得完整的数据(我在数据结尾处有特殊字符可以检测)。

问题:

那么现在如何使用通道保存这部分数据或如何处理部分读取问题?还是全球性的?

我在某处阅读附件不好。

2 个答案:

答案 0 :(得分:4)

看一下Reactor模式。以下是Doug Lea教授的基本实施链接:

http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf

这个想法是让单个反应器线程阻塞Selector调用。一旦准备好IO事件,reactor线程就会将事件分派给适当的处理程序。 在上面的pdf中,Reactor中有内部类Acceptor,它接受新的连接。

作者使用单个处理程序来读取和写入事件,并维护此处理程序的状态。我更喜欢使用单独的读取和写入处理程序,但这并不像“状态机”那样容易使用。每个事件只能有一个附件,因此需要某种注入来切换读/写处理程序。

要在后续读/写之间保持状态,您必须执行以下操作:

  • 介绍自定义协议,告诉您何时完整阅读邮件
  • 具有过时连接的超时或清除机制
  • 维护客户特定会话

所以,你可以这样做:

public class Reactor implements Runnable{

    Selector selector = Selector.open();

    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

    public Reactor(int port) throws IOException {

        serverSocketChannel.socket().bind(new InetSocketAddress(port));

        serverSocketChannel.configureBlocking(false);

        // let Reactor handle new connection events
        registerAcceptor();

    }

    /**
     * Registers Acceptor as handler for new client connections.
     * 
     * @throws ClosedChannelException
     */
    private void registerAcceptor() throws ClosedChannelException {


        SelectionKey selectionKey0 = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        selectionKey0.attach(new Acceptor());
    }

    @Override
    public void run(){

        while(!Thread.interrupted()){

            startReactorLoop();

        }

    }

    private void startReactorLoop() {

        try {

            // wait for new events for each registered or new clients
            selector.select();

            // get selection keys for pending events
            Set<SelectionKey> selectedKeys = selector.selectedKeys();

            Iterator<SelectionKey> selectedKeysIterator = selectedKeys.iterator();

            while (selectedKeysIterator.hasNext()) {

                // dispatch even to handler for the given key
                dispatch(selectedKeysIterator.next());

                // remove dispatched key from the collection
                selectedKeysIterator.remove();
            }

        } catch (IOException e) {
            // TODO add handling of this exception
            e.printStackTrace();
        }
    }

    private void dispatch(SelectionKey interestedEvent) {

        if (interestedEvent.attachment() != null) {

            EventHandler handler = (EventHandler) interestedEvent.attachment();

            handler.processEvent();
        }

    }

    private class Acceptor implements EventHandler {

        @Override
        public void processEvent() {

            try {

                SocketChannel clientConnection = serverSocketChannel.accept();

                if (clientConnection != null) {

                    registerChannel(clientConnection);

                }

            } catch (IOException e) {e.printStackTrace();}

        }
    /**
     *  Save Channel - key association - in Map perhaps.
     * This is required for subsequent/partial reads/writes
     */
    private void registerChannel(SocketChannel clientChannel) {


        // notify injection mechanism of new connection (so it can activate Read Handler)
}

处理完读取事件后,通知注入机制可以注入写入处理程序。

当新的Connection可用时,注入机制会创建一个新的读写处理程序实例。该注入机制根据需要切换处理程序。查找每个Channel的处理程序是通过方法`registerChannel()在连接Acceptance处填充的Map完成的。

读取和写入处理程序具有ByteBuffer个实例,并且由于每个Socket通道都有自己的处理程序对,因此您现在可以在部分读取和写入之间保持状态。

提高绩效的两个提示:

  • 尝试在接受连接时立即先读取。只有当您没有读取自定义协议中标题定义的足够数据时,才能为读取事件注册频道兴趣。

  • 尝试先写入而不注册写入事件的兴趣,并且只有在不写入所有数据的情况下,才能注册 写。

这将减少Selector唤醒次数。

这样的事情:

SocketChannel socketChannel;

byte[] outData;

final static int MAX_OUTPUT = 1024;

ByteBuffer output = ByteBuffer.allocate(MAX_OUTPUT);

// if message was not written fully
if (socketChannel.write(output) < messageSize()) {

// register interest for write event
SelectionKey selectionKey = socketChannel.register(selector, SelectionKey.OP_WRITE); 
        selectionKey.attach(writeHandler);
        selector.wakeup();

}

最后,应该有一个定时的Task来检查Connections是否还活着/ SelectionKeys被取消了。如果客户端断开TCP连接,服务器通常不会知道这一点。因此,内存中将有多个事件处理程序,作为附件绑定到过时的连接,这将导致内存泄漏。

这就是为什么你可能会说附件不好,但问题可以解决。

这里有两个简单的方法来解决这个问题:

  • 可以启用TCP keep alive

  • 定期任务可以检查给定频道上最后一个活动的时间戳。如果长时间闲置,服务器应终止连接。

答案 1 :(得分:1)

来自亚马逊的某个古老且非常不准确的NIO博客,其中错误地断言关键附件是内存泄漏。完整而彻底的BS。甚至不符合逻辑。这也是他断言你需要各种补充队列的那个。在大约13年的NIO中,从来没有这样做过。

您需要的是每个频道ByteBuffer 或可能是两个,一个用于读取,一个用于写入。您可以存储一个作为附件本身:如果您想要两个,或者要存储其他数据,您需要自己定义一个Session类,其中包含缓冲区以及您想要与该通道关联的任何其他内容,例如客户端凭据,并使用Session对象作为附件。

对于所有频道,你真的无法在NIO中获得一个缓冲区。