乐趣区

关于java:Netty网络编程NIO编程介绍

1.NIO 的基本概念和三大外围组件

2.NIO 的缓冲区(Buffer)介绍

3.NIO 的通道(Channel)介绍

4.NIO 的选择器(Selector)介绍

5.NIO 的原理剖析图

6.NIO 的编程案例

1.NIO 的基本概念和三大外围组件

1.1)NIO 的基本概念
在咱们学习过后面的 Netty 网络编程——Netty 的根本介绍与 BIO 当前,咱们来学习 NIO。
NIO 的概念:同步非阻塞

在 NIO 的模式中,所有都是非阻塞的

也就是说,客户端调用服务器端,仅仅能获取到目前可用的数据 ,如果 有数据则返回数据 ,如果 没有数据间接返回空 而不是放弃线程阻塞,非要等到服务器端解决实现再返回客户端。所以直至数据变得可读以前,该线程能够做其它的事件。

而且,在 NIO 模式中,一个线程是能够解决多个操作的
当一个客户端与服务器进行链接,这个 socket 连贯就会退出到一个数组中 ,隔一段时间遍历一次, 这样一个线程就能解决多个客户端的链接和数据了,不像之前的阻塞 IO 那样,一个连贯调配一个线程。

1.2)NIO 的三大外围组件

其实 NIO 是面向缓冲区,或者面向块编程的。 把数据读取到缓冲区 ,而后 让客户端线程从缓冲区读取数据,这就减少了处理过程中的灵活性。

NIO 有三大外围局部:Channel(通道),Buffer(缓冲区), Selector(选择器)。

咱们先来看一张图:

1.2.1)服务端和客户端进行连贯之后,通过 buffer(缓冲区) 进行数据的交互。
1.2.2)服务端和客户端进行连贯之后,通过各自的 channel(通道)进行操作缓冲区。
1.2.3)在一个 Channel 有事件收回后(读、写、连贯等等),selector 就会切换到对应的 channel 进行解决数据。
1.2.4)Selector 会依据不同的事件,在各个通道上进行切换
1.2.5)Buffer 就是一个内存块,底层是一个数组。
1.2.6)Buffer 能够读也能够写,须要调用 flip 办法进行切换。

2.NIO 的缓冲区(Buffer)介绍
缓冲区(Buffer):
缓冲区实质上是一个能够读写数据的内存块 ,能够了解成是 一个蕴含数字的容器对象,该对象提供了一组 api,能够让开发者轻松地应用内存块。同时缓冲区也设置了一些机制,可能跟踪和记录缓冲区的状态变动状况。

2.1)Buffer 类及其子类
在 Buffer 中,Buffer 是一个顶层的父类。

它领有各种类型的 buffer,比方 ByteBuffer, 是存储字节数组的缓冲区。
咱们能够得出 java 中的根本数据类型除了 boolean 以外,都有一个 Buffer 类型与之对应。最罕用的当然是 ByteBuffer。

2.2)Buffer 的重要属性
Buffer 定义了 四个属性来提供蕴含的数据元素的信息:

属性 形容
mark 标记
position 地位,下一个要被读写的元素的索引,每次读写缓冲区数据时值都会扭转,为下次读写做筹备。
limit 示意缓冲区以后的起点,不能对超过缓冲区极限的地位进行读写操作,极限是能够批改的。
capacity 容量,就是能包容的最大数据量,在缓冲区创立的时候就不能扭转。

咱们可能感觉 positionlimit的概念了解有点 不太容易辨别 ,其实 有以下的区别

当往 buffer 对象里 数据的时候,limit 始终等于 capacity, 示意 最多能往 buffer 对象里写的数据

当调用 buffer 对象的 flip() 办法后 ( 切换模式 ,改为 buffer 读模式),想从 buffer 对象读取数据的时候,position 会被置为 0,limit 会被设置为之前是 position,这个时候 limit 示意最多能从 Buffer 读多少数据。

2.3)Buffer 的 api

2.4)ByteBuffer 的 api
ByteBuffer 作为最罕用的 Buffer,其 api 的次要办法如下:

3.NIO 的通道(Channel)介绍
channel 代表 连贯 每个 client 连贯都会建设一个 channnel,channel 能够对 buffer 进行操作。

同时,channel 能够从 buffer 中读数据,也能够将数据写入 buffer。

罕用的 channel 类有 FileChannel、DatagramChannel、ServerSocketChannel 和 SocketChannel。

咱们能够先拿一个常见的 FileChannel(文件数据的读写)进行举例:

3.1)应用 FileChannel 进行本地文件写数据

咱们对一个本地文件进行写入操作。

public class NIOFileChannel01 {public static void main(String[] args) throws Exception {
        String msg = "hello,netty!";
        // 创立一个输入流
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt");

        // 通过 fileOutPutStream 取得对应的 CileChannel
        // 这个 fileChannel 实在类型是 FileChannelImpl
        FileChannel fileChannel = fileOutputStream.getChannel();

        // 创立缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        // 把数据放入缓冲区
        byteBuffer.put(msg.getBytes());

        // 对 byteBuffer 进行 flip flip 之后,就能够进行写操作
        byteBuffer.flip();

        // 将 byteBuffer 写入到 fileChannel
        fileChannel.write(byteBuffer);
        fileOutputStream.close();}

}

3.2)应用 FileChannel 进行本地文件读数据

@Slf4j
public class NIOFileChannel02 {public static void main(String[] args) throws Exception {
        // 创立一个输出
        FileInputStream fileInputStream = new FileInputStream("d:\\file01.txt");

        // 通过 fileInputStream 取得对应的 CileChannel
        // 这个 fileChannel 实在类型是 FileChannelImpl
        FileChannel fileChannel = fileInputStream.getChannel();

        // 创立缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        // 将 channel 的数据读入缓冲区
        fileChannel.read(byteBuffer);
        log.info(new String(byteBuffer.array()));

        fileInputStream.close();}

}

3.3)应用一个 Buffer 实现文件读取、写入

@Slf4j
public class NIOFileChannel03 {public static void main(String[] args) throws Exception {
        // 创立一个输出
        FileInputStream fileInputStream = new FileInputStream("d:\\file01.txt");
        FileChannel inputChannel = fileInputStream.getChannel();

        // 创立一个输入
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\file02.txt");
        FileChannel outputChannel = fileOutputStream.getChannel();

        // 创立一个 buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(512);

        while(true){byteBuffer.clear();
            // 把数据读入 buffer
            int read = inputChannel.read(byteBuffer);
            log.info("read="+read);
            if(read==-1){
                // 读完
                break;
            }
            // 转化为写
            byteBuffer.flip();
            outputChannel.write(byteBuffer);

        }

        // 敞开流
        fileInputStream.close();
        fileOutputStream.close();}

}

3.4)应用拷贝文件 transferFrom 办法

@Slf4j
public class NIOFileChannel04 {public static void main(String[] args) throws Exception {
        // 创立一个输出
        FileInputStream fileInputStream = new FileInputStream("d:\\file01.txt");
        FileChannel inputChannel = fileInputStream.getChannel();

        // 创立一个输入
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\file03.txt");
        FileChannel outputChannel = fileOutputStream.getChannel();

       // 应用 transferForm 实现拷贝
        outputChannel.transferFrom(inputChannel,0,inputChannel.size());

        // 敞开流
        fileInputStream.close();
        fileOutputStream.close();}

}

4.NIO 的选择器(Selector)介绍

咱们在学习 BIO 的时候,发现每多创立一个客户端连贯,都须要创立一个线程,那么 NIO 时如何办到 一个工作线程解决多了连贯 的呢?那就是Selector

Selector 能够 应用一个线程,解决多个客户端连贯。首先创立的连贯都会被注册到 selector 上 ,selector 可能检测多个注册的通道上是否有事件产生, 如果有事件产生,便获取事件而后针对每个事件进行相应的解决。这样就能够用一个单线程去治理多个通道,也就是一个线程治理多个链接申请。如图所示:

长处:
1)能够 不必为每一个连贯都创立一个线程 ,一个线程就能够解决 N 个客户端的链接和读写操作,从根本上解决了传统同步阻塞 IO,一链接一线程的模型。
2) 只有在连贯真正有事件产生的时候(事件驱动),才会进行读写 ,就大大减少了零碎开销。
3) 防止了多线程之间的上下文切换开销。
4)当 没有工作可用的时候,该线程就能够执行其它工作。

大抵罕用办法:

咱们会在上面进行应用。

5.NIO 的原理剖析图

6.NIO 的编程案例

服务器端:

@Slf4j
public class NIOServer {public static void main(String[] args) throws Exception {

        // 创立 ServerSocketChannel
        // 客户端连贯 通过 serverSocketChannel 获取 socketChannel,再和 selector 绑定
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        // 绑定一个端口,在服务器端监听
        serverSocketChannel.socket().bind(new InetSocketAddress(6677));
        // 设置为非阻塞
        serverSocketChannel.configureBlocking(false);

        // 取得一个 selector 对象
        Selector selector = Selector.open();
        // 把这个 channel 注册到 selector 上
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        log.info("注册后的 selectionKey 数量 ="+selector.keys().size());

        // 循环期待客户端连贯
        while(true){
            // 当没有连贯产生的时候
            if(selector.select(1000) == 0){log.info("服务器期待一秒,无连贯");
                continue;
            }
            // 当有链接产生的时候
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            log.info("selectionKeys 数量 ="+selectionKeys.size());

            // 遍历 Set 应用迭代器
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            while(iterator.hasNext()){
                // 失去 key
                SelectionKey key = iterator.next();
                // 依据 key 的对应通道产生的事件做相应解决
                // 产生连贯事件
                if(key.isAcceptable()){
                    // 如果是有新客户端连贯
                    // 通过该客户端生成 socketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    log.info("客户端连贯胜利,生成了一个 socketChannel"+socketChannel.hashCode());

                    // 将 socketChannel 设置为非阻塞
                    socketChannel.configureBlocking(false);

                    // 将 socketChannel 注册到 selector,关注事件为 read,同时关联一个 buffer
                    socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));

                    log.info("客户端连贯后,注册的 selectionKey 数量"+selector.keys().size());

                }
                // 产生读事件
                if(key.isReadable()){
                    // 通过 key 反向获取到对应 channel
                    SocketChannel channel = (SocketChannel)key.channel();

                    // 获取 key 绑定的 buffer
                    ByteBuffer buffer = (ByteBuffer)key.attachment();
                    channel.read(buffer);
                    log.info("客户端发送信息 ="+new String(buffer.array()));

                }
                iterator.remove();}

        }


    }

}

客户端:

@Slf4j
public class NIOClient {public static void main(String[] args) throws Exception {

        // 失去一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        // 设置非阻塞
        socketChannel.configureBlocking(false);
        // 提供服务器端的 ip 和端口
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6677);

        if(!socketChannel.connect(address)){while(!socketChannel.finishConnect()){log.info("因为连贯须要工夫,客户端不会阻塞,能够做其它工作..");
            }
        }

        String msg = "hello,netty!";

        ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
        // 发送数据,将 buffer 写入 channel
        socketChannel.write(buffer);
        System.in.read();}
}

流程:
5.1)创立 ServerSocketChannel,监听端口
5.2) 取得一个 selector 对象,把这个 channel 注册到这个 selector 上
5.3) 循环 select,查看是否有事件产生。
5.4)依据不同的事件做相应的解决。
5.5)移除 key,避免有限循环。

退出移动版