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

46次阅读

共计 6714 个字符,预计需要花费 17 分钟才能阅读完成。

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,避免有限循环。

正文完
 0