Java NIO学习笔记(全面详解)

此文档已在语雀中分享,点击查看。

1. 引言
1.1 什么是Netty

https://netty.io/
Netty是一个异步事件驱动的网络应用框架。
用于快速开发可维护的高性能协议服务器和客户端。

Netty是jboss提供的一个java开源框架,Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可用性的网络服务器和客户端程序。也就是说Netty是一个基于NO的编程框架,使用Netty可以快速的开发出一个网络应用。
由于Java 自带的NIO api使用起来非常复杂,并且还可能出现 Epoll Bug,这使得我们使用原生的NIO来进行网络编程存在很大的难度且非常耗时。但是Netty良好的设计可以使开发人员快速高效的进行网络应用开发。

1.2 为什么要学习Netty
1. Netty已经是行业内网络通信编程的标准,广泛应用于通信领域和很多其他中间件技术的底层。
 基本上都是使用Netty作为网络通信的底层框架
2. 应用非常广泛
   1. 游戏行业
   2. 很多框架的通信底层,解决进程间通信。
      Spring WebFlux、rocketMQ、dubbo、HBase、Gateway等,是分布式系统,通信的核心。

面试“绝招”

除了提升技术水平之外,另一个大家比较重视的就是面试了。如果有时间,一定要系统性地学习Netty。如果没有掌握Netty的核心原理,那么永远都是Java的初学者。

  • Netty的粘包/拆包是怎么处理的,有哪些实现?
  • 同步与异步、阻塞与非阻塞的区别?
  • BIO、NIO、AIO分别是什么?
  • select、poll、epoll的机制及其区别?

所以,深入学习Netty,也是跳槽面试、升职加薪的必备“绝招”。

2. NIO编程
1. NIO全称成为None Blocking IO (非阻塞IO)。【JDK1.4】
2. 非阻塞 主要应用在网络通信中,能够合理利用资源,提高系统的并发效率。支持高并发的系统访问。
2.1 传统网络通信中的开发方式及问题

Java NIO学习笔记(全面详解)_第1张图片
线程也是要占用内存的,一个线程占用1M,内存吃不消。cpu要轮转执行线程,线程有线程上下文记录当前线程执行的状态。传统的web开发,线程的创建是不可控的,怎么控制线程数呢?这个时候就要引入池化思想。提前创建好线程池。

2.1.2 线程池版的网络编程

Java NIO学习笔记(全面详解)_第2张图片
没有线程可用,就会到队列中进行等待。TreadPoolExecutor中的参数说明:第一个参数core pool size,最小的线程数,cup的核数,jdk8之后支持,命令行参数指定cpu核数。第二个参数:最多的线程数。第三个参数:最大等待时长。第五个参数:等待队列,队列中最大多少个线程。
这个时候如果客户端发生阻塞,那么线程也会发生阻塞,等待客户端解除阻塞。比如键盘输入、IO操作。这种模式造成线程无法复用造成资源浪费。

2.2 NIO网络通信中的非阻塞编程

Java NIO学习笔记(全面详解)_第3张图片
传统的网络通信都是通过InputStream、OutputStream流进行操作,但是在NIO的网络通信使用管道的方式进行通信。
NIO中引入了Seletor,区别于传统的网络通信,Seletor去监控管道正常的通信,正常的读写,没有阻塞的话,就会为其分配线程。
在运行过程中,客户端等待输入,发生阻塞,Seletor是可以监控到的。监控到之后,就会把分配的线程解放出来,让其他的客户端使用。

3.NIO的基本开发方式
3.1 Channel简介
1. IO通信的通道,类似于InputStream、OutputStream
2. Channel没有方向性

Java NIO学习笔记(全面详解)_第4张图片

  • 常见Channel
1. 文件操作
   FileChannel,读写文件中的数据。
2. 网络操作
   SocketChannel,通过TCP读写网络中的数据。
   ServerSockectChannel,监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
   DatagramChannel,通过UDP读写网络中的数据。
  • 获得方式
1. FileInputStreanm/FileOutputStream
2. RandomAccessFile

3. Socket
4. ServerSocket
5. DatagramSocket
3.2 Buffer简介

管道中数据最终是要流向Buffer中的,既然Channel没有方向,那么Buffer就要有方向性,用于区分是读还是写。Buffer作为缓存。多个字节缓存后与程序交互,提高IO的利用率。

1. Channel读取或者写入的数据,都要写到Buffer中,才可以被程序操作。
2. 因为Channel没有方向性,所以Buffer为了区分读写,引入了读模式、写模式进行区分。

Java NIO学习笔记(全面详解)_第5张图片
读模式:站在Buffer的角度,往程序中读,供程序使用。

  • 常见Buffer
1. ByteBuffer
2. CharBuffer
3. DoubleBuffer
4. FloatBuffer
5. IntBuffer
6. LongBuffer
7. ShortBuffer
8. MappedByteBuffer..
  • 获得方式
 1. ByteBuffer.allocate(10);
 2. encode()
3.4 第一个NIO程序分析
 public class TestNIO1 {
    public static void main(String[] args) throws IOException {
        //1 创建Channel通道  FileChannel
        FileChannel channel = new FileInputStream("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data.txt").getChannel();

        //2 创建缓冲区
        //1234567890
        ByteBuffer buffer = ByteBuffer.allocate(10);

        while (true) {
            //3把通道内获取的文件数据,写入缓冲区
            int read = channel.read(buffer);

            if (read == -1) break;

            //4.程序读取buffer内容,后续的操作。设置buffer为读模式。
            buffer.flip();

            //5.循环读取缓冲区中的数据
            while (buffer.hasRemaining()) {
                byte b = buffer.get();
                System.out.println("(char)b = " + (char) b);
            }

            //6. 设置buffer的写模式
            buffer.clear();
        }
    }
}


public class TestNIO2 {
    public static void main(String[] args) {
        //RadomAccessFile 异常处理
        FileChannel channel = null;
        try {
            channel = new RandomAccessFile("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data.txt", "rw").getChannel();

            ByteBuffer buffer = ByteBuffer.allocate(10);

            while (true) {
                int read = channel.read(buffer);
                if (read == -1) break;

                buffer.flip();
                while (buffer.hasRemaining()) {
                    byte b = buffer.get();
                    System.out.println("(char) b = " + (char) b);
                }

                buffer.clear();
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (channel != null) {
                try {
                    channel.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

public class TestNIO3 {
    public static void main(String[] args) {

        try (FileChannel channel = FileChannel.open(Paths.get("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data.txt"), StandardOpenOption.READ);) {

            ByteBuffer buffer = ByteBuffer.allocate(10);
            while (true) {
                int read = channel.read(buffer);
                if (read == -1) break;

                buffer.flip();
                while (buffer.hasRemaining()) {
                    byte b = buffer.get();
                    System.out.println("(char)b = " + (char) b);
                }

                buffer.clear();
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    } 
}
3.5 NIO开发的步骤总结
1. 获取Channel FileChannel SocketChannel ServerSocketChannel
2. 创建Buffer ByteBuffer
3. 循环的从Channel中获取数据,读入到Buffer中。进行操作.
    channel.read(buffer);

    buffer.flip();//设置读模式
    循环从buffer中获取数据。
    buffer.get();
    buffer.clear();//设置写模式
     buffer.compact();//写模式
4. ByteBuffer详解

思考一个问题,什么时候设计成接口,什么时候设计为抽象类?

名词设计为抽象类,如汽车、形状。
动词设计为接口。DAO、Service。
特例:InputStrem、OutPutStream,除此之外没用例外。

ByteBuffer是一个抽象类,它有两个主要实现类Java NIO学习笔记(全面详解)_第6张图片

1. HeapByteBuffer    堆ByteBuffer        JVM内的堆内存  --->  读写操作 效率低 会收到GC影响 
2. MappedByteBuffer(DirectByteBuffer)    OS内存        --->   读写操作 效率高 不会收到GC影响 。 不主动析构,会造成内存的泄露  

内存泄露:本来有100M,但实际上只能用80M
内容溢出:运行程序要100M,而你只有80M
内存泄露的主要原因:

  • 没用主动释放内存
  • 内存碎片
4.2 获取方式
 1. ByteBuffer.allocate(10);//一旦分配空间,不可以动态调整
 2. encode()
4.3 核心结构
ByteBuffer是一个类似数组的结构,整个结构中包含三个主要的状态
1. Capacity 
   buffer的容量,类似于数组的size
2. Position
   buffer当前缓存的下标,在读取操作时记录读到了那个位置,在写操作时记录写到了那个位置。从0开始,每读取一次,下标+1
3. Limit
   读写限制,在读操作时,设置了你能读多少字节的数据,在写操作时,设置了你还能写多少字节的数据
   
所谓的读写模式,本质上就是这几个状态的变化。主要有Position和Limit联合决定了Buffer的读写数据区域。

Java NIO学习笔记(全面详解)_第7张图片

最后总结一下
写入Buffer数据之前要设置写模式
1. 写模式 
  a. 新创建的Buffer自动是写模式
  b. 调用了clear,compact方法
读取Buffer数据之前要设置读模式
2. 读模式
  1. 调用flip方法
4.4 核心API
  • buffer中写入数据[写模式 创建一个bytebuffer ,clear(),compact()]
1. channel的read方法
   channel.read(buffer)
2. buffer的put方法
   buffer.put(byte)    buffer.put((byte)'a')..
   buffer.put(byte[])
  • 从buffer中读出数据
1. channel的write方法

2. buffer的get方法 //每调用一次get方法会影响,position的位置。

3. rewind方法(手风琴),可以将postion重置成0 ,用于复读数据。

4. mark&reset方法,通过mark方法进行标记(position),通过reset方法跳回标记,从新执行.

5. get(i) 方法,获取特定position上的数据,但是不会对position的位置产生影响。
4.5 字符串操作
字符串存储到Buffer中
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("sunshuai".getBytes());

buffer.flip();
while (buffer.hasRemaining()) {
  System.out.println("buffer.get() = " + (char)buffer.get());
}
buffer.clear();


ByteBuffer buffer = Charset.forName("UTF-8").encode("sunshuai");

1、encode方法自动 把字符串按照字符集编码后,存储在ByteBuffer.
2、自动把ByteBuffer设置成读模式,且不能手工调用flip方法。

ByteBuffer buffer = StandardCharsets.UTF_8.encode("sunshuai");

while (buffer.hasRemaining()) {
  System.out.println("buffer.get() = " + (char) buffer.get());
}
buffer.clear();
1、encode方法自动 把字符串按照字符集编码后,存储在ByteBuffer.
2、自动把ByteBuffer设置成读模式,且不能手工调用flip方法。
  
ByteBuffer buffer = ByteBuffer.wrap("sunshuai".getBytes());
while (buffer.hasRemaining()) {
  System.out.println("buffer.get() = " + (char) buffer.get());
}
buffer.clear();
  • Buffer中的数据转换成字符串
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("孙".getBytes());

buffer.flip();
CharBuffer result = StandardCharsets.UTF_8.decode(buffer);
System.out.println("result.toString() = " + result.toString());
4.6 粘包与半包
  1. \n 作为分割符,进行行的区分。
  2. compact进行处理,把第一次没有读取完的数据,向前移动和后面的内容进行整合。

Java NIO学习笔记(全面详解)_第8张图片

//1. 半包 粘包
public class TestNIO10 {
    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(50);
        buffer.put("Hi sunshuai\nl love y".getBytes());
        doLineSplit(buffer);
        buffer.put("ou\nDo you like me?\n".getBytes());
        doLineSplit(buffer);
    }

    // ByteBuffer接受的数据 \n
    private static void doLineSplit(ByteBuffer buffer) {
        buffer.flip();
        for (int i = 0; i < buffer.limit(); i++) {
            if (buffer.get(i) == '\n') {
                //为了处理一行中有多个换行符,浪费空间的问题
                int length = i + 1 - buffer.position();
                ByteBuffer target = ByteBuffer.allocate(length);
                for (int j = 0; j < length; j++) {
                    target.put(buffer.get());
                }
                //截取工作完成
                target.flip();
                System.out.println("StandardCharsets.UTF_8.decode(target).toString() = " + StandardCharsets.UTF_8.decode(target).toString());
            }
        }
        buffer.compact();
    }
}
5. NIO的开发使用
5.1 文件操作
5.1.1 读取文件内容
1. 第一个程序 读文件的内容,读Buffer---> String ---->程序中使用了
public class TestNIO1 {
    public static void main(String[] args) throws IOException {
        //1 创建Channel通道  FileChannel
        FileChannel channel = new FileInputStream("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data.txt").getChannel();

        //2 创建缓冲区
        //1234567890
        ByteBuffer buffer = ByteBuffer.allocate(10);
        while (true) {
            //3把通道内获取的文件数据,写入缓冲区
            int read = channel.read(buffer);
            if (read == -1) break;
            //4.程序读取buffer内容,后续的操作。设置buffer为读模式。
            buffer.flip();
            //5.循环读取缓冲区中的数据
            while (buffer.hasRemaining()) {
                byte b = buffer.get();
                System.out.println("(char)b = " + (char) b);
            }
            //Charset.forName("UTF-8").decode(buffer).toString ---> String

            //6. 设置buffer的写模式
            buffer.clear();
        }
    }
}
5.1.2 写入文件内容
public class TestNIO11 {
    public static void main(String[] args) throws IOException {


        //1 获得Channel  FileOutputStream, RandomAccessFile
        FileChannel channel = new FileOutputStream("data1").getChannel();

        //2 获得Buffer
        ByteBuffer buffer = Charset.forName("UTF-8").encode("sunshuai");

        //3write
        channel.write(buffer);

    }
5.1.3 文件的复制
public class TestNIO12 {
    public static void main(String[] args) throws IOException {
        //data---data2
      /*  FileInputStream inputStream = new FileInputStream("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data.txt");
        FileOutputStream fileOutputStream = new FileOutputStream("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data2.txt");

        byte[] buffer = new byte[1024];

        while (true) {
            int read = inputStream.read(buffer);
            if (read == -1) break;
            fileOutputStream.write(buffer, 0, read);
        }*/

       /* FileInputStream inputStream = new FileInputStream("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data.txt");
        FileOutputStream fileOutputStream = new FileOutputStream("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data2.txt");

        IOUtils.copy(inputStream,fileOutputStream);*/

        FileChannel from = new FileInputStream("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data.txt").getChannel();
        FileChannel to = new FileOutputStream("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data2.txt").getChannel();

        //传输数据上线的 2G-1
        // 若果实际文件大小就是超过2G 如何进行文件的copy
        //from.transferTo(0, from.size(), to);

        long left = from.size();
        while (left > 0) {
            left = left - from.transferTo(from.size()-left, left, to);
        }


    }
}

NIO的拷贝效率更高,因为NIO涉及到零拷贝,后面我们会讲到什么是零拷贝。
NIO的数据拷贝是有传输上限的,上限大小是2G-1

5.2 网络编程

服务器用于接收请求,建立连接ServerSocketChannel
客户端用于发送请求

1. 服务端 接受请求、并发、如何接入 ---》  ServerScoketChannel

2. 进行实际通信          ScoketChannel

Tip:端口号是和协议挂钩的,不同的协议端口号可以相同,不会冲突。

第一版代码
通过这版代码 证明了 服务器端 存在2中阻塞
1. 连接阻塞 ---->  accept方法存在阻塞---> ServerSocketChannel阻塞。 
2. IO阻塞   ----> channel的read方法存在阻塞---> SocketChannel阻塞。
上述分析 对应着的2个问题。
public class MyServer {
    public static void main(String[] args) throws IOException {
        //1. 创建ServerScoketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //2. 设置服务端的监听端口:---》client通过网络进行访问 ip:port http://localhost:8989
        serverSocketChannel.bind(new InetSocketAddress(8000));

        List<SocketChannel> channelList = new ArrayList<>();

        ByteBuffer buffer = ByteBuffer.allocate(20);

        //3. 接受client的连接
        while (true) {
            //4. ScoketChannle 代表 服务端与Client链接的一个通道
            System.out.println("等待连接服务器...");
            SocketChannel socketChannel = serverSocketChannel.accept();//阻塞 程序等待client
            System.out.println("服务器已经连接..."+socketChannel);

            channelList.add(socketChannel);

            //5. client与服务端 通信过程 NIO代码
            for (SocketChannel channel : channelList) {
                System.out.println("开始实际的数据通信....");
                channel.read(buffer);//阻塞 对应的IO通信的阻塞
                buffer.flip();
                CharBuffer decode = Charset.forName("UTF-8").decode(buffer);
                System.out.println("decode.toString() = " + decode.toString());
                buffer.clear();
                System.out.println("通信已经结束....");

            }
        }

    }
}

存在问题:会有两种阻塞,一是服务端等待客户端连接阻塞,而是客户端等待输入阻塞。

第二版代码
public static void main(String[] args) throws IOException {
        //1.创建ServerScoketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //ip不需要设置,哪台机器运行,Ip就是哪台机器
        serverSocketChannel.bind(new InetSocketAddress(8000));
        serverSocketChannel.configureBlocking(false);
        //2.接收客户端请求 因为要一直接收,所以要用死循环
        List<SocketChannel> channelList = new ArrayList<>();
        ByteBuffer buffer = ByteBuffer.allocate(20);
        while (true) {
            //3.返回值就是接收到的客户端请求
            SocketChannel channel = serverSocketChannel.accept();//不再阻塞
            if (channel != null) {
                channel.configureBlocking(false);
                channelList.add(channel);
            }
            //4.将客户端请求存放到集合中
            for (SocketChannel socketChannel : channelList) {
                int read = socketChannel.read(buffer);//发生阻塞,IO阻塞,等待客户端传输数据
                if (read > 0) {
                    System.out.println("开始实际数据通信....");
                    buffer.flip();
                    CharBuffer decode = Charset.forName("UTF-8").decode(buffer);
                    System.out.println("decode.toString() = " + decode.toString());
                    System.out.println("数据通信结束....");
                    buffer.clear();
                }
            }
        }
    }

存在问题:第二版代码,虽然解决了阻塞问题,但还有一个问题没有解决,就是CPU一直轮转的问题,无限的循环,我们需要一个组件帮我们监控什么时候有客户端连接进来,什么时候客户端进行读写操作,而不是一直循环。
监控是什么?连接的创建,IO的读写,引入NIO中的Selector。
监控的是什么对象?ServerSocketChannel、SocketChannel。
作为Seletor,不会一直监控这些对象,只有这些对象有特殊状态时候才会监管。

  • ACCEPT 建立连接 SSC
  • READ SC
  • WRITE SC
  • CONNECT 客户端

Seletor怎么来监管?
Seletor中有两个属性,一个是keys,一旦被注册,都会被放置在keys属性当中,另一个属性是SelectionKeys,当selector方法真正监控到之后,把实际存在的Accept SSC和READ、WRITE的SC放到SelectionKeys中。

第三版代码
public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8000));
        serverSocketChannel.configureBlocking(false);//只有在非阻塞的情况下,才可以用Seletor

        Selector selector = Selector.open();
        //将ServerSocketChannel注册到Seletor
        SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);
        //设置感兴趣的事件,监控的事件
        selectionKey.interestOps(SelectionKey.OP_ACCEPT);
        System.out.println("MyServer2.main");
        //监控
        while (true){
            selector.select();//等待
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();//拿到监控的对象,SSC、SC
            System.out.println("------------------");
            while (iterator.hasNext()){
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()) {
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel sc = channel.accept();
                    sc.configureBlocking(false);
                    SelectionKey sck = sc.register(selector, 0, null);
                    sck.interestOps(SelectionKey.OP_READ);
                }else if (key.isReadable()){
                    SocketChannel sc = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(6);
                    sc.read(buffer);
                    buffer.flip();
                    System.out.println("Charset.defaultCharset().decode(buffer).toString() = " + Charset.defaultCharset().decode(buffer).toString());
                }

            }

        }
    }

这里为什么要执行terator.remove()呢?因为当第二次执行的时候会存在空指针。当再次需要用到的时候,会从keys中再次放到Selectkey中。
这版代码解决了ServerSocketChannel的等待客户端连接阻塞,已经SocketChannel的读写阻塞。但是还存在一个问题,就是当客户端发的数据足够多,已经我们在读操作中设定的Buffer空间不够大时,会出现无法一次性读取客户端所有数据的情况。这种情况未读完的数据会被Seletor再次监控到,客户端只是进行了一次写入。

结论:当client连接服务器发起操作后,服务器必须全部处理完成,整个交互才算完成,如果没有全部处理完成,select方法会被一直调用。
在某些特殊清空下,客户端无法处理,select就会频繁调用。如:当客户端关闭的时候,会向服务器写入一个-1表示状态,并不是真正的数据,服务器接收到之后,并不会处理,所以这个状态一直存在,会导致select方法一直被调用。我们应该如何解决呢?如果服务器要解决一些没有用的,且必须要处理的,可以调用cacle方法。

网络中传输数据是按照包传递的,一个包的大小1460B

第四版代码
Seletor循环监听事件的方式 解决死循环空转的问题。
  public class MyServer2 {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8000));
        serverSocketChannel.configureBlocking(false);//Selector 只有在非阻塞的情况下 才可以使用。

        //引入监管者
        Selector selector = Selector.open();//1. 工厂,2. 单例

        //监管者 管理谁? selector.xxxx(ssc); //管理者 ssc  ---> Accept
        SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);
        // selector监控 SSC ACCEPT
        // selector
        //   keys --> HashSet
        //  register注册 ssc
        selectionKey.interestOps(SelectionKey.OP_ACCEPT);

        System.out.println("MyServler2.main");

        //监控
        while (true) {
            selector.select();//等待.只有监控到了 有实际的连接 或者 读写操作 ,才会处理。
            //对应的 有ACCEPT状态的SSC 和 READ WRITE状态的 SC 存起来
            // SelectionsKeys HashSet

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

            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {//ServerSocketChannel ScoketChannel
                SelectionKey key = iterator.next();
                //用完之后 就要把他从SelectedKeys集合中删除掉。问题? ServerScoketChannel---SelectedKeys删除 ,后续 SSC建立新的连接?
                iterator.remove();

                if (key.isAcceptable()) {
                    //serverSocketChannel.accept();
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel sc = channel.accept();
                    sc.configureBlocking(false);
                    //监控sc状态 ---> keys
                    SelectionKey sckey = sc.register(selector, 0, null);
                    sckey.interestOps(SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    try {
                        SocketChannel sc = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(5);
                        int read = sc.read(buffer);
                        if (read == -1) {
                            key.cancel();
                        } else {
                            buffer.flip();
                            System.out.println("Charset.defaultCharset().decode(buffer).toString() = " + Charset.defaultCharset().decode(buffer).toString());
                        }
                    } catch (IOException e) {
                        //发生异常后的处理。
                        e.printStackTrace();
                        key.cancel();
                    }
                }
            }

        }
    }
}

存在问题:当出现异常时,为了不影响其他线程,我们要手动处理异常。这版代码存在半包和粘包问题,我们可以用以前写的方法加入处理半包粘包问题,随之而来,又出现一个问题,会导致数据缺失,当出现粘包问题时,buffer.compact(),会保存未处理完的结果,但是这个buffer并没有被再次使用,我们每次都会新创建一个buffer。下面就要着重解决一个问题:如何保证多个操作ByteBuffer是同一个?
我们知道ByteBuffer是和Channel相关的,如果将Channel和ByteBuffer绑定在一起就行了。

第五版代码
public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8000));
        serverSocketChannel.configureBlocking(false);//只有在非阻塞的情况下,才可以用Seletor

        Selector selector = Selector.open();
        //将ServerSocketChannel注册到Seletor
        SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);
        //设置感兴趣的事件,监控的事件
        selectionKey.interestOps(SelectionKey.OP_ACCEPT);
        System.out.println("MyServer3.main");
        //监控
        while (true) {
            selector.select();//等待
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();//拿到监控的对象,SSC、SC
            System.out.println("------------------");
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()) {
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel sc = channel.accept();
                    sc.configureBlocking(false);
                    ByteBuffer buffer = ByteBuffer.allocate(7);
                    SelectionKey sck = sc.register(selector, 0, buffer);
                    sck.interestOps(SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    try {
                        SocketChannel sc = (SocketChannel) key.channel();
                        ByteBuffer buffer = (ByteBuffer) key.attachment();
//                        ByteBuffer buffer = ByteBuffer.allocate(20);
                        int read = sc.read(buffer);
                        if (read == -1) {
                            key.cancel();
                        }else {
                            /*buffer.flip();
                            System.out.println("Charset.defaultCharset().decode(buffer).toString() = " + Charset.defaultCharset().decode(buffer).toString());*/
                            doLineSplit(buffer);
                        }

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

    // ByteBuffer接受的数据 \n
    private static void doLineSplit(ByteBuffer buffer) {
        buffer.flip();
        for (int i = 0; i < buffer.limit(); i++) {
            if (buffer.get(i) == '\n') {
                //为了处理一行中有多个换行符,浪费空间的问题
                int length = i + 1 - buffer.position();
                ByteBuffer target = ByteBuffer.allocate(length);
                for (int j = 0; j < length; j++) {
                    target.put(buffer.get());
                }
                //截取工作完成
                target.flip();
                System.out.println("StandardCharsets.UTF_8.decode(target).toString() = " + StandardCharsets.UTF_8.decode(target).toString());
            }
        }
        buffer.compact();
    }

问题:上面的代码虽然解决了数据缺失问题,但是当客户端发送的数据大,或者我们的缓冲区太小,一句话中没有’\n’,这个时候需要扩容。但是扩容之后空间会变大(下一版代码),客户端发送的消息变小了,我们也要考虑缩容的问题(Netty)。ByteBuffer的拷贝,效率问题。

第六版代码
public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8000));
        serverSocketChannel.configureBlocking(false);//只有在非阻塞的情况下,才可以用Seletor

        Selector selector = Selector.open();
        //将ServerSocketChannel注册到Seletor
        SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);
        //设置感兴趣的事件,监控的事件
        selectionKey.interestOps(SelectionKey.OP_ACCEPT);
        System.out.println("MyServer3.main");
        //监控
        while (true) {
            selector.select();//等待
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();//拿到监控的对象,SSC、SC
            System.out.println("------------------");
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()) {
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel sc = channel.accept();
                    sc.configureBlocking(false);
                    ByteBuffer buffer = ByteBuffer.allocate(7);
                    SelectionKey sck = sc.register(selector, 0, buffer);
                    sck.interestOps(SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    try {
                        SocketChannel sc = (SocketChannel) key.channel();
                        ByteBuffer buffer = (ByteBuffer) key.attachment();
//                        ByteBuffer buffer = ByteBuffer.allocate(20);
                        int read = sc.read(buffer);
                        if (read == -1) {
                            key.cancel();
                        }else {
                            /*buffer.flip();
                            System.out.println("Charset.defaultCharset().decode(buffer).toString() = " + Charset.defaultCharset().decode(buffer).toString());*/
                            doLineSplit(buffer);
                            //缓冲区空间不够 满了 需要扩容
                            if (buffer.position()==buffer.limit()){
                                ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
                                //将原先的缓冲区数据写到新的
                                buffer.flip();
                                newBuffer.put(buffer);
                                //新的buffer和channel绑定 替换
                                key.attach(newBuffer);
                            }
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                        key.cancel();
                    }
                }
            }

        }
    }

上面的代码使用‘\n’区分不同信息,实际上还有其他方法解决半包粘包问题:头体分离
Java NIO学习笔记(全面详解)_第9张图片
Channel和buffer绑定到一起,可以解决粘包问题

SelectionKey sckey = sc.register(selector,0,buffer);
iterator.remove()
把用过的SelectionKeySeletionKeys集合中剔除
selectionKey.cancle();
把一些无法实际解决的内容,通过cancle()来取消。避免每一次都被select()获取到从新进行循环过程。
读取数据
1. 通过附件的形式,把byteBuffer和channel进行了绑定,从而可以多次处理数据。
2. ByteBuffer的扩容。
数据的写出 
1. 第一个问题 写一次数据,当发现数据没有写完,设置WRITE监听状态。
2. 每一次发生Write的状态,都把剩下的数据写出去。
5.3 Reactor模式
单线程版本

先来看一些图示,这就是Reactor单线程版本。
Java NIO学习笔记(全面详解)_第10张图片
服务端由一个线程去处理服务端的连接,以及客户端的读写。通过select()监听到不同的状态,以及Channel和Buffer绑定,完成客户端的读写操作,这就是Reactor单线程版本。
Reactor单线程模式有什么问题?由一个线程处理,效率肯定是很低的。下面是一些单线程版代码。
服务端

public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8000));

        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true){
            selector.select();
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()){
                SelectionKey sscKeys = iterator.next();
                iterator.remove();
                if (sscKeys.isAcceptable()){
                    SocketChannel sc = serverSocketChannel.accept();
                    sc.configureBlocking(false);
                    sc.register(selector,SelectionKey.OP_READ);
                    //准备数据
                    StringBuffer sb = new StringBuffer();
                    for (int i = 0; i < 2000000; i++) {
                        sb.append("l");
                    }
                    //NIO buffer存储数据 channel写
                    ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
                    while (buffer.hasRemaining()){
                        int write = sc.write(buffer);
                        System.out.println("write = " + write);
                    }
                }else if (sscKeys.isReadable()){

                }
            }
        }

    }

客户端

public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress(8000));


        int read = 0;
        while (true) {
            ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
            read += socketChannel.read(buffer);
            System.out.println("read = " + read);
            buffer.clear();
        }
    }

通过运行代码,发现以下结果:服务器向客户端发送的数据有很多空包,这就是在TCP传输过程中流量控制,服务端发送数据到客户端,发现客户端接收不过来,服务端就会发送一些空包,减轻客户端压力。
Java NIO学习笔记(全面详解)_第11张图片
站在服务端的角度,这种流量控制有问题吗?对于发送空数据操作,没有意义,但是也在发这种数据,导致有新的客户端连接进来,只能处于等待。
如何处理这种情况?就需要监控Writable状态,允许写的时候再写。

public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8000));

        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            selector.select();//selector注册 集合 SocketChannel
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey sscKey = iterator.next();
                iterator.remove();
                if (sscKey.isAcceptable()) {
                    SocketChannel sc = serverSocketChannel.accept();
                    sc.configureBlocking(false);
                    SelectionKey sckey = sc.register(selector, SelectionKey.OP_READ);
                    //准备数据
                    StringBuffer sb = new StringBuffer();
                    for (int i = 0; i < 20000000; i++) {
                        sb.append("s");
                    }
                    //NIO  Buffer存储数据 channel写
                    ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
                    int write = sc.write(buffer);
                    System.out.println("write = " + write);
                    if (buffer.hasRemaining()) {
                        //为当前的SoketChannel增加 Write的监听
                        //READ + Write
                        sckey.interestOps(sckey.interestOps() + SelectionKey.OP_WRITE);
                        //把剩余数据存储的buffer传递过去
                        sckey.attach(buffer);
                    }
                } else if (sscKey.isWritable()) {
                    //循环含义的
                    //channel
                    SocketChannel sc = (SocketChannel) sscKey.channel();
                    //buffer
                    ByteBuffer buffer = (ByteBuffer) sscKey.attachment();
                    //写操作
                    int write = sc.write(buffer);
                    System.out.println("write = " + write);
                    if (!buffer.hasRemaining()) {
                        sscKey.attach(null);
                        sscKey.interestOps(sscKey.interestOps() - SelectionKey.OP_WRITE);
                    }
                }
            }
        }
    }

Java NIO学习笔记(全面详解)_第12张图片
这个时候发现服务端不会再发空包给客户端了,减少了服务端的压力,从而可以腾出手来处理其他的线程请求。

主从版

主从式架构,主干活从也干活,但是干的是不一样的活。 和主备式架构的区别就是,主备式架构有备用的主,当主挂掉之后,备用的替上。Redis中的哨兵机制就是典型的主备架构。
Java NIO学习笔记(全面详解)_第13张图片

主从reactor模式
  
public class ReactorBossServer {

    private static final Logger log = LoggerFactory.getLogger(ReactorBossServer.class);

    public static void main(String[] args) throws IOException, InterruptedException {
        log.debug("boss thread start ....");

        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.bind(new InetSocketAddress(8000));

        Selector selector = Selector.open();
        ssc.register(selector, SelectionKey.OP_ACCEPT);

        //模拟多线程的环境,在实际开发中,还是要使用线程池
        /*
        Worker worker = new Worker("worker1");
        */

        Worker[] workers = new Worker[2];
        for (int i = 0; i < workers.length; i++) {
            workers[i] = new Worker("worker - " + i);//worker-0 worker-1
        }

        AtomicInteger index = new AtomicInteger();


        while (true) {
            selector.select();

            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey sscSelectionKey = iterator.next();
                iterator.remove();

                if (sscSelectionKey.isAcceptable()) {
                    SocketChannel sc = ssc.accept();
                    sc.configureBlocking(false);

                    //sc.register(selector, SelectionKey.OP_READ);
                    log.debug("boss invoke worker register ...");
                    //worker-0 worker-1 worker-0 worker-1
                    //hash取摸    x%2= 0  1 [0,1,0,1]
                    workers[index.getAndIncrement()% workers.length].register(sc);
                    log.debug("boss invoked worker register");
                }
            }
        }


    }
}

public class Worker implements Runnable {
    private static final Logger log = LoggerFactory.getLogger(Worker.class);
    private Selector selector;
    private Thread thread;
    private String name;

    private volatile boolean isCreated;//false

    private ConcurrentLinkedQueue<Runnable> runnables = new ConcurrentLinkedQueue<>();
    //构造方法

    //为什么不好?
    //Select Thread
    public Worker(String name) throws IOException {
        this.name = name;
       /* thread = new Thread(this, name);
        thread.start();
        selector = Selector.open();*/
    }

    //线程的任务
    public void register(SocketChannel sc) throws IOException, InterruptedException {
        log.debug("worker register invoke....");
        if (!isCreated) {
            thread = new Thread(this, name);
            thread.start();
            selector = Selector.open();
            isCreated = true;
        }
        runnables.add(() -> {
            try {
                sc.register(selector, SelectionKey.OP_READ);//reigster  select方法之前运行 。。
            } catch (ClosedChannelException e) {
                throw new RuntimeException(e);
            }
        });
        selector.wakeup();//select
    }

    @Override
    public void run() {
        while (true) {
            log.debug("worker run method invoke....");
            try {
                selector.select();

                Runnable poll = runnables.poll();
                if (poll != null) {
                    poll.run();
                }

                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey sckey = iterator.next();
                    iterator.remove();

                    if (sckey.isReadable()) {
                        SocketChannel sc = (SocketChannel) sckey.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(30);
                        sc.read(buffer);
                        buffer.flip();
                        String result = Charset.defaultCharset().decode(buffer).toString();
                        System.out.println("result = " + result);
                    }

                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

        }
    }
}

如果selector.select()运行在 sc.register(selector, SelectionKey.OP_READ)之前,那么线程将阻塞,就算后面注册了感兴趣的事件,select()也不会被唤醒。所以必须保证这两个代码在一个线程中运行,使用ConcurrentLinkedQueue队列,将代码传递过去。selector.wakeup()特点就是在之前阻塞可以唤醒,在之后阻塞也可以唤醒。

5.4 零拷贝的问题

Java JVM进程不能直接操作硬件,必须要借助操作系统来完成,另外操作硬件需要驱动,没有驱动无法操作硬件。这就注定Java借助操作系统(Linux、Windows、Mac)来操作硬件。
Java NIO学习笔记(全面详解)_第14张图片
应用程序所占用的内存属于用户内存空间,而操作系统运行在内核地址空间,这两种空间是相互独立,互不侵犯的,可以借助操作系统提供的API通信,所以与操作系统有关的资源,用完就关闭。
Java NIO学习笔记(全面详解)_第15张图片
JVM想读取文件到应用缓存,不能直接读取硬盘上的文件,实际上是由操作系统读取硬盘上的文件,数据存储在操作系统的内核空间(高速页缓存),随后操作系统的数据再传递到Java应用缓存。所以整个流程上,数据经过了两次数据拷贝。
当JVM想传输数据时,也不能直接写到网卡,因为中间隔着操作系统,只能先写到操作系统,然后由操作系统在写到网卡。
显而易见,这种操作效率很低,在NIO当中提供的一种方法内存映射来减少数据的拷贝。通过在操作系统开辟共享的内存空间,JVM可以直接操作操作系统中的内存,这就减少了一次数据拷贝。坏处就是我们需要用完内存之后手动析构,Java中GC不会对这块内存进行管理。不析构会带来内存泄露。
Java NIO学习笔记(全面详解)_第16张图片
Linux2.1提供了一个sendFile方法,可以直接将告诉页缓存中的文件拷贝到socket缓存,不再需要从JVM中拷贝到socket缓存,又减少了一次拷贝,这就是零拷贝,没有虚拟机参与的拷贝。
Java NIO学习笔记(全面详解)_第17张图片
在Linux2.4版本之后,又对拷贝做个改进,可以直接通过高速页缓存拷贝到网卡,不再拷贝到socket,又减少了一次拷贝。
Java NIO学习笔记(全面详解)_第18张图片

你可能感兴趣的:(java,nio,学习,Netty)