NIO的一些坑

转载自:http://blog.youkuaiyun.com/u013970991/article/details/52036223

public class EchoServer {  
    public static int DEFAULT_PORT = 7777;  

    public static void main(String[] args) throws IOException {  
        System.out.println("Listening for connection on port " + DEFAULT_PORT);  

        Selector selector = Selector.open();  
        initServer(selector);  

        while (true) {  
            selector.select();  

            for (Iterator<SelectionKey> itor = selector.selectedKeys().iterator(); itor.hasNext();) {  
                SelectionKey key = (SelectionKey) itor.next();  
                itor.remove();  
                try {  
                    if (key.isAcceptable()) {  
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();  
                        SocketChannel client = server.accept();  
                        System.out.println("Accepted connection from " + client);  
                        client.configureBlocking(false);  
                        SelectionKey clientKey = client.register(selector, SelectionKey.OP_WRITE|SelectionKey.OP_READ);  
                        ByteBuffer buffer = ByteBuffer.allocate(100);  
                        clientKey.attach(buffer);  
                    }  
                    if (key.isReadable()) {  
                        SocketChannel client = (SocketChannel) key.channel();  
                        ByteBuffer buffer = (ByteBuffer) key.attachment();  
                        client.read(buffer);  
                    }  
                    if (key.isWritable()) {  
                        // System.out.println("is writable...");  
                        SocketChannel client = (SocketChannel) key.channel();  
                        ByteBuffer buffer = (ByteBuffer) key.attachment();  
                        buffer.flip();  
                        client.write(buffer);  
                        buffer.compact();  
                    }  
                } catch (IOException e) {  
                    key.cancel();  
                    try { key.channel().close(); } catch (IOException ioe) { }  
                }  
            }  
        }  
    }  

    private static void initServer(Selector selector) throws IOException,  
            ClosedChannelException {  
        ServerSocketChannel serverChannel = ServerSocketChannel.open();  
        ServerSocket ss = serverChannel.socket();  
        ss.bind(new InetSocketAddress(DEFAULT_PORT));  
        serverChannel.configureBlocking(false);  
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);  
    }  
}  

上面的代码很典型,运行结果似乎也是正确的。但是如果top用看一下发现服务器进程CPU占用到95%以上,如果取消掉32行的注释,服务器会不断地输出”is writable…”,这是为什么呢?来分析当第一个客户端连接上时发生什么情况。

在连接之前,selector.select()处于阻塞状态。当阻塞时,内核会将这个进程调度至休眠状态,此时基本不耗CPU。当客户端发起一个连接时,服务器检测到客户端连接,selector.select()返回。selector.selectedKeys()返回已就绪的SelectionKey的集合,在这种情况下,它只包含一个key,也就是注册的SelectionKey.OP_ACCEPT。服务器开始运行if (key.isAcceptable())块的代码,server.accept()返回代码客户端连接的socket,之后在socket上注册OP_READ和OP_WRITE,表示当socket可读或者可写时就会通知selector。

接着服务器重新进入循环,尽管这时客户端还没有任何输入,但这时selector.select()不会阻塞,因为socket注册了写操作,而socket只要send buffer不满就可以写,刚开始send buffer为空,socket总是可以写,于是server.select()立即返回,包含SelectionKey.OP_WRITE。由于这个key可写,所以服务器会运行if (key.isReadable())块的代码,但是这时buffer为空,client.write(buffer)没有向socket写任何东西,立即返回0。

接着服务器又回到循环,由于客户端连接socket可以写,这时selector.select()会立即返回,然后又运行if (key.isReadable())块的代码,这样不断循环,服务器却实际没有干事情,却耗大量的CPU。


从上面的分析可以看出问题在于没有数据可写时就在socket上注册了OP_WRITE,导致服务器浪费大量CPU资源。一个简单方法就是不要在同一个socket同时注册多个操作。对于上面的EchoServer来说就是不要同时注册OP_READ和OP_WRITE,要么只注册OP_READ,要么只注册OP_WRITE。下面的EchoServer修正了以上的错误:

public static void main(String[] args) throws IOException {  
    System.out.println("Listening for connection on port " + DEFAULT_PORT);  

    Selector selector = Selector.open();  
    initServer(selector);  

    while (true) {  
        selector.select();  

        for (Iterator<SelectionKey> itor = selector.selectedKeys().iterator(); itor.hasNext();) {  
            SelectionKey key = (SelectionKey) itor.next();  
            itor.remove();  
            try {  
                if (key.isAcceptable()) {  
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();  
                    SocketChannel client = server.accept();  
                    System.out.println("Accepted connection from " + client);  
                    client.configureBlocking(false);  
                    SelectionKey clientKey = client.register(selector, SelectionKey.OP_READ);  
                    ByteBuffer buffer = ByteBuffer.allocate(100);  
                    clientKey.attach(buffer);  
                } else if (key.isReadable()) {  
                    SocketChannel client = (SocketChannel) key.channel();  
                    ByteBuffer buffer = (ByteBuffer) key.attachment();  
                    int n = client.read(buffer);  
                    if (n > 0) {  
                        buffer.flip();  
                        key.interestOps(SelectionKey.OP_WRITE);     // switch to OP_WRITE  
                    }  
                } else if (key.isWritable()) {  
                    System.out.println("is writable...");  
                    SocketChannel client = (SocketChannel) key.channel();  
                    ByteBuffer buffer = (ByteBuffer) key.attachment();  
                    client.write(buffer);  
                    if (buffer.remaining() == 0) {  // write finished, switch to OP_READ  
                        buffer.clear();  
                        key.interestOps(SelectionKey.OP_READ);  
                    }  
                }  
            } catch (IOException e) {  
                key.cancel();  
                try { key.channel().close(); } catch (IOException ioe) { }  
            }  
        }  
    }  
}  

上面的代码不够优雅,它将处理服务器Socket和客户连接Socket的代码搅在一起,对于简单的EchoServer这样做没什么问题,当服务器变得复杂,使用命令模式将它们分开变显得非常必要。首先创建一个接口来抽象对SelectionKey的处理。

interface Handler {  
    void execute(Selector selector, SelectionKey key);  
}  

再来看main函数:

public static void main(String[] args) throws IOException {  
    System.out.println("Listening for connection on port " + DEFAULT_PORT);  

    Selector selector = Selector.open();  
    initServer(selector);  

    while (true) {  
        selector.select();  

        for (Iterator<SelectionKey> itor = selector.selectedKeys().iterator(); itor.hasNext();) {  
            SelectionKey key = (SelectionKey) itor.next();  
            itor.remove();  
            Handler handler = (Handler) key.attachment();  
            handler.execute(selector, key);  
        }  
    }  
}  

private static void initServer(Selector selector) throws IOException,  
        ClosedChannelException {  
    ServerSocketChannel serverChannel = ServerSocketChannel.open();  
    ServerSocket ss = serverChannel.socket();  
    ss.bind(new InetSocketAddress(DEFAULT_PORT));  
    serverChannel.configureBlocking(false);  
    SelectionKey serverKey = serverChannel.register(selector, SelectionKey.OP_ACCEPT);  
    serverKey.attach(new ServerHandler());  
}  

main函数非常简单,迭代SelectionKey,对每个key的attachment为Handler,调用它的execute的方法,不用管它是服务器Socket还是客户Socket。注意initServer方法将serverKey附加了一个ServerHandler。下面是ServerHandler的代码:

class ServerHandler implements Handler {  
    public void execute(Selector selector, SelectionKey key) {  
        ServerSocketChannel server = (ServerSocketChannel) key.channel();  
        SocketChannel client = null;  
        try {  
            client = server.accept();  
            System.out.println("Accepted connection from " + client);  
        } catch (IOException e) {  
            e.printStackTrace();  
            return;  
        }  

        SelectionKey clientKey = null;  
        try {  
            client.configureBlocking(false);  
            clientKey = client.register(selector, SelectionKey.OP_READ);  
            clientKey.attach(new ClientHandler());  
        } catch (IOException e) {  
            if (clientKey != null)  
                clientKey.cancel();  
            try { client.close(); } catch (IOException ioe) { }  
        }  
    }  
}  

ServerHandler接收连接,为每个客户Socket注册OP_READ操作,返回的clientKey附加上ClientHandler。

class ClientHandler implements Handler {  
    private ByteBuffer buffer;  

    public ClientHandler() {  
        buffer = ByteBuffer.allocate(100);  
    }  

    public void execute(Selector selector, SelectionKey key) {  
        try {  
            if (key.isReadable()) {  
                readKey(selector, key);  
            } else if (key.isWritable()) {  
                writeKey(selector, key);  
            }  
        } catch (IOException e) {  
            key.cancel();  
            try { key.channel().close(); } catch (IOException ioe) { }  
        }  
    }  

    private void readKey(Selector selector, SelectionKey key) throws IOException {  
        SocketChannel client = (SocketChannel) key.channel();  
        int n = client.read(buffer);  
        if (n > 0) {  
            buffer.flip();  
            key.interestOps(SelectionKey.OP_WRITE);     // switch to OP_WRITE  
        }  
    }  

    private void writeKey(Selector selector, SelectionKey key) throws IOException {  
        // System.out.println("is writable...");  
        SocketChannel client = (SocketChannel) key.channel();  
        client.write(buffer);  
        if (buffer.remaining() == 0) {  // write finished, switch to OP_READ  
            buffer.clear();  
            key.interestOps(SelectionKey.OP_READ);  
        }  
    }  
}  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值