Netty学习

Netty学习文档

说到Netty, 我们应该知道Netty是基于NIO的再一次封装,那么,先让我们了解一下BIO,NIO的一些概念。

 

从下网上是java io 编程的发展体系图

  1. BIO

1.1 bio的基本介绍

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

1.2 bio线程模型和工作机制

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

1.3 bio的一些问题

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

  1. NIO

2.1 NIO的基本介绍

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

2.2 NIO与BIO比较

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

2.3 NIO 三大核心原理示意图

 

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

  1. Netty

3.1 Netty的介绍

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

3.2 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. 它的 NettyService 基于 Netty 框架二次封装实现。

3.3 为什么使用Netty

原生NIO存在的问题

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

Netty的优点

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

3.4 线程模型

Netty的线程模型是基于主从Reactor的,所以我们得先了解一下Reactor线程模型,分为三种,①单Reactor单线程模型,②单Reactor多线程模型,③主从Reactor多线程模型

·单线程Reactor,模型图如下

 

多个客户端请求链接,然后Reactor通过selector轮询判断哪些通道是有事件发生的,如果是连接事件,就到Acceptor中建立连接;如果是其他读写事件,就有dispatch分发到对应的handler中进行处理。这种模式缺点就是Reactor和handler在一个线程中,如果Handler阻塞了,那么程序就阻塞了。

·Reactor多线程,模型图如下

 

处理流程

①多个客户端发起请求

②Reactor对象通过Selector监听客户端事件,通过dispatch进行分发

③如果是连接事件,分发给acceptor,通过accept方法处理连接请求,然后创建一个Handler对象响应事件

④如果不是连接请求,由Reactor对象调用对应handler对象进行处理;handler只响应事件,不做具体的业务处理,handler通过read方法读取数据以后,会把数据分发给线程池中某个线程进行业务处理,并且将处理结果返回给handler

⑤handler收到响应后,通过send方法把处理的结果返回给客户端client

相比于单Reactor单线程,这里将业务处理的事情交给了不同的线程去做,发挥了多核cpu性能,但是Reactor只有一个,所有事件的监听和响应都只有一个Reactor去做,并发性还是不好。

·主从Reactor多线程(Netty线程模型就是基于此),模型图如下

 

这个模型跟前面的单Reactor多线程模型的区别就是:专门搞了一个MainReactor来处理连接事件,如果不是连接事件就分发给SubReactor进行处理。图中的SubReactor只有一个,但是可以有多个去分发请求,所以性能就上去了。

①优点:父线程与子线程的交互简单、职责明确,父线程负责接收连接,子线程负责完成后续的业务处理;

②缺点:编程复杂度高

Netty线程模型简单版本

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

Netty线程模型复杂版本

 

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

3.5 Netty的核心组件

3.5.1BootStrap,ServerBootStrap

BootStrap意思是引导,一个Netty程序的开始总是从BootStrap开始,BootStrap作用是配置整个Netty,将各个组件联合起来,BootStrap是客户端的引导类,ServerBootStrap是服务器端的引导类

两个引导类中的一些常见方法和作用如下

public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup)

这个方法应用于服务器端,用来设置两个事件循环组,BossEventGroup,WorkerEventGroup

public B group(EventLoopGroup group)

这个方法应用于客户端,用于客户端EventLoopGroup设置

public B channel(Class<? extends C> channelClass)

用于设置服务器端的通道实现

public <T> B option(ChannelOption<T> option, T value)

给serverChannel添加配置

public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value)

服务端给接收到的通道添加配置

public ServerBootstrap childHandler(ChannelHandler childHandler)

该方法用来设置业务处理类(自定义的handler方法)

public ChannelFuture bind(int inetPort)

该方法用于服务器端,设置占用的端口号

public ChannelFuture connect(String inetHost, int inetPort)

用于客户端,用来与服务器端连接

3.5.2 Future,ChannelFuture

Netty中的所有操作都是异步的,不能立刻得知消息是否被正确执行。但是可以等一会儿,或者直接注册一个监听,具体的实现就是通过Future和ChannelFuture,用他们可以注册一个监听,当IO操作执行成功或者失败后自动触发监听事件

常见方法:

Channel channel(),返回当前正在进行 IO 操作的通道

ChannelFuture sync(),等待异步操作执行完毕

3.5.3 Channel

①Netty网络通信的组件,用于执行网络IO操作

②通过Channel可以获得当前网络连接的通道的状态

③通过Channel可以获取网络连接的一些参数,例如缓冲区大小

④Channel提供异步的网络IO操作,如建立连接,读写数据等;调用立即返回一个ChannelFuture实例,通过注册监听器到ChannelFuture上,可以在IO操作成功,失败或者取消时调用回调函数通知调用方

⑤不同协议有不同的阻塞类型与之对应

NioSocketChannel 异步的客户端TCP 连接

NioServerSocketChannel 异步的服务端 TCP连接

NioDatagramChannel 异步的 UDP 连接

3.5.4 Selector

Netty基于Selector实现IO多路复用,通过Selector一个线程可以监听多个连接的Channel事件

当向一个Selector注册Channel后,Selector就可以不断的查询这些注册的Channel是否有就绪IO事件,如果有进入响应的channelHandler进行处理,这样就可以一个Selector管理多个Channel

3.5.5 ChannelHandler及其实现类

ChannelHandler是一个接口,处理IO事件或者拦截IO操作,并且将其转发到ChannelpipeLine(业务处理链中的下一个处理程序)

ChannelHandler本身并没有提供多少方法,我们可以自定义一个handler类去继承ChannelInboundHandlerAdapter,然后根据业务去重写里面的方法

3.5.6 PipeLine和ChannelPipeLine

ChannelPipeLine是一个Handler集合,负责处理和拦截入站出站事件的操作;相当于贯穿一整个Netty的链,就相当于时保存channelHandler的list,用于处理和拦截channel的一系列操作

Netty中的每一个channel中有且仅有一个ChannelPipeLine与之对应,对应关系如下图

 

3.5.7 ChannelHandlerContext

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

绑定了对应的pipeline和channel的信息,方便对channelHandler进行调用

常用方法

 ChannelFuture close(),关闭通道

 ChannelOutboundInvoker flush(),刷新

 ChannelFuture writeAndFlush(Object msg),将数据写到ChannelPipeline 中当前 ChannelHandler 的下一个 ChannelHandler 开始处理(出站)

3.5.8 ChannelOption

Netty通过ChannelOption设置通道Channel的一些参数,如缓存大小

3.5.9 EventLoopGroup和其实现类NioEventLoopGroup

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

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

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

3.6 Netty基本使用

1.引入pom依赖

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.20.Final</version>
</dependency>

2.编写服务端

/**
 * netty服务端
 * 简单使用
 */
public class Server {
    public static void main(String[] args) {
        //BossGroup 用于接收客户端连接请求的工作组
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        //WorkerGroup 用于接收客户端连接后处理读写请求的工作线程组 线程数量可以指定 默认为 cpu核数*2
        EventLoopGroup workerGroup = new NioEventLoopGroup(4);
        try {
            //创建ServerBootstrap 服务端启动引导类 帮助我们创建Netty服务
            ServerBootstrap b = new ServerBootstrap();
            //用ServerBootstrap 创建启动器
            b.group(bossGroup, workerGroup) //绑定工作组
                    .channel(NioServerSocketChannel.class) //设置通道类型 NioServerSocketServer 非阻塞
                    .option(ChannelOption.SO_BACKLOG, 1024) //针对服务端配置 设置tcp缓冲区
                    .childOption(ChannelOption.SO_SNDBUF, 32 * 1024) //针对客户端连接通道配置 设置发送数据的缓存大小
                    .childOption(ChannelOption.SO_RCVBUF, 32 * 1024) //设置读取数据的缓存大小
                    .childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持长连接
                    //初始化绑定服务通道
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) {
                            //对workerGroup的socketChannel设置处理器 数据传输过来的时候会进行拦截,可以设置多个handler
                            socketChannel.pipeline().addLast(new ServerHandler());
                        }
                    });
            System.out.println("netty server is start ......");
            //绑定一个端口并且同步, 生成一个channelFuture异步对象,通过isDone()等方法可以判断异步事件的执行情况
            //启动服务器并且绑定端口,bind是异步操作,sync是等待异步操作执行完毕
            ChannelFuture cf = b.bind(9000).sync();
            // 等待服务端监听端口关闭,closeFuture是异步操作
            // 通过sync方法同步等待通道关闭处理完毕,这里会阻塞等待通道关闭完成,内部调用的是Object的wait()方法
            cf.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //释放连接
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

3.编写处理器Handler,处理数据

/**
 * 服务端监听器
 */
public class ServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 当通道激活时触发此监听
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("====================服务端通道激活========================");
    }

    /**
     * 当我们的通道里有数据进行读取的时候触发此监听
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        try {
            //NIO通信(传输的数据是什么?)
            ByteBuf buf = (ByteBuf) msg;
            //定义byte数组
            byte[] req = new byte[buf.readableBytes()];
            //从缓冲区取到数据 req
            buf.readBytes(req);
            //将读取到的数据转化成字符串
            String body = new String(req, "utf-8");
            System.out.println("服务端读取到数据:" + body);
            //响应给客户端的数据
            ctx.writeAndFlush(Unpooled.copiedBuffer("netty response data".getBytes()));
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            //释放数据
            ReferenceCountUtil.release(msg);
        }
    }

    /**
     *  当服务端读取完数据后触发的监听
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        System.out.println("========================服务端读取数据完成========================");
    }

    /**
     * 当读取数据出现异常时触发的监听
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("========================服务端读取数据出现异常=======================");
        ctx.close();
    }
}

4.编写客户端

public class Client {
    public static void main(String[] args) throws InterruptedException {
        //客户端需要一个事件循环组
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        //辅助类,帮助创建Netty服务
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(workerGroup) //绑定工作组
                .channel(NioSocketChannel.class) //设置Nio模式
                .handler(new ChannelInitializer<SocketChannel>() { //初始化绑定服务通道
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        //为通道进行初始化:数据传输过来的时候会进行拦截和执行 (可以有多个拦截器)
                        socketChannel.pipeline().addLast(new ClientHandler());
                    }
                });
        System.out.println("netty client start......");
        ChannelFuture cf = bootstrap.connect("127.0.0.1", 9000).syncUninterruptibly();
        cf.channel().writeAndFlush(Unpooled.copiedBuffer("Netty client request client......".getBytes()));
        //关闭连接
        cf.channel().closeFuture().sync();
        workerGroup.shutdownGracefully();
    }
}

5.编写客户端处理器

/**
 * 客户端 监听器
 */
public class ClientHandler extends ChannelInboundHandlerAdapter {

    /**
     * 通道被激活时 触发此监听
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("====================客户端通道被激活========================");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        super.channelRead(ctx, msg);
    }

    /**
     * 客户端读取数据完成触发
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        System.out.println("====================客户端读取数据完成========================");
    }

    /**
     *  读取数据异常时触发
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("====================数据读取异常===========================");
        ctx.close();
    }
}

6. 启动服务器客户端发送数据

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值