Netty

Netty

Netty介绍和应用场景

Netty的介绍

  1. Netty 是由 JBOSS 提供的一个 Java 开源框架,现为 Github上的独立项目。
  2. Netty 是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可 靠性的网络 IO 程序
  3. Netty主要针对在TCP协议下,面向Clients端的高并发应用,或者Peer-to-Peer(P2P)场景下 的大量数据持续传输的应用。
  4. Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景
  5. 要透彻理解Netty , 需要先学习 NIO , 这样我们才能阅读 Netty 的源码

Netty的应用场景

  • 互联网行业

    1. 互联网行业:在分布式系统中,各个 节点之间需要远程服务调用,高性能 的 RPC 框架必不可少,Netty 作为异步 高性能的通信框架,往往作为基础通 信组件被这些 RPC 框架使用
    2. 典型的应用有:阿里分布式服务框架 Dubbo 的 RPC 框架使用 Dubbo 协议进 行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现各 进程节点之间的内部通信
  • 游戏行业

    1. 无论是手游服务端还是大型的网络游戏, Java 语言得到了越来越广泛的应用
    2. Netty 作为高性能的基础通信组件,提 供了 TCP/UDP 和 HTTP 协议栈,方便定制和开发私有协议栈,账号登录服务器
    3. 地图服务器之间可以方便的通过 Netty 进行高性能的通信
  • 大数据领域

    1. 经典的 Hadoop 的高性能通信和 序列化组件 Avro 的 RPC 框架, 默认采用 Netty 进行跨界点通信
    2. 它的 Netty Service 基于 Netty 框 架二次封装实现。
  • 其它开源项目使用到Netty

    1. 网址: https://netty.io/wiki/related-projects.html

Netty的学习参考资料

  • 《Netty In Action》
  • 《Netty 权威指南》

Java BIO编程

I/O模型

I/O 模型基本说明

  1. I/O 模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能
  2. Java共支持3种网络编程模型/IO模式:BIO、NIO、AIO
  3. Java BIO : 同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端 有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成 不必要的线程开销
  4. Java NIO : 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理
  5. 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 io 编程,其相关的类和接口在 java.io
  2. BIO(blocking I/O) : 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连 接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造 成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。
  3. BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高, 并发局限于应用中,JDK1.4以前的唯一选择,程序简单易理解

Java BIO 工作机制

  • 工作原理图

    Scoket
    Scoket
    Scoket
    read/write
    read/write
    read/write
    Thread
    Thread
    Thread
  • BIO编程简单流程

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

应用程序比如浏览器、电子邮件、文件传输服务器等产生的数据,会通过传输层协议进行传输。而应用程序是不会和传输层直接建立联系的,而是有一个能够连接应用层和传输层之间的套件,这个套件就是 Socket

Java BIO 应用实例

  • 实例说明
  1. 使用BIO模型编写一个服务器端,监听6666端口,当有客户端连接时,就启动一个线程与之通讯
  2. 要求使用线程池机制改善,可以连接多个客户端
  3. 服务器端可以接收客户端发送的数据(telnet 方式即)
1.什么是Telnet?
  对于Telnet的认识,不同的人持有不同的观点,可以把Telnet当成一种通信协议,但是对于入侵者而言,Telnet只是一种远程登录的工具。一旦入侵者与远程主机建立了Telnet连接,入侵者便可以使用目标主机上的软、硬件资源,而入侵者的本地机只相当于一个只有键盘和显示器的终端而已。
2.为什么需要telnet?
  telnet就是查看某个端口是否可访问。我们在搞开发的时候,经常要用的端口就是 8080。那么你可以启动服务器,用telnet 去查看这个端口是否可用。
  Telnet协议是TCP/IP协议家族中的一员,是Internet远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。在终端使用者的电脑上使用telnet程序,用它连接到服务器。终端使用者可以在telnet程序中输入命令,这些命令会在服务器上运行,就像直接在服务器的控制台上输入一样。可以在本地就能控制服务器。要开始一个telnet会话,必须输入用户名和密码来登录服务器。Telnet是常用的远程控制Web服务器的方法。
3.Windows XP怎么执行telnet 命令?
	1)、点击开始 → 运行 → 输入CMD,回车。
	2)、在出来的DOS界面里,输入telnet测试端口命令: telnet IP 端口 或者 telnet 域名 端口,回车。
  如果端口关闭或者无法连接,则显示不能打开到主机的链接,链接失败;端口打开的情况下,链接成功,则进入telnet页面(全黑的),证明端口可用。
  Telnet 客户端命常用命令:
  open : 使用 openhostname 可以建立到主机的 Telnet 连接。
  close : 使用命令 close 命令可以关闭现有的 Telnet 连接。
  display : 使用 display 命令可以查看 Telnet 客户端的当前设置。
  send : 使用 send 命令可以向 Telnet 服务器发送命令。支持以下命令:
  ao : 放弃输出命令。
  ayt : “Are you there”命令。
  esc : 发送当前的转义字符。
  ip : 中断进程命令。
  synch : 执行 Telnet 同步操作。
  brk : 发送信号。
  上表所列命令以外的其他命令都将以字符串的形式发送至 Telnet 服务器。例如,sendabcd 将发送字符串 abcd 至 Telnet 服务器,这样,Telnet 会话窗口中将出现该字符串。
  quit :使用 quit 命令可以退出 Telnet 客户端。
  
windows10 中telnet默认是关闭的,需要打开才能使用
控制面板 -> 程序 -> 启动或关闭Windows功能 -> 勾选 Telnet客户端 后点击确定
如果 Telnet客户端 依旧无法使用或无法建立多个连接,则需重启计算机

使用步骤
1. window+r —> cmd —> telnet回车 —> 输入quit退出telnet模式
2. telnet IP PORT -> 连接成功后会跳到Tenlet控制页面(全黑,啥也看不到),输入crlt+] 进入命令模式 -> send hello world -> 退出Telnet控制台,quit指令
/**
 * 线程工厂
 *
 * @author 
 * @since 
 */
public class ThreadFactoryBuild {

    private final String NUM = "%d";

    private final String STR = "%s";

    /**
     * %d 数字
     * %s 字符串
     */
    private String nameType = STR;

    private String name;

    private final ThreadFactory nameThreadFactory;

    public ThreadFactoryBuild() {
        nameThreadFactory = r -> {
            if (STR.equals(nameType)) {
                name = UUID.randomUUID().toString().replace("-", "");
            } else {
                name = String.valueOf(System.currentTimeMillis());
            }
            return new Thread(r, name);
        };
    }

    public ThreadFactoryBuild setName(String nameType) {
        this.nameType = nameType;
        return this;
    }

    public ThreadFactory build() {
        return this.nameThreadFactory;
    }
}

/**
 * BIO 服务器
 *
 * @author 
 * @since 
 */
public class BioService {
    /**
     * 自定义线程名称,方便出错的时候朔源
     */
    private static final ThreadFactory NAME_THREAD_FACTORY = new ThreadFactoryBuild().setName("%s").build();

    /**
     * 编码集
     */
    private static final Charset GBK = Charset.forName("GBK");

    /**
     * 引导程序
     *
     * @param port 端口号
     */
    public static void bootstrap(int port) throws IOException {
        // 思路
        // 1. 创建一个线程池
        // 2. 如果有客户端链接就创建一个线程与之通讯(单独写一个方法)
        // 获取CPU核数
        int processors = Runtime.getRuntime().availableProcessors();
        System.out.println("CPU cores: " + processors);
        // 线程池机制
        ThreadPoolExecutor pool = new ThreadPoolExecutor(processors, processors, 0L, TimeUnit.MICROSECONDS, new LinkedBlockingQueue<>(), NAME_THREAD_FACTORY);
        // 创建ServerSocket
        ServerSocket serverSocket = new ServerSocket(port);
        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("连接到一个客户端");
            // 创建一个线程与之通讯(单独写一个方法)
            pool.execute(() -> {
                handler(socket);
            });
        }
    }

    /**
     * 与客户端通讯处理
     *
     * @param socket 套接字
     */
    private static void handler(Socket socket) {
        long id = Thread.currentThread().getId();
        try {
            System.out.println("线程信息 id = " + id + " 名称 = " + Thread.currentThread().getName());
            byte[] buff = new byte[1024];
            // 通过socket获取输入流
            InputStream inputStream = socket.getInputStream();
            // 循环读取客户端发送数据
            while (true) {
                System.out.println("id = " + id + "=>read...");
                int read = inputStream.read(buff);
                if (read != -1) {
                    // 输出客户端发送数据
                    System.out.println("id = " + id + "=>" + new String(buff, 0, read, GBK));
                } else {
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            System.out.println("id = " + id + "=>关闭与client的连接");
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * BIO 启动类
 *
 * @author 
 * @since 
 */
public class BioStudy {
    public static void main(String[] args) throws IOException {
        BioService.bootstrap(6666);
    }
}

建立多个连接,当线程池线程用完后,多出的连接无法收到消息

Java BIO 问题分析

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

Java NIO编程

Java NIO 基本介绍

  1. Java NIO 全称 java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出 的新特性,被统称为 NIO(即 New IO),是同步非阻塞
  2. NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。
  3. NIO 有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
  4. NIO是 面向缓冲区 ,或者面向 块 编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就 增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络
  5. Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得 到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线 程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞 写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情
  6. 通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来, 根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,非得分配10000个
  7. HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级

案例说明NIO Buff

public class BasicBuffer {
    public static void main(String[] args) {
        // 举例说明Buffer的使用(简单说明)
        // 创建一个Buffer, 大小为5,即可以存放5个int
        IntBuffer buffer = IntBuffer.allocate(5);
        // 向buffer存放数据 <code>buffer.put(5) | buffer.put(2) | buffer.put(0)</code>
        for (int i = 0; i < buffer.capacity(); i++) {
            buffer.put(i * 2);
        }
        // 如何从buffer读取数据
        // 将 buffer 转换, 读写切换(!!!)
        buffer.flip();
        while (buffer.hasRemaining()) {
            System.out.println(buffer.get());
        }
    }
}

NIO 和 BIO 的比较

  1. BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多
  2. BIO 是阻塞的,NIO 则是非阻塞的
  3. BIO基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进 行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。 Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道

NIO 三大核心原理示意

  • Selector 、Channel 和 Buffer 的关系图(简单版)
Selector
Channel
Channel
Channel
Buffer
Buffer
Buffer
程序
程序
程序
Thread
  • 关系图的说明
    1. 每个channel 都会对应一个Buffer
    2. Selector 对应一个线程, 一个线程对应多个channel(连接)
    3. 该图反应了有三个channel 注册到 该selector
    4. 程序切换到哪个channel 是有事件决定的, Event 就是一个重要的概念
    5. Selector 会根据不同的事件,在各个通道上切换
    6. Buffer 就是一个内存块 , 底层是有一个数组
    7. 数据的读取写入是通过Buffer, 这个和BIO不同 , BIO 中要么是输入流,或者是 输出流, 不能双向,但是NIO的Buffer 是可以读也可以写, 需要 flip 方法切换
    8. channel 是双向的, 可以返回底层操作系统的情况, 比如Linux ,底层的操作系统 通道就是双向的

缓冲区(Buffer)

基本介绍

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

data
Channel
NIO程序
缓冲区
文件

Buffer 类及其子类

  1. 在 NIO 中,Buffer 是一个顶层父类,它是一个抽象类(类的层级关系查看 IDEA Buffer类 按 Ctrl + H)

    • 常用Buffer子类一览
      1. ByteBuffer,存储字节数据到缓冲区
      2. ShortBuffer,存储短整型数据到缓冲区
      3. CharBuffer,存储字符数据到缓冲区
      4. IntBuffer,存储整数数据到缓冲区
      5. LongBuffer,存储长整型数据到缓冲区
      6. DoubleBuffer,存储双精度小数到缓冲区
      7. FloatBuffer,存储单精度小数到缓冲区
    1. Buffer类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素 的信息:
属性 描述
Capacity 容量,即可以容纳的最大数据量;在缓 冲区创建时被设定并且不能改变
Limit 表示缓冲区的当前终点,不能对缓冲区 超过极限的位置进行读写操作。且极限 是可以修改的
Position 位置,下一个要被读或写的元素的索引, 每次读写缓冲区数据时都会改变改值, 为下次读写作准备
Mark 标记
// 可自己 debug 对着源码查看
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity
/*
	position (相当于一个指针)
buffer 刚创建时
	capacity = 5 // 不变
	limit = 5//不能对缓冲区超过极限的位置limit进行读写操作
*/
  1. Buffer类相关方法 一览

    public abstract class Buffer {
    	//JDK1.4时,引入的api
        //返回此缓冲区的容量
    	public final int capacity();
        
        //返回此缓冲区的位置
    	public final int position();
        
        //设置此缓冲区的位置
    	public final Buffer position (int newPositio);
        
        //返回此缓冲区的限制
    	public final int limit();
        
        //设置此缓冲区的限制
    	public final Buffer limit (int newLimit);
            
        //在此缓冲区的位置设置标记
    	public final Buffer mark();
        
        //将此缓冲区的位置重置为以前标记的位置
    	public final Buffer reset();
        
        //清除此缓冲区, 即将各个标记恢复到初始状态,但是数据并没有真正擦除, 后面操作会覆盖
    	public final Buffer clear();
        
        //反转此缓冲区
    	public final Buffer flip();
        
        //重绕此缓冲区
    	public final Buffer rewind();
        
        //返回当前位置与限制之间的元素数
    	public final int remaining();
        
        //告知在当前位置和限制之间是否有元素
    	public final boolean hasRemaining();
        
        //告知此缓冲区是否为只读缓冲区
    	public abstract boolean isReadOnly();
        
    	//JDK1.6时引入的api
        //告知此缓冲区是否具有可访问的底层实现数组
    	public abstract boolean hasArray();
        
        //返回此缓冲区的底层实现数组
    	public abstract Object array();
        
        //返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量
    	public abstract int arrayOffset();
        
        //告知此缓冲区是否为直接缓冲区
    	public abstract boolean isDirect();
    }
    

ByteBuffer

从前面可以看出对于 Java 中的基本数据类型(boolean除外),都有一个 Buffer 类型与之 相对应,最常用的自然是ByteBuffer 类(二进制数据),该类的主要方法如下 (IDEA 找到 ByteBuffer 类 按 Alt+ 7 或者 Ctrl + F12)

public abstract class ByteBuffer {
	//缓冲区创建相关api
    //创建直接缓冲区
	public static ByteBuffer allocateDirect(int capacity);
    
    //设置缓冲区的初始容量
	public static ByteBuffer allocate(int capacity);
    
    //把一个数组放到缓冲区中使用
	public static ByteBuffer wrap(byte[] array);
    
	//构造初始化位置offset和上界length的缓冲区
	public static ByteBuffer wrap(byte[] array,int offset, int length);
    
	//缓存区存取相关API
    //从当前位置position上get,get之后,position会自动+1
	public abstract byte get();
    
    //从绝对位置get
	public abstract byte get (int index);
    
    //从当前位置上添加,put之后,position会自动+1
	public abstract ByteBuffer put (byte b);
    
    //从绝对位置上put
	public abstract ByteBuffer put (int index, byte b);
}

allocate(int capacity) ,allocateDirect(int capacity)

为什么要提供两种方式呢?这与Java的内存使用机制有关。第一种分配方式产生的内存开销是在JVM中的,而另外一种的分配方式产生的开销在JVM之外,以就是系统级的内存分配。当Java程序接收到外部传来的数据时,首先是被系统内存所获取,然后在由系统内存复制复制到JVM内存中供Java程序使用。所以在另外一种分配方式中,能够省去复制这一步操作,效率上会有所提高。可是系统级内存的分配比起JVM内存的分配要耗时得多,所以并非不论什么时候allocateDirect的操作效率都是最高的.

通道(Channel)

基本介绍

  1. NIO的通道类似于流,但有些区别如下

    • 通道可以同时进行读写,而流只能读或者只能写

    • 通道可以实现异步读写数据

    • 通道可以从缓冲读数据,也可以写数据到缓冲:

      Channel
      Buffer
  2. BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道 (Channel)是双向的,可以读操作,也可以写操作

  3. Channel在NIO中是一个接口 public interface Channel extends Closeable{}

  4. 常用的 Channel 类有:FileChannel、 DatagramChannel、ServerSocketChannel 和 SocketChannel。【ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket】

  5. FileChannel 用于文件的数据读写, DatagramChannel 用于 UDP 的数据读写, ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写。

FileChannel 类

FileChannel主要用来对本地文件进行 IO 操作,常见的方法

  1. public int read(ByteBuffer dst) ,从通道读取数据并放到缓冲区中
  2. public int write(ByteBuffer src) ,把缓冲区的数据写到通道中
  3. public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道
  4. public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道

应用实例1-本地文件写数据

实例要求:

  1. 使用前面学习后的ByteBuffer(缓冲) 和 FileChannel(通道), 将 "hello,你好呀" 写入 到file01.txt 中

  2. 文件不存在就创建

  3. 代码演示

    public class Demo01 {
        public static void main(String[] args) throws IOException {
            String str = "hello,你好呀";
            // 创建一个输出流 channel
            String path = "E:\\JavaProject\\study\\netty-study\\nio-study\\src\\main\\resources\\file";
            File file = new File(path);
            if (!file.exists()) {
                boolean mkdir = file.mkdirs();
                System.out.println("创建文件夹:" + mkdir);
            }
            file = new File(file, "file01.txt");
            FileOutputStream fos = new FileOutputStream(file);
            // 通过 fileOutStream 获取对应的 FileChannel
            // 这个 FileChannel 真实的类型是 FileChannelImpl
            FileChannel fileChannel = fos.getChannel();
            // 创建缓冲区 ByteBuffer
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            // 将str放入byteBuffer
            byteBuffer.put(str.getBytes(StandardCharsets.UTF_8));
            // 将byteBuffer进行flip
            byteBuffer.flip();
            // 将 byteBuffer 数据写入到 fileChannel
            fileChannel.write(byteBuffer);
            fileChannel.close();
            fos.close();
        }
    }
    

应用实例2-本地文件读数据

实例要求:

​ 1. 使用前面学习后的ByteBuffer(缓冲) 和 FileChannel(通道), 将 file01.txt 中的数据读入到程序,并显示在控制台屏幕

​ 2.假定文件已经存在

  1. 代码演示

    public class Demo02 {
        public static void main(String[] args) throws IOException {
            // 创见文件输入流
            String path = "E:\\JavaProject\\study\\netty-study\\nio-study\\src\\main\\resources\\file\\file01.txt";
            FileInputStream fis = new FileInputStream(path);
            // 通过文FileInputStream 获取 FileChannel -> 实际类型 FileChannelImpl
            FileChannel channel = fis.getChannel();
            // 创建缓存区
            ByteBuffer byteBuffer = ByteBuffer.allocate(fis.available());
            // 将通道数据读入到 Buffer
            channel.read(byteBuffer);
            // 将byteBuffer 字节数据转换为 String
            System.out.println(new String(byteBuffer.array(), StandardCharsets.UTF_8));
            channel.close();
            fis.close();
        }
    }
    

应用实例3-使用一个Buffer完成文件读取

实例要求:

  1. 使用 FileChannel(通道) 和 方法 read , write,完成文件的拷贝

  2. 拷贝一个文本文件 1.txt , 放在项目下即可

  3. 代码演示

    public class Demo03 {
        public static void main(String[] args) throws IOException {
            String dirPath = "E:\\JavaProject\\study\\netty-study\\nio-study\\src\\main\\resources\\file";
            FileInputStream fis = new FileInputStream(new File(dirPath, "file01.txt"));
            FileChannel inChannel = fis.getChannel();
            FileOutputStream fos = new FileOutputStream(new File(dirPath, "file02.txt"));
            FileChannel outChannel = fos.getChannel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // 循环读取
            while (true) {
                // 这里一个很重要的操作不要忘了
                /**
                 * public final Buffer clear() {
                 *     position = 0;
                 *     limit = capacity;
                 *     mark = -1;
                 *     return this;
                 * }
                 */
                buffer.clear();
                int read = inChannel.read(buffer);
                System.out.println("read = " + read);
                // 表示读完了
                if (read == -1) {
                    break;
                }
                // 将buffer写入到 outChannel
                buffer.flip();
                outChannel.write(buffer);
            }
            // 关闭相关资源
            outChannel.close();
            fos.close();
            inChannel.close();
            fis.close();
        }
    
    
        private static void copy1() throws IOException {
            // 创见文件输入流
            String dirPath = "E:\\JavaProject\\study\\netty-study\\nio-study\\src\\main\\resources\\file";
            File source = new File(dirPath, "file01.txt");
            FileInputStream fis = new FileInputStream(source);
            // 通过文FileInputStream 获取 FileChannel -> 实际类型 FileChannelImpl
            FileChannel inChannel = fis.getChannel();
            // 创建缓存区
            ByteBuffer byteBuffer = ByteBuffer.allocate(fis.available());
            // 将通道数据读入到 Buffer
            inChannel.read(byteBuffer);
            // 将byteBuffer 字节数据转换为 String
            byteBuffer.flip();
            FileOutputStream fos = new FileOutputStream(new File(dirPath, "file02.txt"));
            FileChannel outChannel = fos.getChannel();
            outChannel.write(byteBuffer);
            outChannel.close();
            fos.close();
            inChannel.close();
            fis.close();
        }
    }
    

应用实例4-拷贝文件transferFrom 方

实例要求:

  1. 使用 FileChannel(通道) 和 方法 transferFrom ,完成文件的拷贝

  2. 拷贝一张图片

  3. 代码演示

    public class Demo04 {
        public static void main(String[] args) throws IOException {
            // 创建相关的流
            String dirPath = "E:\\JavaProject\\study\\netty-study\\nio-study\\src\\main\\resources\\file";
            FileInputStream fis = new FileInputStream(new File(dirPath, "1.jpeg"));
            FileOutputStream fos = new FileOutputStream(new File(dirPath, "2.jpeg"));
            // 获取各个流的 FileChannel
            FileChannel sourceCh = fis.getChannel();
            FileChannel destCh = fos.getChannel();
            // 使用 transferFrom 完成拷贝
            // destCh.transferFrom(sourceCh, 0, sourceCh.size());
            // 使用 transferTo 完成拷贝
            sourceCh.transferTo(0, sourceCh.size(), destCh);
            // 关闭相关资源
            destCh.close();
            sourceCh.close();
            fos.close();
            fis.close();
        }
    }
    

关于Buffer 和 Channel的注意事项和细

  1. ByteBuffer 支持类型化的put 和 get, put 放入的是什么数据类型,get就应该使用 相应的数据类型来取出,否则可能有 BufferUnderflowException 异常

    public class Care01 {
        public static void main(String[] args) {
            // 创建一个 Buffer
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            // 类型化方式存放数据
            byteBuffer.putInt(100);
            byteBuffer.putLong(9L);
            byteBuffer.putChar('尚');
            byteBuffer.putShort((short) 4);
            // 取出
            byteBuffer.flip();
            System.out.println(byteBuffer.getInt());
            System.out.println(byteBuffer.getLong());
            System.out.println(byteBuffer.getChar());
            System.out.println(byteBuffer.getShort());
        }
    }
    // 经测试,报错有点困难,但是如果顺序不对,取出来的值会和put的不一致
    
  2. 可以将一个普通Buffer 转成只读Buffer

    public class Care02 {
        public static void main(String[] args) {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // java.nio.HeapByteBuffer
            System.out.println(buffer.getClass());
            for (int i = 0; i < 64; i++) {
                buffer.put((byte) i);
            }
            // 读取
            buffer.flip();
            // 得到一个只读的Buffer
            ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
            // java.nio.HeapByteBufferR
            System.out.println(readOnlyBuffer.getClass());
            // 读取
            while (readOnlyBuffer.hasRemaining()) {
                System.out.println(readOnlyBuffer.get());
            }
            // ReadOnlyBufferException
            readOnlyBuffer.put((byte) 100);
        }
    }
    
  3. NIO 还提供了 MappedByteBuffer, 可以让文件直接在内存(堆外的内存)中进行修改, 而如何同步到文件由NIO 来完成

    /**
     * 说明
     * 1.MappedByteBuffer 可让文件直接在内存(堆外内存)修改,操作系统不需要再拷贝一次
     */
    public class Care03 {
        public static void main(String[] args) throws IOException {
            String dirPath = "E:\\JavaProject\\study\\netty-study\\nio-study\\src\\main\\resources\\file";
            // RandomAccessFile的唯一父类是Object,与其他流父类不同。
            // 是用来访问那些保存数据记录的文件的,这样你就可以用seek()方法来访问记录,并进行读写了。
            // 这些记录的大小不必相同;但是其大小和位置必须是可知的。
            RandomAccessFile randomAccessFile = new RandomAccessFile(dirPath + "\\file01.txt", "rw");
            // 获取对应通道
            FileChannel channel = randomAccessFile.getChannel();
            // 参数1:FileChannel.MapMode.READ_WRITE 使用读写模式
            // 参数2:0:可直接修改的起始位置
            // 参数3:5:是映射到内存中的大小(不是索引位置),即将file01.txt的多少个字节映射到内存
            // 可直接修改的范围就是0-5
            // 实际类型是 java.nio.DirectByteBuffer
            MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
            mappedByteBuffer.put(0, (byte) 'H');
            mappedByteBuffer.put(1, (byte) 'E');
            mappedByteBuffer.put(2, (byte) 'L');
            mappedByteBuffer.put(3, (byte) 'L');
            mappedByteBuffer.put(4, (byte) 'O');
            // IndexOutOfBoundsException
            // mappedByteBuffer.put(5, (byte) 'W');
            randomAccessFile.close();
            System.out.println("修改成功~");
        }
    }
    // IDEA 看不到变化请到系统文件夹里面去查询(IDEA缓存原因)
    
    1. 前面我们讲的读写操作,都是通过一个Buffer 完成的,NIO 还支持 通过多个 Buffer (即 Buffer 数组) 完成读写操作,即 Scattering 和 Gathering.
    /**
     * 说明
     * Scattering: 将数据写入到buffer时,可以采用buffer数组,依次写入 [分散]
     * Gathering: 从buffer读取数据时,可以采用buffer数组,依次写读 [聚集]
     */
    public class Care04 {
        public static void main(String[] args) throws IOException {
            // 使用 ServiceSocketChannel 和 SocketChannel 网络
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
            // 绑定端口并启动
            serverSocketChannel.socket().bind(inetSocketAddress);
            // 创建Buffer数组
            ByteBuffer[] byteBuffers = new ByteBuffer[2];
            byteBuffers[0] = ByteBuffer.allocate(5);
            byteBuffers[1] = ByteBuffer.allocate(3);
            // 等待客户端连接(telnet)
            SocketChannel socketChannel = serverSocketChannel.accept();
            // 假定从客户端接收8个字节
            int msgLen = 8;
            // 循环读取
            while (true) {
                int byteRead = 0;
                while (byteRead < msgLen) {
                    long read = socketChannel.read(byteBuffers);
                    // 累计读取的字节数
                    byteRead += read;
                    System.out.println("byteRead = " + byteRead);
                    // 使用流打印,看看当前的这个 buffer 的 position 和 limit
                    Arrays.stream(byteBuffers).map(byteBuffer ->
                            "position:" + byteBuffer.position()
                                    + ",limit:" + byteBuffer.limit()).forEach(System.out::println);
                    // 将所有的 buffer 进行 flip
                    Arrays.stream(byteBuffers).forEach(ByteBuffer::flip);
                    // 将读到的数据显示到客户端
                    long byteWrite = 0;
                    while (byteWrite < msgLen) {
                        long write = socketChannel.write(byteBuffers);
                        byteWrite += write;
                    }
                    // 将所有buffer进行clear
                    Arrays.stream(byteBuffers).forEach(ByteBuffer::clear);
                    System.out.println("byteRead:" + byteRead + " byteWrite:" + byteWrite + " msgLen:" + msgLen);
                }
            }
        }
    }
    

Selector(选择器)

基本介绍

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

Selector示意图和特点说明

Scoket
Scoket
Scoket
read/write
read/write
read/write
selector
Thread

特点再说明:

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

Selector类相关方法

Selector 类是一个抽象类, 常用方法和说明如下:

public abstract class Selector implements Closeable { 
    //得到一个选择器对象
    public static Selector open(); 
    
    // 监控所有注册的通道,当其中有IO操作可以进行时,将对应的 SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
    public int select(long timeout); 
    
    // 从内部集合中得 到所有的 SelectionKey
    public Set selectedKeys(); 
}

注意事项

  1. NIO中的 ServerSocketChannel功能类似ServerSocket,SocketChannel功能类 似Socket

  2. selector 相关方法说明

    // 阻塞
    selector.select()
        
    // 阻塞1000毫秒,在1000毫秒后返回    
    selector.select(1000);
    
    // 唤醒selector
    selector.wakeup();
    
    // 不阻塞,立马返还
    selector.selectNow();
    

NIO 非阻塞网络编程原理分

NIO 非阻塞网络编程相关的(Selector、SelectionKey、 ServerScoketChannel和SocketChannel) 关系梳理图

![image-20220426142539332](D:\Program Files\Typora\img\Netty\image-20220426142539332.png)

对上图的说明:

  1. 当客户端连接时,会通过 ServerSocketChannel 得到 SocketChannel
  2. Selector 进行监听 select 方法, 返回有事 件发生的通道的个数.
  3. 将socketChannel注册到Selector上, register(Selector sel, int ops), 一个 selector上可以注册多个SocketChannel
  4. 注册后返回一个 SelectionKey, 会和该 Selector 关联(集合)
  5. 进一步得到各个 SelectionKey (有事件发 生)
  6. 在通过 SelectionKey 反向获取 SocketChannel , 方法 channel()
  7. 可以通过 得到的 channel , 完成业务处 理
  8. 代码撑腰。。。

NIO 非阻塞 网络编程快速入门

案例要求:

  1. 编写一个 NIO 入门案例,实现服务器端和客户端之间的数据简单通讯(非阻塞)

  2. 目的:理解NIO非阻塞网络编程机制

  3. 代码演示

    public class NioService {
        public static void main(String[] args) throws IOException {
            // 创建 ServerSocketChannel -> ServerSocket
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            // 得到一个 Selector 对象
            Selector selector = Selector.open();
            // 绑定一个端口 6666,在服务器端监听
            serverSocketChannel.socket().bind(new InetSocketAddress(6666));
            // 设置为非阻塞
            serverSocketChannel.configureBlocking(false);
            // 把 serverSocketChannel 注册到 selector 关注的事件为 OP_ACCEPT
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            // 循环等待客户端连接
            while (true) {
                // 这里我们等待一秒,如果没有事件发生,返回
                // 没有事件发生
                if (selector.select(TimeUnit.SECONDS.toMillis(1L)) == 0) {
                    System.out.println("服务器等待了一秒,无连接");
                    continue;
                }
                // 如果返回的 > 0, 就获取对应的 selectionKey 集合
                // 1. 如果返回的 > 0, 表示已经获取到关注的事件
                // 2. selector.selectedKeys() 返回关注事件的集合
                // 通过 selectionKeys 反向获取通道
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                // 遍历 Set<SelectionKey> , 使用迭代器遍历
                Iterator<SelectionKey> selectionKeyIterator = selectionKeys.iterator();
                if (selectionKeyIterator.hasNext()) {
                    // 获取到 SelectionKey
                    SelectionKey selectionKey = selectionKeyIterator.next();
                    // 根据 key 对应的通道发生的事件做相应的处理
                    // 如果是 OP_ACCEPT,有新的客户端连接
                    if (selectionKey.isAcceptable()) {
                        try {
                            // 客户端生成一个 SocketChannel
                            SocketChannel socketChannel = serverSocketChannel.accept();
                            System.out.println("客户端连接成功生成一个SocketChannel:" + socketChannel.hashCode());
                            // 将 socketChannel 设置为非阻塞
                            socketChannel.configureBlocking(false);
                            // 将 socketChannel 注册到 selector, 关注 OP_READ, 同时给 socketChannel 关联一个 Buffer
                            socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    // 发生 OP_READ
                    if (selectionKey.isReadable()) {
                        // 通过 key 反向获取 对应的channel
                        SocketChannel channel = (SocketChannel) selectionKey.channel();
                        // 得到 该 channel 关联的buffer
                        ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                        try {
                            channel.read(buffer);
                            System.out.println("from 客户端 " + new String(buffer.array(), 0, buffer.position(), Charset.forName("GBK")));
                            buffer.clear();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    // 手动从集合中移除当前 selectionKey
                    selectionKeyIterator.remove();
                }
            }
        }
    }
    
    public class NioClient {
        public static void main(String[] args) throws IOException {
            // 得到一个网络通道
            SocketChannel socketChannel = SocketChannel.open();
            // 设置非阻塞
            socketChannel.configureBlocking(false);
            // 提供服务器端的 ip 和 端口
            InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
            // 连接服务器
            if (!socketChannel.connect(inetSocketAddress)) {
                while (!socketChannel.finishConnect()) {
                    System.out.println("因为连接需要时间,客户端不会阻塞,可以做其它工作...");
                }
            }
            // 如果连接成功,就发生数据
            String str = "hello,你好呀~";
            // 将字节数组包装到缓冲区中
            ByteBuffer buffer = ByteBuffer.wrap(str.getBytes(StandardCharsets.UTF_8));
            // 发送数据, 将 buffer 数据写入 channel
            socketChannel.write(buffer);
            System.in.read();
        }
    }
    

SelectionKey

  1. SelectionKey,表示 Selector和网络通道的注册关系, 共四种

    int OP_ACCEPT:有新的网络连接可以 accept,值为16

    int OP_CONNECT:代表连接已经建立,值为8

    int OP_READ:代表读操作,值为1

    int OP_WRITE:代表写操作,值为4

    源码中:

    public static final int OP_READ = 1 << 0; 
    public static final int OP_WRITE = 1 << 2; 
    public static final int OP_CONNECT = 1 << 3; 
    public static final int OP_ACCEPT = 1 << 4;
    
  2. SelectionKey相关方法

    public abstract class SelectionKey {
        // 得到与之关联的 Selector 对象
    	public abstract Selector selector();
    
        // 得到与之关联的通道
    	public abstract SelectableChannel channel();
    
        // 得到与之关联的共享数据
    	public final Object attachment();
    
        // 设置或改变监听事件
    	public abstract SelectionKey interestOps(int ops);
    
        // 是否可以 accept
    	public final boolean isAcceptable();
        
        // 是否可以读
    	public final boolean isReadable();
        
        // 是否可以写
    	public final boolean isWritable();
    }
    

ServerSocketChannel

  • ServerSocketChannel 在服务器端监听新的客户端 Socket 连接

  • 相关方法如下

    public abstract class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel{
        // 得到一个 ServerSocketChannel 通道
    	public static ServerSocketChannel open();
        
        // 设置服务器端端口号
    	public final ServerSocketChannel bind(SocketAddress local);
    
        // 设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
    	public final SelectableChannel configureBlocking(boolean block);
        
        // 接受一个连接,返回代表这个连接的通道对象
    	public SocketChannel accept();
    
        // 注册一个选择器并设置监听事件
    	public final SelectionKey register(Selector sel, int ops);
    }
    

SocketChannel

  • SocketChannel,网络 IO 通道,具体负责进行读写操作。NIO 把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区

  • 相关方法如下

    public abstract class SocketChannel extends AbstractSelectableChannel
    implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel{
        // 得到一个 SocketChannel 通道
    	public static SocketChannel open();
        
        // 设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
    	public final SelectableChannel configureBlocking(boolean block);
    	
        // 连接服务器
    	public boolean connect(SocketAddress remote);
        
        // 如果上面的方法连接失败,接下来就要通过该方法完成连接操作
    	public boolean finishConnect();
    	
        // 往通道里写数据
    	public int write(ByteBuffer src);
        
        // 从通道里读数据
    	public int read(ByteBuffer dst);
        
        // 注册一个选择器并设置监听事件,最后一个参数可以设置共享数据
    	public final SelectionKey register(Selector sel, int ops, Object att);
    	
        // 关闭通道
    	public final void close();
    }
    

NIO 网络编程应用实例-群聊系

实例要求:

  1. 编写一个 NIO 群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)

  2. 实现多人群聊

  3. 服务器端:可以监测用户上线,离线,并实现消息转发功能

  4. 客户端:通过channel 可以无阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息(由服务器转发得到)

  5. 目的:进一步理解NIO非阻塞网络编程机制

  6. 代码演示

    /**
     * 群聊服务器端
     */
    public class GroupChatServer {
        // 定义属性
        private Selector selector;
        private ServerSocketChannel serverSocketChannel;
        private final int PORT = 6667;
    
        // 构造器
        // 初始化工作
        public GroupChatServer() {
            try {
                // 得到选择器
                selector = Selector.open();
                // serverSocketChannel
                serverSocketChannel = ServerSocketChannel.open();
                // 绑定端口
                serverSocketChannel.bind(new InetSocketAddress(PORT));
                // 设置为非阻塞模式
                serverSocketChannel.configureBlocking(false);
                // 将该 serverSocketChannel 注册到 selector 并监听连接事件
                serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        public void listen() {
            // 循环处理
            // noinspection InfiniteLoopStatement
            while (true) {
                try {
                    // 阻塞 等待事件
                    int count = selector.select();
                    // 有事件处理
                    if (count > 0) {
                        // 遍历得到 selectorKey 集合
                        Set<SelectionKey> selectionKeys = selector.selectedKeys();
                        Iterator<SelectionKey> selectionKeysIterator = selectionKeys.iterator();
                        while (selectionKeysIterator.hasNext()) {
                            // 取出 selectionKey
                            SelectionKey selectionKey = selectionKeysIterator.next();
                            // 监听到 accept
                            if (selectionKey.isAcceptable()) {
                                SocketChannel socketChannel = serverSocketChannel.accept();
                                // 将该 socketChannel 注册到 selector 并监听读事件
                                socketChannel.register(selector, SelectionKey.OP_READ);
                                // 提示
                                System.out.println(socketChannel.getRemoteAddress() + " 上线");
                            }
                            // 通道发送 read 事件, 即通道是可读的状态
                            if (selectionKey.isReadable()) {
                                // 处理读 (专门写方法)
                                readData(selectionKey);
                            }
                            // 删除当前的 key,防止重复处理
                            selectionKeysIterator.remove();
                        }
                    } else {
                        System.out.println("等待...");
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    
        /**
         * 读取客户端消息
         *
         * @param selectionKey Selector和网络通道的注册关系
         */
        private void readData(SelectionKey selectionKey) {
            // 得到关联的 channel
            SocketChannel channel = null;
            try {
                // 得到 channel
                channel = (SocketChannel) selectionKey.channel();
                // 创建 buffer
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                int count = channel.read(buffer);
                // 根据 count 的值做处理
                if (count > 0) {
                    // 把缓冲区数据转换为字符串
                    String msg = new String(buffer.array(), 0, count, StandardCharsets.UTF_8);
                    System.out.println("form 客户端:" + msg);
                    // 向其它客户端转发自己的消息(去掉自己),专门写一个方法来处理
                    sendIntoOtherClients(msg, channel);
                }
            } catch (IOException e) {
                if (channel != null) {
                    try {
                        System.out.println(channel.getRemoteAddress() + " 离线了");
                        // 取消注册
                        selectionKey.cancel();
                        // 关闭通道
                        channel.close();
                    } catch (IOException ex) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        /**
         * 转发给其它客户端(通道)
         *
         * @param msg  消息
         * @param self 自己
         */
        private void sendIntoOtherClients(String msg, SocketChannel self) {
            System.out.println("服务器转发消息中...");
            // 遍历 所有注册到 selector 上到 socketChannel 并排除自己
            for (SelectionKey selectionKey : selector.keys()) {
                // 通过 selectionKey 取出对应的 SocketChannel
                Channel targetChannel = selectionKey.channel();
                // 排除自己
                if (targetChannel instanceof SocketChannel && targetChannel != self) {
                    // 转型
                    SocketChannel dest = (SocketChannel) targetChannel;
                    // 将 msg 转存到 ByteBuffer
                    ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes(StandardCharsets.UTF_8));
                    // 将数据写入通道
                    try {
                        dest.write(buffer);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        public static void main(String[] args) {
            // 创建服务器对象
            GroupChatServer groupChatServer = new GroupChatServer();
            groupChatServer.listen();
        }
    }
    
    /**
     * 群聊客户端
     */
    public class GroupChatClient {
        // 定义相关属性
        private final String HOST = "127.0.0.1";
        private final int PORT = 6667;
        private Selector selector;
        private SocketChannel socketChannel;
        private String username;
    
        public GroupChatClient() {
            try {
                selector = Selector.open();
                // 连接服务器
                socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
                // 设置为非阻塞
                socketChannel.configureBlocking(false);
                // 将 socketChannel 注册到 selector
                socketChannel.register(selector, SelectionKey.OP_READ);
                // 得到 usernaselectorme
                username = socketChannel.getLocalAddress().toString().substring(1);
                System.out.println(username + " is ok...");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        // 向服务器发送消息
        public void sendInfo(String info) {
            info = username + " 说:" + info;
            try {
                socketChannel.write(ByteBuffer.wrap(info.getBytes(StandardCharsets.UTF_8)));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        // 读取从服务器端回复的消息
        public void readInfo() {
            try {
                int readChannel = selector.select();
                // 有可用的通道
                if (readChannel > 0) {
                    Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator();
                    while (selectionKeyIterator.hasNext()) {
                        SelectionKey selectionKey = selectionKeyIterator.next();
                        if (selectionKey.isReadable()) {
                            // 得到相关通道
                            SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                            // 得到一个 Buffer
                            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                            // 读取
                            socketChannel.read(byteBuffer);
                            // 把读取到的缓冲区数据转成字符串
                            String msg = new String(byteBuffer.array(), 0, byteBuffer.position(), StandardCharsets.UTF_8);
                            if (msg.length() > 0) {
                                System.out.println(msg);
                            }
                        }
                        selectionKeyIterator.remove();
                    }
                } else {
                    System.out.println("没有可以用的通道...");
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) {
            GroupChatClient groupChatClient = new GroupChatClient();
            // 启动一个线程
            new Thread(() -> {
                while (true) {
                    groupChatClient.readInfo();
                    try {
                        Thread.sleep(TimeUnit.SECONDS.toMillis(3L));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
            // 发送数据给服务器
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()) {
                String msg = scanner.nextLine();
                groupChatClient.sendInfo(msg);
            }
        }
    }
    

NIO与零拷贝

零拷贝基本介绍

  1. 零拷贝是网络编程的关键,很多性能优化都离不开。
  2. 在 Java 程序中,常用的零拷贝有 mmap(内存映射) 和 sendFile。那么,他们在 OS 里,到底是怎么样的一个的设计?我们分析 mmap 和 sendFile 这两个零拷贝
  3. 另外我们看下NIO 中如何使用零拷贝

传统IO数据读写

  1. Java 传统 IO 和 网络编程的一段代码

    File file = new File("test.txt");
    RandomAccessFile raf = new RandomAccessFile(file, "rw");
    
    byte[] arr = new byte[(int) file.length()];
    raf.read(arr);
    
    Socket socket = new ServerSocket(8080).accept();
    socket.getOutputStream().write(arr);
    

    百度:传统IO模型

    DMA: direct memory access 直接内存拷贝(不使用CPU)

mmap 优化

  1. mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数。
  2. 百度了解mmap示意图

sendFile 优化

  1. Linux 2.1 版本提供了sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换

  2. 示意图(百度了解)和小结

    提示:零拷贝从操作系统角度,是没有cpu拷贝

  3. Linux 在 2.4 版本中,做了 一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少 了数据拷贝。具体如下图(百度了解)和小结:

    • 这里其实有 一次cpu拷贝 kernel buffer -> socket buffer 但是,拷贝的信息很少,比如 lenght , offset , 消耗低,可以忽略

零拷贝的再次理解

  1. 我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer
  2. 零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下 文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。

mmap 和 sendFile 的区别

  1. mmap 适合小数据量读写,sendFile 适合大文件传输
  2. mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最 少 2 次数据拷贝。
  3. sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝 到 Socket 缓冲区)。

NIO 零拷贝案例

案例要求

  1. 使用传统的IO 方法传递一个大文件
  2. 使用NIO 零拷贝方式传递(transferTo)一个大文件
  3. 看看两种传递方式耗时时间分别是多少
public class NewIoServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        ServerSocket serverSocket = serverSocketChannel.socket();
        serverSocket.bind(new InetSocketAddress(7001));
        // 创建Buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
        while (true) {
            SocketChannel socketChannel = serverSocketChannel.accept();
            int readCount = 0;
            while (readCount != -1) {
                try {
                    readCount = socketChannel.read(byteBuffer);
                } catch (IOException e) {
                    // e.printStackTrace();
                    break;
                }
                // 倒带 position = 0; mark = -1;
                byteBuffer.rewind();
            }
        }
    }
}
public class NewIoClient {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress(7001));
        String dirPath = "E:\\JavaProject\\study\\netty-study\\nio-study\\src\\main\\resources\\file";
        String fileName = "protoc-3.6.1-win32.zip";
        // 得到一个文件 channel
        FileInputStream fis = new FileInputStream(dirPath + "\\" + fileName);
        FileChannel fileChannel = fis.getChannel();
        // 准备发送
        long startTime = System.currentTimeMillis();
        // 在 Linux 下一个 transferTo 方法就可以完成传输
        // 在 windows 下 一次调用 transferTo 只能发送 8m, 就需要分段传输文件,而且要注意传输文件为位置
        // transferTo 底层使用零拷贝
        long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
        System.out.println("发送总字节数 = " + transferCount + "耗时:" + (System.currentTimeMillis() - startTime) + "ms");
        // 关闭
        fileChannel.close();
        fis.close();
    }
}

Java AIO 基本介绍

  1. JDK 7 引入了 Asynchronous I/O,即 AIO。在进行 I/O 编程中,常用到两种模式: Reactor和 Proactor。Java 的 NIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的处理
  2. AIO 即 NIO2.0,叫做异步不阻塞的 IO。AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用
  3. 目前 AIO 还没有广泛应用,Netty 也是基于NIO, 而不是AIO, 因此我们就不详解 AIO了,有兴趣的同学可以参考 http://www.52im.net/thread-306-1-1.html

BIO、NIO、AIO对比表

BIO NIO AIO
IO模型 同步阻塞 同步非阻塞(多路复用) 异步非阻塞
编程难度 简单 复杂 复杂
可靠性
吞吐量

举例说明

  1. 同步阻塞:到理发店理发,就一直等理发师,直到轮到自己理发。
  2. 同步非阻塞:到理发店理发,发现前面有其它人理发,给理发师说下,先干其他事情,一会过来看是否轮到自己
  3. 异步非阻塞:给理发师打电话,让理发师上门服务,自己干其它事情,理发师自己来家给你理发

Netty 概述

原生NIO存在的问题

  1. NIO 的类库和 API 繁杂,使用麻烦:需要熟练掌握 Selector、ServerSocketChannel、 SocketChannel、ByteBuffer 等
  2. 需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序
  3. 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等
  4. JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。直到 JDK 1.7 版本该问题仍旧存在,没有被根本解决

Netty官网说明

官网:https://netty.io/

Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & client
> Netty是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端
  1. Netty 是由 JBOSS 提供的一个 Java 开源框架。Netty 提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络 IO 程序
  2. Netty 可以帮助你快速、简单的开发出一个网络应用,相当于简化和流程化了 NIO的开发过程
  3. Netty 是目前最流行的 NIO 框架,Netty 在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,知名的 Elasticsearch 、Dubbo 框架内部都采用了 Netty

Netty的优点

Netty 对 JDK 自带的 NIO 的 API 进行了封装,解决了上述的问题

  1. 设计优雅:适用于各种传输类型的统一 API 阻塞和非阻塞 Socket;基于灵活且可扩展 的事件模型,可以清晰地分离关注点;高度可定制的线程模型 - 单线程,一个或多个 线程池.
  2. 使用方便:详细记录的 Javadoc,用户指南和示例;没有其他依赖项,JDK 5(Netty 3.x)或 6(Netty 4.x)就足够了
  3. 高性能、吞吐量更高:延迟更低;减少资源消耗;最小化不必要的内存复制
  4. 安全:完整的 SSL/TLS 和 StartTLS 支持
  5. 社区活跃、不断更新:社区活跃,版本迭代周期短,发现的 Bug 可以被及时修复, 同时,更多的新功能会被加入

Netty版本说明

  1. netty版本分为 netty3.x 和 netty4.x、netty5
  2. 因为Netty5出现重大bug,已经被官网废弃了,目前推荐使用的是Netty4.x的稳定版本
  3. 目前在官网可下载的版本 netty3.x netty4.0.x 和 netty4.1.x
  4. 在本套课程中,我们讲解 Netty4.1.x 版
  5. netty 下载地址: https://bintray.com/netty/download

Netty 高性能架构设计

线程模型基本介绍

  1. 不同的线程模式,对程序的性能有很大影响,为了搞清Netty 线程模式,我们来系统 的讲解下 各个线程模式, 最后看看Netty 线程模型有什么优
  2. 目前存在的线程模型有:
    • 传统阻塞 I/O 服务模型
    • Reactor 模式
  3. 根据 Reactor 的数量和处理资源池线程的数量不同,有 3 种典型的
    • 单 Reactor 单线程
    • 单 Reactor 多线程
    • 主从 Reactor 多线
  4. Netty 线程模式(Netty 主要基于主从 Reactor 多线程模型做了一定的改进,其中主从 Reactor 多线程模型有多个 Reactor)

传统阻塞 I/O 服务模型

![image-20220504084121516](D:\Program Files\Typora\img\Netty\image-20220504084121516.png)

工作原理图

黄色的框表示对象, 蓝色的框表示线程 白色的框表示方法(API)

模型特点

  1. 采用阻塞IO模式获取输入的数据
  2. 每个连接都需要独立的线程完成数据的输入,业务处理, 数据返回

问题分析

  1. 当并发数很大,就会创建大量的线程,占用很大系统资源
  2. 连接创建后,如果当前线程暂时没有数据可读,该线程 会阻塞在read 操作,造成线程资源浪费

Reactor 模式

针对传统阻塞 I/O 服务模型的 2 个缺点,解决方案

  1. 基于 I/O 复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等 待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应 用程序,线程从阻塞状态返回,开始进行业务处理 Reactor 对应的叫法: 1. 反应器模式 2. 分发者模式(Dispatcher) 3. 通知者模式(notifier)
  2. 基于线程池复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理 任务分配给线程进行处理,一个线程可以处理多个连接的业务。

![image-20220504085236949](D:\Program Files\Typora\img\Netty\image-20220504085236949.png)

I/O 复用结合线程池,就是 Reactor 模式基本设计思想, 如图

![image-20220504085344931](D:\Program Files\Typora\img\Netty\image-20220504085344931.png)

说明:

  1. Reactor 模式,通过一个或多个输 入同时传递给服务处理器的模式 (基于事件驱动)
  2. 服务器端程序处理传入的多个请求, 并将它们同步分派到相应的处理线程, 因此Reactor模式也叫 Dispatcher模式
  3. Reactor 模式使用IO复用监听事件, 收到事件后,分发给某个线程(进程), 这点就是网络服务器高并发处理关键

Reactor 模式中核心组成

  1. Reactor:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处 理程序来对 IO 事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并 将线路转移到适当的联系人
  2. Handlers:处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之交谈的公 司中的实际官员。Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行 非阻塞操作。

Reactor 模式分类

根据 Reactor 的数量和处理资源池线程的数量不同,有 3 种典型的实

  1. 单 Reactor 单线程
  2. 单 Reactor 多线程
  3. 主从 Reactor 多线程

单 Reactor 单线程

  • 工作原理示意图:

![image-20220504090252568](D:\Program Files\Typora\img\Netty\image-20220504090252568.png)

方案说明

  1. Select 是前面 I/O 复用模型介绍的标准网络编程 API,可以实现应用程序通过一个阻 塞对象监听多路连接请求
  2. Reactor 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发
  3. 如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后的后续业务处理
  4. 如果不是建立连接事件,则 Reactor 会分发调用连接对应的 Handler 来响应
  5. Handler 会完成 Read→业务处理→Send 的完整业务流程

结合实例:服务器端用一个线程通过多路复用搞定所有的 IO 操作(包括连接,读、写 等),编码简单,清晰明了,但是如果客户端连接数量较多,将无法支撑,前面的 NIO 案例就属于这种模型。

方案优缺点分析

  1. 优点:模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成
  2. 缺点:性能问题,只有一个线程,无法完全发挥多核 CPU 的性能。Handler 在处理某 个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶
  3. 缺点:可靠性问题,线程意外终止,或者进入死循环,会导致整个系统通信模块不 可用,不能接收和处理外部消息,造成节点故障
  4. 使用场景:客户端的数量有限,业务处理非常快速,比如 Redis在业务处理的时间复 杂度 O(1) 的情

单Reactor多线程

工作原理示意图

![image-20220504154701575](D:\Program Files\Typora\img\Netty\image-20220504154701575.png)

方案说明

  1. Reactor 对象通过select 监控客户端请求事件, 收到事件后,通过dispatch进行分发

  2. 如果建立连接请求, 则由Acceptor 通过 accept 处理连接请求, 然后创建一个Handler对象处理完成连接后的各种事件

  3. 如果不是连接请求,则由reactor分发调用连接对应的handler来处理

  4. handler 只负责响应事件,不做具体的业务处理, 通过read 读取数据后,会分发给后面的worker线 程池的某个线程处理业务

  5. worker 线程池会分配独立线程完成真正的业务, 并将结果返回给handler

  6. handler收到响应后,通过send 将结果返回给 client

方案优缺点分析

  1. 优点:可以充分的利用多核cpu 的处理能力
  2. 缺点:多线程数据共享和访问比较复杂, reactor 处理所有的事件的监听和响应,在 单线程运行, 在高并发场景容易出现性能瓶颈

主从 Reactor 多线

工作原理示意图

针对单 Reactor 多线程模型中,Reactor 在单线程中运 行,高并发场景下容易成为性能瓶颈,可以让 Reactor 在多线程中运行

![image-20220504155240815](D:\Program Files\Typora\img\Netty\image-20220504155240815.png)

方案说明

  1. Reactor主线程 MainReactor 对象通过select 监听连接事件, 收 到事件后,通过Acceptor 处理连接事
  2. 当 Acceptor 处理连接事件后,MainReactor 将连接分配给 SubReactor
  3. subreactor 将连接加入到连接队列进行监听,并创建handler进行各种事件处理
  4. 当有新事件发生时,subreactor 就会调用对应的handler处理
  5. handler 通过read 读取数据,分发给后面的worker 线程处理
  6. worker 线程池分配独立的worker 线程进行业务处理,并返回结果
  7. handler收到响应的结果后,再通过send将结果返回给client
  8. Reactor主线程可以对应多个reactor子线程,即MainReactor可以关联多个SubReactor

主从 Reactor 多线

Scalable IO in Java 对 Multiple Reactors 的原理图解

![image-20220504155926048](D:\Program Files\Typora\img\Netty\image-20220504155926048.png)

Doug Lea
Doug Lea,中文名为道格·利。美国国籍,现担任纽约州立大学Oswego分校教师。
中文名:道格·利
外文名:Doug Lea
国籍:美国
职业:教师
主要成就:java.util.concurrent包作者
隶属:纽约州立大学Oswego分校
	如果IT的历史,是以人为主体串接起来的话,那么肯定少不了Doug Lea。这个鼻梁挂着眼镜,留着德王威廉二世的胡子,脸上永远挂着谦逊腼腆笑容,服务于纽约州立大学Oswego分校计算机科学系的老大爷。
	说他是这个世界上对Java影响力最大的一个人,一点也不为过。因为两次Java历史上的大变革,他都间接或直接的扮演了举足轻重的角色。2004年所推出的Tiger。Tiger广纳了15项JSRs(Java Specification Requests)的语法及标准,其中一项便是JSR-166。JSR-166是来自于Doug编写的util.concurrent包。
	值得一提的是: Doug Lea也是JCP (Java社区项目)中的一员。
	Doug是一个无私的人,他深知分享知识和分享苹果是不一样的,苹果会越分越少,而自己的知识并不会因为给了别人就减少了,知识的分享更能激荡出不一样的火花。《Effective JAVA》这本Java经典之作的作者Joshua Bloch便在书中特别感谢Doug Lea是此书中许多构想的共鸣板,感谢Doug Lea大方分享丰富而又宝贵的知识。

方案优缺点说明

  1. 优点:父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理。
  2. 优点:父线程与子线程的数据交互简单,Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据。
  3. 缺点:编程复杂度较高

结合实例:这种模型在许多项目中广泛使用,包括 Nginx 主从 Reactor 多进程模型, Memcached 主从多线程,Netty 主从多线程模型的支持

Reactor 模式小结

3 种模式用生活案例来理解

  1. 单 Reactor 单线程,前台接待员和服务员是同一个人,全程为顾客服
  2. 单 Reactor 多线程,1 个前台接待员,多个服务员,接待员只负责接待
  3. 主从 Reactor多线程,多个前台接待员,多个服务生

Reactor 模式具有如下的优点

  1. 响应快,不必为单个同步时间所阻塞,虽然 Reactor 本身依然是同步的
  2. 可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程 的切换开销
  3. 扩展性好,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源
  4. 复用性好,Reactor 模型本身与具体事件处理逻辑无关,具有很高的复用性

Netty模型

工作原理示意图1-简单版

Netty 主要基于主从 Reactors 多线程模型(如图)做了一定的改进,其中主从 Reactor 多线程模型有多个 Reactor

![image-20220504213121355](D:\Program Files\Typora\img\Netty\image-20220504213121355.png)

对上图说明:

  1. BossGroup 线程维护Selector, 只关注Accecpt
  2. 当接收到Accept事件,获取到对应的 SocketChannel, 封装成 NIOScoketChannel并注册到 Worker 线程(事件循环), 并进行维护
  3. 当Worker线程监听到selector 中通道发生自己感兴趣的事件后,就进行处理(就由handler), 注意 handler 已经加入到通道

工作原理示意图2-进阶版

Netty 主要基于主从 Reactors 多线程模型(如图)做了一定的改进,其中主从 Reactor 多线程模型有多个 Reactor

![image-20220504213428001](D:\Program Files\Typora\img\Netty\image-20220504213428001.png)

工作原理示意图-详细版

![image-20220504213548623](D:\Program Files\Typora\img\Netty\image-20220504213548623.png)

  1. Netty抽象出两组线程池 BossGroup 专门负责接收客户端的连接, WorkerGroup 专门负责网络的读写
  2. BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup
  3. NioEventLoopGroup 相当于一个事件循环组, 这个组中含有多个事件循环 ,每一个事件循环是 NioEvent
  4. NioEventLoop 表示一个不断循环的执行处理任务的线程, 每个 NioEventLoop 都有一个selector , 用于监听绑定在其上的socket的网络通讯
  5. NioEventLoopGroup 可以有多个线程, 即可以含有多个NioEventLoop
  6. 每个BossNioEventLoop 循环执行的步骤
    • 轮询accept 事件
    • 处理accept 事件 , 与client建立连接 , 生成NioScocketChannel , 并将其注册到某个worker NIOEventLoop 上的 selector
    • 处理任务队列的任务 , 即 runAllTasks
  7. 每个 WorkerNIOEventLoop 循环执行的步骤
    • 轮询read, write 事件
    • 处理i/o事件, 即read , write 事件,在对应NioScocketChannel 处理
    • 处理任务队列的任务 , 即 runAllTasks
  8. 每个WorkerNIOEventLoop 处理业务时,会使用pipeline(管道), pipeline可以获取到对应的通道,管道中维护了很多的处理器

Netty快速入门实例-TCP服务

  1. 实例要求:使用IDEA 创建Netty项目

  2. Netty 服务器在 6668 端口监听,客户端能发送消息给服务器 "hello, 服务~"

  3. 服务器可以回复消息给客户端 "hello, 客户端~"

  4. 目的:对Netty 线程模型 有一个初步认识, 便于理解Netty 模型理论

  5. 代码演示

    • 编写服务端
    • 编写客户端
    • 对netty 程序进行分析,看看netty模型特点

    说明: 创建Maven项目,并引入Netty 包

    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
        <version>4.1.74.Final</version>
    </dependency>
    
    public class NettyServer {
        public static void main(String[] args) throws InterruptedException {
            // 创建Maven项目,并引入Netty 包
            // 说明:
            // 1. 创建两个线程组 boosGroup 和 workerGroup
            // 2. boosGroup 只是处理连接请求,真正的和客户端业务处理,会交给 workerGroup 完成
            // 3. 两个都是无限循环
            // 4. boosGroup 和 workerGroup 含有的子线程 (NioEventLoop) 的个数 默认实际 CPU 核数 * 2
            NioEventLoopGroup boosGroup = new NioEventLoopGroup(1);
            NioEventLoopGroup workerGroup = new NioEventLoopGroup();
            try {
                // 创建服务器端的启动对象,配置参数
                ServerBootstrap bootstrap = new ServerBootstrap();
                // 使用链式编程来进行设置
                // 设置两个线程组
                bootstrap.group(boosGroup, workerGroup)
                        // 使用 NioSocketChannel作为服务器的通道实现
                        .channel(NioServerSocketChannel.class)
                        // 设置线程队列得到的连接个数
                        .option(ChannelOption.SO_BACKLOG, 128)
                        // 设置保持活动连接状态
                        .childOption(ChannelOption.SO_KEEPALIVE, true)
                        // 创建一个通道测试对象(匿名对象)
                        // 给我们的 workerGroup 的 EventLoop 对应的管道设置处理器
                        .childHandler(new ChannelInitializer<SocketChannel>() {
                            // 给 pipeline 设置处理器
                            @Override
                            protected void initChannel(SocketChannel socketChannel) throws Exception {
                                socketChannel.pipeline().addLast(new NettyServerHandler());
                            }
                        });
                System.out.println("... 服务器 is ready ...");
                // 绑定一个端口并且同步,生成一个 ChannelFuture 对象
                // 启动服务器并绑定端口
                ChannelFuture cf = bootstrap.bind(6684).sync();
                // 对关闭通道进行监听
                cf.channel().closeFuture().sync();
            } finally {
                workerGroup.shutdownGracefully();
                boosGroup.shutdownGracefully();
            }
        }
    }
    
    /**
     * 说明:
     * 1. 我们自定义一个 Handler 需要继承 netty 规定好的 某个 HandlerAdapter(规范)
     * 2. 这时我们自定义一个 Handler, 才能称为一个 handler
     */
    public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    
        /**
         * 读取数据实际(这里我们可以读取客户端发送的消息)
         * <p>
         * 1.ChannelHandlerContext ctx: 上下文对象,含有管道 pipeline, 通道 channel, 地址
         * 2.Object msg:就是客户端发送的数据 默认 Object
         */
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println("服务器读取线程 " + Thread.currentThread().getName());
            System.out.println("server ctx = " + ctx);
            System.out.println("看看 channel 和 pipeline 的关系");
            // 本质是一个双向链表,出栈入栈
            Channel channel = ctx.channel();
            ChannelPipeline pipeline = ctx.pipeline();
            // 将 msg 转成一个 ByteBuf
            // ByteBuf 是 Netty 提供的, 不是 NIO 的 ByteBuf
            ByteBuf byteBuf = (ByteBuf) msg;
            System.out.println("客户端发送消息是:" + byteBuf.toString(StandardCharsets.UTF_8));
            System.out.println("客户端地址:" + channel.remoteAddress());
        }
    
        /**
         * 数据读取完毕
         */
        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            // writeAndFlush 是 write + flush
            // 将数据写入缓存并刷新
            // 一般来讲,我们对这个发送的数据进行编码
            ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端ฅʕ•̫͡•ʔฅ", StandardCharsets.UTF_8));
        }
    
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
            ctx.close();
        }
    }
    
    public class NettyClient {
        public static void main(String[] args) {
            // 客户端需要一个事件循环组
            NioEventLoopGroup group = new NioEventLoopGroup();
            try {
                // 创建客户端启动对象
                // 注意客户端使用的不是ServerBootstrap 而是 Bootstrap
                Bootstrap bootstrap = new Bootstrap();
                // 设置相关参数
                // 设置线程组
                bootstrap.group(group)
                        // 设置客户端通道的实现类(反射)
                        .channel(NioSocketChannel.class)
                        // 加入自己的处理器
                        .handler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel socketChannel) throws Exception {
                                socketChannel.pipeline().addLast(new NettyClientHandler());
                            }
                        });
                System.out.println("客户端 ok...");
                // 启动客户端去连接服务器端
                // 关于 ChannelFuture 要分析,涉及到 netty 的异步模型
                ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6684).sync();
                // 给关闭通道进行监听
                channelFuture.channel().closeFuture().sync();
            } catch (Exception e) {
                group.shutdownGracefully();
            }
        }
    }
    
    public class NettyClientHandler extends ChannelInboundHandlerAdapter {
        /**
         * 当通道就绪就会触发该方法
         */
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("client " + ctx);
            ctx.writeAndFlush(Unpooled.copiedBuffer("hello,server:♪(^∇^*)", StandardCharsets.UTF_8));
        }
    
        /**
         * 当通道有读取事件时会触发
         */
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            ByteBuf byteBuf = (ByteBuf) msg;
            System.out.println("服务器回复消息:" + byteBuf.toString(StandardCharsets.UTF_8));
            System.out.println("服务器的地址:" + ctx.channel().remoteAddress());
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
            ctx.close();
        }
    }
    

任务队列中的 Task 有 3 种典型使用场景

  1. 用户程序自定义的普通任务
  2. 用户自定义定时任务
  3. 非当前 Reactor 线程调用 Channel 的各种方法

例如在推送系统的业务线程里面,根据用户的标识,找到对应的 Channel 引用,然后调用 Write 类方法向该用户推送消息,就会进入到这种场景。最终的 Write 会提交到任务队列中后被异步消费

/**
 * 说明
 * 1. 我们自定义一个 Handler 需要继承 netty 规定好的某个 HandlerAdapter(规范)
 * 2. 这时我们自定义一个 Handler, 才能称为 handler
 */
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    /**
     * 读取实际数据
     *
     * @param ctx 上下文对象,含有 管道 pipeline, 通道 channel, 地址
     * @param msg 就是客户端发送的消息, 默认 Object
     * @throws Exception 异常
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 比如 这里我们有一个非常耗时长的业务 -> 异步执行 -> 提交该 channel 对应的 NioEventLoop 的 taskQueue 中
        // 解决方案一 用户程序自定义的普通任务
        ctx.channel().eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5 * 1000);
                    ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端 2", CharsetUtil.UTF_8));
                    System.out.println("channel code = " + ctx.channel().hashCode());
                } catch (InterruptedException e) {
                    System.out.println("发生异常" + e.getMessage());
                }
            }
        });

        ctx.channel().eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5 * 1000);
                    ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端 3", CharsetUtil.UTF_8));
                    System.out.println("channel code = " + ctx.channel().hashCode());
                } catch (InterruptedException e) {
                    System.out.println("发生异常" + e.getMessage());
                }
            }
        });

        // 解决方案二 用户程序自定义定时任务 -> 该任务提交到 scheduledTaskQueue 中
        ctx.channel().eventLoop().schedule(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5 * 1000);
                    ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端 4", CharsetUtil.UTF_8));
                    System.out.println("channel code = " + ctx.channel().hashCode());
                } catch (InterruptedException e) {
                    System.out.println("发生异常" + e.getMessage());
                }
            }
        }, 5, TimeUnit.SECONDS);

        System.out.println("go on ...");
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        // writeAndFlush 是 write + flush
        // 将数据写入缓存,并刷新
        // 一般讲,我们对这个发送的数据进行编码
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端 1", CharsetUtil.UTF_8));
    }

    /**
     * 处理异常,一般是需要关闭通道
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

方案再说明

  1. Netty 抽象出两组线程池,BossGroup 专门负责接收客户端连接,WorkerGroup 专门负责网络读写操作
  2. NioEventLoop 表示一个不断循环执行处理任务的线程,每个 NioEventLoop 都有一个 selector,用于监听绑定在其上的 socket 网络通道
  3. NioEventLoop 内部采用串行化设计,从消息的读取->解码->处理->编码->发送,始终由 IO 线程 NioEventLoop 负责
    • NioEventLoopGroup 下包含多个 NioEventLoop
    • 每个 NioEventLoop 中包含有一个 Selector,一个 taskQueue
    • 每个 NioEventLoop 的 Selector 上可以注册监听多个 NioChannel
    • 每个 NioChannel 只会绑定在唯一的 NioEventLoop 上
    • 每个 NioChannel 都绑定有一个自己的 Channel Pipeline

异步模型

基本介绍

  1. 异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者
  2. Netty 中的 I/O 操作是异步的,包括 Bind、Write、Connect 等操作会简单的返回一个 ChannelFuture
  3. 调用者并不能立刻获得结果,而是通过 Future-Listener 机制,用户可以方便的主动获 取或者通过通知机制获得 IO 操作结果
  4. Netty 的异步模型是建立在 future 和 callback 的之上的。callback 就是回调。重点说 Future,它的核心思想是:假设一个方法 fun,计算过程可能非常耗时,等待 fun返回 显然不合适。那么可以在调用 fun 的时候,立马返回一个 Future,后续可以通过 Future去监控方法 fun 的处理过程(即 : Future-Listener 机制

Future 说明

  1. 表示异步的执行结果, 可以通过它提供的方法来检测执行是否完成,比如检索计算等等
  2. ChannelFuture 是一个接口 : public interface ChannelFuture extends Future 我们可以添加监听器,当监听的事件发生时,就会通知到监听器.

工作原理示意图

![image-20220508154405992](D:\Program Files\Typora\img\Netty\image-20220508154405992.png)

说明:

  1. 在使用 Netty 进行编程时,拦截操作和转换出入栈数据只需要您提供 callback 或利用 future 即可。这使得链式操作简单、高效, 并有利于编写可重用的、通用的代码
  2. Netty 框架的目标就是让你的业务逻辑从网络基础应用编码中分离出来、解脱出来

Future-Listener 机制

  1. 当 Future 对象刚刚创建时,处于非完成状态,调用者可以通过返回的 ChannelFuture 来获取操作执行的状态,注册监听函数来执行完成后的操作

  2. 常见有如下操作

    • 通过 isDone 方法来判断当前操作是否完成
    • 通过 isSuccess 方法来判断已完成的当前操作是否成功
    • 通过 getCause 方法来获取已完成的当前操作失败的原因
    • 通过 isCancelled 方法来判断已完成的当前操作是否被取消
    • 通过 addListener 方法来注册监听器,当操作已完成(isDone 方法返回完成),将会通知指定的监听器;如果 Future 对象已完成,则通知指定的监听器
  3. 举例说明

    演示:绑定端口是异步操作,当绑定操作处理完,将会调用相应的监听器处理逻辑

    public class NettyServer {
        public static void main(String[] args) throws InterruptedException {
            NioEventLoopGroup boosGroup = new NioEventLoopGroup(1);
            NioEventLoopGroup workerGroup = new NioEventLoopGroup();
            try {
                ServerBootstrap bootstrap = new ServerBootstrap();
                bootstrap.group(boosGroup, workerGroup)
                        .channel(NioServerSocketChannel.class)
                        .option(ChannelOption.SO_BACKLOG, 128)
                        .childOption(ChannelOption.SO_KEEPALIVE, true)
                        .childHandler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel socketChannel) throws Exception {
                                socketChannel.pipeline().addLast(new NettyServerHandler());
                            }
                        });
                // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
                // 绑定一个端口并且同步,生成一个 ChannelFuture 对象
                // 启动服务器(并绑定端口)
                int port = 6684;
                ChannelFuture cf = bootstrap.bind(port).sync();
                // 给 ChannelFuture 注册监听器,监听我们关心的事件
                cf.addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture channelFuture) throws Exception {
                        if (channelFuture.isSuccess()) {
                            System.out.println("监听端口 " + port + " 成功");
                        } else {
                            System.out.println("监听端口 " + port + " 失败");
                        }
                    }
                });
                // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
                cf.channel().closeFuture().sync();
            } finally {
                workerGroup.shutdownGracefully();
                boosGroup.shutdownGracefully();
            }
        }
    }
    

    小结:相比传统阻塞 I/O,执行 I/O 操作后线程会被阻塞住, 直到操作完成;异步处理的好处是不会造成线程阻塞,线程在 I/O 操作期间可以执行别的程序,在高并发情形下会更稳定和更高的吞吐量

快速入门实例-HTTP服务

  1. 实例要求:使用IDEA 创建Netty项目

  2. Netty 服务器在 6668 端口监听,浏览器发出请求 "http://localhost:8080/ "

  3. 服务器可以回复消息给客户端 "Hello! 我是服务器" , 并 对特定请求资源进行过滤

  4. 目的:Netty 可以做Http服务开发,并且理解Handler实例 和客户端及其请求的关系

  5. 代码演示

    public class HttpServer {
        public static void main(String[] args) throws InterruptedException {
            NioEventLoopGroup bootGroup = new NioEventLoopGroup(1);
            NioEventLoopGroup workGroup = new NioEventLoopGroup();
            try {
                ServerBootstrap serverBootstrap = new ServerBootstrap();
                serverBootstrap.group(bootGroup, workGroup)
                        .channel(NioServerSocketChannel.class)
                        .childHandler(new ServerInitializer());
                ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
                channelFuture.channel().closeFuture().sync();
            } finally {
                workGroup.shutdownGracefully();
                bootGroup.shutdownGracefully();
            }
        }
    }
    
    public class ServerInitializer extends ChannelInitializer<SocketChannel> {
        @Override
        protected void initChannel(SocketChannel socketChannel) throws Exception {
            // 向管道加入处理器
            // 得到管道
            ChannelPipeline pipeline = socketChannel.pipeline();
            // 加入一个 netty 提供的 httpServerCodec codec => [coder - decoder]
            // HttpServerCodec 说明
            // 1. HttpServerCodec 是 netty 提供的处理 http 的 编-解码器
            pipeline.addLast("MyHttpServerCodec", new HttpServerCodec());
            // 增加一个自定义的 handler
            pipeline.addLast("MyHttpServerHandler", new HttpServerHandler());
        }
    }
    
    /**
     * 说明
     * 1. SimpleChannelInboundHandler 是 ChannelInboundHandlerAdapter
     * 2. HttpObject 客户端和服务器端互相通讯的数据被封装成 HttpObject
     */
    public class HttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
        /**
         * channelRead0 读取客户端数据
         */
        @Override
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject httpObject) throws Exception {
            // 判断 msg(httpObject) 是不是 httpRequest
            if (httpObject instanceof HttpRequest) {
                System.out.println("pipeline hashCode=" + channelHandlerContext.pipeline().hashCode()
                        + " HttpServerHandler hashCode=" + this.hashCode());
                System.out.println("msg(httpObject) 类型=" + httpObject.getClass());
                System.out.println("客户端地址" + channelHandlerContext.channel().remoteAddress());
                // 获取到
                HttpRequest httpRequest = (HttpRequest) httpObject;
                // 获取 uri, 过滤指定资源
                URI uri = new URI(httpRequest.uri());
                // 过滤网站图标请求
                if ("/favicon.ico".equals(uri.getPath())) {
                    System.out.println("请求了 favicon.ico, 不做响应");
                    return;
                }
                // 回复信息给浏览器 [http协议]
                // 编码符合浏览器解析编码
                ByteBuf content = Unpooled.copiedBuffer("hello, 我是服务器", Charset.forName("GBK"));
                // 构造一个 http 的响应,即 httpResponse
                FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
                response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
                response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
                // 构建好 response 返回
                channelHandlerContext.writeAndFlush(response);
            }
        }
    }
    
    Netty 编写http服务器,浏览器无法访问6668端口
    在写netty的http server的例子过程中,但是一直发现浏览器使用端口号6668一直无法连接,使用postman就可以连接。
    通过查找资料发现6668端口是被浏览器认为不安全的端口,6666~6669这几个端口是IRC协议使用的缺省端口,存在很大的风险,很容易被木马程序利用.
    

Netty 核心模块

Bootstrap、ServerBootstrap

  1. Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类, ServerBootstrap 是服务端启动引导类
  2. 常见的方法有
    • public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup),该方法 用于服务器端,用来设置两个 EventLoop
    • public B group(EventLoopGroup group) ,该方法用于客户端,用来设置一个 EventLoop
    • public B channel(Class channelClass),该方法用来设置一个服务器端的通道实现
    • public B option(ChannelOption option, T value),用来给 ServerChannel 添加配置
    • public ServerBootstrap childOption(ChannelOption childOption, T value),用来给接收到的通道添加配置
    • public ServerBootstrap childHandler(ChannelHandler childHandler),该方法用来设置业务处理类(自定义的 handler)
    • public ChannelFuture bind(int inetPort) ,该方法用于服务器端,用来设置占用的端口号
    • public ChannelFuture connect(String inetHost, int inetPort) ,该方法用于客户端,用来连接服务器 端

Future、ChannelFuture

  1. Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。但是可以 过一会等它执行完成或者直接注册一个监听,具体的实现就是通过 Future 和 ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件
  2. 常见的方法有
    • Channel channel(),返回当前正在进行 IO 操作的通道
    • ChannelFuture sync(),等待异步操作执行完毕

Channel

  1. Netty 网络通信的组件,能够用于执行网络 I/O 操作。
  2. 通过Channel 可获得当前网络连接的通道的状态
  3. 通过Channel 可获得网络连接的配置参数 (例如接收缓冲区大小)
  4. Channel 提供异步的网络 I/O 操作(如建立连接,读写,绑定端口),异步调用意味着 任何 I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成
  5. 调用立即返回一个 ChannelFuture 实例,通过注册监听器到 ChannelFuture 上,可以 I/O 操作成功、失败或取消时回调通知调用方
  6. 支持关联 I/O 操作与对应的处理程序
  7. 不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应,常用的 Channel 类型:
    • NioSocketChannel,异步的客户端 TCP Socket 连接。
    • NioServerSocketChannel,异步的服务器端 TCP Socket 连接。
    • NioDatagramChannel,异步的 UDP 连接。
    • NioSctpChannel,异步的客户端 Sctp 连接。
    • NioSctpServerChannel,异步的 Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO。

Selector

  1. Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件
  2. 当向一个 Selector 中注册 Channel 后,Selector 内部的机制就可以自动不断地查询 (Select) 这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接 完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel

ChannelHandler 及其实现类

  1. ChannelHandler 是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序

  2. ChannelHandler 本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类

  3. ChannelHandler 及其实现类一览图

    ![image-20220509154758637](D:\Program Files\Typora\img\Netty\image-20220509154758637.png)

    • ChannelInboundHandler 用于处理入栈 I/O 事件
    • ChannelOutboundHandler 用于 处理出栈 I/O 操作

    适配器

    • ChannelInboundHandlerAdapter 用于处理入栈 I/O 事件
    • ChannelOutboundHandlerAdapt er 用于处理出栈 I/O 操作
    • ChannelDuplexHandler 用于处理 入栈和出栈事件
  4. 我们经常需要自定义一 个 Handler 类去继承 ChannelInboundHandlerAdapter,然后通过重写相应方法实现业务逻辑, 我们接下来看看一般都 需要重写哪些方法

    public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelInboundHandler {
        public ChannelInboundHandlerAdapter() {
        }
    
        @Skip
        public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
            ctx.fireChannelRegistered();
        }
    
        @Skip
        public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
            ctx.fireChannelUnregistered();
        }
    
        // 通道就绪事件
        @Skip
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            ctx.fireChannelActive();
        }
    
        @Skip
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            ctx.fireChannelInactive();
        }
    
        // 通道读取事件
        @Skip
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            ctx.fireChannelRead(msg);
        }
    
        // 通道读取完毕事件
        @Skip
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            ctx.fireChannelReadComplete();
        }
    
        @Skip
        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
            ctx.fireUserEventTriggered(evt);
        }
    
        @Skip
        public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
            ctx.fireChannelWritabilityChanged();
        }
    
        @Skip
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            ctx.fireExceptionCaught(cause);
        }
    }
    

Pipeline 和 ChannelPipeline

ChannelPipeline 是一个重点:

  1. ChannelPipeline 是一个 Handler 的集合,它负责处理和拦截 inbound 或者 outbound 的事件和操作,相当于一个贯穿 Netty 的链。(也可以这样理解: ChannelPipeline 是 保存 ChannelHandler 的 List,用于处理或拦截 Channel 的入栈事件和出栈操作)

  2. ChannelPipeline 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel 中各个的 ChannelHandler 如何相互交互

  3. 在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应,它们的组成关系如下

    ![image-20220509224322693](D:\Program Files\Typora\img\Netty\image-20220509224322693.png)

    • 一个 Channel 包含了一个 ChannelPipeline,而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandle
    • 入栈事件和出栈事件在一个双向链表中,入栈事件会从链表 head 往后传递到最后一个入站的 handler, 出站事件会从链表 tail 往前传递到最前一个出栈的 handler,两种类型的 handler 互不干扰
  4. 常用方法

    • ChannelPipeline addFirst(ChannelHandler... handlers),把一个业务处理类(handler) 添加到链中的第一个位置
    • ChannelPipeline addLast(ChannelHandler... handlers),把一个业务处理类(handler) 添加到链中的最后一个位置

ChannelHandlerContext

  1. 保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象

  2. 即ChannelHandlerContext 中 包 含 一 个 具 体 的 事 件 处 理 器 ChannelHandler , 同 时ChannelHandlerContext 中也绑定了对应的 pipeline 和 Channel 的信息,方便对 ChannelHandler进行调用

  3. 常用方法

    • ChannelFuture close(),关闭通道

    • ChannelOutboundInvoker flush(),刷新

    • ChannelFuture writeAndFlush(Object msg) , 将数据写到 ChannelPipeline 中当前

      ChannelHandler 的下一个 ChannelHandler 开始处理(出栈)

ChannelOption

  1. Netty 在创建 Channel 实例后,一般都需要设置 ChannelOption 参数。

  2. ChannelOption 参数如下

    • ChannelOption.SO_BACKLOG

      对应 TCP/IP 协议 listen 函数中的 backlog 参数,用来初始化服务器可连接队列大小。服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog 参数指定了队列的大小。

    • ChannelOption.SO_KEEPALIVE

      一直保持连接活动状态

EventLoopGroup 和其实现类 NioEventLoopGrou

  1. EventLoopGroup 是一组 EventLoop 的抽象,Netty 为了更好的利用多核 CPU 资源, 一般会有多个 EventLoop 同时工作,每个 EventLoop 维护着一个 Selector 实例

  2. EventLoopGroup 提供 next 接口,可以从组里面按照一定规则获取其中一个 EventLoop来处理任务。在 Netty 服务器端编程中,我们一般都需要提供两个 EventLoopGroup,例如:BossEventLoopGroup 和 WorkerEventLoopGroup。

  3. 通常一个服务端口即一个 ServerSocketChannel对应一个Selector 和一个EventLoop 线程。BossEventLoop 负责接收客户端的连接并将 SocketChannel 交给 WorkerEventLoopGroup 来进行 IO 处理,如下图所

    ![image-20220509225719803](D:\Program Files\Typora\img\Netty\image-20220509225719803.png)

    • BossEventLoopGroup 通常是一个单线程的 EventLoop,EventLoop 维护着一 个注册了ServerSocketChannel 的 Selector 实例BossEventLoop 不断轮询 Selector 将连接事件分离出来
    • 通常是 OP_ACCEPT 事件,然后将接收 到的 SocketChannel 交给 WorkerEventLoopGroup
    • WorkerEventLoopGroup 会由 next 选择 其中一个 EventLoop来将这个 SocketChannel 注册到其维护的 Selector 并对其后续的 IO 事件进行处理
  4. 常用方法

    • public NioEventLoopGroup(),构造方法
    • public Future shutdownGracefully(),断开连接,关闭线程

Unpooled 类

  1. Netty 提供一个专门用来操作缓冲区(即Netty的数据容器)的工具类

  2. 常用方法如下所示

    • public static ByteBuf copiedBuffer(CharSequence string, Charset charset)通过给定的数据和字符编码返回一个 ByteBuf 对象(类似于 NIO 中的 ByteBuffer 但有区别)
  3. 举例说明Unpooled 获取 Netty的数据容器ByteBuf 的基本使用 【案例演示】

    /**
     * 案例一
     */
    public class NettyByteBuf01 {
        public static void main(String[] args) {
            // 创建一个 ByteBuf
            // 说明
            // 1. 创建对象,该对象包含一个数组an, 是一个byte[10]
            // 2. netty 的 buffer 中,不需要使用 flip 反转, 底层维护了 readerIndex 和 writerIndex
            // 3. 通过 readerIndex 和 writerIndex 和 capacity, 将 buffer 分成三个区域
            // 0 --- readerIndex  已读取的区域
            // readerIndex --- writerIndex 可读取的区域
            // writerIndex --- capacity, 可写的区域
            ByteBuf buffer = Unpooled.buffer(10);
            for (int i = 0; i < 10; i++) {
                buffer.writeByte(i);
            }
            System.out.println("buffer.capacity() = " + buffer.capacity());
            // 输出
            for (int i = 0; i < buffer.capacity(); i++) {
                // <pre>System.out.println(buffer.getByte(i));</pre>
                System.out.println(buffer.readByte());
            }
            System.out.println("执行完毕");
        }
    }
    
    /**
     * 案例二
     */
    public class NettyByteBuf02 {
        public static void main(String[] args) {
            // 创建 ByteBuf
            ByteBuf byteBuf = Unpooled.copiedBuffer("hello,world", StandardCharsets.UTF_8);
            // 使用相关方法
            if (byteBuf.hasArray()) {
                byte[] content = byteBuf.array();
                // 将 content 转成字符串
                System.out.println(new String(content, StandardCharsets.UTF_8));
                System.out.println("byteBuf = " + byteBuf);
                System.out.println("byteBuf.arrayOffset() = " + byteBuf.arrayOffset());
                System.out.println("byteBuf.readerIndex() = " + byteBuf.readerIndex());
                System.out.println("byteBuf.writerIndex() = " + byteBuf.writerIndex());
                System.out.println("byteBuf.capacity() = " + byteBuf.capacity());
                System.out.println("byteBuf.readByte() = " + (char) byteBuf.readByte());
                System.out.println("byteBuf.getByte(0) = " + (char) byteBuf.getByte(0));
                // 可读的字节数
                System.out.println("byteBuf.readableBytes() = " + byteBuf.readableBytes());
                // 使用 for 取出各个字节
                for (int i = 0; i < byteBuf.readableBytes(); i++) {
                    System.out.println((char) byteBuf.getByte(i));
                }
                // 按照某个范围读取
                byteBuf.getCharSequence(0, 4, StandardCharsets.UTF_8);
                byteBuf.getCharSequence(4, 6, StandardCharsets.UTF_8);
            }
        }
    }
    

Netty应用实例-群聊系统

实例要求:

  1. 编写一个 Netty 群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)

  2. 实现多人群聊

  3. 服务器端:可以监测用户上线,离线,并实现消息转发功能

  4. 客户端:通过channel 可以无阻塞发送消息给其它所有用户,同时可以接受其它用 户发送的消息(有服务器转发得到)

  5. 目的:进一步理解Netty非阻塞网络编程机制

  6. 代码演示

    public class GroupChatServer {
        /**
         * 监听端口
         */
        private final int port;
    
        public GroupChatServer(int port) {
            this.port = port;
        }
    
        /**
         * 编写 run 方法,处理客户端的请求
         */
        public void run() throws InterruptedException {
            // 创建两个线程组
            NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
            NioEventLoopGroup workGroup = new NioEventLoopGroup();
            try {
                ServerBootstrap serverBootstrap = new ServerBootstrap();
                serverBootstrap.group(bossGroup, workGroup)
                        .channel(NioServerSocketChannel.class)
                        .option(ChannelOption.SO_BACKLOG, 128)
                        .childHandler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel socketChannel) {
                                // 获取到  pipeline
                                ChannelPipeline pipeline = socketChannel.pipeline();
                                // 向 pipeline 加入解码器
                                pipeline.addLast("decoder", new StringDecoder());
                                // 向 pipeline 加入编码器
                                pipeline.addLast("encoder", new StringEncoder());
                                // 加入自己的业务处理
                                pipeline.addLast(new GroupChatServerHandler());
                            }
                        });
                System.out.println("服务器启动");
                ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
                // 监听关闭
                channelFuture.channel().closeFuture().sync();
            } finally {
                workGroup.shutdownGracefully();
                bossGroup.shutdownGracefully();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            new GroupChatServer(7000).run();
        }
    }
    
    public class GroupChatServerHandler extends SimpleChannelInboundHandler<String> {
        /**
         * <code>
         * public static List<Channel> channels = new ArrayList<>();
         * // 使用一个 HashMap 管理
         * public static Map<String, Channel> channels = new HashMap<>();
         * </code>
         * 定义一个 channel 组,管理所有的 channel
         * GlobalEventExecutor.INSTANCE 是全局的事件执行器,是单例
         */
        public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    
        private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
        /**
         * handlerAdded 表示建立连接,一但连接,第一个被执行
         * 将当前 channel 加入到 channelGroup
         */
        @Override
        public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
            Channel channel = ctx.channel();
            // 将该客户端加入群聊的消息推送给其它在线的客户端
            // 该方法会将 channelGroup 中所有的 channel 遍历,并发送消息,我们不需要自己遍历
            channelGroup.writeAndFlush("[客户端]" + channel.remoteAddress() + " 加入聊天" + sdf.format(new Date()) + "\n");
            channelGroup.add(channel);
        }
    
        /**
         * 断开连接,将 xx 客户离开的信息推送给当前在线的客户
         */
        @Override
        public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
            Channel channel = ctx.channel();
            channelGroup.writeAndFlush("[客户端]" + channel.remoteAddress() + " 离开了\n");
            System.out.println("channelGroup.size() = " + channelGroup.size());
        }
    
        /**
         * 表示 channel 处于活动状态,提示 xx 离线了
         */
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println(ctx.channel().remoteAddress() + " 上线了~");
        }
    
        /**
         * 表示 channel 处于不活动状态,提示 xx 离线了
         */
        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            System.out.println(ctx.channel().remoteAddress() + " 离线了~");
        }
    
        /**
         * 读取数据
         */
        @Override
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
            // 获取当前 channel
            Channel channel = channelHandlerContext.channel();
            // 这时我们遍历 channelGroup,根据不同的情况,回送不同的消息
            channelGroup.forEach(ch -> {
                if (ch != channel) {
                    // 不是当前的channel
                    ch.writeAndFlush("[客户]" + channel.remoteAddress() + "发送了消息:" + s + "\n");
                } else {
                    // 回显自己发送的消息给自己
                    ch.writeAndFlush("[自己]发送了消息:" + s + "\n");
                }
            });
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            // 关闭通道
            ctx.close();
        }
    }
    
    public class GroupChatClient {
        /**
         * 属性
         */
        private final String host;
    
        private final int port;
    
    
        public GroupChatClient(String host, int port) {
            this.host = host;
            this.port = port;
        }
    
        public void run() throws InterruptedException {
            NioEventLoopGroup group = new NioEventLoopGroup();
            try {
                Bootstrap bootstrap = new Bootstrap()
                        .group(group)
                        .channel(NioSocketChannel.class)
                        .handler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel socketChannel) throws Exception {
                                // 得到 pipeline
                                ChannelPipeline pipeline = socketChannel.pipeline();
                                // 加入相应的 handler
                                pipeline.addLast("decoder", new StringDecoder());
                                pipeline.addLast("encoder", new StringEncoder());
                                // 加入自定义 handler
                                pipeline.addLast(new GroupChatClientHandler());
                            }
                        });
                ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
                // 得到 channel
                Channel channel = channelFuture.channel();
                System.out.println("-----" + channel.localAddress() + "-----");
                // 客户端需要输入信息,创建一个扫描器
                Scanner scanner = new Scanner(System.in);
                while (scanner.hasNext()) {
                    String msg = scanner.nextLine();
                    // 通过 channel 发送到服务器端
                    channel.writeAndFlush(msg + "\r\n");
                }
            } finally {
                group.shutdownGracefully();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            new GroupChatClient("127.0.0.1", 7000).run();
        }
    }
    
    public class GroupChatClientHandler extends SimpleChannelInboundHandler<String> {
    
        @Override
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
            System.out.println(s.trim());
        }
    }
    

Netty心跳检测机制案例

实例要求:

  1. 编写一个 Netty心跳检测机制案例, 当服务器超过3秒没有读时,就提示读空闲

  2. 当服务器超过5秒没有写操作时,就提示写空闲

  3. 实现当服务器超过7秒没有读或者写操作时,就提示读写空闲

    public class HeartBeatServer {
        public static void main(String[] args) throws InterruptedException {
            // 创建两个线程组
            NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
            NioEventLoopGroup workGroup = new NioEventLoopGroup();
            try {
                ServerBootstrap serverBootstrap = new ServerBootstrap()
                        .group(bossGroup, workGroup)
                        .channel(NioServerSocketChannel.class)
                        .handler(new LoggingHandler(LogLevel.INFO))
                        .childHandler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel socketChannel) throws Exception {
                                ChannelPipeline pipeline = socketChannel.pipeline();
                                // 加入 netty 提供的 IdleStateHandler
                                // 说明
                                // 1. IdleStateHandler 是 netty 提供的处理休闲状态的处理器
                                // 2. long readerIdleTime:表示多长时间没有读,就发送一个心跳检测包检测是否连接
                                // 3. long writerIdleTime:表示多长时间没有写,就发送一个心跳检测包检测是否连接
                                // 4. long allIdleTime:表示多长时间没有读写,就发送一个心跳检测包检测是否连接
                                // 5. 文档说明 triggers an {@link IdleStateHandler} when a {@link Channel}
                                //          has not performed read, write, or both operation for a while.
                                // 6. 当 IdleStateHandler 触发后, 会传递给通道的下一个 handler 去处理,
                                //      通过调用(触发)下一个 handler 的  userEventTriggered,
                                //      在该方法中去处理 IdleStateHandler(读空闲, 写空闲, 读写空闲)
                                pipeline.addLast(new IdleStateHandler(3, 5, 7, TimeUnit.SECONDS));
                                // 加入一个对空闲检测进一步处理的 handler (自定义)
                                pipeline.addLast(new HeartBeatServerHandler());
                            }
                        });
                // 启动服务器
                ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
                channelFuture.channel().closeFuture().sync();
            } finally {
                workGroup.shutdownGracefully();
                bossGroup.shutdownGracefully();
            }
        }
    }
    
    public class HeartBeatServerHandler extends ChannelInboundHandlerAdapter {
        /**
         * @param ctx 上下文
         * @param evt 事件
         * @throws Exception 异常
         */
        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
            if (evt instanceof IdleStateEvent) {
                // 将 evt 向下转型 IdleStateHandler
                IdleStateEvent event = (IdleStateEvent) evt;
                String eventType = null;
                switch (event.state()) {
                    case READER_IDLE:
                        eventType = "读空闲";
                        break;
                    case WRITER_IDLE:
                        eventType = "写空闲";
                        break;
                    case ALL_IDLE:
                        eventType = "读写空闲";
                        break;
                    default:
                }
                System.out.println(ctx.channel().remoteAddress() + "--超时时间--" + eventType);
                System.out.println("服务器做相应处理...");
                // ctx.channel().close();
            }
        }
    }
    

    服务器启动后记得启动客户端连接服务器

Netty 通过WebSocket编程实现服务器和客户端长连接

实例要求:

  1. Http协议是无状态的, 浏览器和服务器间的请求响应一次,下一次会重新创建连接.

  2. 要求:实现基于webSocket的长连接的全双工的交互

  3. 改变Http协议多次请求的约束,实现长连接了,服务器可以发送消息给浏览器

  4. 客户端浏览器和服务器端会相互感知,比如服务器关闭了,浏览器会感知,同样浏览器关闭了,服务器会感知

    public class WebsocketServer {
        public static void main(String[] args) throws InterruptedException {
            NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
            NioEventLoopGroup workGroup = new NioEventLoopGroup();
            try {
                ServerBootstrap serverBootstrap = new ServerBootstrap()
                        .group(bossGroup, workGroup)
                        .channel(NioServerSocketChannel.class)
                        .handler(new LoggingHandler(LogLevel.INFO))
                        .childHandler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel socketChannel) throws Exception {
                                ChannelPipeline pipeline = socketChannel.pipeline();
                                // 因为基于 http 协议,使用 http 的编码和解码器
                                pipeline.addLast(new HttpServerCodec());
                                // 是以块方式写,添加 ChunkedWriteHandler 处理器
                                pipeline.addLast(new ChunkedWriteHandler());
                                // 说明
                                // 1. http数据在传输过程中是分段,HttpObjectAggregator, 就是可以将多段聚合
                                // 2. 这就是为什么,当浏览器发送大量数据时,就会发出多次 http 请求
                                pipeline.addLast(new HttpObjectAggregator(8192));
                                // 说明
                                // 1. 对应 Websocket, 它的数据是以帧(frame)形式传递
                                // 2. 可以看到 WebsocketFrame 下面有六个子类 (Ctrl + H)
                                // 3. 浏览器请求 ws://localhost:7000/hello 表是请求的 uri
                                // 4. WebsocketServerProtocolHandler 核心的功能是将 http 协议升级为 ws 协议,保持长连接
                                // 5. 是通过一个 状态码 101
                                pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));
                                // 自定义的 handler, 处理业务逻辑
                                pipeline.addLast(new TextWebSocketFrameHandler());
                            }
                        });
                // 启动服务器
                ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
                channelFuture.channel().closeFuture().sync();
            } finally {
                workGroup.shutdownGracefully();
                bossGroup.shutdownGracefully();
            }
        }
    }
    
    public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
        @Override
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
            System.out.println("服务器收到消息 " + textWebSocketFrame.text());
            // 回复消息
            channelHandlerContext.channel().writeAndFlush(new TextWebSocketFrame("服务器时间" + LocalDateTime.now() + " " + textWebSocketFrame.text()));
        }
    
        /**
         * web 客户端连接后,触发方法
         */
        @Override
        public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
            // id 表示唯一值, LongText 是唯一的,ShortText 不是唯一
            System.out.println("handlerAdded 被调用" + ctx.channel().id().asLongText());
            System.out.println("handlerAdded 被调用" + ctx.channel().id().asShortText());
        }
    
        @Override
        public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
            System.out.println("handlerRemoved 被调用" + ctx.channel().id().asLongText());
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            System.out.println("异常发生 " + cause.getMessage());
            // 关闭连接
            ctx.close();
        }
    }
    
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Hello</title>
    </head>
    <body>
    <form onsubmit="return false">
        <label>
            <textarea name="message" style="height: 300px;width: 300px"></textarea>
        </label>
        <input type="button" value="发送消息" onclick="send(this.form.message.value)">
        <label>
            <textarea id="responseText" style="height: 300px;width: 300px"></textarea>
        </label>
        <input type="button" value="清空内容" onclick="document.getElementById('responseText').value=''">
    </form>
    <script>
        let socket;
        // 判断当前浏览器是否支持 websocket
        if (window.WebSocket) {
            // go on
            socket = new WebSocket("ws://localhost:7000/hello");
            // 相当于 channelRead0, ev 收到服务器回送的消息
            socket.onmessage = function (ev) {
                const rt = document.getElementById("responseText");
                rt.value = rt.value + "\n" + ev.data;
            }
            // 相当于连接开启(感知到连接开启)
            socket.onopen = function (ev) {
                const rt = document.getElementById("responseText");
                rt.value = '连接开启了...';
            }
            // 相当于连接关闭(感知到连接关闭)
            socket.onclose = function (ev) {
                const rt = document.getElementById("responseText");
                rt.value = rt.value + "连接关闭了...";
            }
        } else {
            alert("当前浏览器不支持webSocket")
        }
    
        // 发送消息到服务器
        function send(message) {
            // 先判断 socket 是否创建好
            if (!socket) {
                return;
            }
            if (socket.readyState === WebSocket.OPEN) {
                // 通过 socket 发送消息
                socket.send(message);
            } else {
                alert("连接没有开启");
            }
        }
    </script>
    </body>
    </html>
    

Google Protobu

编码和解码的基本介绍

  1. 编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送 数据时就需要编码,接收数据时就需要解码

  2. codec(编解码器) 的组成部分有两个:decoder(解码器)和 encoder(编码器)。 encoder 负责把业务数据转换成字节码数据,decoder 负责把字节码数据转换成 业务数据

    服务器端python
    业务数据
    解码decoder
    客户端java
    业务数据
    编码encoder
    传输二进制字节码

Netty 本身的编码解码的机制和问题分析

  1. Netty 自身提供了一些 codec(编解码器)
  2. Netty 提供的编码器
    • StringEncoder,对字符串数据进行编码
    • ObjectEncoder,对java对象进行编码
    • ...
  3. Netty 提供的解码器
    • StringDecoder,对字符串数据进行解码
    • ObjectDecoder,对java对象进行解码
    • ...
  4. Netty 本身自带的 ObjectDecoder 和 ObjectEncoder 可以用来实现 POJO 对象或各种业务对象的编码和解码,底层使用的仍是 Java 序列化技术 , 而Java 序列化技术本身效率就不高,存在如下问题
    • 无法跨语言
    • 序列化后的体积太大,是二进制编码的 5 倍多
    • 序列化性能太低

Protobuf

Protobuf基本介绍和使用示意图

  1. Protobuf 是 Google 发布的开源项目,全称 Google Protocol Buffers,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 **RPC[远程过程调用 remote procedure call ] 数据交换格式 **。 目前很多公司 http+json → tcp+protobuf

  2. 参考文档 语言指南

  3. Protobuf 是以 message 的方式来管理

  4. 支持跨平台、跨语言,即[客户端和服务器端可以是不同的语言编写的](支持目前绝大多数语言,例如 C++、C#、Java、python 等)

  5. 高性能,高可靠性

  6. 使用 protobuf 编译器能自动生成代码,Protobuf 是将类的定义使用.proto 文件进行描述。说明,在idea 中编写 .proto 文件时,会自动提示是否下载 .ptotot 编写插件. 可以让语法高亮

  7. 然后通过 protoc.exe 编译器根据.proto 自动生成.java

  8. protobuf 使用示意图

    服务器端java
    解码decoder
    ProtuBufDecoder
    业务数据对象
    -> 使用
    客户端java
    业务数据
    Yy.proto
    -> protoc.exe
    -> Xx.java
    编码encoder
    ProtuBufEncoder
    传输二进制字节码

Protobuf快速入门实例

编写程序,使用Protobuf完成如下功能

  1. 客户端可以发送一个Student PoJo 对象到服 务器 (通过 Protobuf 编

  2. 服务端能接收Student PoJo 对象,并显示信 息(通过 Protobuf 解码)

  3. 代码演示

    // Student.proto
    synctax = "proto3"; //版本
    option java_user_classname = "StudentPOJO"; //生成外部类名,同时也是文件名
    // protobuf 使用 meaasge 管理数据
    message Student { //会在 StudentPOJO 外部类生成一个内部类 Student,他是真正发送的POJO对象
    	int32 id = 1; //Student 类中有一个属性 名字为 id 类型为 int32(protobuf类型,与java类型的对应关系可以查找对照表) 1表示属性序号,不是值
    	String name = 2;
    }
    
    编译
    protoc.exe -- java_out=.Student.proto
    将生成的 StudentPOJO 放入项目使用
    

Protobuf快速入门实例2

  1. 客户端可以随机发送Student PoJo/ Worker PoJo 对象到服务器 (通过 Protobuf 编码)

  2. 服务端能接收Student PoJo/ Worker PoJo 对象(需要 判断是哪种类型),并显示信息(通过 Protobuf 解码)

  3. 代码演示

    // Student.proto
    
    synctax = "proto3";
    option optimize_for=SPEED; //加快解析
    option java_package="com.atguigu.netty.codec2"; //指定生成到哪个包下
    option java_outer_classname="MyDataInfo"; //外部类名,文件名
    // protobuf 可以使用 meaasge 管理其它的 meaasge
    message MyMessage {
    	// 定义一个枚举类型
    	enum DataType{
    		StudentType = 0; //在 proto3 要求 enum 的编号从0开始
    		WorkerType = 1;
    	}
    	
    	//用 DataType 来标识传入的是哪一个枚举
    	DataType data_type = 1;
    	
    	//表示每次枚举类型最多只能出现其中的一个,节省空间
    	oneof dataBody{
    		Student student = 2;
    		Worker worker = 3;
    	}
    }
    
    message Student {
    	// Student类属性
    	int32 id = 1;
    	String name = 2;
    }
    
    message Worker {
    	String name = 1;
    	int32 age = 2;
    }
    

用 Netty 实现 Dubbo RPC

RPC 基本介绍

  • RPC (Remote Procedure Call) 远程过程调用,是计算机的一种通信协议,该协议允许运行于一台计算机的程序调用另外一台计算机的子程序,而程序员无需为这个交互作用编程
  • 两个或多个应用都分布在不同的服务器上,他们之间的调用就像是本地调用一样
  • 常见的RPC框架有,阿里的Dubbo,google的gRPC,Go语言的rpcx,Apache的thrift,Spring的Spring MVC

RPC 调用流程

10/9.接收/发送
3/4.发送/接收
7.响应
8.编码
5.解码
6.调用API
Service
编码/解码
ServiceStub
1.请求
2.编码
11.解码
12.得到结果
Client
编码/解码
ClientStub

RPC 调用流程说明

  1. 服务消费方(client)以本地方式调用服务
  2. client sub 接收到调用后负责将方法,参数等封装成能够进行网络传输的消息本
  3. client sub 将消息进行编码并发送到服务器端
  4. service sub 收到消息后进行解码
  5. service sub 根据解码结果调用本地的服务
  6. 本地服务执行完并将结果返回给 service sub
  7. service sub 将返回导入结果进行编码并发送至消费方
  8. client sub 接收到消息并进行解码
  9. 服务消费方(client)得到结果

小结:RPC 的目标是将2-8这些步骤封装起来,用户无需关心这些细节,可以像调用本地方法一样即可完成远程服务调用

实现 Dubbo RPC (基于 Netty)

需求说明

  1. dubbo 底层使用了Netty 作为网络通信框架,要求用netty 实现一个简单的RPC框架。
  2. 模仿dubbo,消费者和提供者约定接口和协议,消费者远程调用提供者的服务,提供者返回一个字符串,消费者打印提供者返回的数据。底层网络通信给予Netty 4.x

设计说明

  1. 创建一个接口,定义抽象方法。用于消费者和提供者之间的约定。
  2. 创建一个提供者,该类需要监听消费者的请求,并按照约定返回数据。
  3. 创建一个消费者,该类需要透明的调用自己不存在的方法,内部需要使用netty请求提供者返回数据

common

public class Const {
    /**
     * 符号
     */
    public static final String SYMBOL = "#";

    /**
     * 协议
     */
    public static final String PROTOCOL = "(●'◡'●)" + SYMBOL;
}
/**
 * 这是一个接口,服务提供方和服务消费方都需要
 */
public interface HelloService {
    String hello(String msg);
}

provider

public class HelloServiceImpl implements HelloService {

    private static int count = 0;

    /**
     * 当有消费方调用该方法时就返回一个结果
     */
    @Override
    public String hello(String msg) {
        System.out.println("收到客户端的消息=" + msg);
        // 根据 msg 返回不同的结果
        if (msg != null) {
            return "你好客户端,我已收到你的消息【" + msg + "】第 " + ++count + " 次";
        } else {
            return "你好客户端,我已收到你的消息";
        }
    }
}
public class ServiceBootstrap {
    public static void main(String[] args) {
        NettyServer.startServer("127.0.0.1", 7000);
    }
}
public class NettyServer {
    public static void startServer(String hostname, int port) {
        startServer0(hostname, port);
    }

    /**
     * 编写一个方法完成对 NettyServer 的初始化和启动
     */
    private static void startServer0(String hostname, int port) {
        NioEventLoopGroup boosGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(boosGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        pipeline.addLast(new StringDecoder());
                        pipeline.addLast(new StringEncoder());
                        // 业务处理器
                        pipeline.addLast(new NettyServerHandler());
                    }
                });
        try {
            ChannelFuture channelFuture = serverBootstrap.bind(hostname, port).sync();
            System.out.println("服务提供方开始提供服务~");
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            workerGroup.shutdownGracefully();
            boosGroup.shutdownGracefully();
        }
    }
}
/**
 * 服务器 Handler 比较简单
 */
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 获取客户端消息,并调用服务
        System.out.println("msg = " + msg);
        // 客户端在调用服务器的api时,我们需要定义一个协议
        // 比如我们要求 每次发消息都必须以某个字符串开头
        if (msg.toString().startsWith(Const.PROTOCOL)) {
            String msgStr = msg.toString();
            String result = new HelloServiceImpl().hello(msgStr.substring(msgStr.indexOf(Const.SYMBOL) + 1));
            ctx.writeAndFlush(result);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

customer

public class ClientBootstrap {
    public static void main(String[] args) {
        // 创建一个消费者
        NettyClient customer = new NettyClient();
        // 创建代理对象
        HelloService service = (HelloService) customer.getBean(HelloService.class, Const.PROTOCOL);
        while (true) {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 通过代理对象调用服务提供者的方法(服务)
            String res = service.hello("你好 ~ dubbo");
            System.out.println("调用的结果 res = " + res);
        }
    }
}
@SuppressWarnings("all")
public class NettyClient {
    /**
     * 创建线程池
     */
    private static ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    private static NettyClientHandler client;

    private int count;

    /**
     * 编写方法使用代理模式,获取一个代理对象
     */
    public Object getBean(final Class<?> serviceClass, final String providerName) {
        return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class<?>[]{serviceClass}, (proxy, method, args) -> {
                    System.out.println("(proxy, method, args) 进入..." + ++count + " 次");
                    // {} 部分的代码,客户端每调用一次 hello,就会进入到该代码
                    if (client == null) {
                        initClient();
                    }
                    // 设置要发送给服务器端的信息
                    // providerName 协议头 arg[0] 就是客户端调用 api hello(...) 参数
                    client.setParam(providerName + args[0]);
                    return executor.submit(client).get();
                });
    }

    /**
     * 初始化客户端
     */
    private void initClient() {
        client = new NettyClientHandler();
        // 创建 EventLoopGroup
        NioEventLoopGroup group = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.TCP_NODELAY, true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        pipeline.addLast(new StringDecoder());
                        pipeline.addLast(new StringEncoder());
                        pipeline.addLast(client);
                    }
                });
        try {
            bootstrap.connect("127.0.0.1", 7000).sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class NettyClientHandler extends ChannelInboundHandlerAdapter implements Callable<Object> {
    /**
     * 上下文
     */
    private ChannelHandlerContext context;
    /**
     * 返回的结果
     */
    private String result;
    /**
     * 客户端调用方法时传入的参数
     */
    private String param;

    /**
     * 客户端连接创建后就会被调用,这个方法是第一个被调用 ①
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channelActive 被调用");
        // 因为我们在其它方法会使用到 ctx
        context = ctx;
    }

    /**
     * 收到服务器的数据后,调用方法 ④
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("channelRead 被调用");
        result = msg.toString();
        // 唤醒等待线程
        synchronized (this) {
            notify();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

    /**
     * 被代理对象调用,发送数据给服务器 --> wait --> 等待被唤醒 (ChannelRead) --> ③ ⑤
     */
    @Override
    public synchronized Object call() throws Exception {
        System.out.println("call1 被调用");
        context.writeAndFlush(param);
        // 等待 wait
        // 等待 channelRead 方法获取到服务器的结果后,唤醒
        wait();
        System.out.println("call2 被调用");
        // 服务器返回的结果
        return result;
    }

    public void setParam(String param) {
        System.out.println("setParam 被调用");
        this.param = param;
    }
}

本文作者:Zzzy君不见

本文链接:https://www.cnblogs.com/Zzzyyw/p/16182198.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Zzzy君不见  阅读(154)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起