Netty入门之------基于netty实现网络通信

        从前面几篇博文中我们已经学习了从BIO到NIO再到Reactor线程模型+NIO实现了客户端与服务端的一个通信,但这样写起代码来还是比较繁琐的,而Netty则极大的简化我们这种网络通信的开发,它是基于NIO之上的也同时实现了Reactor线程模型,而且使用JDK自带的NIO需要了解很多复杂的概念,一不小心就bug飞起,既然这样那我们就一起来学习下这个神秘的网络框架吧。

Netty概念介绍

       Netty是由JBOSS提供的一个java开源框架,是一个高性能、高可扩展性的异步事件驱动的网络应用程序框架,它极大地简化了TCP和UDP客户端和服务器开发等网络编程。

  1. 设计特点:统一的API,支持多种传输类型(阻塞和非阻塞),简单而强大的线程模型,真正的无连接数据报套接字支持,基于灵活且可扩展的事件模型,可以清晰地分离关注点。
  2. 易于使用:翔实的Javadoc和大量的实例集 没有其他依赖项,JDK 5(Netty 3.x)或6(Netty 4.x)就足够了。(一些可选的特性可 能需要Java1.7+或额外的依赖) 
  3. 性能:拥有比Java的核心API更高的吞吐量以及更低的延迟 得益于池化和复用(Unpooled.buffer()非池化和Unpooled.directBuffer()池化),拥有更低的资源消耗 最小化不必要的内存复制
  4. 健壮性:不会因为慢速、快速或超载的连接而导致OutOfMemoryError 消除在高速网络中NIO应用程序常见的不公平读/写比率
  5. 安全性: 完整的SSL / TLS和StartTLS支持 ,并且拥有丰富活跃的社区

多Reactor线程模型

        为了让netty更好的处理任务,也实现了Reactor线程模型,四个核心概念:1.Resources 资源(请求/任务)   2. Request Handler 请求处理器   3.Dispatcher 分配     4.Synchronous Event Demultiplexer 同步事件复用器,它的线程模型跟我之前一篇博文提到的多Reactor线程模型很类似(https://blog.csdn.net/qq_40826106/article/details/93404421),mainReactor线程去处理客户端的连接请求,并分配subReactor线程组去处理,基于一个subReactor线程里就有一个selector所以可以监听多个客户端,一旦客户端发来了请求,subReactor线程又去分配线程池或者其它业务线程中的业务线程去处理业务。唯一不同的就是subRector自己有一个工作队列,这个是响应给客户端的,就是说当我们IO耗时比较长时,subReactor不会一直在阻塞,而是响应式的事件驱动,会等线程池处理完业务之后,把该返回结果或者说业务放到subReactor队列中,然后等到它空闲时,它再去处理队列中的任务。可以说是尽善尽美了。

职责链设计模式        

       除了上述的多Reactor线程模型还涉及到了职责链模式(Chain of Responsibility Pattern),这也是一种设计模式,就是为请求创建了一个处理对象的链,发送请求和处理请求进行一个解耦,职责链上的处理者负责处理请求,客户只需要请求发送到职责链上即可,无需关心请求的处理细节和请求的传递,这样即使你需要增加新的工序,也只要加一个AbstractHandler的实现类即可,就跟我们工厂流水线一样,先经过工序一,再把工序一完成的半成品交给工序二,然后再将工序二的半成品交给工序三,类似。有点类似于我们的链表结构,那么现在就用代码来实现以下netty中的职责链模式。这里推荐:https://www.processon.com/  作为类图或者流程的分析,非常强大好用,主要是免费,推荐给大家(不过文件超过9个就要收费了)。

package com.dongnaoedu.network.humm.netty;

/**
 * @author Heian
 * @time 19/07/20 21:18
 * @description:链表形式调用,netty就是类似的这种形式
 */
public class PipelineDemo {
    //因为只有HandlerChainContext里有工序实现这个属性,所以只有通过这个HandlerChainContext来看它是否有
    HandlerChainContext headContext = new HandlerChainContext (new AbstractHandler () {
        @Override
        void doHandler(HandlerChainContext chainContext, Object args) {
            chainContext.runNext (args);
        }
    });

    /**
     * 从第一个节点是headContext,它这个类保存的工序为handler1,所以先会执行工序一的具体实现,如果该节点的还有下个节点,
     * 则继续执行下个节点的方法,类似于递归执行,所以每个工序的方法都能得到执行
     */
    void doProcessHandler(Object args){
        this.headContext.runCurrent (args);
    }

    //增加工序
    public void addCircle(AbstractHandler abstractHandler){
        //每次添加一个对象,都会从头到尾添加一遍,保证添加的元素是处于最尾部
        HandlerChainContext context = headContext;
        while (context.chainContextNext != null){
            context = context.chainContextNext;
        }
        //把传入的工序赋值给下一个节点
       context.chainContextNext = new HandlerChainContext (abstractHandler);

    }

    public static void main(String[] args) {
        PipelineDemo pipeline = new PipelineDemo ();
        //此时头部节点 已经加载出来(类的初始化:局部变量通过关键字new加载)
        pipeline.addCircle (new Handler1 ());// header1
        pipeline.addCircle (new Handler2 ());// header1
        pipeline.addCircle (new Handler2 ());// header1
        //分别执行每一个节点所包含的工序实现方法的信息
        pipeline.doProcessHandler ("火车头");


    }



}

/**
 * 下面就写一个字符串的叠加 1 + 2 +3...
 */

//第一步:定义处理器抽象类(就是工序的抽象) 和 负责维护链和链的执行
    //工序的抽象:参数1:chainContext  参数2:传入的参数
abstract class AbstractHandler{
    abstract void doHandler(HandlerChainContext chainContext,Object args);
}
    //节点,承上启下负责A流程--》B流程的执行
class HandlerChainContext{
    //既然承上启下,就必须要知道下一个工序和处理工序的实现
    HandlerChainContext chainContextNext;
    AbstractHandler abstractHandler;

    //构造方法:切换上下文只需要关心抽象工序即可
    public HandlerChainContext(AbstractHandler abstractHandler){
        this.abstractHandler = abstractHandler;
    }
    //运行当前节点信息
    void runCurrent(Object args){
        this.abstractHandler.doHandler (this,args);
    }

    //节点运行下一个具体实现的方法,并且保存节点信息
    void runNext(Object args){
        if (this.chainContextNext != null){
            this.chainContextNext.runCurrent (args);
        }
    }
}
//第二步:工序的实现类
class Handler1 extends AbstractHandler{
    @Override
    void doHandler(HandlerChainContext chainContext, Object args) {
        args = args.toString () + "我是工序一   ";
        System.out.println ("One:" + args);
        chainContext.runNext (args);
    }
}
class Handler2 extends AbstractHandler{
    @Override
    void doHandler(HandlerChainContext chainContext, Object args) {
        args = args.toString () + "我是工序二   ";
        System.out.println ("Two:" + args);
        chainContext.runNext (args);
    }
}
//备注:以节点为中心展开思维,节点保存节点和对应节点的工序。
//One:火车头我是工序一   
//Two:火车头我是工序一   我是工序二   
//Two:火车头我是工序一   我是工序二   我是工序二   

Pipeline管道保存了通道所有处理器信息,当客户端连接进来,每一个channel时自动创建一个专有的pipeline。 入站事件和出站操作会调用pipeline上的处理器。上面的伪代码Context是单向的,只有指向下一个Context,而在Netty中是双向的,其中保存了headContex头部和tail尾部Context,如果你要添加一个MyContext,则默认是按照先进先出原则加到中间。

Netty中事件的定义
inbound入站事件 描述 outbound出站事件 描述
fireChannelRegistered  channel注册事件 bind 端口绑定时间
fireChannelUnregistered  channel解除注册事件 connect 连接时间
fireChannelActive  channel活跃事件 disconnect 断开连接事件
fireChannelInactive  channel非活跃事件 close 关闭事件
fireExceptionCaught  异常事件 deregister 解除注册事件
fireUserEventTriggered  用户自定义事件 flush 刷新数据到网络事件
fireChannelRead  channel读事件 read 读事件,用于op_read到selector
fireChannelReadComplete  channel读取完成事件 write 写事件
fireChannelWritabilityChanged  channel写状态变化事件 writeAndFlush  写出数据事件
  1. 入站事件:通常指I/O线程生成了入站数据。 (通俗理解: 从socket底层自己往上冒上来的事件都是入站) 比如EventLoop收到selector的OP_READ/Accept事件,入站处理器调用socketChannel.read(ByteBuffer) 接收到数据后,这将导致通道的ChannelPipeline中包含的下一个中的channelRead方法被调用。
  2. 出站事件:经常是指I/O线程执行实际的输出操作。 (通俗理解:想主动往socket底层操作的事件的都是出站) 比如bind方法用意是请求server socket绑定到给定的SocketAddress,这将导致通道的 ChannelPipeline中包含的下一个出站处理器中的bind方法被调用。应用层(Tcp)往传输层(Http等)写数据:写数据、连接服务器、邦迪端口、连接关闭等等。
  3. ChannelHandler:用于处理I/O事件或拦截I/O操作,并转发到ChannelPipeline中的下一个处理器。 这个顶级接口定义功能很弱,实际使用时会去实现下面两大子接口: 处理入站I/O事件的ChannelInboundHandler、处理出站I/O操作的ChannelOutboundHandler
  4. ChannelHandlerContext:实际存储在Pipeline中的对象并非ChannelHandler,而是上下文对象。 将handler,包裹在上下文对象中,通过上下文对象与它所属的ChannelPipeline交互,向上或向下传递事件 或者修改pipeline都是通过上下文对象。
  5. 适配器类:为了开发方便,避免所有handler去实现一遍接口方法,Netty提供了简单的实现类: ChannelInboundHandlerAdapter处理入站I/O事件 ChannelOutboundHandlerAdapter来处理出站I/O操作 ChannelDuplexHandler来支持同时处理入站和出站事件。

      ChannelPipeline是线程安全的,ChannelHandler(你要添加的流水线作业)可以在任何时候添加或删除。 例如,你可以在即将交换敏感信息时插入加密处理程序,并在交换后删除它。 一般操作,初始化的时候增加进去,较少删除。下面是Pipeline中管理handler的API。

Netty 的核心组件

  1. Channel:Netty中自己定义的Channel,增强版的通道概念
  2. EventLoop:由线程驱动,处理Channel的所有I/O事件
  3. ChannelPipeline:事件处理机制
  4. ChannelHandler:事件处理器
  5. ByteBuf:增强的ByteBuf缓冲区
  6. Bootstrap:启动器,引导Netty应用程序启动

Netty实现通信Demo

   服务端代码: 

package com.dongnaoedu.network.humm.netty.echo;

import com.dongnaoedu.network.netty.echo.EchoServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

/**
 * @author Heian
 * @time 19/07/21 23:40
 * @description:服务端
 */
public class NettyServer {

    static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));

    public static void main(String[] args) throws Exception {
        // 1、创建EventLoopGroup
        EventLoopGroup bossGroup = new NioEventLoopGroup (1);//类比mainReactor
        EventLoopGroup workerGroup = new NioEventLoopGroup();//类比subReactor线程组  默认大小是cpu核心数X2
        final ServerHandler serverHandler = new ServerHandler();
        try {
            ServerBootstrap b = new ServerBootstrap();// 2、创建启动器
            b.group(bossGroup, workerGroup)// 3、配置启动器
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 100)
                    .handler(new LoggingHandler (LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel> () {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline p = ch.pipeline();
                            p.addLast(serverHandler);//3、向流水线添加作业 ChannelHandler
                        }
                    });
            // 等待客户端连接
            ChannelFuture f = b.bind(PORT).sync();// 4、启动器启动
            f.channel().closeFuture().sync();// 5、等待服务端channel关闭,不关闭则一直阻塞
        } finally {
            // 6.释放资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }


}

服务端业务代码:

package com.dongnaoedu.network.humm.netty.echo;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 * @author Heian
 * @time 19/07/21 23:40
 * @description:服务端添加需要入栈的
 */
//ChannelHandler接口的子类  ChannelInboundHandlerAdapter-->ChannelInboundHandler-->ChannelHandler
public class ServerHandler extends ChannelInboundHandlerAdapter {

    //读取通道内消息,并返回给客户端
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        System.out.println("收到客户端数据,还给客户端:" + msg);
        ctx.write(msg);
    }

    //读取完成刷新
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    //异常则关闭ChannelHandlerContext连接
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }


}

客户端代码:

package com.dongnaoedu.network.humm.netty.echo;

import com.dongnaoedu.network.netty.echo.EchoClientHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

/**
 * @author Heian
 * @time 19/07/21 23:41
 * @description:客户端
 */
public class NettyClient {
    static final String HOST = System.getProperty("host", "127.0.0.1");
    static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));

    public static void main(String[] args) throws Exception {
        // 1、创建EventLoopGroup
        EventLoopGroup group = new NioEventLoopGroup ();//类比subReactor线程组  默认大小是cpu核心数X2
        try {
            Bootstrap b = new Bootstrap();//// 2、创建启动器
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel> () {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline p = ch.pipeline();
                            p.addLast(new ClientHandler ());//3、向流水线添加作业 ChannelHandler
                        }
                    });

            // 连接服务端
            ChannelFuture f = b.connect(HOST, PORT).sync();// 4、启动器启动
            f.channel().closeFuture().sync();// 5、等待服务端channel关闭,不关闭则一直阻塞
        } finally {
            // 6.释放资源
            group.shutdownGracefully();
        }
    }
}

客户端业务代码:

package com.dongnaoedu.network.humm.netty.echo;

import com.dongnaoedu.network.netty.echo.EchoClientHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

/**
 * @author Heian
 * @time 19/07/21 23:41
 * @description:客户端
 */
public class NettyClient {
    static final String HOST = System.getProperty("host", "127.0.0.1");
    static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));

    public static void main(String[] args) throws Exception {
        // 1、创建EventLoopGroup
        EventLoopGroup group = new NioEventLoopGroup ();//类比subReactor线程组  默认大小是cpu核心数X2
        try {
            Bootstrap b = new Bootstrap();//// 2、创建启动器
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel> () {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline p = ch.pipeline();
                            p.addLast(new ClientHandler ());//3、向流水线添加作业 ChannelHandler
                        }
                    });

            // 连接服务端
            ChannelFuture f = b.connect(HOST, PORT).sync();// 4、启动器启动
            f.channel().closeFuture().sync();// 5、等待服务端channel关闭,不关闭则一直阻塞
        } finally {
            // 6.释放资源
            group.shutdownGracefully();
        }
    }
}

上述代码完成一个回复的功能:

截取部分客户端控制台打印信息:

  • 给服务器发送数据:UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 256, cap: 256)
  • 收到服务端数还给服务器:PooledUnsafeDirectByteBuf(ridx: 0, widx: 256, cap: 1024)
  • 收到服务端数还给服务器:PooledUnsafeDirectByteBuf(ridx: 0, widx: 256, cap: 1024)
  • 收到服务端数还给服务器:PooledUnsafeDirectByteBuf(ridx: 0, widx: 256, cap: 512)
  • 收到服务端数还给服务器:PooledUnsafeDirectByteBuf(ridx: 0, widx: 256, cap: 512)

截取部分服务端控制台打印信息:

  • 收到客户端数据,还给客户端:PooledUnsafeDirectByteBuf(ridx: 0, widx: 256, cap: 1024)
  • 收到客户端数据,还给客户端:PooledUnsafeDirectByteBuf(ridx: 0, widx: 256, cap: 1024)
  • 收到客户端数据,还给客户端:PooledUnsafeDirectByteBuf(ridx: 0, widx: 256, cap: 512)
  • 收到客户端数据,还给客户端:PooledUnsafeDirectByteBuf(ridx: 0, widx: 256, cap: 512)

jar包成本地源码,方便调试和阅读

小提示:如果你想要更好的阅读源码,建议利用IDE的工具特性去更换源码地址,这样可以在源码写注释,方便自己理解记忆,还可以利用自带的show Diagraims来查看类图的层级关系。(netty源码注释版本:https://github.com/nbpeak/netty/tree/netty-4.1.34)

发布了73 篇原创文章 · 获赞 18 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_40826106/article/details/95749026
今日推荐