美文网首页Java
Java NIO(二)NIO入门实例

Java NIO(二)NIO入门实例

作者: 清雨季 | 来源:发表于2019-07-20 11:45 被阅读0次

    一 Java 源生api的核心概念

    1.1 Channel

    Channel:通道,BIO模型中使用流来传输数据,在NIO中使用Channel来传输数据,它是双向的,一个Channel即可以读也可以写(BIO中流是单向的,所以分了InputStream和OutputStream)。

    网络编程中用到的Channel只有ServerSocketChannel和SocketChannel,可以类比于ServerSocket和Socket,一个在服务端使用,一个在客户端使用。

    1.2 Buffer

    在BIO中,可以将数据直接写入到流当中,但是在NIO中,数据只能写入到缓冲区中,Buffer就是缓冲区。

    一个Buffer其实就是一个字节数组,最常用的Buffer是ByteBuffer。

    1.3 Selector

    在文章Java NIO(一)select 和 epoll底层实现原理中,我们提到select/epoll函数可以同时监听多个socket,在socket准备就绪后会返回。

    Java中使用Selector对象完成select/epoll函数的功能,Selector可以监控多个Channel,当某一个Channel有数据可以读取时,Selector会把它选取出来,然后交给线程处理。

    二 使用NIO构建一个简单的服务器

    基本的步骤如下:

    • 创建一个ServerSocketChannel对象,绑定端口并配置成非阻塞模式。
    • 创建一个Selector,并把第一步创建的ServerSocketChannel交给Selector监听。
    • 不停的从Selector获取准备就绪的Channel,当有客户端连接时,ServerSocketChannel就会被选取出来。
    • 从ServerSocketChannel获取SocketChannel对象,这个对象代表了客户端,需要使用它来与客户端读写数据。
    • 把SocketChannel也交给Selector监听,当SocketChannel可以读取数据时,也会被选取出来
    • SocketChannel被选取出来后,从中读取数据解析出请求,并写入返回数据。

    代码如下:

    public class NioServerTest {
        public static void main(String[] args) throws Exception{
            //创建一个ServerSocketChannel对象,绑定端口并配置成非阻塞模式。
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(8888), 1024);
            //下面这句必需要,否则ServerSocketChannel会使用阻塞的模式,那就不是NIO了
            serverSocketChannel.configureBlocking(false);
    
            //把ServerSocketChannel交给Selector监听
            Selector selector = Selector.open();
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    
            //循环,不断的从Selector中获取准备就绪的Channel,最开始的时候Selector只监听了一个ServerSocketChannel
            //但是后续有客户端连接时,会把客户端对应的Channel也交给Selector对象
            while (true) {
                //这一步会阻塞,当有Channel准备就绪时或者超过1000秒后会返回。
                selector.select(1000);
                //获取所有的准备就绪的Channel,SelectionKey中包含中Channel信息
                Set<SelectionKey> selectionKeySet = selector.selectedKeys();
                //遍历,每个Channel都可处理
                for (SelectionKey selectionKey : selectionKeySet) {
                    //如果Channel已经无效了,则跳过(如Channel已经关闭了)
                    if(!selectionKey.isValid()) {
                        continue;
                    }
                    //判断Channel具体的就绪事件,如果是有客户端连接,则建立连接
                    if (selectionKey.isAcceptable()) {
                        acceptConnection(selectionKey, selector);
                    }
                    //如果有客户端可以读取请求了,则读取请求然后返回数据
                    if (selectionKey.isReadable()) {
                        System.out.println(readFromSelectionKey(selectionKey));
                    }
                }
                //处理完成后把返回的Set清空,如果不清空下次还会再返回这些Key,导致重复处理
                selectionKeySet.clear();
            }
        }
    
        //客户端建立连接的方法
        private static void acceptConnection(SelectionKey selectionKey, Selector selector) throws Exception{
            System.err.println("accept connection...");
            //SelectionKey中包含选取出来的Channel的信息,我们可以从中获取,对于建立连接来说,只会有ServerSocketChannel可能触发,
            //因此这里可以把它转成ServerSocketChannel对象
            ServerSocketChannel ssc = ((ServerSocketChannel) selectionKey.channel());
            //获取客户端对应的SocketChannel,也需要配置成非阻塞模式
            SocketChannel socketChannel = ssc.accept();
            socketChannel.configureBlocking(false);
            //把客户端的Channel交给Selector监控,之后如果有数据可以读取时,会被select出来
            socketChannel.register(selector, SelectionKey.OP_READ);
        }
    
        //从客户端读取数据的庐江
        private static String readFromSelectionKey(SelectionKey selectionKey) throws Exception{
            //从SelectionKey中包含选取出来的Channel的信息把Channel获取出来
            SocketChannel socketChannel = ((SocketChannel) selectionKey.channel());
            //读取数据到ByteBuffer中
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            int len = socketChannel.read(byteBuffer);
            //如果读到-1,说明数据已经传输完成了,可以并闭
            if (len < 0) {
                socketChannel.close();
                selectionKey.cancel();
                return "";
            } else if(len == 0) { //什么都没读到
                return "";
            }
            byteBuffer.flip();
            doWrite(selectionKey, "Hello Nio");
            return new String(byteBuffer.array(), 0, len);
        }
    
        private static void doWrite(SelectionKey selectionKey, String responseMessage) throws Exception{
            System.err.println("Output message...");
            SocketChannel socketChannel = ((SocketChannel) selectionKey.channel());
            ByteBuffer byteBuffer = ByteBuffer.allocate(responseMessage.getBytes().length);
            byteBuffer.put(responseMessage.getBytes());
            byteBuffer.flip();
            socketChannel.write(byteBuffer);
        }
    }
    
    

    咋一看,有点多,其实很简单,两分钟就能看完了。另外,这只是个简单的示例代码,没有处理异常,关闭Channel和连接等操作,也没有考虑并发问题。

    使用浏览器访问http://localhost:8888/,可以在控制台上看到请求头信息就说明服务器搭建成功了(浏览器中是不会看到信息的,因为我们返回的数据就不是一个符合HTTP协议的数据)

    image.png

    把Channel交给Selector对象的代码中,有一个参数:

    socketChannel.register(selector, SelectionKey.OP_READ);
    

    这个参数表明了关注的内容,比如ServerSocketChannel,我们关注它什么时候需要建立连接,而SocketChannel我们关注它什么时候可以读写数据。

    三 构建NIO的简单客户端

    创建客户端的方式与服务端类似,这里不再重复,把步骤放在代码的注释中了。

    public class NioClient {
        public static void main(String[] args) throws Exception{
            //创建一个SocketChannel对象,配置成非阻塞模式
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
    
            //创建一个选择器,并把SocketChannel交给selector对象
            Selector selector = Selector.open();
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
    
            //发起建立连接的请求,这里会立即返回,当连接建立完成后,SocketChannel就会被选取出来
            socketChannel.connect(new InetSocketAddress("localhost", 8888));
    
            //遍历,不段的从Selector中选取出已经就绪的Channel,在这个例子中,Selector只监控了一个SocketChannel
            while (true) {
                selector.select(1000);
                Set<SelectionKey> selectionKeySet = selector.selectedKeys();
    
                for (SelectionKey selectionKey : selectionKeySet) {
                    if(!selectionKey.isValid()) {
                        continue;
                    }
                    //连接建立完成后的操作:直接发送请求数据
                    if (selectionKey.isConnectable()) {
                        if(socketChannel.finishConnect()) {
                            socketChannel.register(selector, SelectionKey.OP_READ);
                            doWriteRequest(((SocketChannel) selectionKey.channel()));
                        }
                    }
                    //如果当前已经可以读数据了,说明服务端已经响应完了,读取数据
                    if (selectionKey.isReadable()) {
                        doRead(selectionKey);
                    }
                }
                //最后同样要清除所有的Key
                selectionKeySet.removeAll(selectionKeySet);
            }
        }
    
        //发送请求
        private static void doWriteRequest(SocketChannel socketChannel) throws Exception{
            System.err.println("start connect...");
    
            //创建ByteBuffer对象,会放入数据
            ByteBuffer byteBuffer = ByteBuffer.allocate("Hello Server!".getBytes().length);
            byteBuffer.put("Hello Server!".getBytes());
            byteBuffer.flip();
            //写数据
            socketChannel.write(byteBuffer);
            if(!byteBuffer.hasRemaining()) {
                System.err.println("Send request success...");
            }
        }
    
        //读取服务端的响应
        private static void doRead(SelectionKey selectionKey) throws Exception{
            SocketChannel socketChannel = ((SocketChannel) selectionKey.channel());
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            int len = socketChannel.read(byteBuffer);
            System.out.println("Recv:" + new String(byteBuffer.array(), 0 ,len));
        }
    }
    

    先启动服务端,再启动客户端,可以在客户端看到服务端返回的数据:


    客户端打印信息

    也可以在服务端看到客户端的请求数据:


    服务端打印信息

    相关文章

      网友评论

        本文标题:Java NIO(二)NIO入门实例

        本文链接:https://www.haomeiwen.com/subject/ztorlctx.html