netty 原生NIO与Netty

希望看完没篇文章后,会 哦~~ 原来就个这呀~~~

1. java NIO

这里强调一点,netty 仅是在NIO 的基础上进行了优化,简化了API 的使用,解决了一些bug ,并没有从0到1的开发NIO; so , 一些NIO 的核心特性,我们还是要学习原生的;

javaNIO 有几个核心的对象需要掌握: 缓冲区(Buffer)、选择器(Selector)、通道(channel)

1.1 Buffer

在NIO 库中,所有数据都是用缓冲区处理的。

在读数据时,它是直接读到缓冲区;

在写数据时,它也是写入到缓冲区中;

在面向流I/O 系统中,所有数据都是直接写入或者将数据读取到Stream 对象中。

所有的缓冲区类型都继承于抽象类Buffer, 最常用的就是ByteBuffer ; 当然 java 基本数据类型都有对应的Buffer ;

基本操作代码

public class BufferDemo {

    //put/get
    public static void main(String args[]) throws Exception {
        //这用用的是文件IO处理
        String path = BufferDemo.class.getResource("/application.properties").getPath();
        FileInputStream fin = new FileInputStream(path);

        //创建文件的操作管道
        FileChannel fc = fin.getChannel();

        //分配一个10个大小缓冲区,说白了就是分配一个10个大小的byte数组
        ByteBuffer buffer = ByteBuffer.allocate(10);
        output("初始化", buffer);

        //先读一下
        fc.read(buffer);
        output("调用read()", buffer);

        //准备操作之前,先锁定操作范围
        buffer.flip();
        output("调用flip()", buffer);

        System.out.println("===============================================");
        //判断有没有可读数据
        while (buffer.remaining() > 0) {
            byte b = buffer.get();
            System.out.print(((char)b  + "  "));
            // System.out.println(".......");
           output("调用get()", buffer);
        }

        System.out.println("===============================================");

        output("调用get()", buffer);

        //可以理解为解锁
        buffer.clear();
        output("调用clear()", buffer);

        //最后把管道关闭
        fin.close();
    }

    //把这个缓冲里面实时状态给答应出来
    public static void output(String step, ByteBuffer buffer) {
        System.out.println(step + " : ");
        //容量,数组大小
        System.out.print("capacity: " + buffer.capacity() + ", ");
        //当前操作数据所在的位置,也可以叫做游标
        System.out.print("position: " + buffer.position() + ", ");
        //锁定值,flip,数据操作范围索引只能在position - limit 之间
        System.out.println("limit: " + buffer.limit());
        System.out.println();
    }
}
复制代码

运行结果:

初始化 : 
capacity: 10, position: 0, limit: 10

调用read() : 
capacity: 10, position: 5, limit: 10

调用flip() : 
capacity: 10, position: 0, limit: 5

===============================================
h  调用get() : 
capacity: 10, position: 1, limit: 5

e  调用get() : 
capacity: 10, position: 2, limit: 5

l  调用get() : 
capacity: 10, position: 3, limit: 5

l  调用get() : 
capacity: 10, position: 4, limit: 5

o  调用get() : 
capacity: 10, position: 5, limit: 5

===============================================
调用get() : 
capacity: 10, position: 5, limit: 5

调用clear() : 
capacity: 10, position: 0, limit: 10
复制代码

核心对象解释

  • capactity: 指定缓冲区的最大数据容量,即底层数组的大小;
  • position : 指下一个要被读取或者写入的位置;
  • limit : 指还有多少数据可以被读取 或者 写入 的位置;

位置关系: 0 <= position <= limit <= capacity

重点方法

// 反转这个缓冲区,开始读取操作;
public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
}
复制代码

1.2 选择器Selector

NIO 中非阻塞I/O 采用基于Reactor 模式的工作方式,I/O 调用不会被阻塞,采用的是将感兴趣的特定I/O 事件注册到Selector ; Selector 为操作系统底层提供的函数; 当事件发生时,Selector 可以告诉我们所发生的事件;

如下图:

当有读或者写 等任何注册时间发生时,可以从Selector 中获得相应的SelectionKey, 同时从SelectionKey 中可以找到与该事件发生关联的SelectableChannel ,以获取客户端发送过来的数据;

使用 NIO 中非阻塞 I/O 编写服务器处理程序,大体上可以分为下面三个步骤:

  • 向Selector 对象注册感兴趣的事件;
  • 从Selector 中获取感兴趣的事件;
  • 根据不同的事件进行相应的处理;

向Selector对象注册感兴趣的事件,示例代码如下:

/*
 * 注册事件
 */
private Selector getSelector() throws IOException {

    // 创建可选择通道,并配置为非阻塞模式
    ServerSocketChannel server = ServerSocketChannel.open();
    server.configureBlocking(false);
    
    // 绑定通道到指定端口
    ServerSocket socket = server.socket();
    InetSocketAddress address = new InetSocketAddress(port);
    socket.bind(address);
    
    // 创建 Selector 对象
    Selector sel = Selector.open();
    
    // 向 Selector 中注册感兴趣的事件
    server.register(sel, SelectionKey.OP_ACCEPT);
    return sel;

}
复制代码

从Selector 中获取感兴趣的事件,即开始监听,进入内部循环:

/*

* 开始监听

*/

public void listen() {

    System.out.println("listen on " + port);
    
    try {
        while(true) {
            
            // 该调用会阻塞,直到至少有一个事件发生
            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iter = keys.iterator();
            
            while (iter.hasNext()) {
                SelectionKey key = (SelectionKey) iter.next();
                iter.remove();
                
                // 处理相应的事件
                process(key);
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }

}
复制代码

在非阻塞I/O中,内部循环都是遵循这个方式;

首先调用select() 方法,该方法是阻塞的,直到有事件发生;

然后再使用selectKey() 方法获取发生事件的 SelectionKey, 在使用迭代器进行循环;

最后就是根据不同的事件,编写相应的处理逻辑;

/*

* 根据不同的事件做处理

*/

private void process(SelectionKey key) throws IOException{

    // 接收请求
    if (key.isAcceptable()) {
        ServerSocketChannel server = (ServerSocketChannel) key.channel();
        SocketChannel channel = server.accept();
        channel.configureBlocking(false);
        channel.register(selector, SelectionKey.OP_READ);
    
    }
    
    // 读信息
    else if (key.isReadable()) {
       // TODO
    }
    
    // 写事件
    else if (key.isWritable()) {
        // TODO
    }

}
复制代码

此处分别判断是接收请求、读数据 还是 写事件,分别作不同的处理;java1.4 中推出 NIO , 这是一个面向 块的I/O系统,系统以块的方式处理数据;

1.3 通道Channel

通道是一个对象,通过它可以读取和写入数据,当然了所有数据都通过Buffer对象来处理。

  • 我们永远不会将字节直接写入通道中,而是将数据写入包含一个或者多个字节的缓冲区;
  • 也同样不会从通道中读取数据,而是将数据读入缓冲区,再从缓冲区读取这个数据;

在NIO 中提供了多种通道对象,而所有的通道对象都实现了Channel 接口,它们之间的继承关系如下:

使用NIO读取数据

  • 从FileInputStream 获取Channel;
  • 创建Buffer;
  • 将数据从Channel 读取到Buffer 中;

下面是一个简单读取、写入示例:

读取:

public class DirectBuffer {
    static public void main(String args[]) throws Exception {
        //首先我们从磁盘上读取刚才我们写出的文件内容

        String filePath = DirectBuffer.class.getResource("/application.properties").getFile();

        FileInputStream fin = new FileInputStream(filePath);
        FileChannel fcin = fin.getChannel();


        //把刚刚读取的内容写入到一个新的文件中
        String outfile = filePath.replace("application.properties","application-copy.properties");
        FileOutputStream fout = new FileOutputStream(outfile);
        FileChannel fcout = fout.getChannel();


        // 使用 allocateDirect,而不是 allocate
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
        while (true) {
            buffer.clear();
            int r = fcin.read(buffer);
            if (r == -1) {
                break;
            }
            buffer.flip();
            fcout.write(buffer);
        }
    }
}
复制代码

写入:

public class FileOutputDemo {

    static private final byte message[] = "hello shiqi".getBytes(StandardCharsets.UTF_8);

    static public void main( String args[] ) throws Exception {


        String path = FileOutputDemo.class.getResource("/application.properties").getPath();

        String outfile = path.replace("application.properties","application-copy.properties");


        FileOutputStream fout = new FileOutputStream( outfile );

        FileChannel fc = fout.getChannel();

        ByteBuffer buffer = ByteBuffer.allocate( 1024 );

        for (int i=0; i<message.length; ++i) {
            buffer.put( message[i] );
        }
        buffer.flip();
        fc.write( buffer );

        fout.close();

    }
}
复制代码

IO多路复用

  • 由一个/一组专门的线程来处理所有的IO事件,并负责分发;
  • 事件驱动机制:事件到的时候去触发,而不是同步的去监视事件;
  • 线程通信:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都有意义。减少不必要的线程切换。

下面给我最简单的单线程 Reactor 模式的NIO 处理流程图:

2. Netty 与NIO

2.1 官方介绍

Netty 是一个异步、事件驱动的用来做高性能、高可靠性的网络应用框架。主要的优点有:

  • 框架设计优雅,底层模型随意切换适应不同的网络协议要求;
  • 提供很多标准的协议、安全、编码解码的支持;
  • 解决了很多NIO不易用的问题;
  • 社区更为活跃,在很多开源框架中使用,例如:Dubbo、RocketMq、Spark 等;

由官方的架构图,我们得出以下几点:

  • 核心 :零拷贝、通用API、可扩展的事件模型;
  • 传输服务:管道通信(不太清楚)、Http隧道、TCP和UDP;
  • 协议支持:支持市面上主流服务;

2.2 Netty 采用NIO而非AIO的理由

  • Netty 不看重windows 上的使用,在linux 系统上,AIO 的底层使用EPOLL, 没有很好的实现AIO; 在性能上没有明显的优势;
  • AIO 不够成熟(? 对不起,我们分了吧,我觉得你不够成熟;???);
  • AIO 是 proactor 模型,与Reactor 模型匹配不优好;

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享