Netty学习之Bootstrapping

Netty学习之Bootstrapping

前言

在前面的内容中,我们基本把Netty的核心组件都学习完了,各个组件的作用及组件之间的关系也基本理清楚了,一个完整的Netty应用基本上也能写出来了,当然,还差最后一步,启动应用,本小节我们来学习如何启动一个Netty应用。

Bootstrap Class

Bootstrap类包含两个子类,BootstrapServerBootstrap,分别对应于客户端应用及服务端应用,他们的区别在于,服务端需要两个Channel,父Channel用于建立连接,子Channel用于管理已经建立的连接。

Bootstrap常用API

  • group(),指定所要使用的EventLoopGroup
  • channel(),选择所要使用的channel
  • localAddress(),指定所要绑定的地址
  • option(),设置ChannelOption
  • handler(),设置ChannelHandler
  • remoteAddress(),设置远程地址
  • connect(),连接到远程服务,并且返回一个ChannelFuture,用于通知结果
  • bind(),绑定Channel,并且返回一个ChannelFuture,用于通知绑定结果

启动客户端

启动流程基本如下

public void start() {
    // 指定EventLoopGroup
    EventLoopGroup group = new NioEventLoopGroup();
    // 创建Bootstrap
    Bootstrap bootstrap = new Bootstrap();
    // 绑定所要使用的EventLoopGroup
    bootstrap.group(group)
            .remoteAddress(new InetSocketAddress(HOST, PORT))
            // 指定所要使用的Channel
            // 要注意,Channel的类型必须跟EventLoop的类型相匹配
            .channel(NioSocketChannel.class)
            // 指定对应的处理器
            // 如果只有一个handler,也可以直接添加即可
            // 使用ChannelInitializer主要是用于添加多个handler
            .handler(new ChannelInitializer() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new EchoClientHandler());
                }
            });
    try {
        ChannelFuture future = bootstrap.connect().sync();
        future.channel().closeFuture().sync();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }finally {
        try {
            group.shutdownGracefully().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

需要注意的是,启动客户端,也就是调用bind()或者connect()之前,必须要配置group()channel()/channelFactory()handler(),不然会报IllegalStateException,其中hanlder()是非常重要的,用于配置处理的逻辑操作。

在配置的时候,要注意指定的group()channel()必须匹配,如NioEventLoopGroup必须与NioSocketChannel或者NioServerSocketChannel,不能混用NioOio,不然也会报IllegalStateException

可供使用的EventLoopGroup及Channel如下

channel
  |--nio
  |     NioEventLoopGroup
  |--oio
  |     OioEventLoopGroup
  |--socket
     |--nio
     |    NioDatagramChannel
     |    NioServerSocketChannel
     |    NioSocketChannel
     |--oio
     |    OioDatagramChannel
     |    OioServerSocketChannel
     |    OioSocketChannel

启动服务端

private void start() {
    final EchoServerHandler serverHandler = new EchoServerHandler();
    // 创建一个EventLoopGroup--boss
    EventLoopGroup boss = new NioEventLoopGroup();
    // 创建一个EventLoopGroup--worker
    EventLoopGroup worker = new NioEventLoopGroup();
    // ServerBootstrap
    ServerBootstrap bootstrap = new ServerBootstrap();
    int port = 8888;
    // 如果只配置一个group,则表示同一个group同于两个用途:父Channel、子Channel
    // 如果配置两个,则分别使用啦
    bootstrap.group(boss, worker)
            // 设置地址
            .localAddress(new InetSocketAddress(port))
            // 指定使用NioServerSocketChannel
            .channel(NioServerSocketChannel.class)
            // 添加子处理器,用于处理建立之后的连接
            // 这里需要注意,handler()方法是用于配置ServerChannel本身
            // childHandler()才是用于配置建立的连接
            .childHandler(new ChannelInitializer() {
                @Override
                protected void initChannel(SocketChannel ch) {
                    ch.pipeline().addLast(serverHandler);
                }
            });
    try {
        // .sync()表示等待绑定完成,当前线程会阻塞
        ChannelFuture future = bootstrap.bind().sync();
        // 等待关闭
        future.channel().closeFuture().sync();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }finally {
        try {
            worker.shutdownGracefully().sync();
            boss.shutdownGracefully().sync();            
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

如果需要添加多个Handler,可以通过添加一个ChannelInitialize的实现类对象,然后在该对象的protected vodi initChannel(Channel ch)中通过channel.pipeline()获取对应的pipeline,然后通过pipeline注册多个handler。

从Channel中启动一个客户端

有时候我们的服务端也需要充当客户端去连接其他的服务端,比如请求oauth授权、或者代理等。

可以直接在建立的channel中再起一个boostrap用于去连接第三方服务,但是这种操作不是很合理,这种方式需要重新起一个EventLoop(新建一个Channel,则会重新绑定了新的EventLoop),所以当在两个不同的channel交换数据时会带来额外的线程开销和上下文切换 。

更好地方式是通过调用group()方法,共享已经建立连接的EventLoop,这样子对应的子channel也是在同一个线程上下文中,所以避免了上下文切换的消耗。

public class Test {
    public static void main(String[] args) {
        ServerBootstrap bootstrap = new ServerBootstrap();
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();
        bootstrap.group(boss, worker)
                .channel(NioServerSocketChannel.class)
                .childHandler(new SimpleChannelInboundHandler() {
                    ChannelFuture connectFuture;
                    /**
                    * 通道连接建立后,建立与第三方的连接
                    */
                    @Override
                    public void channelActive(ChannelHandlerContext ctx) throws Exception {
                        // 启动bootstrap充当客户端去连接新的服务
                        Bootstrap client = new Bootstrap();
                        client.channel(NioSocketChannel.class)
                                .handler(new SimpleChannelInboundHandler() {
                                    @Override
                                    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
                                        System.out.println("Received data: " + msg.toString(CharsetUtil.UTF_8));
                                    }
                                });
                        // 重点是这里,复用了父channel的eventLoop
                        client.group(ctx.channel().eventLoop());
                        connectFuture = client.connect(new InetSocketAddress("www.baidu.com", 80));
                    }

                    @Override
                    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
                        // 连接建立完成
                        if (connectFuture.isDone()) {
                            // 其他的操作,比如发送请求等
                        }
                    }
                });
        try {
            ChannelFuture future = bootstrap.bind(8080).sync();
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            worker.shutdownGracefully();
            boss.shutdownGracefully();
        }
    }
}

使用ChannelOptions或者attributes

如果手动为每个建立的Channel都进行配置,是一件非常痛苦的事情,所以Netty提供了option()方法用于传入一个ChannelOptions对象,每个配置会自动应用到所有建立的channel中。

此外,Netty还提供了AttributeMap及其子类AttributeKey用于在Channel中传递额外的属性信息,然后通过channel#attr("key")可以将attributeKey获取出来,然后通过其get方法就能将对应的属性值获取出来。

public class Test {

    public static void main(String[] args) {;
        Bootstrap bootstrap = new Bootstrap();
        EventLoopGroup group = new NioEventLoopGroup();

        // 新建一个id属性键
        final AttributeKey id = AttributeKey.newInstance("ID");

        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .handler(new SimpleChannelInboundHandler() {
                    @Override
                    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
                        // ops
                    }

                    @Override
                    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
                        Integer integer = ctx.channel().attr(id).get();
                    }
                });
        bootstrap.option(ChannelOption.SO_KEEPALIVE, true)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
        // 设置属性键及其值
        bootstrap.attr(id, 123456);
        ChannelFuture future = bootstrap.connect("host", 8080);
        future.syncUninterruptibly();
    }
}

UDP

在之前的例子中,我们使用的都是基于TCP的连接方式,Netty3之后,同样支持UDP连接方式,只需要使用*DatagramChannel*类,同时不使用connect()或者bind()即可。

public static void main(String[] args) {
    Bootstrap bootstrap = new Bootstrap();
    bootstrap.group(new OioEventLoopGroup())
            .channel(OioDatagramChannel.class)
            .handler(new SimpleChannelInboundHandler() {
                @Override
                protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception {
                            
                }
            });
}

关闭

关闭Netty应用的时候,需要关闭EventLoopGroup,EventLoopGroup绑定了很多线程嘛,通过调用eventLoopGroup.shutdownGracefully()即可,由于该操作同样是异步操作,所以要么阻塞,要么注册一个监听器,该方法会释放所有资源并且关闭所有在使用的Channel,也可以显示调用Channel.close(),然后再关闭EventLoopGroup

在关闭的时候,我们需要根据情况,看是关闭当前的channel还是关闭整个服务,如果是关闭整个服务,则应该关闭当前channel对应的父channel(对于服务端来说),客户端只需要关闭当前channel即可。

关闭之后,Netty会发送“关闭事件”给服务端,并触发对应的事件,即一开始我们所编写的

// 绑定地址并且获取对应的channel,此时的channel是父channel
ChannelFuture future = bootstrap.bind(PORT).sync();
// 阻塞直至连接关闭
// 父channel关闭时,该操作会收到通知,进而关闭应用
future.channel().closeFuture().sync();

总结

本小节我们主要学习了Netty的Boostrap,这个组件是Netty必备组件的最后一个组件了,通过该组件将前面所有涉及到的组件串联起来,并且绑定地址,启动服务或者客户端,在Netty中,如果能复用EventLoop就应该尽量复用EventLoop,从而可以减少线程上下文的切换,比如在服务端需要重新启动另一个客户端的时候,这时就可以直接复用当前channel的EventLoop即可。

你可能感兴趣的:(Netty学习之Bootstrapping)