Netty笔记 上----学习孙哥&不良人课程,整理学习笔记文章

88 阅读18分钟

引言

什么是Netty

Netty是一个异步事件驱动的网络应用框架。 用于快速开发可维护的高性能协议服务器和客户端。 Netty是一个NIO客户服务器框架,它能够快速和容易地开发网络应用,如协议服务器和客户端。它大大简化和精简了网络编程,如TCP和UDP套接字服务器。 快速和简单 "并不意味着开发出来的应用程序会出现可维护性或性能问题。Netty的设计是经过精心设计的,其经验来自于许多协议的实施,如FTP、SMTP、HTTP以及各种基于二进制和文本的遗留协议。因此,Netty成功地找到了一种方法来实现开发的简易性、性能、稳定性和灵活性,而没有任何妥协。

为什么要学习Netty

  1. Netty是行业网络通信编程的标准,广泛应用于通信领域和很多其他中间件技术的底层
  2. 应用非常广泛
    1. 互联网行业:
      1. 在分布式系统中,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用
      2. 典型的应用有:阿里分布式服务框架 Dubbo 的 RPC 框 架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现各进程节点之间的内部通信。
    2. 游戏行业
      1. Netty 作为高性能的基础通信组件,提供了 TCP/UDP 和 HTTP 协议栈,方便定制和开发私有协议栈,账号登录服务器。
      2. 地图服务器之间可以方便的通过 Netty 进行高性能的通信。
    3. 大数据行业:
      1. 经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨界点通信
      2. 它的 NettyService 基于 Netty 框架二次封装实现。

Java BIO编程

I/O模型

  1. I/O模型就是:用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。
  2. Java 支持 3 种网络编程模型 I/O模式 : BIO,NIO,AIO。
  3. Java BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。 如图所示

  1. Java NIO:同步非阻塞,服务器实现式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理。【简单示意图】

  1. Java AIO(NIO.2) :异步非阻塞,AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。

BIO,NIO,AIO使用场景

  1. BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序简单易理解。
  2. NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4 开始支持。
  3. AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。

Java BIO介绍

  1. Java BIO 就是传统的 Java I/O 编程,其相关的类和接口在 java.io。
  2. BIO(BlockingI/O):同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。【后有应用实例】
  3. BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,程序简单易理解。

Java BIO工作机制

对 BIO 编程流程的梳理

  1. 服务器端启动一个 ServerSocket。
  2. 客户端启动 Socket 对服务器进行通信,默认情况下服务器端需要对每个客户建立一个线程与之通讯。
  3. 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝。
  4. 如果有响应,客户端线程会等待请求结束后,在继续执行。

Java BIO应用实例

  1. 使用 BIO 模型编写一个服务器端,监听 8080 端口,当有客户端连接时,就启动一个线程与之通讯。
  2. 要求使用线程池机制改善,可以连接多个客户端。
  3. 服务器端可以接收客户端发送的数据(telnet 方式即可)。
package com.huy.bio;

import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BIOServer {

    public static void main(String[] args) throws Exception {
        //线程池机制
        //思路
        //1. 创建一个线程池
        //2. 如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        //创建ServerSocket
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("服务器启动了");
        while (true) {
            System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
            //监听,等待客户端连接
            System.out.println("等待连接....");
            final Socket socket = serverSocket.accept();
            System.out.println("连接到一个客户端");
            //就创建一个线程,与之通讯(单独写一个方法)
            newCachedThreadPool.execute(new Runnable() {
                public void run() {//我们重写
                    //可以和客户端通讯
                    handler(socket);
                }
            });
        }
    }

    //编写一个handler方法,和客户端通讯
    public static void handler(Socket socket) {
        try {
            System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
            byte[] bytes = new byte[1024];
            //通过socket获取输入流
            InputStream inputStream = socket.getInputStream();
            //循环的读取客户端发送的数据
            while (true) {
                System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
                System.out.println("read....");
                int read = inputStream.read(bytes);
                if (read != -1) {
                    System.out.println(new String(bytes, 0, read));//输出客户端发送的数据
                } else {
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("关闭和client的连接");
            try {
                socket.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

Java BIO问题分析

  1. 每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write。
  2. 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
  3. 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费。

多线程网络编程

线程池版网络编程

NIO编程

NIO全程:None Blocking IO (非阻塞IO) 非阻塞:主要应用在网络通信中,能够合理利用资源,提高系统的并发效率。支持高并发的系统访问 NIO 有三大核心部分: Channel(通道)、Buffer(缓冲区)、Selector(选择器)

Channel简介

  1. IO通信的通道,类似于InputStream、OutputStream
  2. Channel没有方向性

  • ** 常见的Channel:**
  1. 文件操作:FileChannel,读写文件中的数据。
  2. 网络操作:
    1. SocketChannel,通过TCP读写网络中的数据。
    2. ServerSockectChannel,监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
    3. DatagramChannel,通过UDP读写网络中的数据。
  • 获取Channel的方式
  1. FileInputStreanm/FileOutputStream
FileChannel channel = new FileInputStream("data.txt").getChannel();
FileChannel channel = new FileOutputStream("data2.txt").getChannel();
  1. RandomAccessFile
  2. Socket
SocketChannel socketChannel=SocketChannel.open();
  1. ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
  1. DatagramSocket

应用案例1-本地文件写操作

实例要求:

  1. 使用前面学习后的 ByteBuffer(缓冲)和 FileChannel(通道),将 "hello,尚硅谷" 写入到 file01.txt 中
  2. 文件不存在就创建
  3. 代码演示
public class NIOFileChannel01 {
   public static void main(String[] args) throws IOException {

      String str="hello NIOFileChannel01";
      FileOutputStream fileOutputStream=new FileOutputStream("text.txt");
      FileChannel channel = fileOutputStream.getChannel();
      ByteBuffer byteBuffer= Charset.defaultCharset().encode(str);

      channel.write(byteBuffer);
       channel.close();
      fileOutputStream.close();
   }

}

应用案例2-本地文件读操作

实例要求:

  1. 使用前面学习后的 ByteBuffer(缓冲)和 FileChannel(通道),将 file01.txt 中的数据读入到程序,并显示在控制台屏幕
  2. 假定文件已经存在
  3. 代码演示
public class NIOFileChannel02 {
   public static void main(String[] args) throws IOException {
      FileInputStream fileInputStream=new FileInputStream("text.txt");
      FileChannel channel = fileInputStream.getChannel();
      ByteBuffer buffer=ByteBuffer.allocate(64);
      channel.read(buffer);
      buffer.flip();
      System.out.println(Charset.defaultCharset().decode(buffer));
      channel.close();
      fileInputStream.close();

   }
}

应用案例3-使用一个Buffer完成文件读取,写入

  1. 使用 FileChannel(通道)和方法 read、write,完成文件的拷贝
  2. 拷贝一个文本文件 1.txt,放在项目下即可
  3. 代码演示

image.png

public class NIOFileChannel03 {
   public static void main(String[] args) throws IOException {
      FileInputStream fileInputStream=new FileInputStream("text.txt");
      FileOutputStream fileOutputStream=new FileOutputStream("text_copy.txt");
      FileChannel channel1 = fileInputStream.getChannel();
      FileChannel channel2 = fileOutputStream.getChannel();
      ByteBuffer buffer=ByteBuffer.allocate(64);

      while (true){
         buffer.clear();
         int read = channel1.read(buffer);
         if(read==-1){
            break;
         }
         buffer.flip();
         channel2.write(buffer);
      }
      channel2.close();
      channel1.close();
      fileOutputStream.close();
      fileInputStream.close();


   }
}

应用案例3-拷贝文件transferFrom 方法

  1. 实例要求:
  2. 使用 FileChannel(通道)和方法 transferFrom,完成文件的拷贝
  3. 拷贝一张图片
  4. 代码演示
public class NIOFileChannel04 {
   public static void main(String[] args) throws IOException {
      FileInputStream fileInputStream=new FileInputStream("myp.png");
      FileOutputStream fileOutputStream=new FileOutputStream("myp_copy.png");
      FileChannel channel1 = fileInputStream.getChannel();
      FileChannel channel2 = fileOutputStream.getChannel();

       //从 0 开始 copy channel1.size()个
      channel2.transferFrom(channel1,0,channel1.size());

      channel2.close();
      channel1.close();
      fileOutputStream.close();
      fileInputStream.close();
   }
}

Buffer 和 Channel的 注意事项和细节

  1. ByteBuffer 支持类型化的 put 和 get,put 放入的是什么数据类型,get 就应该使用相应的数据类型来取出,否则可能有 BufferUnderflowException 异常。【举例说明】
public class NIOByteBufferPutGet {

    public static void main(String[] args) {
        
        //创建一个 Buffer
        ByteBuffer buffer = ByteBuffer.allocate(64);

        //类型化方式放入数据
        buffer.putInt(100);
        buffer.putLong(9);
        buffer.putChar('尚');
        buffer.putShort((short) 4);

        //取出
        buffer.flip();
        
        System.out.println();
        
        System.out.println(buffer.getInt());
        System.out.println(buffer.getLong());
        System.out.println(buffer.getChar());
        System.out.println(buffer.getShort());
    }
}
  1. 可以将一个普通 Buffer 转成只读 Buffer【举例说明】
public class ReadOnlyBuffer {

    public static void main(String[] args) {

        //创建一个 buffer
        ByteBuffer buffer = ByteBuffer.allocate(64);

        for (int i = 0; i < 64; i++) {
            buffer.put((byte) i);
        }

        //读取
        buffer.flip();

        //得到一个只读的 Buffer
        ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
        System.out.println(readOnlyBuffer.getClass());

        //读取
        while (readOnlyBuffer.hasRemaining()) {
            System.out.println(readOnlyBuffer.get());
        }

        readOnlyBuffer.put((byte) 100); //ReadOnlyBufferException
    }
}
  1. NIO 还提供了 MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件由 NIO 来完成。【举例说明】
/**
 * 说明 1.MappedByteBuffer 可让文件直接在内存(堆外内存)修改,操作系统不需要拷贝一次
 */
public class MappedByteBufferTest {

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

        RandomAccessFile randomAccessFile = new RandomAccessFile("data.txt", "rw");
        //获取对应的通道
        FileChannel channel = randomAccessFile.getChannel();

        /**
         * 参数 1:FileChannel.MapMode.READ_WRITE 使用的读写模式
         * 参数 2:0:可以直接修改的起始位置
         * 参数 3:5: 是映射到内存的大小(不是索引位置),即将 1.txt 的多少个字节映射到内存
         * 可以直接修改的范围就是 0-5
         * 实际类型 DirectByteBuffer
         */
        MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);

        mappedByteBuffer.put(0, (byte) 'I');
        mappedByteBuffer.put(1, (byte) 'K');
        mappedByteBuffer.put(2, (byte) 'U');
        mappedByteBuffer.put(3, (byte) 'N');//IndexOutOfBoundsException

        randomAccessFile.close();
        System.out.println("修改成功~~");
    }
}

Buffer

基本介绍

:::info 缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer,如图:【后面举例说明】 :::

Buffer 类以及子类

  1. 在 NIO 中,Buffer 是一个顶层父类,它是一个抽象类,类的层级关系图:

image.png

ByteBuffer详解

:::info ByteBuffer时抽象类,他的主要实现类为

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

获取方法:

  1. ByteBuffer.allocate(10);//一旦分配空间,不可以动态调整
  2. encode()

核心结构: ByteBuffer是一个类似数组的结构,整个结构中包含三个主要的状态

  1. Capacity buffer的容量,类似于数组的size

  2. Position buffer当前缓存的下标,在读取操作时记录读到了那个位置,在写操作时记录写到了那个位置。从0开始,每读取一次,下标+1

  3. Limit 读写限制,在读操作时,设置了你能读多少字节的数据,在写操作时,设置了你还能写多少字节的数据

所谓的读写模式,本质上就是这几个状态的变化。主要有Position和Limit联合决定了Buffer的读写数据区域。 ::: :::info 总结: 写入Buffer数据之前要设置写模式

  1. 写模式
    1. 新创建的Buffer自动是写模式
    2. 调用了clear,compact方法

读取Buffer数据之前要设置读模式 2. 读模式

  1. 调用flip方法 :::

核心API

  • buffer中写入数据[写模式 创建一个bytebuffer ,clear(),compact()] :::info
  1. channel的read方法 channel.read(buffer)
  2. buffer的put方法 buffer.put(byte) buffer.put((byte)'a').. buffer.put(byte[]) :::
  • 从buffer中读出数据 :::info
  1. channel的write方法

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

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

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

  5. get(i) 方法,获取特定position上的数据,但是不会对position的位置产生影响。 :::

字符串操作

字符串存储到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());

半包粘包

:::info 处理方法:

  1. \n 作为分割符,进行行的区分。
  2. compact进行处理,把第一次没有读取完的数据,向前移动和后面的内容进行整合。 :::
//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();
    }
}

Selector(选择器)

基本介绍

  1. Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)。
  2. Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。【示意图】
  3. 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
  4. 避免了多线程之间的上下文切换导致的开销。

Selector 示意图和特点

:::info 特点:

  1. Netty的IO线程NioEventLoop 聚合了Selector(选择器,也就多路复用器),可以同时并发处理成百上千个客户端连接
  2. 当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务
  3. 线程通常将非阻塞IO 的空闲时间用于其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道
  4. 由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂起。
  5. 一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。 :::

Selector类相关方法

image.png 其中 :::info keys(选择键): 在 Selector 中,keys 是一个 Set 集合,包含了所有已经注册到 Selector 上的通道和与之相关的信息。每个 key 对应一个通道,它包含了关于通道和通道所关联事件的信息。 通过 keys 集合,你可以遍历所有已注册的通道,检查它们的状态以及关联的事件。 selectorKeys(选择器键): selectorKeys 是一个在 Selector 上注册的通道与其关联事件的表示。每个 selectorKey 包含了与特定通道关联的信息,包括所关注的事件和相关的通道。 当 Selector 检测到某个通道准备好执行某种 I/O 操作时,它会返回一组 selectorKeys,以便应用程序可以确定哪些通道已经准备好处理 I/O。 :::

注意事项

:::info

  1. NIO 中的 ServerSocketChannel 功能类似 ServerSocket、SocketChannel 功能类似 Socket。
  2. Selector 相关方法说明
  • selector.select(); //阻塞
  • selector.select(1000); //阻塞 1000 毫秒,在 1000 毫秒后返回
  • selector.wakeup(); //唤醒 selector
  • selector.selectNow(); //不阻塞,立马返还 :::

NIO网络编程

:::info

  1. 服务端 接受请求 ---》 ServerScoketChannel

  2. 进行实际通信 ScoketChannel :::

通过这版代码 证明了 服务器端 存在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("通信已经结束....");

            }
        }

    }
}
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();
                    }
                }
            }

        }


    }
}

:::info iterator.remove() 把用过的SelectionKey从SeletionKeys集合中剔除

selectionKey.cancle(); 把一些无法实际解决的内容,通过cancle()来取消。避免每一次都被select()获取到从新进行循环过程。

读取数据

  1. 通过附件的形式,把byteBuffer和channel进行了绑定,从而可以多次处理数据。
  2. ByteBuffer的扩容。

数据的写出

  1. 第一个问题 写一次数据,当发现数据没有写完,设置WRITE监听状态。
  2. 每一次发生Write的状态,都把剩下的数据写出去。 :::
  • 主从Reactor模式
主从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);
            }

        }
    }
}

零拷贝问题