Netty框架

文章较长,建议在电脑上查看
Netty概述

Netty是一款异步的事件驱动的网络应用程序框架,支持快速开发可维护、高性能且面向协议的服务器和客户端。Netty主要是对Java的NIO包进行的封装。

第一个Netty应用程序

网络上有一个形象的比喻来形容Netty客户端和服务器端的交互模式。把一个人比作一个Client,把山比作一个Server,人走到山旁,就和山建立了连接,人向山大喊了一声,就代表向山发送了数据,人的喊声经过山的反射形成了回声,这个回声就是服务器的响应数据。如果人离开,就代表断开了连接,当然人也可以再回来。好多人可以同时向山大喊,他们的喊声一定会得到山的回应。

首先写一个简单的Demo,具体步骤如下:

步骤01:完整的NettyServer包含两部分:BootsTrapping用于配置服务器端基本信息;ServerHandler用于真正的业务逻辑处理。首先我们开发服务类NettyServer,具体代码如下所示:

package com.sixj.demo.network;


import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

import java.net.InetSocketAddress;

/**
 * 描述:服务端
 * @author sixiaojie
 * @date 2020-03-22-10:59
 */
public class NettyServer {
    private static final int PORT = 8080;

    public static void main(String[] args) {
        try {
            // 启动服务端
            new NettyServer().start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void start() throws InterruptedException {
        // Bootstrap 主要作用是配置整个Netty程序,串联各个组件
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        // 通过NIO方式来接受连接和处理连接
        NioEventLoopGroup group = new NioEventLoopGroup();
        try{
            serverBootstrap.group(group);
            // 设置NIO类型的channel
            serverBootstrap.channel(NioServerSocketChannel.class);
            // 设置监听端口
            serverBootstrap.localAddress(new InetSocketAddress(PORT));
            // 连接到达时会创建一个通道
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    // 流水线管理通道中的处理程序(Handler),在通道队列中添加一个处理程序来处理业务
                    socketChannel.pipeline().addLast("myHandler",new NettyServerHandler());
                }
            });
            // 配置完成,开始绑定server,通过调用sync同步方法阻塞直到绑定成功
            ChannelFuture channelFuture = serverBootstrap.bind().sync();
            System.out.println("Server started and listen on"+channelFuture.channel().localAddress());
            // 应用程序会一直等待,直到通道关闭
            channelFuture.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            // 关闭EventLoopGroup,释放掉所有资源,包括创建的线程
            group.shutdownGracefully().sync();
        }
    }
}
  1. 创建一个ServerBootstrap实例。

  2. 创建EventLoopGroup处理各种事件,如处理连接请求,发送、接收数据等。

  3. 定义本地InetSocketAddress(port),让Server进行绑定。

  4. 创建childHandler来处理每一个连接请求。

  5. 所有准备就绪后,调用ServerBootstrap.bind()方法绑定Server。

接下来开发NettyServerHandler类来处理真正的业务,具体代码如下所示:

package com.sixj.demo.network;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
* 描述:NettyServerHandler
* @author sixiaojie
* @date 2020-03-22-11:24
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
   /**
    * 描述:读取客户端发送的消息
    * @param ctx
    * @param msg
    * @throws Exception
    */
   @Override
   public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
       ByteBuf result = (ByteBuf) msg;
       byte[] content = new byte[result.readableBytes()];
       // msg中存储的是ByteBuf类型的数据,把数据读取到byte[]
       result.readBytes(content);
       // 接收并打印客户端的信息
       System.out.println("Client said:"+new String(content));
       // 释放资源,这行很关键
       result.release();
       // 向客户端发送消息
       String response = "hello client!";
       // 在当前场景下,发送的数据必须转换成ByteBuf数组
       ByteBuf encoded = ctx.alloc().buffer(4 * response.length());
       encoded.writeBytes(response.getBytes());
       ctx.write(encoded);
       ctx.flush();
   }

   /**
    * 描述:信息获取完毕后操作
    * @param ctx
    * @throws Exception
    */
   @Override
   public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
       // flush掉所有写回的数据
       ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
               // 当flush完成后关闭channel
               .addListener(ChannelFutureListener.CLOSE);
   }

   /**
    * 描述:用于处理异常
    * @param ctx
    * @param cause
    * @throws Exception
    */
   @Override
   public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
       // 捕捉异常信息
       cause.printStackTrace();
       // 出现异常关闭channel
       ctx.close();
   }
}

步骤02:开发NettyClient,连接到Server,向Server写数据,等待Server返回数据,最后关闭连接。和Server端类似,只不过Client端要同时指定连接主机的IP和Port。具体代码如下所示:

package com.sixj.demo.network;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

import java.net.InetSocketAddress;

/**
 * @author sixiaojie
 * @date 2020-03-22-11:46
 */
public class NettyClient {
    private final String host;
    private final int port;
    public NettyClient(String host,int port){
        this.host = host;
        this.port = port;
    }
    public void start() throws Exception{
        NioEventLoopGroup group = new NioEventLoopGroup();
        try{
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group);
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.remoteAddress(new InetSocketAddress(host,port));
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    socketChannel.pipeline().addLast(new NettyClientHandler());
                }
            });
            ChannelFuture channelFuture = bootstrap.connect().sync();
            channelFuture.addListener((ChannelFutureListener) future ->{
                if(future.isSuccess()){
                    System.out.println("Client connected......");
                }else{
                    System.out.println("server connected failed......");
                    future.cause().printStackTrace();
                }
            });
            channelFuture.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully().sync();
        }
    }

    public static void main(String[] args) throws Exception {
        new NettyClient("127.0.0.1",8080).start();
    }
}
  1. 创建一个ServerBootstrap实例。

  2. 创建一个EventLoopGroup来处理各种事件,如处理连接请求,发送、接收数据等。

  3. 定义一个远程InetSocketAddress。

  4. 连接完成之后,Handler会被执行一次。

  5. 所有准备就绪后,调用ServerBootstrap.connect()方法连接Server。

同样继承一个SimpleChannelInboundHandler来实现业务逻辑代码NettyClientHandler,需要重写其中的3个方法,具体代码如下所示:

package com.sixj.demo.network;

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

/**
* 描述:客户端业务处理类
* @author sixiaojie
* @date 2020-03-26-16:53
*/
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
   /**
    * 描述:此方法会在连接到服务器后被调用
    * @param ctx
    * @throws Exception
    */
   @Override
   public void channelActive(ChannelHandlerContext ctx) throws Exception {
       String msg = "hello Server!";
       ByteBuf encoded = ctx.alloc().buffer(4 * msg.length());
       encoded.writeBytes(msg.getBytes());
       ctx.write(encoded);
       ctx.flush();
   }

   /**
    * 描述:此方法会在接收到服务器数据后调用
    * @param ctx
    * @param msg
    * @throws Exception
    */
   @Override
   public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
       ByteBuf result = (ByteBuf)msg;
       byte[] content = new byte[result.readableBytes()];
       result.readBytes(content);
       System.out.println("Server said:"+new String(content));
       result.release();
   }

   /**
    * 描述:捕捉到异常
    * @param ctx
    * @param cause
    * @throws Exception
    */
   @Override
   public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
       cause.printStackTrace();
       ctx.close();
   }
}

步骤03:运行NettyServer类启动服务端,可在控制台中查看打印的信息:

//省略信息
//服务启动并监听8080端口
Server started and listen on /0:0:0:0:0:0:0:0:8080

运行NettyClient类启动客户端,可在控制台中查看打印的信息:

//客户端打印的信息
client connected......
Server said:hello client!

同时,也可以在服务端控制台再次查看打印的信息:

//服务端打印的信息
Client said:hello Server!

步骤04:至此,第一个Netty Demo开发完成。

Netty架构设计

为了更好地理解和进一步深入Netty,先总体认识一下Netty用到的组件及它们在整个Netty架构中是如何协调工作的。Netty应用中必不可少的组件有:

Bootstrap或ServerBootstrap
EventLoop
EventLoopGroup
ChannelPipeline
Channel
Future或ChannelFuture
ChannelInitializer
Handler

Bootstrap或ServerBootstrap:一个Netty应用,通常由一个Bootstrap开始,它的主要作用是配置整个Netty程序,串联起各个组件。

Handler:为了支持各种协议和处理数据的方式,便诞生了Handler组件。Handler主要用来处理各种事件,这里的事件很广泛,可以是连接、数据接收、异常、数据转换等。ChannelInboundHandler是一个最常用的Handler,作用是处理接收到数据时的事件,也就是说,我们的业务逻辑一般就写在Handler里面,ChannelInboundHandler用来处理我们的核心业务逻辑。

ChannelInitializer:当一个连接建立时,我们需要知道如何接收或者发送数据。当然,我们有各种各样的Handler实现来处理它,ChannelInitializer便是用来配置这些Handler的,它会提供一个ChannelPipeline,并把Handler加入ChannelPipeline。

ChannelPipeline:一个Netty应用,基于ChannelPipeline机制,这种机制需要依赖于EventLoop和EventLoopGroup,这三个组件(ChannelPipeline、EventLoop以及EventLoopGroup)都和事件或者事件处理相关。

EventLoop:目的是为Channel处理IO操作,一个EventLoop可以为多个Channel服务。

EventLoopGroup:包含多个EventLoop。

Channel:代表一个Socket连接,或者其他和IO操作相关的组件,它和EventLoop一起用来参与IO处理。

Future:在Netty中所有的IO操作都是异步的。因此,你不能立刻得知消息是否被正确处理,但是可以过一会等它执行完成,或者直接注册一个监听,具体的实现是通过Future和ChannelFuture完成的。它们可以注册一个监听,当操作执行成功或失败时监听会自动触发。总之,所有的操作都会返回一个ChannelFuture。

一个Channel会对应一个EventLoop,而一个EventLoop会对应一个线程,也就是说,仅有一个线程在负责一个Channel的IO操作。当一个连接到达,Netty会注册一个Channel,然后EventLoopGroup会分配一个EventLoop绑定到Channel上,在这个Channel的整个生命周期中,都会由绑定的这个EventLoop来为它服务,而EventLoop就是一个线程。

EventLoop和EventLoopGroup的关系如何呢?我们前面说过一个EventLoopGroup包含多个Eventloop,EventLoop其实继承自EventloopGroup,也就是说,在某些情况下,我们可以把一个EventLoopGroup当作一个EventLoop来用。

我们利用Bootstrapping来配置Netty应用,它有两种类型:Bootstrap和ServerBootstrap。Bootstrap用于Client端,ServerBootstrap用于Server端。

ServerBootstrap用于Server端,通过调用bind()方法来绑定到一个端口监听连接;Bootstrap用于Client端,需要调用connect()方法来连接服务器端,但我们也可以通过调用bind()方法返回的ChannelFuture获取Channel去连接服务器端。

客户端的Bootstrap一般用一个EventLoopGroup,而服务器端的ServerBootstrap会用到两个(这两个也可以是同一个实例)。为何服务器端要用到两个EventLoopGroup呢?这么设计有明显的好处,如果一个ServerBootstrap有两个EventLoopGroup,就可以把第一个EventLoopGroup专门用来负责绑定到端口监听连接事件,而把第二个EventLoopGroup用来处理每个接收到的连接,如果仅由一个EventLoopGroup处理所有请求和连接的话,在并发量很大的情况下,这个EventLoopGroup就可能会忙于处理已经接收到的连接而不能及时处理新的连接请求,用两个的话,会有专门的线程来处理连接请求,不会导致请求超时的情况,大大提高了并发处理能力。

我们知道一个Channel需要由一个EventLoop来绑定,而且两者一旦绑定就不会再改变。一般情况下,一个EventLoopGroup中的EventLoop数量会少于Channel数量,因此很有可能出现多个Channel共用一个EventLoop的情况,这意味着如果一个Channel中的EventLoop很忙的话,就会影响这个Eventloop对其他Channel的处理,这也是我们不能阻塞EventLoop的原因。

当然,我们的Server也可以只用一个EventLoopGroup,由一个实例来处理连接请求和IO事件,具体如图所示。

我们的应用程序中用到的最多的应该是ChannelHandler,可以这么想象,数据在一个ChannelPipeline中流动,而ChannelHandler便是其中一个个小阀门,这些数据会经过每一个ChannelHandler并且被它处理。

ChannelHandler有两个子类:ChannelInboundHandler和ChannelOutboundHandler。这两个子类对应两个数据流向,如果数据是从外部流入我们的应用程序的,就看作是Inbound,相反便是Outbound。其实ChannelHandler和Servlet有些类似,一个ChannelHandler处理完接收到的数据会传给下一个Handler,或者什么都不处理,直接传递给下一个。ChannelPipeline具体原理如图所示。

一个ChannelPipeline可以将ChannelInboundHandler和ChannelOutboundHandler混合在一起,当一个数据流进入ChannelPipeline时,它会从ChannelPipeline头部开始传给第一个ChannelInboundHandler,当第一个处理完后再传给下一个,一直传递到管道的尾部。与之相对应的是,当数据被写出时,它会从管道的尾部开始,先经过管道尾部“最后”一个ChannelOutboundHandler,当它处理完成后会传递给前一个ChannelOutboundHandler。
数据在各个Handler之间传递,需要调用方法中传递的ChanneHandlerContext来操作,Netty的API中提供了两个基类:ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter,它们仅仅实现了调用ChanneHandlerContext来把消息传递给下一个Handler,因为我们只关心处理数据,因此程序中可以继承这两个基类来帮助我们做这些,而我们仅需实现处理数据的部分即可。

InboundHandler和OutboundHandler在ChannelPipeline中是混合在一起的,因为它们各自实现的是不同的接口。对于Inbound Event,Netty会自动跳过OutboundHandler,相反若是Outbound Event,ChannelInboundHandler会被忽略掉。

当一个ChannelHandler被加入ChannelPipeline中时,它便会获得一个ChannelHandlerContext的引用,而ChannelHandlerContext可以用来读写Netty中的数据流。因此,现在有两种方式来发送数据,一种是把数据直接写入Channel,另一种是把数据写入ChannelHandlerContext,它们的区别是写入Channel的话,数据流会从Channel的头开始传递,而如果写入ChannelHandlerContext,数据流就会流入管道中的下一个Handler。
Netty中有很多Handler,具体是哪种Handler,还要看它们继承的是InboundAdapter还是OutboundAdapter。当然,Netty还提供了一系列的Adapter来帮助我们简化开发。我们知道在ChannelPipeline中每一个Handler都负责把Event传递给下一个Handler,有了这些辅助Adapter,这些额外的工作都可以自动完成,我们只需要覆盖实现真正关心的部分即可。此外,还有一些Adapter会提供一些额外的功能,比如编码和解码。下面我们就来看一下其中的3种常用的ChannelHandler。

(1)Encoder(编码器)和Decoder(解码器)
在网络传输时只能传输字节流,需要把message转换为bytes,与之对应,我们在接收数据后,必须把接收到的bytes再转换成message。我们把bytes转换成message这个过程称作Decode(解码),把message转换成bytes这个过程称为Encode(编码)。
Netty中提供了很多现成的编码/解码器,从它们的名字中便可以知道其用途,如ByteToMessageDecoder、MessageToByteEncoder以及专门用来处理Google ProtoBuf协议的ProtobufEncoder、ProtobufDecoder。
对于Decoders,很容易便可以知道它是继承自ChannelInboundHandlerAdapter或ChannelInboundHandler的,因为解码是把ChannelPipeline传入的bytes解码成我们可以理解的message。Decoder会覆盖其中的ChannelRead()方法,在方法中调用具体的decode方法解码传递过来的字节流,然后通过调用ChannelHandlerContext.fireChannelRead(decodedMessage)方法把编码好的message传递给下一个Handler。

(2)SimpleChannelInboundHandler
其实我们最关心的事情是如何处理接收到的解码后的数据,真正的业务逻辑便是处理接收到的数据。Netty提供了一个常用的基类SimpleChannelInboundHandler < T >,其中T就是这个Handler处理的数据的类型,消息到达这个Handler时,Netty会自动调用这个Handler中的channelRead0(ChannelHandlerContext,T)方法,T是传递过来的数据对象,在这个方法中可以任意编写我们所需的业务逻辑。

参考文献:《分布式微服务架构 原理与实战》

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值