Netty系列第三篇——正式进入Netty的世界

629 阅读10分钟

每日一句

做人如果没有梦想,跟咸鱼有什么分别 ——《少林足球》

 

  经历了前2篇文章的铺垫,这一节我们开始正式进入Netty的世界,根据国际惯例,还是从最基础的HelloWord示例代码着手,我在这呢也再给大家保证一次,就是说您第一次看Netty的这些代码可能会很陌生,很迷惑,这都没关系,因为Netty早已经想到了这一点,所以它把代码的编写套路已经高度统一化了,后续所有的代码,都是一个套路,不同之处也就是咱们各自的业务代码、参数配置这些。扩展点也非常一致。所以大家大可放心,Netty的代码你会越看越熟悉,越来越轻松。  

提出需求

  在这一篇里咱们为了把主要精力都放在熟悉Netty的代码实现上,所以把业务逻辑设计的傻瓜一点,就做一个简单的字符串处理程序,所谓处理程序指的是客户端随意发过来一段字符串,服务端里边会对字符串做一些拼接、调整,然后再把处理结果返回给客户端,放心,都是最简单的打印输出,阅读代码完全没压力的。

    因为这是一个虚拟出来的需求,所以一会在看代码的时候,关于什么边界值问题、异常处理、空指针等等这些问题咱们也就不考虑了,因为代码本身没有经过充足的测试,也没有经过标准TDD流程来设计,只是为了快速落地,让咱们赶紧看到Netty编程的基本范式而已。  

把代码端上来吧


import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

import java.net.InetSocketAddress;
import java.net.SocketAddress;

/**
 * @author 晴天听夜曲
 */
public class NettyServerDemo {

    public static void main(String[] args) {

        // 服务端的端口号
        int port = 7890;

        // todo explain-1
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workGroup = new NioEventLoopGroup();

        // todo explain-2
        ServerBootstrap serverBootstrap = new ServerBootstrap();

        // todo explain-3
        ChildChannelHandler childChannelHandler = new ChildChannelHandler();

        // todo explain-4
        serverBootstrap.group(bossGroup, workGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 1024)
                .childHandler(childChannelHandler);

        ChannelFuture channelFuture;

        try {
            // todo explain-5
            channelFuture = serverBootstrap.bind(port).sync();
            System.out.println(">>>服务端启动成功 port = " + port);

            // todo explain-6
            channelFuture.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // todo explain-7
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }
    static class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();

            // todo explain-8
            RepeaterInHandler inHandler = new RepeaterInHandler();
            RepeaterInTwoHandler inTwoHandler = new RepeaterInTwoHandler();


            RepeaterOutHandler outHandler = new RepeaterOutHandler();
            RepeaterOutTwoHandler outTwoHandler = new RepeaterOutTwoHandler();

            // todo explain-9
            pipeline.addFirst(outHandler);
            pipeline.addFirst(outTwoHandler);

            // todo explain-10
            pipeline.addLast(inHandler);
            pipeline.addLast(inTwoHandler);
        }
    }

    @ChannelHandler.Sharable
    static class RepeaterInHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            // todo explain-11
            System.out.print(">>>[inbound - 1]新连接建立,");
            SocketAddress socketAddress = ctx.channel().remoteAddress();
            InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress;
            System.out.print("客户端主机地址: " + inetSocketAddress.getAddress() + " ");
            System.out.print("客户端主机端口: " + inetSocketAddress.getPort() + "\n");

            ctx.fireChannelActive();
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            // todo explain-12
            System.out.print(">>>[inbound - 1]连接断开,");
            SocketAddress socketAddress = ctx.channel().remoteAddress();
            InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress;
            System.out.print("断开客户端主机地址: " + inetSocketAddress.getAddress() + " ");
            System.out.print("断开客户端主机端口: " + inetSocketAddress.getPort() + "\n");

            ctx.fireChannelInactive();
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            // todo explain-13
            System.out.println(">>>[inbound - 1]发生异常: " + cause.getMessage());
            ctx.fireExceptionCaught(cause);
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            // todo explain-14
            System.out.println(">>>[inbound - 1]读取到客户端发来的数据: ");

            ByteBuf buf = (ByteBuf) msg;
            byte[] bytes = new byte[buf.readableBytes()];
            buf.getBytes(buf.readerIndex(), bytes);
            String str = new String(bytes, 0, buf.readableBytes());

            // 此处可以根据需要对入站的数据进行再加工,然后传递给后续的handler,后续handler拿到的就是这一层加工之后的数据了
            str = str + "000000";
            ByteBuf resp = Unpooled.copiedBuffer(str.getBytes());

            System.out.println(str);
            ctx.fireChannelRead(resp);
        }

        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            // todo explain-15
            System.out.println(">>>[inbound - 1]数据读取完毕");
            ctx.fireChannelReadComplete();
        }

        @Override
        public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
            // todo explain-16
            System.out.println(">>>[inbound - 1]当前通道处理器被成功注册");
            ctx.fireChannelRegistered();
        }
    }

    @ChannelHandler.Sharable
    static class RepeaterInTwoHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.print(">>>[inbound - 2]新连接建立,");
            SocketAddress socketAddress = ctx.channel().remoteAddress();
            InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress;
            System.out.print("客户端主机地址: " + inetSocketAddress.getAddress() + " ");
            System.out.print("客户端主机端口: " + inetSocketAddress.getPort() + "\n");

            ctx.fireChannelActive();
        }
        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            System.out.print(">>>[inbound - 2]连接断开,");
            SocketAddress socketAddress = ctx.channel().remoteAddress();
            InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress;
            System.out.print("断开客户端主机地址: " + inetSocketAddress.getAddress() + " ");
            System.out.print("断开客户端主机端口: " + inetSocketAddress.getPort() + "\n");

            ctx.fireChannelInactive();
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            System.out.println(">>>[inbound - 2]发生异常: " + cause.getMessage());
            ctx.fireExceptionCaught(cause);
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println(">>>[inbound - 2]读取到客户端发来的数据: ");
            ByteBuf buf = (ByteBuf) msg;
            byte[] bytes = new byte[buf.readableBytes()];
            buf.getBytes(buf.readerIndex(), bytes);
            String str = new String(bytes, 0, buf.readableBytes());
            // 此处可以根据需要对入站的数据进行再加工,然后传递给后续的handler,后续handler拿到的就是这一层加工之后的数据了
            str = str + "111111";
            ByteBuf resp = Unpooled.copiedBuffer(str.getBytes());

            System.out.println(str);
            ctx.channel().write(resp);
        }

        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            System.out.println(">>>[inbound - 2]数据读取完毕");
            ctx.fireChannelReadComplete();
        }
        @Override
        public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
            System.out.println(">>>[inbound - 2]当前通道处理器被成功注册");
        }
    }

    @ChannelHandler.Sharable
    static class RepeaterOutHandler extends ChannelOutboundHandlerAdapter {
        @Override
        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
            // todo explain-17
            System.out.println(">>>[outbound - 1开始执行");
            ByteBuf buf = (ByteBuf) msg;
            byte[] bytes = new byte[buf.readableBytes()];
            buf.getBytes(buf.readerIndex(), bytes);
            String str = new String(bytes, 0, buf.readableBytes());
            str = str + str;
            System.out.println(">>>[outbound - 1]处理完毕,本层handler处理之后的数据结果为: " + str);
            ByteBuf resp = Unpooled.copiedBuffer(str.getBytes());
            ctx.write(resp);
        }
    }

    @ChannelHandler.Sharable
    static class RepeaterOutTwoHandler extends ChannelOutboundHandlerAdapter {
        @Override
        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
            System.out.println(">>>[outbound - 2]开始执行");
            ByteBuf buf = (ByteBuf) msg;
            byte[] bytes = new byte[buf.readableBytes()];
            buf.getBytes(buf.readerIndex(), bytes);
            String str = new String(bytes, 0, buf.readableBytes());
            str = str + str;
            System.out.println(">>>[outbound - 2]处理完毕,本层handler处理之后的数据结果为: " + str);
            ByteBuf resp = Unpooled.copiedBuffer(str.getBytes());
            ctx.writeAndFlush(resp);
        }
    }
}



一起拆解

  200多行代码,不算长也不算短,可能代码敏感的同学看完这段之后应该已经有些感觉了,没关系,不管有没有感觉咱们都把重点的地方进行详细拆解。

todo explain-1

  表面看就是创建了2个EventLoopGroup对象,只不过对象命名不同而已。那EventLoopGroup又是什么鬼?Event是事件,Loop是循环,Group是组,连上念:事件循环组?哦,不对,根据老外的英语习惯是把定语后置了,那就是“循环事件的组”,循环事件,大概能理解,因为NIO里边有这个概念,就是往复不停的去循环发生在通道上的读写连接关闭等事件,那这个组又是什么?什么组?组织谁?java里据我所知就只提供过一个线程组的概念吧。。是的,不卖关子了,就是线程组(池),就是开辟了一组线程来负责循环事件,那为什么是2个对象,这个也好解释,因为Netty是标准的主从Reactor模型,多条线程负责循环事件,多条线程负责处理用户请求。boss和worker就分别对应着主与从。

todo explain-2

  ServerBootstrap,服务端启动器,对的,就是一个启动器而已,可以让我们简单快捷的启动一个服务端进程。

todo explain-3

  ChildChannelHandler,子通道处理器,这是我们自己定义的一个内部类,继承了ChannelInitializer这个父类,这块如果根据官方的说法可能会特别绕,我试着用我理解的程度来给翻译一下,就是在子处理器里边有一个负责注册的方法,可以把我们的业务处理器分别根据顺序、含义等特征注册到通道处理器中,作用就是请求进来的每一条数据,都会按照我们注册的handle顺序来流转。就是一个注册器。

todo explain-8、9、10

  既然看到了通道处理器,那就进来坐坐吧,我们一共定义了4个业务处理器,2个是In模式,2个是Out模式,所谓In模式就是数据流入的方向,也叫入站,Out就是代表数据流出的方向,也叫出站。addLast就是真正“注册”的方法,您得先添加进去(注册进去),它才能得到执行对吧。这里要注意的是,入站处理器的执行顺序与注册顺序是一致的,而出站处理器的执行顺序与注册顺序是相反的,这也不是我规定的,是Netty规定的,这一点在ChannelPipeline这个类的javadoc中可以得以证明。(再次证明看javadoc的重要性,官方文档才是第一手正宗资料)

todo explain-4

  看起来像一个建造者模式的应用,可以连续的点调用,也比较好理解,就是根据serverBootstrap提供的方法进行内容设置,我的代码中首先是设置了主从EventLoopGroup,Nio的channel类型,一个tcp的BACKLOG参数,以及业务handler的指定。

backlog参数的作用是当socket套接字在监听端口时,内核会为这个套接字分配一个队列,在服务端来不及处理请求时(请求堆积),就会暂时将请求缓存到这个队列中。如果队列已经被客户端socket占满了,但是还有新的连接过来,那么Server端就会拒绝新的连接。backlog提供了队列容量的限制功能,避免太多的客户端socket占用太多服务器资源。推荐阅读

todo explain-5

  绑定监听端口并启动进程。后边的sync方法是为了把操作转化为同步模式,因为Netty世界中万事皆异步。请记住,万事皆异步,我这里将其转化为同步是为了避免立即操作对象时出现报错(比如对象未初始完全),等到初始化完成,阻塞就会结束。

todo explain-6

  这个语句的主要目的也是阻塞,如果缺失这行代码,则main方法所在的线程,即主线程会在执行完bind().sync()方法后,会进入finally代码块,那么之前启动的NettyServer也会随之关闭掉,整个程序都结束了,所以也是通过sync同步阻塞了closeFuture方法。

todo explain-7

  程序结束时让主从线程池都可以优雅退出。所谓优雅退出就是说可以确保在它关闭它自己之前没有任务在执行了,不会生硬的立即关闭。避免造成一些意外的异常。(与优雅关闭线程池同理)

todo explain-11

  第一个入站处理器,在建立连接时的回调方法。

todo explain-12

  第一个入站处理器,在连接断开时的回调方法。

todo explain-13

  第一个入站处理器,在发生异常时的回调方法。

todo explain-14

  第一个入站处理器,在读取到数据时的回调方法

todo explain-16

  第一个入站处理器,在数据全部读取完成时的回调方法。

todo explain-17

  第一个出站处理器,在需要写出数据时的回调方法。

请再一次好好理解这里的处理方式,因为以后我们编写的Nety代码,几乎全都是围绕着这些出入站回调来做。

效果演示

nettyDemo.gif

总结

  200多行的代码,白话了大半篇文章,但是它相比于传统NIO程序,优势也是显而易见的,代码更简洁一些,只有少数几个Netty自己抽象出来的概念需要提前看doc了解,其余都和普通socket程序有很高的的共性,整体开发难度还是很低的,不同业务需求只需要开发自己的handler即可,扩展性也更好。非常的适合作为基础通信框架来被开发人员所使用。(文中的代码你可以复制直接运行,不客气)

  本章只是一个最最基础的入门示例,对于netty的强大功能我们还没有完全覆盖到,比如编解码能力,半拆包问题,自定义协议能力等等,当然,更多的入门示例您也可以借助Netty-Example依赖来继续学习,在io.netty.example包中有更多更值得学习的示例程序。帮助您理解Netty。

  好了,本篇文章到这里就结束了,希望您能有所收获,祝您生活愉快。