美文网首页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