从零开始学Netty (二)-- 初始Netty核心组件 及 聊天室Demo

我们今天将通过一个Demo来理解Netty的几个主要组件。Netty的开发,主要是配置和实现各种Handler处理器,而其他是由Netty来替我们实现,所以我更愿意称其为“模板化”的开发模型。

“模板化”的Netty

首先看一个聊天室的Demo

服务端代码

public class DemoServer {

    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(boss, worker).channel(NioServerSocketChannel.class)
                    .localAddress(new InetSocketAddress(8090))
                    .childHandler(new DemoServerInitializer());
            ChannelFuture channelFuture = serverBootstrap.bind().sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            // 优雅关闭
            boss.shutdownGracefully().sync();
            worker.shutdownGracefully().sync();
        }
    }

}

服务端启动需要两个EventLoopGroup, 其中“boss”负责接收请求,“worker”负责处理请求,Netty服务端启动这块代码基本都是类似的,不需要做太多更改。我们开发时,一般更需要关注DemoServerInitializer里的各种Handler。

public class DemoServerInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        pipeline.addLast(new DelimiterBasedFrameDecoder(4096, Delimiters.lineDelimiter()));
        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
        pipeline.addLast(new DemoServerHandler());
    }
}

这里的pipeline管道定义了各种处理器Handler,类似责任链模式。每个请求都需要依次经过所有的Handler,这里我们定义了编码解码和自定义的DemoServerHandler。

public class DemoServerHandler extends SimpleChannelInboundHandler<String>  {

    private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
        Channel channel = channelHandlerContext.channel();

        channelGroup.forEach(ch -> {
            ch.writeAndFlush(channel.remoteAddress() + " :" + s + "\n");
        });
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        // 广播给其他客户端
        channelGroup.writeAndFlush("【server】:" + channel.remoteAddress() + " added\n");
        // 加入连接池
        channelGroup.add(channel);
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        // 广播给其他客户端
        channelGroup.writeAndFlush("【server】:" + channel.remoteAddress() + " removed\n");
        // 连接中断后,netty会自动执行channelGroup.remove(channel);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        System.out.println(channel.remoteAddress() + " online!");
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        System.out.println(channel.remoteAddress() + " offline!");
    }

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

SimpleChannelInboundHandler 指接收到请求的Handler,这里实现了各种事件,当Channel 发生相应改变时被调用。

客户端代码

public class DemoClient {

    public static void main(String[] args) throws Exception {
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
                    .handler(new DemoClientInitializer());

            Channel channel = bootstrap.connect("localhost", 8090).sync().channel();
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            for(;;) {
                channel.writeAndFlush(br.readLine() + "\r\n");
            }
        } finally {
            eventLoopGroup.shutdownGracefully();
        }
    }
}

客户端只需要一个EventLoopGroup,负责处理请求。类似的,我们也更关注DemoClientInitializer和里面的Handler。DemoClientInitializer和DemoClientHandler也跟服务端类似。

public class DemoClientInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        pipeline.addLast(new DelimiterBasedFrameDecoder(4096, Delimiters.lineDelimiter()));
        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
        pipeline.addLast(new DemoClientHandler());
    }

}
public class DemoClientHandler extends SimpleChannelInboundHandler<String> {

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
        System.out.println(s);
    }
}

 

Netty核心组件说明

下面会具体介绍几个核心组件。

Bootstrap

Netty有两种类型的引导:一种用于客户端(简单地称为Bootstrap),而另一种(ServerBootstrap)用于服务器。无论你的应用程序使用哪种协议或者处理哪种类型的数据,唯一决定它使用哪种引导类的是它是作为一个客户端还是作为一个服务器。

 

Bootstrap

ServerBootstrap

绑定端口

远程主机和端口

本地端口

EventLoopGroup 的数目  

1

2

 

ServerBootstrap 将绑定到一个端口,因为服务器必须要监听连接,而Bootstrap 则是由想要连接到远程节点的客户端应用程序所使用的。引导一个Bootstrap只需要一个EventLoopGroup,但是一个ServerBootstrap 则需要两个(也可以是同一个实例)。因为服务器需要两组不同的Channel。第一组将只包含一个ServerChannel,代表服务器自身的已绑定到某个本地端口的正在监听的套接字。而第二组将包含所有已创建的用来处理传入客户端连接(对于每个服务器已经接受的连接都有一个)的Channel。与ServerChannel 相关联的EventLoopGroup 将分配一个负责为传入连接请求创建Channel 的EventLoop。一旦连接被接受,第二个EventLoopGroup 就会给它的Channel分配一个EventLoop。

Netty内置的通信模式:

  • NIO:io.netty.channel.socket.nio 使用java.nio.channels 包作为基础,基于选择器的方式,也是我们最常用的模式。
  • Epoll:io.netty.channel.epoll 由 JNI 驱动的 epoll()和非阻塞 IO,利用了Linux系统特性,所以这个传输模式只支持Linux系统,性能上比NIO更快。
  • OIO:io.netty.channel.socket.oio 使用java.net 包作为基础,使用阻塞流模式。
  • Local:io.netty.channel.local 可以在VM 内部通过管道进行通信的本地传输
  • Embedded:io.netty.channel.embedded Embedded 传输,允许使用ChannelHandler 而又不需要一个真正的基于网络的传输,常用于测试用例。

 

 

EventGroupLoop和EventLoop

EventLoopGroup 主要负责Netty里的调度,而EventLoop 是Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。两者的关系类似jdk的线程池和线程。一个EventLoop 将由一个永远都不会改变的Thread 驱动,同时任务(Runnable 或者Callable)可以直接提交给EventLoop 实现,以立即执行或者调度执行,所有的I/O操作和事件都由已经被分配给了EventLoop的那个Thread来处理。根据配置和可用核心的不同,可能会创建多个EventLoop 实例用以优化资源的使用,并且单个EventLoop 可能会被指派用于服务多个Channel。

  • 一个EventLoopGroup 包含一个或者多个EventLoop;
  • 一个EventLoop 在它的生命周期内只和一个Thread 绑定;
  • 所有由EventLoop 处理的I/O 事件都将在它专有的Thread 上被处理;
  • 一个Channel 在它的生命周期内只注册于一个EventLoop;
  • 一个EventLoop 可能会被分配给一个或多个Channel。

Channel

Channel 接口概念类似 Socket ,包含了底层网络传输的IO操作。Netty 的Channel 接口所提供的API,被用于所有的I/O 操作。大大地降低了直接使用Socket 类的复杂性。此外,Channel 也是拥有许多预定义的、专门化实现的广泛类层次结构的根。每个Channel 都将会被分配一个ChannelPipeline 和ChannelConfig。ChannelConfig 包含了该Channel 的所有配置设置,并且支持热更新。由于Channel 是独一无二的,所以为了保证顺序将Channel 声明为java.lang.Comparable 的一个子接口。因此,如果两个不同的Channel 实例都返回了相同的散列码,那么AbstractChannel 中的compareTo()方法的实现将会抛出一个Error。

Channel的生命周期状态

  • ChannelUnregistered Channel 已经被创建,但还未注册到EventLoop
  • ChannelRegistered Channel 已经被注册到了EventLoop
  • ChannelActive Channel 处于活动状态(已经连接到它的远程节点)。它现在可以接收和发送数据了
  • ChannelInactive Channel 没有连接到远程节点

主要方法:

  • eventLoop:返回分配给Channel 的EventLoop
  • pipeline:返回分配给Channel 的ChannelPipeline
  • isActive:如果Channel 是活动的,则返回true。活动的意义可能依赖于底层的传输。例如,一个Socket 传输一旦连接到了远程节点便是活动的,而一个Datagram 传输一旦被打开便是活动的。
  • localAddress:返回本地的SokcetAddress
  • remoteAddress:返回远程的SocketAddress
  • write:将数据写到远程节点。这个数据将被传递给ChannelPipeline,并且排队直到它被冲刷
  • flush:将之前已写的数据冲刷到底层传输,如一个Socket
  • writeAndFlush:一个简便的方法,等同于调用write()并接着调用flush()

ChannelFuture

同样类似jdk的Future,由于Netty的IO都是异步执行的,ChannelFuture用于在之后的某个时间点得到其结果。其中 addListener()方法注册了一个ChannelFutureListener,以便在某个操作完成时(无论是否成功)得到通知。可以将ChannelFuture 看作是将来要执行的操作的结果的占位符。它究竟什么时候被执行则可能取决于若干的因素,因此不可能准确地预测,但是可以肯定的是它将会被执行。

ChannelHandler

ChannelHandler充当了所有处理入站和出站数据的应用程序逻辑的容器。ChannelHandler 的方法是由网络事件触发的。事实上,ChannelHandler 可专门用于几乎任何类型的动作,例如将数据从一种格式转换为另外一种格式,例如各种编解码,或者处理转换过程中所抛出的异常。

下面列出了interface ChannelHandler 定义的生命周期操作,在ChannelHandler被添加到ChannelPipeline 中或者被移除时会调用这些操作。这些方法中的每一个都接受一个ChannelHandlerContext 参数。

  • handlerAdded 当把ChannelHandler 添加到ChannelPipeline 中时被调用
  • handlerRemoved 当从ChannelPipeline 中移除ChannelHandler 时被调用
  • exceptionCaught 当处理过程中在ChannelPipeline 中有错误产生时被调用

Netty 定义了下面两个重要的ChannelHandler 子接口:

  • ChannelInboundHandler——处理入站数据以及各种状态变化;
  • ChannelOutboundHandler——处理出站数据并且允许拦截所有的操作;

Netty提供了抽象基类ChannelInboundHandlerAdapter 和ChannelOutboundHandlerAdapter。我们可以使用ChannelInboundHandlerAdapter 和ChannelOutboundHandlerAdapter类作为自己的ChannelHandler 的起始点。这两个适配器分别提供了ChannelInboundHandler和ChannelOutboundHandler 的基本实现。通过扩展抽象类ChannelHandlerAdapter,它们获得了它们共同的父类接口ChannelHandler 的方法。ChannelHandlerAdapter 还提供了实用方法isSharable()。如果其对应的实现被标注为Sharable,那么这个方法将返回true,表示它可以被添加到多个ChannelPipeline。

ChannelPipeline

当Channel 被创建时,它会被自动地分配到它专属的ChannelPipeline。每一个新创建的Channel 都将会被分配一个新的ChannelPipeline。这项关联是永久性的;Channel 既不能附加另外一个ChannelPipeline,也不能分离其当前的。在Netty 组件的生命周期中,这是一项固定的操作,不需要开发人员的任何干预。使得事件流经ChannelPipeline 是ChannelHandler 的工作,它们是在应用程序的初始化或者引导阶段被安装的。这些对象接收事件、执行它们所实现的处理逻辑,并将数据传递给链中的下一个ChannelHandler。它们的执行顺序是由它们被添加的顺序所决定的。

核心方法:

  • addFirst、addBefore、addAfter、addLast:将一个ChannelHandler 添加到ChannelPipeline 中
  • remove:将一个ChannelHandler 从ChannelPipeline 中移除
  • replace:将ChannelPipeline 中的一个ChannelHandler 替换为另一个ChannelHandler
  • get:通过类型或者名称返回ChannelHandler
  • context:返回和ChannelHandler 绑定的ChannelHandlerContext
  • names:返回ChannelPipeline 中所有ChannelHandler 的名称

ChannelHandlerContext

当ChannelHandler 被添加到ChannelPipeline 时,它将会被分配一个ChannelHandlerContext,其代表了ChannelHandler 和ChannelPipeline 之间的绑定。ChannelHandlerContext 的主要功能是管理它所关联的ChannelHandler 和在同一个ChannelPipeline 中的其他ChannelHandler 之间的交互。

当使用ChannelHandlerContext时,需要注意:

  • ChannelHandlerContext 和ChannelHandler 之间的关联(绑定)是永远不会改变的,所以缓存对它的引用是安全的;
  • 如同我们在本节开头所解释的一样,相对于其他类的同名方法,ChannelHandler Context的方法将产生更短的事件流,应该尽可能地利用这个特性来获得最大的性能;

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值