美文网首页
Netty基础知识之NIO

Netty基础知识之NIO

作者: 彭阿三 | 来源:发表于2020-10-29 13:35 被阅读0次

Netty基础知识之NIO

Netty是一款提供异步的、事件驱动的网络应用程序框架和工具,是基于NIO客户端、服务器端的编程框架。所以这里我们先以NIO和依赖相关的基础铺垫来进行剖析讲解,从而作为Netty学习之旅的一个开端。

NIO核心组件介绍

Channel

在NIO中,基本所有的IO操作都是从Channel开始的,Channel通过Buffer(缓冲区)进行读写操作。read()表示读取通道中数据到缓冲区,write()表示把缓冲区数据写入到通道。


image.png

Channel有好多实现类,这里有三个最常用:

  • SocketChannel:一个客户端发起TCP连接的Channel
  • ServerSocketChannel:一个服务端监听新连接的TCP Channel,对于每一个新的Client连接,都会建立一个对应的SocketChannel
  • FileChannel:从文件中读写数据
    其中SocketChannel和ServerSocketChannel是网络编程中最常用的,代码实现如下:
public class NioClient {
  public static void main(String[] args) {
      SocketChannel socketChannel = null;
      Selector selector = null;
      try {
          //1.创建socketChannel 通道   实例话
          socketChannel = SocketChannel.open();
          //2.设置socketChannel 线程模型
          socketChannel.configureBlocking(false);
          //3.创建selector选择器  实例话
          selector = Selector.open();
          //4.把channel 注册到selector
          //SelectionKey的四种属性 SelectionKey.OP_CONNECT —— 连接就绪事件,表示客户与服务器的连接已经建立成功
          socketChannel.register(selector, SelectionKey.OP_CONNECT);
          //5.建立连接监听端口
          socketChannel.connect(new InetSocketAddress(9000));

          while (true){
              //等待请求(阻塞方法)
              selector.select();
              //获取到所有事件进行遍历
              Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    //连接事件
                    if (key.isConnectable()){
                        socketChannel = (SocketChannel) key.channel();
                        if (socketChannel.isConnectionPending()){
                            socketChannel.finishConnect();
                            System.out.println("建立连接完成");
                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                            buffer.put("你好,server".getBytes());
                            buffer.flip();
                            //注意:buffer.flip();一定得有,如果没有,就是从文件最后开始读取的,当然读出来的都是byte=0时候的字符。通过buffer.flip();这个语句,就能把buffer的当前位置更改为buffer缓冲区的第一个位置
                            socketChannel.write(buffer);
                        }
                        socketChannel.register(selector,SelectionKey.OP_READ);
                    }
                    //读事件
                    if (key.isReadable()){
                        socketChannel = (SocketChannel) key.channel();
                        ByteBuffer buffer =ByteBuffer.allocate(1024);
                        int read = socketChannel.read(buffer);
                        if (read>0){
                            System.out.println("client收到了:"+new String(buffer.array(), 0, read));
                        }
                    }
                    //写事件
                    if (key.isWritable()){
                        //业务逻辑
                    }
                    //删除已经处理的key,以防重复处理
                    iterator.remove();
                }

          }
      } catch (IOException e) {
          e.printStackTrace();
      }finally{
          if (socketChannel != null){
              try {
                  //关闭channel
                  socketChannel.close();
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
          if (selector != null){
              try {
                  //关闭selector
                  selector.close();
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
      }
  }
}
public class NioServer {
    private static  Selector selector;
    public static void main(String[] args) {
    //初始化方法
      init();
      //监听方法
      listen();
  }

    private static void listen() {
      //循环  一直监听
       while (true){
           try {
               //等待请求进来 阻塞方法
               selector.select();

               Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
               //遍历所有事件
               while (iterator.hasNext()){
                   SelectionKey key = iterator.next();
                   //处理请求方法
                   handleRequest(key);

                   iterator.remove();

               }
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
    }

    private static void handleRequest(SelectionKey key) {
        SocketChannel channel= null;
        try {

            //监听事件
            if (key.isAcceptable()){
                ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                channel=serverSocketChannel.accept();
                channel.configureBlocking(false);
                System.out.println("server监听到新的channel");
                channel.register(selector,SelectionKey.OP_READ);
            }
            //读取事件
            if (key.isReadable()){
                channel = (SocketChannel) key.channel();
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                int read = channel.read(buffer);
                if (read > 0){
                    System.out.println("server 收到了:"+ new String(buffer.array(),0,read));

                }
                ByteBuffer write = ByteBuffer.allocate(1024);
                write.put("你好,client".getBytes());
                write.flip();
                channel.write(write);
                channel.register(selector,SelectionKey.OP_WRITE);
            }
            //写事件
            if (key.isWritable()){
                //业务逻辑
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private static void init() {
        ServerSocketChannel  serverSocketChannel= null;
        try {
            //创建channel通道
            serverSocketChannel = ServerSocketChannel.open();
            //创建selector选择器
            selector = Selector.open();
            //设置io模型 非阻塞
            serverSocketChannel.configureBlocking(false);

            //绑定端口
            serverSocketChannel.socket().bind(new InetSocketAddress(9000));

            //把channel注册到selector中
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            System.out.println("服务端初始化启动完成");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

总结:自己多敲几次代码执行一下会理解的更透彻网络io到底是如何运行的是怎么一回事。实践能让我们更加透彻的了解API

Buffer

概念

Buffer也被成为内存缓冲区,本质上就是内存中的一块,我们可以将数据写入这块内存,之后从这块内存中读取数据。也可以将这块内存封装成NIO Buffer对象,并提供一组常用的方法,方便我们对该块内存进行读写操作。
Buffer在java.nio中被定义为抽象类:


image.png

我们可以将Buffer理解为一个数组的封装,我们最常用的ByteBuffer对应的数据结构就是byte[]

属性

Buffer中有4个非常重要的属性:capacity、limit、position、mark

public abstract class Buffer {

    /**
     * The characteristics of Spliterators that traverse and split elements
     * maintained in Buffers.
     */
    static final int SPLITERATOR_CHARACTERISTICS =
        Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED;

    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
  1. capacity属性:容量,Buffer能够容纳的数据元素的最大值,在Buffer初始化创建的时候被赋值,而且不能被修改。 Buffer初始化容量为8,所以capacity=8

  2. limit属性:代表Buffer可读可写的上限。

    • 写模式下:limit 代表能写入数据的上限位置,这个时候limit = capacity
    • 读模式下:在Buffer完成所有数据写入后,通过调用flip()方法,切换到读模式,此时limit等于Buffer中实际已经写入的数据大小。因为Buffer可能没有被写满,所以limit<=capacity
  3. position属性:代表读取或者写入Buffer的位置。默认为0。

    • 写模式下:每往Buffer中写入一个值,position就会自动加1,代表下一次写入的位置。
    • 读模式下:每往Buffer中读取一个值,position就自动加1,代表下一次读取的位置。


      image.png
  4. mark属性:代表标记,通过mark()方法,记录当前position值,将position值赋值给mark,在后续的写入或读取过程中,可以通过reset()方法恢复当前position为mark记录的值。
    总结:0 <= mark <= position <= limit <= capacity

Buffer常见操作

创建Buffer
  • allocate(int capacity)
ByteBuffer buffer = ByteBuffer.allocate(1024);
//把channel数据读取到buffer
int count = channel.read(buffer);

例子中创建的ByteBuffer是基于堆内存的一个对象。

  • wrap(array) wrap方法可以将数组包装成一个Buffer对象
ByteBuffer buffer = ByteBuffer.wrap("hello world".getBytes());
channel.write(buffer);
  • allocateDirect(int capacity)
    通过allocateDirect方法也可以快速实例化一个Buffer对象,和allocate很相似,这里区别的是allocateDirect创建的是基于堆外内存的对象。

堆外内存不在JVM堆上,不受GC的管理。堆外内存进行一些底层系统的IO操作时,效率会更高。

Buffer写操作

Buffer写入可以通过put()和channel.read(buffer)两种方式写入。
通常我们NIO的读操作的时候,都是从Channel中读取数据写入Buffer,这个对应的是Buffer的写操作。

Buffer读操作

Buffer读取可以通过get()和channel.write(buffer)两种方式读入。
还是同上,我们对Buffer的读入操作,反过来说就是对Channel的写操作。读取Buffer中的数据然后写入Channel中。


image.png
其他常见方法
  • rewind():重置position位置为0,可以重新读取和写入buffer,一般该方法适用于读操作,可以理解为对buffer的重复读。
    public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}

  • flip():很常用的一个方法,一般在写模式切换到读模式的时候会经常用到。也会将position设置为0,然后设置limit等于原来写入的position。
public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}
  • clear():重置buffer中的数据,该方法主要是针对于写模式,因为limit设置为了capacity,读模式下会出问题。
public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}
  • mark()&reset(): mark()方法是保存当前position到变量markz中,然后通过reset()方法恢复当前position为mark,实现代码很简单,如下:
public final Buffer mark() {
    mark = position;
    return this;
}

public final Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m;
    return this;
}

常用的读写方法可以用一张图总结一下:

image.png

Selector

概念

Selector是NIO中最为重要的组件之一,我们常常说的多路复用器就是指的Selector组件。
Selector组件用于轮询一个或多个NIO Channel的状态是否处于可读、可写。通过轮询的机制就可以管理多个Channel,也就是说可以管理多个网络连接


image.png

轮询机制

  1. 首先,需要将Channel注册到Selector上,这样Selector才知道需要管理哪些Channel
  2. 接着Selector会不断轮询其上注册的Channel,如果某个Channel发生了读或写的时间,这个Channel就会被Selector轮询出来,然后通过SelectionKey可以获取就绪的Channel集合,进行后续的IO操作。


    image.png

属性操作

创建Selector

通过open()方法,我们可以创建一个Selector对象。
Selector selector = Selector.open();

注册Channel到Selector中

我们需要将Channel注册到Selector中,才能够被Selector管理。
channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

某个Channel要注册到Selector中,那么该Channel必须是非阻塞,所有上面代码中有个configureBlocking()的配置操作。

在register(Selector selector, int interestSet)方法的第二个参数,标识一个interest集合,意思是Selector对哪些事件感兴趣,可以监听四种不同类型的事件:

  • Connect事件 :连接完成事件( TCP 连接 ),仅适用于客户端,对应 SelectionKey.OP_CONNECT。
  • Accept事件 :接受新连接事件,仅适用于服务端,对应 SelectionKey.OP_ACCEPT 。
  • Read事件 :读事件,适用于两端,对应 SelectionKey.OP_READ ,表示 Buffer 可读。
  • Write事件 :写时间,适用于两端,对应 SelectionKey.OP_WRITE ,表示 Buffer 可写。
    当然,Selector是可以同时对多个事件感兴趣的,我们使用或运算即可组合多个事件:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

Selector其他一些操作

选择Channel

public abstract int select() throws IOException; public abstract int select(long timeout) throws IOException; public abstract int selectNow() throws IOException;
当Selector执行select()方法就会产生阻塞,等到注册在其上的Channel准备就绪就会立即返回,返回准备就绪的数量。

select(long timeout)则是在select()的基础上增加了超时机制。
selectNow()立即返回,不产生阻塞。

获取可操作的Channel

Set selectedKeys = selector.selectedKeys();

当有新增就绪的Channel,调用select()方法,就会将key添加到Set集合中。

总结

回顾一下使用 NIO 开发服务端程序的步骤:

  1. 创建 ServerSocketChannel 和业务处理线程池。
  2. 绑定监听端口,并配置为非阻塞模式。
  3. 创建 Selector,将之前创建的 ServerSocketChannel 注册到 Selector 上,监听 SelectionKey.OP_ACCEPT。
  4. 循环执行 Selector.select() 方法,轮询就绪的Channel。
  5. 轮询就绪的 Channel 时,如果是处于 OP_ACCEPT 状态,说明是新的客户端接入,调用 ServerSocketChannel.accept 接收新的客户端
  6. 设置新接入的 SocketChannel 为非阻塞模式,并注册到 Selector 上,监听 OP_READ。
  7. 如果轮询的 Channel 状态是 OP_READ,说明有新的就绪数据包需要读取,则构造 ByteBuffer 对象,读取数据。

NIO 原生 API 的弊端 :

NIO 组件复杂

使用原生 NIO 开发服务器端与客户端 , 需要涉及到 服务器套接字通道 ( ServerSocketChannel ) , 套接字通道 ( SocketChannel ) , 选择器 ( Selector ) , 缓冲区 ( ByteBuffer ) 等组件 , 这些组件的原理 和API 都要熟悉 , 才能进行 NIO 的开发与调试 , 之后还需要针对应用进行调试优化

NIO 开发基础

NIO门槛略高 , 需要开发者掌握多线程、网络编程等才能开发并且优化 NIO 网络通信的应用程序

原生 API 开发网络通信模块的基本的传输处理

网络传输不光是实现服务器端和客户端的数据传输功能 , 还要处理各种异常情况 , 如 连接断开重连机制 , 网络堵塞处理 , 异常处理 , 粘包处理 , 拆包处理 , 缓存机制 等方面的问题 , 这是所有成熟的网络应用程序都要具有的功能 , 否则只能说是入门级的 Demo

NIO BUG

NIO 本身存在一些 BUG , 如 Epoll , 导致 选择器 ( Selector ) 空轮询 , 在 JDK 1.7 中还没有解决

相关文章

网友评论

      本文标题:Netty基础知识之NIO

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