随记——netty入门(一)——20201012

教程:Netty核心技术及源码剖析——尚硅谷

前言

前置课程:JAVA OOP编程、多线程编程、IO编程、网络编程、常用设计模式
书籍:
《Netty IN ACTION》
《Netty 权威指南》(基于Netty5)
《Netty 进阶之路》

Netty介绍

  • Netty是由JBOSS提供的一个java开源框架,现为GitHub上的独立项目
  • Netty是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络IO程序
  • Netty主要针对在TCP协议下,面向客户端的高并发应用,或P2P场景下的大量数据持续传输的应用
  • Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景
  • 要透彻理解Netty,需要先学习NIO

从上至下:
Netty
NIO
原生JDK io/网络
TCP/IP

Netty应用场景

  1. 互联网行业:在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,Netty作为异步高性能的通信框架,往往作为基础通信组件被这些RPC框架使用。
  2. 典型的应用有:阿里分布式服务框架Dubbo的RPC框架使用Dubbo协议进行节点间通信,Dubbo协议默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信。
  3. 游戏行业:Netty作为高性能的基础通信组件,提供了TCP/UDP和HTTP协议栈,方便定制和开发私有协议栈,账号登陆服务器;地图服务器之间可以方便的通过Netty进行高性能的通信。
  4. 大数据领域:经典的Hadoop的高性能通信和序列化组件(AVRO实现数据文件共享)的RPC框架,默认使用Netty进行跨界点通信;它的Netty service基于Netty框架二次封装实现。

IO模型

IO模型基本说明

  • 简单理解:用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能
  • Java支持3中网络编程IO模型:BIO NIO AIO
    • BIO :同步并阻塞,服务器实现模式位一个连接一个线程,即客户端有连接请求时服务端就需要启动一个线程进行处理,如果这个连接不做任何事情就会造成不必要的线程开销。
    • NIO:同步非阻塞,服务器实现模式位一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求就进行处理
    • AIO(NIO.2):异步非阻塞,AIO引入异步通道的概念,采用了proactor模式,简化了程序编写,有效的请求才启动线程;它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。

BIO NIO AIO适用场景

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

BIO基本介绍

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

BIO工作机制

[Socket] —— (read/write) —— [Thread]

流程:

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

BIO应用实例(线程池)

用命令行窗口创建客户端

>telnet 127.0.0.1 portId
# 进入127.0.0.1窗口之后 输入 ctrl+]
> send hello #即可发送hello 

客户端

  1. 新建一个线程池
  2. 新建ServerSocket,监听一个接口
  3. while(true) ServerSocket.accept(),当有连接时,新建一个线程进行下一步操作。否则,一直等待(阻塞)
  4. 对与ServerSocket.accept(),得到它的输入流,打印输入内容
  5. 没有打印内容,但连接未关闭,read()会阻塞,等待输入内容。

问题分析:

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

NIO基本介绍

NIO与网络编程

一个server,新建一个Thread,对应一个Selector,每个Selector可以在多个通道之间选择,一个通道对应一个Buffer,双向读写,每个Buffer对应一个连接。

  • Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会湖区,而不是保持线程阻塞;所以直至数据变得可以读取之前,该线程可以继续做其它的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
  • 通俗理解:NIO是可以做到用一个线程处理多个操作的。假设有10000个请求过来,根据实际情况,可分配50/100个线程来处理,而不是像BIO一样,一定要分配10000个线程。
  • HTTP2.0使用了多路复用技术,左到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。

NIO相关的Selector、 SelectionKey、ServerSocketChannel 和 SocketChannel 关系梳理

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

demo

服务端:

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

        final int portId = 6666;
        try (
                // 创建ServerSocketChannel
                ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
                Selector selector = Selector.open();
                ) {


            System.out.println("服务器(监听端口:"+portId+")启动......");
            //绑定端口
            serverSocketChannel.socket().bind(new InetSocketAddress(portId));
            //设为非阻塞
            serverSocketChannel.configureBlocking(false);
            // 注册选择器
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);


            //循环等待客户端连接
            while (true){
                System.out.println("等待客户端连接......");
                //监听,等待客户连接
                if (selector.select(1000)==0){//没有时间发生
                    // 或selectNow()
                    System.out.println("服务器等待了1秒,无连接。");
                    continue;
                }
                //如果返回的>0,表示以及获取到关注的事件
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
                while (keyIterator.hasNext()){
                    SelectionKey key = keyIterator.next();
                    if (key.isAcceptable()){//有新的客户端连接
                        //给客户端生成一个socketChannel
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        //将当前socketChannel也注册到选择器,同时关联一个Buffer
                        socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                        System.out.println("客户端连接成功");
                    }else if (key.isReadable()){
                        //通过 key反向获取channel
                        SocketChannel channel = (SocketChannel) key.channel();
                        // 获取 关联buffer
                        ByteBuffer buffer = (ByteBuffer) key.attachment();
                        channel.read(buffer);
                        System.out.println("来自客户端"+new String(buffer.array()));
                    }
                    // 手动移除key,防止重复操作
                    keyIterator.remove();
                }


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

客户端:

public class NIOClient {
    public static void main(String[] args) {
        try (SocketChannel socketChannel = SocketChannel.open()) {
            socketChannel.configureBlocking(false);
            InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);

            //连接服务器,如果连接失败
            if (!socketChannel.connect(inetSocketAddress)) {
                while (!socketChannel.finishConnect()) {
                    System.out.println("因为连接需要时间,客户端不会阻塞,可以做其他工作");
                }
            }
            //如果连接成功
            String str = "hello,链接成功";
            // 产生一个buffer,里面存储str,buffer的大小等于str字节数组的大小
            ByteBuffer wrap = ByteBuffer.wrap(str.getBytes());
            //发送数据,buffer数据写入channel
            socketChannel.write(wrap);
            //会阻塞在此处
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

出现了异常

Exception in thread "main" java.nio.channels.IllegalBlockingModeException
	at java.nio.channels.spi.AbstractSelectableChannel.register(AbstractSelectableChannel.java:201)
	at com.km.mpdemo001.netty.nio.NIOServer.main(NIOServer.java:59)

原因:未将连接的socketChannel设为非阻塞模式

//给客户端生成一个socketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);

有个问题:会重复打印接收数据?
解决:把给客户端生成的socketChannel 关掉, channel.close();

NIO群聊系统

  • 实现多人群聊
  • 服务器端可以检测用户上线,离线,并实现消息准发
  • 客户端,无阻塞发送消息给其它用户,也能接收到其它用户的消息

代码有蛮多问题,先不放。

NIO与零拷贝

傻瓜三歪让我教他「零拷贝」——敖丙
零拷贝是网络编程的关键,与性能优化有关。
DMA:direct memory access
0拷贝指的不是不拷贝,而是没有CPU copy

传统IO经过4次拷贝,3次切换

  • DMA Copy: Hard drive --> [Kernel space]kernel buffer
  • CPU copy: [Kernel space] kernel buffer --> [User space] user buffer
  • CPU copy: [User space] user buffer -> [Kernel space]socket buffer
  • DMA Copy: [Kernel space]socket buffer–>protocol engine(协议栈)

mmap通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样在网络传输时,就可以减少内核空间到用户空间的拷贝次数。

  • DMA Copy: Hard drive --> [Kernel space]kernel buffer
  • [Kernel space] kernel buffer --> [User space] user buffer
  • CPU copy: [Kernel space]kernel buffer -> [Kernel space]socket buffer
  • DMA Copy: [Kernel space]socket buffer–>protocol engine(协议栈)

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

  • DMA Copy: Hard drive --> [Kernel space]kernel buffer
  • CPU copy: [Kernel space]kernel buffer -> [Kernel space]socket buffer
  • DMA Copy: [Kernel space]socket buffer–>protocol engine(协议栈)

Linux2.4版本。避免了从内核缓冲区拷贝到Socket buffer的操作,从而再一次减少了数据拷贝

  • DMA Copy: Hard drive --> [Kernel space]kernel buffer
  • copy desc(拷贝少量描述信息): [Kernel space]kernel buffer -> [Kernel space]socket buffer
  • DMA Copy: [Kernel space]socket buffer–>protocol engine(协议栈)

mmap和sendFile的区别

  1. mmap适合小数据量读写,sendFile适合大文件传输
  2. mmap经过3次拷贝,4次上下文切换;sendFile经过最少2次数据拷贝,3次上下文切换
  3. sendFile可以利用DMA方式,减少CPU拷贝,mmap则不能。

NIO的零拷贝方式传递(transferTo)一个大文件

AIO基本介绍

  • 在进行IO编程时,常用到两种模式:Reactor和Proactor。Java的NIO就是Reactor。当有事件触发时,服务器端得到通知,进行相应的处理。
  • AIO即NIO2.0,叫做异步不阻塞的IO。引入异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
  • 参考《Java新一代网络模型AIO原理及Linux系统AIO介绍》
BIONIOAIO
IO模型同步阻塞同步非阻塞异步非阻塞
编程难度
可靠性
吞吐量

Netty概述

原生NIO存在的问题

  • NIO的类库和API繁杂,使用麻烦:需要熟练掌握selector、socketchannel等。
  • 需要具备其它额外技能:多线程和网络编程
  • 开发工作量和难度都非常大:比如客户端面临断线重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等。
  • NIO的bug:例如臭名昭著的Epoll Bug,会导致selector空轮询,最终导致CPU 100%。

https://netty.io/

知名的Elasticsearch Dubbo框架内部都采用了Netty

Netty的优点:

  • 设计优雅:适用于各种传输类型的统一API 阻塞和非阻塞Socket;基于灵活且可扩展的事件模型,可以清晰的分离关注点;高度可定制的线程模型——单线程,一个或多个线程池
  • 使用方便:详细记录的javadoc;没有其它依赖项。jdk5(Netty3.x)或jdk6(Netty4.x)就够了。
  • 高性能、高吞吐量,低延迟,减少资源消耗,最小化不必要的内存复制。
  • 安全:完整的SLL/TLS StartTLS支持
  • 社区活跃,不断更新

线程模型

  • 传统阻塞IO服务模型
  • Reactor模式
    • 单Reactor单线程
    • 单Reactor多线程
    • 主从Reactor多线程
  • Netty线程模式(主要基于主从Reactor多线程做了一定改进)

传统阻塞IO服务模型

黄色框代表对象
蓝色框代表线程
白色框表示方法
传统阻塞IO服务模型
模型特点:

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

问题分析:

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

Reactor模式

Reactor模式:反应器模式/分发者(dispatcher)模式/通知者(notifier)模式

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

  • 基于IO复用模型,多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个链接有新的数据可以处理时,操作系统通知该应用程序,线程从阻塞状态返回,开始进行业务处理
  • 基于线程池复用线程资源

reactor模式.PNG
reactor模式,通过一个或多个输入同时传递给服务处理器的模式(基于事件驱动)

服务器端程序处理传入的多个请求,并将它们同步分派到相应的处理线程,因此也叫分发者(dispatcher)模式。

reactor模式使用IO复用监听事件,收到事件后,分发给某个线程(进程),这点就是网络服务器高并发处理关键。

单reactor单线程
优点:模型简单
缺点:

  • 性能问题,只有一个线程,无法完全发挥多核CPU 的性能。handler在处理某个连接上的业务时,整个进程无法处理其他连接事件,容易导致性能瓶颈
  • 可靠性问题:若线程意外终止/进入死循环,会导致整个系统通信模块不可用

使用场景:
客户端数量有限,业务处理非常快,如redis在业务处理时间复杂度O(1)的情况

单reactor多线程模式
单reactor多线程模式
主从reactor多线程模式
主从reactor多线程模式.PNG

主从模型在许多项目中广泛使用:Nginx、Memcached、Netty

Netty模型

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

Netty快速入门实例——TCP服务

实现:

  1. Netty服务器在6668端口监听,客户端能发送消息给服务器
  2. 服务器可以回复消息给客户端

引入依赖:
dependency

代码实现(启动)

server 的handler:

package com.km.mpdemo001.netty.tcp;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;

import java.nio.charset.Charset;


/**
 * @description: 自定义Handler
 *
 * @author: 大颗
 * @time: 2020/10/11 19:35
 */
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    /**
     * 读取数据
     * @param ctx : 上下文对象,含有pipeline,channel,地址
     * @param msg : 客户端发送的数据,默认Object类型
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("server ctx: "+ctx);
        //将msg转成一个 ByteBuf,注意不是java.nio.ByteBuffer;
        ByteBuf buf = (ByteBuf)msg;
        System.out.println("客户端发送的消息是" + buf.toString(CharsetUtil.UTF_8));
        System.out.println("客户端地址:"+ctx.channel().remoteAddress());
    }


    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //将数据写入到缓存,并刷新  writeAndFlush(Object:msg)
        //一般对msg进行编码
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端",CharsetUtil.UTF_8));
    }

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

client的handler:

/**
 * @description: 自定义Handler
 *
 * @author: 大颗
 * @time: 2020/10/11 19:35
 */
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
    /**
     * 读取数
     * @param ctx : 上下文对象,含有pipeline,channel,地址
     * @param msg : 客户端发送的数据,默认Object类型
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("server ctx: "+ctx);
        //将msg转成一个 ByteBuf,注意不是java.nio.ByteBuffer;
        ByteBuf buf = (ByteBuf)msg;
        System.out.println("服务器发送的消息是" + buf.toString(CharsetUtil.UTF_8));
        System.out.println("服务器地址:"+ctx.channel().remoteAddress());
    }


//    @Override
//    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//        //将数据写入到缓存,并刷新  writeAndFlush(Object:msg)
//        //一般对msg进行编码
//        ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端",CharsetUtil.UTF_8));
//    }

    /**
     * 处理异常
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }

    /**
     * 当通道就绪就会触发该方法
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("client: " +ctx);
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello ,server",CharsetUtil.UTF_8));
    }
}

server:

public class NettyServer {
    public final static int PORT_ID = 6668;
    public static void main(String[] args) {
        /**
         * 1. 创建两个线程组 bossGroup和workerGroup,无限循环
         * workerGroup 连接请求
         * bossGroup 读写请求
         */
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {

            //创建服务器端的启动对象
            ServerBootstrap bootstrap = new ServerBootstrap();
            //使用链式编程来进行设置
            bootstrap.group(bossGroup,workerGroup) //设置两个线程组
                    .channel(NioServerSocketChannel.class) //使用NioSocketChannel作为服务器的通道实现
                    .option(ChannelOption.SO_BACKLOG,128)//设置线程队列等待连接个数
                    .childOption(ChannelOption.SO_KEEPALIVE,true) // 设置保持活动连接状态
                    .childHandler(new ChannelInitializer<SocketChannel>() { //创建一个通道初始化测试(匿名)对象
                        //给pipeline设置处理器
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    }); //给workerGroup的EventLoop对应的管道设置处理器
            System.out.println("服务器 is ready .....");
            //绑定一个端口并且同步,生成了一个 ChannelFuture 对象
            //启动服务器
            ChannelFuture cf = bootstrap.bind(PORT_ID).sync();
            //对关闭通道进行监听
            cf.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }


    }
}

client:

public class NettyClient {
    public final static int PORT_ID = 6668;
    public final static String aInetHost = "127.0.0.1";
    public static void main(String[] args) {
        //客户端需要一个事件循环组
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            //创建客户端启动对象
            // 注意:客户端是io.netty.bootstrap.Bootstrap,服务端是io.netty.bootstrap.ServerBootstrap;
            Bootstrap bootstrap = new Bootstrap();

            //设置相关参数
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new NettyClientHandler());
                        }
                    });

            System.out.println("客户端启动......");
            ChannelFuture channelFuture = bootstrap.connect(aInetHost, PORT_ID).sync();
            //对关闭通道进行监听
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            group.shutdownGracefully();
        }

    }
}

参数解析

NioEventLoopGroup

new NioEventLoopGroup()的children大小默认为CPU核心数*2,如果要设置,可以初始化时传入参数。
在这里插入图片描述

  • NioEventLoopGroup下包含多个NioEventLoop;NioEventLoop内部采用串行化设计,从消息的读取->解码->处理->编码->发送,始终都由IO线程NioEventLoop负责。
  • 每个NioEventLoop包含一个selector,一个taskQueue
  • 每个NioEventLoop的selector可以注册监听多个NioChannel
  • 每个NioChannel只会绑定在唯一的NioEventLoop上
  • 每个NioChannel都绑定有一个自己的ChannelPipeline
ChannelHandlerContext

toString():

 ctx: ChannelHandlerContext(NettyServerHandler#0, [id: 0x92760a4d, L:/127.0.0.1:6668 - R:/127.0.0.1:55693])
pipeline和channel

toString():

channel:[id: 0x92760a4d, L:/127.0.0.1:6668 - R:/127.0.0.1:55693]
pipeline: DefaultChannelPipeline{(NettyServerHandler#0 = com.km.mpdemo001.netty.tcp.NettyServerHandler)}

pipeline是一个双向链表
pipeline和channel是一一对应的关系

taskQueue

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

  1. 用户程序自定义的普通任务
  2. 用户自定义定时任务
  3. 非当前reactor线程调用channel的各种方法
    例如在推送系统的业务线程里面,根据用户的标识,找到对应的channel引用,然后调用write 类方法向该用户推送消息,就会进入到这种场景。最终的write方法会提交到任务队列中被异步消费。
1. 用户程序自定义的普通任务
ctx.channel().eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10*1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端; tag2\n",CharsetUtil.UTF_8));

            }
        });

类的关系:
taskQueue中的Runnable依次在一个线程中执行: poll and run

NioEventLoop extends SingleThreadEventLoop
.......
SingleThreadEventLoop extends SingleThreadEventExecutor
.......
public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {
	private final Executor executor;
	private final Queue<Runnable> taskQueue;
	......//略
	 @Override
    public void execute(Runnable task) {
        if (task == null) {
            throw new NullPointerException("task");
        }

        boolean inEventLoop = inEventLoop();
        if (inEventLoop) {
            addTask(task);
        } else {
            startThread();
            addTask(task);
            if (isShutdown() && removeTask(task)) {
                reject();
            }
        }

        if (!addTaskWakesUp && wakesUpForTask(task)) {
            wakeup(inEventLoop);
        }
    }
}
2. 用户自定义定时任务
ctx.channel().eventLoop().schedule(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10*1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端; tag4\n",CharsetUtil.UTF_8));

            }
        },5, TimeUnit.SECONDS);
SingleThreadEventExecutor extends AbstractScheduledEventExecutor...
public abstract class AbstractScheduledEventExecutor extends AbstractEventExecutor {
	PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue;
	@Override
    public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
        ObjectUtil.checkNotNull(command, "command");
        ObjectUtil.checkNotNull(unit, "unit");
        if (delay < 0) {
            delay = 0;
        }
        return schedule(new ScheduledFutureTask<Void>(
                this, command, null, ScheduledFutureTask.deadlineNanos(unit.toNanos(delay))));
    }
}

异步模型

基本概念:

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

Future

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

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

  • isDone()
  • isSuccess()
  • getCause()失败原因
  • isCancelled()
  • addListener()注册监听器
/**
             * 注册监听器
             */
            cf.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (cf.isSuccess()){
                        System.out.println("监听端口"+PORT_ID+"成功。");
                    }else {
                        System.out.println("监听端口"+PORT_ID+"失败。");
                    }
                }
            });

入门实例——HTTP服务

  • netty服务器在6668端口监听,浏览器发出请求"http://localhost:6668/"(6668有问题,可能被占用,改成了8998)
  • 服务器可以发送消息给客户端 ,并对特定请求资源进行过滤
  • 目的:Netty可以做http服务开发,并且理解handler实例和客户端及其请求的关系。

server编写与上面基本一样,只是把ServerInitializer 写一个自定义的。

public class ServerInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //加入一个netty提供的 处理Http的编解码器
        pipeline.addLast("MyHttpServerCodec",new HttpServerCodec());
        pipeline.addLast("MyHttpServerHandler",new HttpServerHandler());
    }
}

同时,handler的代码:

/**
 * @description:
 * SimpleChannelInboundHandler是ChannelInboundHandlerAdapter的子类
 * HttpObject 客户端和服务器端相互通信的数据被封装成这个类型
 * @author: 大颗
 * @time: 2020/10/11 22:31
 */

public class HttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {

    /**
     * 读取客户端数据
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
        //判断msg是不是一个HttpRequest请求
        if(msg instanceof HttpRequest){
            System.out.println("msg Class type = "+msg.getClass());
            System.out.println("客户端地址:"+ctx.channel().remoteAddress());

            //回复信息给浏览器
            ByteBuf content = Unpooled.copiedBuffer("hello, i'm a server", CharsetUtil.UTF_8);
            //构建一个 http response
            DefaultHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.OK, content);
            response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH,content.readableBytes());
            //发送response
            ctx.writeAndFlush(response);
        }
    }
}

有一个问题,浏览器每次发出请求,上面handler的channelRead0方法都会执行两次。
原因:浏览器点击一次,其实发出了两个请求,一个是网内页面的请求,一个是对网页图标的请求。那么可以尝试过滤掉第二个请求。

HTTP协议是无状态的协议,因此不同的请求对应的handler不是同一个。

Netty核心模块

Bootstrap、ServerBootstrap

一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务程序的启动引导类,均继承自AbstractBootstrap。

常用的方法有:

  • group:设置两个EventLoopGroup
  • group:该方法用于客户端,用来设置一个EventLoopGroup
  • channel:设置一个服务器端的通道实现
  • childHandler :创建一个channelInitializer的方式;对应的是workerGroup
  • handler:对应的是bossGroup

AbstractBootstrap:

public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable{
	volatile EventLoopGroup group;
	public B group(EventLoopGroup group)
	public B channel(Class<? extends C> channelClass)
	public <T> B option(ChannelOption<T> option, T value) {......}
	public ChannelFuture bind() {
}

ServerBootstrap:

public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerChannel> {
	public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup){......}
	public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value) {.......}
	public ServerBootstrap childHandler(ChannelHandler childHandler) {......}
    
}
public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> {
	public ChannelFuture connect(String inetHost, int inetPort) {......}
}

Future、ChannelFuture

Channel

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

Selector

ChannelHandler

  • ChannelHandler 是一个接口,处理IO事件或拦截IO操作,并将其转发到其ChannelPipeline(业务处理链)中的下一个处理程序。
  • ChannelHandler本身并没有提供很多方法,因为这个接口有很多的方法需要实现,方便使用期间,可以继承它的子类

入站:数据流入channel
子类

  • ChannelInboundHandler 用于处理入站IO事件(接口)
    • ChannelInboundHandlerAdapter:用于处理入站IO事件(类)
  • ChannelOutboundHandler 用于处理出站IO事件
    • ChannelOutboundHandlerAdapter:用于处理出站IO事件(类)
  • ChannelDuplexHandler:用于处理入站和出站事件

Pipeline和(*)ChannelPipeline

ChannelPipeline:

  • ChannelPipeline是一个Handler的集合,它负责处理和拦截inbound或者outbound的事件和操作,相当于一个贯穿Netty的链。(也可以这样理解:ChannelPipeline是保存ChannelHandler的List,用于处理或拦截Channel的入站事件和出站操作)
  • ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户可以完成控制事件的处理方式,以及Channel中各个的ChannelHandler如何互相交互。

Pipeline和ChannelPipeline:

  • 一个Channel包含了一个ChannelPipeline,而ChannelPipeline中又维护了由ChannelHandlerContext(接口)组成的双向链表,并且每个 中又包含着一个ChannelHandler
  • 入站时间和出站事件在一个双向链表中,入站事件会从链表head往后传递到最后一个入站的handler,出站事件会从链表tail往前传递到最前一个出站的handler,两种类型的handler户不干扰。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值