Netty简介及IO模型详解

174 阅读8分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

1.Netty 介绍和应用场景

1.1 介绍

  1. Netty 是jboss的一个开源框架
  1. Netty是一个异步的,基于事件驱动的网络应用框架
  1. 基于nio

1.2 应用场景

  1. Rpc 例如dubbo
  1. 游戏
  1. 大数据

涉及到网络通信的应用都可以使用netty

2. i/o模型

2.1 介绍

  1. bio 同步并阻塞 一个连接对应服务器一个线程   适用于连接数较少的架构 jdk1.4
  1. nio 同步非阻塞 服务器一个线程处理多个连接   适用于连接数较多连接时间短 jdk1.4
  1. aio(nio.2) 异步非阻塞  适用于连接数多且连接时间长 jdk1.7

2.2 bio

blocking i/o

2.2.1 简单demo

开发一个服务端,创建一个线程池,当客户端发送一个请求,服务端对应创建一个线程处理,当有多个客户端请求时,就会创建多个线程对应处理

这里demo的客户端用telnet模拟

public static void handler(Socket socket){
        try(InputStream in = socket.getInputStream();){
            System.out.println("线程信息: id "+Thread.currentThread().getId()+" name " + Thread.currentThread().getName());

            byte[] bytes = new byte[1024];
            while (true){
                int read = in.read(bytes);
                if(read!=-1){
                    System.out.println("输出信息: "+new String(bytes,"UTF-8"));
                }else {
                    break;
                }
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        try(ServerSocket serverSocket = new ServerSocket(6666);) {
            ExecutorService executorService = Executors.newCachedThreadPool();

            System.out.println("线程信息: id "+Thread.currentThread().getId()+" name " + Thread.currentThread().getName());

            while (true){
                System.out.println("等待链接");
                final Socket socket = serverSocket.accept();
                System.out.println("链接到一个客户端");

                executorService.execute(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("线程信息: id "+Thread.currentThread().getId()+" name " + Thread.currentThread().getName());
                        handler(socket);
                    }
                });
            }

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

    }

2.3 nio

non-blocking i/o 非阻塞

2.3.1 简介

三大核心

  1. channel 通道
  1. buffer 缓冲区
  1. selector 选择器

简述操作原理: selector 选择可用的channel, channelbuffer可以相互读写,应用程序并不直接对channel进行操作,而是通过对buffer进行操作,间接操作channel

一个线程中会有多个selector,一个selector中可以注册多个channel,如果并没有数据传输,线程还可以做其他事,并不会一直等待

2.4 nio与bio 的区别

  1. nio非阻塞 bio阻塞
  1. nio用块的方式处理io  bio用流的方式处理io 块的方式比流的方式要快
  1. bio基于字节流/字符流  nio基于缓冲区和通道(channel) selector监听多个通道的事件,因此用一个线程就可以处理多个通道的数据

图示: nio

bio

3. nio详解

3.1 nio模型三大组件的关系

  1. 一个线程对应一个selector
  1. 一个selecor对应多个channel
  1. 一个channel对应一个buffer
  1. 一个线程对应多个channel
  1. channel与buffer都是双向的,就是既可以读也可以写 使用flip()方法切换
  1. buffer就是一个内存块,读写内存比较快
  1. selector会根据不同事件切换不同的channel

3.2 Buffer缓冲区

3.2.1 简介

本质是一个读写数据的内存块,可以理解成一个提供了操作内存块方法容器对象(数组)

缓冲区中内置了一些机制,这些机制可以检测到缓冲区的数据变化,状态变化

channel读写的数据必须都经过Buffer

3.2.2 源码分析

常用的几个操作方法

public static void main(String[] args) {
        //allocate 规定intbuffer的长度
        IntBuffer buffer = IntBuffer.allocate(5);

        //capacity()获取容量
        //put()写入
        for(int i = 0;i<buffer.capacity();i++){
            buffer.put(i*2);
        }

        //flip()反转 由写转为读
        buffer.flip();

        //读取
        //get()每次读取后 索引向后移动一位
        for(int i = 0;i<buffer.capacity();i++){
            System.out.println(buffer.get());

        }
    }

3.2.2.1 定义

IntBuffer中定义了一个int数组,其他类型的buffer类似

public abstract class IntBuffer
    extends Buffer
    implements Comparable<IntBuffer>
{

    // These fields are declared here rather than in Heap-X-Buffer in order to
    // reduce the number of virtual method invocations needed to access these
    // values, which is especially costly when coding small buffers.
    //
    final int[] hb;                  // Non-null only for heap buffers
    final int offset;
    boolean isReadOnly;                 // Valid only for heap buffers

最顶层的Buffer类中定义了四个属性

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; //当前索引的位置,不能超过limit
    private int limit;//最大能读写的长度
    private int capacity;//容量 allocate定义的长度

3.2.2.2 反转

   public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

可以看到,反转之后,由读变为写,或者由写变为读

将索引归0,最大读写长度不能超过上次操作的索引

3.2 channel 通道

3.2.1 简介

  1. 通道类似于流/连接,但是流只能写入或者读取,通道可以即读取也写入
  1. 通道异步读写数据
  1. 通道可以读写数据到缓存区

3.2.2 层级关系

当有客户端发送请求时,服务端会创建一个ServerSocketChannel(实现类:ServerSocketChannelImpl) 再由ServerSocketChannel创建一个SocketChannel(实现类:SocketChannelImpl即真正读写数据的通道),这个SocketChannel就是与这个客户端请求所对应的

3.2.3 案例剖析

3.2.3.1 FileChannle 输出文件流

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * @author: zhangyao
 * @create:2020-08-25 14:50
 **/
public class FileChannelTest {
    public static void main(String[] args) {
        FileOutputStream fileOutputStream = null;
        try {
            //文件输出流
            fileOutputStream = new FileOutputStream("D:\file01.txt");
			//文件输出流包装为FileChannel 此处FileChannel默认实现FileChannelImpl
            FileChannel fileChannel = fileOutputStream.getChannel();

            //创建对应的缓冲区
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

            //数据写入缓冲区
            byteBuffer.put("hello nio".getBytes());

            //反转,因为接下来需要从缓冲区读取数据写入Channel
            byteBuffer.flip();

            //从缓冲区写入Channel
            fileChannel.write(byteBuffer);



        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关闭文件流
            if(fileOutputStream!=null){
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

整体流程就是把数据写入缓冲区,在读取缓存区写入通道Channel,在由文件输出流输出

图示如下

3.2.3.2 FileChanle 输入文件流

public static void main(String[] args) {
    FileInputStream fileInputStream = null;
    try {
        fileInputStream = new FileInputStream("D:\file01.txt");

        //获取Channel
        FileChannel channel = fileInputStream.getChannel();

        //创建byteBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        //从channel中读取数据写入buffer
        channel.read(byteBuffer);

        //反转  下一步需要从buffer中读取数据输出
        byteBuffer.flip();

        //输出
        byte[] array = byteBuffer.array();
        System.out.println(new String(array));

    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            fileInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

与上面的例子刚好相反,从文件中读取数据,通过通道写入buffer缓冲区,在输出

图示

3.2.3.3 FileChannel 拷贝文件

其实就是上面两个例子结合,把一个文件中的数据复制到另外一个文件中

public static void main(String[] args) {
    FileInputStream fileInputStream = null;
    FileOutputStream fileOutputStream = null;

    try {
        fileInputStream = new FileInputStream("D:\file01.txt");
        fileOutputStream = new FileOutputStream("D:\file02.txt");
        FileChannel channel = fileInputStream.getChannel();

        FileChannel channel1 = fileOutputStream.getChannel();

        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        while (true){
            //将byteBuffer复位
            byteBuffer.clear();
            int read = channel.read(byteBuffer);
            if(read==-1){
                break;
            }

            byteBuffer.flip();
            channel1.write(byteBuffer);
        }


    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            fileInputStream.close();
            fileOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


}

这里使用了byteBuffer.clear()方法

因为ByteBuffer缓冲区是有长度的,当读取的文件超过缓冲区的长度时,如果不对缓冲区进行清空,当进行下一次读取时,就会从上一次读取的位置开始读取,会出现死循环的情况

3.2.3.4 FileChannel 拷贝文件之TransferFrom

public static void main(String[] args) {
    FileInputStream fileInputStream = null;
    FileOutputStream fileOutputStream = null;

    try {
        fileInputStream = new FileInputStream("D:\file01.txt");
        fileOutputStream = new FileOutputStream("D:\file02.txt");
        FileChannel channel = fileInputStream.getChannel();

        FileChannel channel1 = fileOutputStream.getChannel();

	
        //从channel通道拷贝到 channel1通道
        channel1.transferFrom(channel, 0, channel.size());

    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            fileInputStream.close();
            fileOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


}

3.2.4 Buffer的分散和聚集

上面的例子都是使用单个buffer进行数据的读写,如果数据过大,也可用使用多个buffer(buffer数组)进行数据的读写,即用空间换时间

3.3 Selector选择器

3.3.1 基本简介

一个selector管理多个channel通道,使用异步的方式处理io

只有读写真正的发生时,才会处理数据,减小了线程的压力,不用每个请求都维护一个线程

避免了多线程之间的上下文切换导致的开销

3.3.2 selector的api

Selector:

  1. select() 阻塞
  1. select(Long timeout) 有超时时间
  1. selectNow() 非阻塞
  1. wakeup() 立即唤醒selector

3.3.2 selecor的工作流程

其实是Selector SelectionKey ServerSocketChannel SorkcetChannel的工作原理

  1. 当客户端链接时,通过ServerSockertChannel 得到SocketChannel 并且注册到 Selector上
    1. 注册源码
public abstract SelectionKey register(Selector sel, int ops)
    throws ClosedChannelException;

这是SocketChannel注册到Selector上的方法,第一个参数为要注册的Selector对象,第二个参数为事件驱动的类型

public abstract class SelectionKey {
    public static final int OP_READ = 1;
    public static final int OP_WRITE = 4;
    public static final int OP_CONNECT = 8;
    public static final int OP_ACCEPT = 16;
    private volatile Object attachment = null;
  1. 当注册完成后返回一个Selectionkey,这个selectionKey会和SocketChannel关联
  1. Selector通过select方法监听Channel,如果有事件发生,返回对应的selectionKey集合
    1. 源码
public int select(long var1) throws IOException {
        if (var1 < 0L) {
            throw new IllegalArgumentException("Negative timeout");
        } else {
            return this.lockAndDoSelect(var1 == 0L ? -1L : var1);
        }
    }

    public int select() throws IOException {
        return this.select(0L);
    }

    public int selectNow() throws IOException {
        return this.lockAndDoSelect(0L);
    }

	private int lockAndDoSelect(long var1) throws IOException {
        synchronized(this) {
            if (!this.isOpen()) {
                throw new ClosedSelectorException();
            } else {
                int var10000;
                synchronized(this.publicKeys) {
                    synchronized(this.publicSelectedKeys) {
                        var10000 = this.doSelect(var1);
                    }
                }

                return var10000;
            }
        }
    }
  1. 通过得到的selectionKey可以反向获取Channel
    1. 源码
    public abstract SelectableChannel channel();
  1. 最后通过channel处理业务

3.3.3 案例

服务端思路:

  1. 创建serverSocketChannel绑定端口6666,把这个channel注册到Selector上,注册事件是OP_ACCEPT
  1. 循环监听,判断是否channel中是否有事件发生,如果有事件发生,判断不同的事件类型进行不同的链接,读/写操作

客户端思路

  1. 创建一个SocketChannel,连接上服务器之后,发送消息,并保持链接不关闭

3.3.3.1 server端

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

/**
 * @author: zhangyao
 * @create:2020-08-26 16:55
 **/
public class ServerChannel {
    public static void main(String[] args) {

        try {
            //生成一个ServerScoketChannel
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            //设置为非阻塞的
            serverSocketChannel.configureBlocking(false);
            //serverSocket监听6666端口
            serverSocketChannel.socket().bind(new InetSocketAddress(6666));

            //创建Selector
            Selector selector = Selector.open();

            //serverSocketChannel注册到Selector
            SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);


            //循环等待链接
            while (true){
                //如果没有事件发生,就继续循环
                if(selector.select(1000) == 0){
                    System.out.println("等待1s,无连接");
                    continue;
                }

                //如果有事件驱动,就需要遍历事件
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    //如果事件是连接
                    if(key.isAcceptable()){
                        try {
                            SocketChannel channel = serverSocketChannel.accept();
                            channel.configureBlocking(false);
                            SelectionKey register = channel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                            System.out.println("链接成功");
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }

                    //如果是读取数据
                    if(key.isReadable()){
                        SocketChannel channel = (SocketChannel) key.channel();
                        ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
                        try {
                            int read = channel.read(byteBuffer);
                            byte[] array = byteBuffer.array();
                            System.out.println("读取数据:"+ new String(byteBuffer.array()));
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    iterator.remove();


                };

            }

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


    }
}

3.3.3.2 客户端

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * @author: zhangyao
 * @create:2020-08-26 17:24
 **/
public class ClientChannel {
    public static void main(String[] args) {

        //创建一个SocketChannel
        try {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
            InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
            if(!socketChannel.connect(inetSocketAddress)){
                while (!socketChannel.finishConnect()){
                    System.out.println("服务器连接中,线程并不阻塞,可以进行其他操作");
                }
            }

            //连接成功
            socketChannel.write(ByteBuffer.wrap("hello ,server".getBytes()));

            System.in.read();




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