JavaEE 企业级分布式高级架构师(十九)异步事件驱动框架Netty(1)

Netty入门篇

Netty概述

Netty简介

在这里插入图片描述

  • Netty是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能服务器和客户端。
  • Netty是一个NIO客户机-服务器框架,它支持快速、简单地开发网络应用程序,如服务器和客户机。它大大简化了网络编程,如 TCP 和 UDP 套接字服务器。
  • “快速和简单”并不意味着生成的应用程序将受到可维护性或性能问题的影响。Netty 经过精心设计,并积累了许多协议(如 ftp、smtp、http)的实施经验,以及各种二进制和基于文本的遗留协议。因此,Netty 成功地找到了一种方法,在不妥协的情况下实现了易于开发、性能、稳定性和灵活性。

谁在使用Netty

  • Dubbo、zk、RocketMQ、ElasticSearch、Spring5(对 HTTP 协议的实现)、GRpc、Spark 等大型开源项目都在使用 Netty 作为底层通讯框架。

Netty中的核心概念

Channel
  • 管道,其是对 Socket 的封装,其包含了一组 API,大大简化了直接与 Socket 进行操作的复杂性。
EventLoopGroup
  • EventLoopGroup 是一个 EventLoop 线程池,包含很多的 EventLoop。
  • Netty 为每个 Channel 分配了一个 EventLoop,用于处理用户连接请求、对用户请求的处理等所有事件。EventLoop 本身只是一个线程驱动,在其生命周期内只会绑定一个线程,让该线程处理一个 Channel 的所有 IO 事件。
  • 一个 Channel 一旦与一个 EventLoop 相绑定,则在 Channel 的整个生命周期内是不会也不能发生变化。但一个 EventLoop 可以与多个 Channel 相绑定。即 Channel 与 EventLoop 的关系是 n:1,而 EventLoop 与线程的关系是 1:1。
ServerBootStrap
  • 用于配置整个 Netty 代码,将各个组件关联起来。服务端使用的是 ServerBootStrap,而客户端使用的是则 BootStrap。
ChannelHandler 和 ChannelPipeline
  • ChannelHandler 是对 Channel 中数据的处理器,这些处理器可以是系统本身定义好的编解码器,也可以是用户自定义的。这些处理器会被统一添加到一个 ChannelPipeline 的对象中,然后按照添加的顺序对 Channel 中的数据进行依次处理。
ChannelFuture
  • Netty 中所有的 I/O 操作都是异步的,即操作不会立即得到返回结果,所以 Netty 中定义了一个 ChannelFuture 对象作为这个异步操作的“代言人”,表示异步操作本身。如果想获取到该异步操作的返回值,可以通过该异步操作对象的 addListener() 方法为该异步操作添加监听器,为其注册回调:当结果出来后马上调用执行。
  • Netty 的异步编程模型都是建立在 Future 与回调概念之上的。

Netty执行流程

在这里插入图片描述

  • Netty Server 端启动,Netty 从 parentGroup 中选出一个 NioEventLoop 对指定 port 端口的连接进行监听;Netty Client 端启动,Netty 从 eventLoopGroup 中选出一个 NioEventLoop 对 Netty Server 连接,并处理 Sever 端发送来的数据。
  • Client 端连接指定 Server 的 port 端口,创建 Channel,Server 端从 childGroup 中选出一个 NioEventLoop 与该 Channel 绑定,用于处理该 Channel 中所有的操作。
  • Client 端通过 Channel 像 Server 端发送数据包 ByteBuf,Server 端的 Pipeline 中的处理器依次对 Channel 中的数据包进行处理。
  • Server 端如果需要向 Client 端发送数据,则需要将数据经 Pipeline 中的处理器处理形成 ByteBuf 数据包,再将数据包 ByteBuf 通过 Channel 发送给 Client 端。
  • Client 端 Pipeline 中的处理器依次对 Channel 中的数据包进行处理。

牛刀小试

  • 创建 Maven 工程,添加 Netty 依赖:
<!-- netty-all 依赖 -->
<dependency>
  <groupId>io.netty</groupId>
  <artifactId>netty-all</artifactId>
  <version>4.1.38.Final</version>
</dependency>

实现Http通信

  • 通过该程序达到的目的是,对 Netty 编程的基本结构及流程有所了解。该程序是通过 Netty 实现 HTTP 请求的处理,即接收 HTTP 请求,返回 HTTP 响应。这个代码相当于“SpringMVC + Tomcat”。
  • 定义服务器启动类:
public class HttpServer {
    public static void main(String[] args) throws InterruptedException {
        // 用于接收客户端请求,并将请求发送给childGroup
        NioEventLoopGroup parentGroup = new NioEventLoopGroup();
        // 用于对请求进行进行具体的处理
        NioEventLoopGroup childGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(parentGroup, childGroup)
                    // 指定使用NIO进行异步非阻塞通信
                    // OioServerSocketChannel 用于指定使用阻塞方式通信(一般不使用)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new HttpChannelInitializer());

            // 指定当前服务器所要绑定的端口,即要监听的端口号
            // bind()是个异步操作,为了使当前主线程在bind()成功后再向下执行,这里调用sync()
            // 实现bind()线程与主线程的同步操作
            ChannelFuture future = bootstrap.bind(8888).sync();
            System.out.println("==>> 服务端启动成功~~");
            future.channel().closeFuture().sync();
        } finally {
            // 优雅关闭
            parentGroup.shutdownGracefully();
            childGroup.shutdownGracefully();
        }
    }
}
  • 定义管道初始化器:
public class HttpChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel channel) throws Exception {
        // 从channel中获取pipeline
        ChannelPipeline pipeline = channel.pipeline();
        // 向pipeline中添加处理器
        pipeline.addLast("httpServerCodec", new HttpServerCodec());
        pipeline.addLast("httpServerHandler", new HttpServerHandler());
    }
}
  • 定义服务端处理器
public class HttpServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof HttpRequest) {
            HttpRequest request = (HttpRequest) msg;
            System.out.println("请求方式: " + request.method().name());
            System.out.println("请求URI: " + request.uri());
            if ("/favicon.ico".equals(request.uri())) {
                System.out.println("不处理/favicon.ico请求");
                return;
            }
            // 手工编码
            ByteBuf content = Unpooled.copiedBuffer("Hello Netty World", CharsetUtil.UTF_8);
            // 创建响应对象
            DefaultFullHttpResponse response = new DefaultFullHttpResponse(
                    HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content
            );
            // 形成响应头
            HttpHeaders headers = response.headers();
            headers.set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
            headers.set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
            // 将响应发送到Channel
            ctx.writeAndFlush(response)
                    // 一旦发送成功,则马上将channel关闭
                    .addListener(ChannelFutureListener.CLOSE);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        // 关闭连接
        ctx.close();
    }
}
  • 启动程序,并访问地址:http://localhost:8888/。

实现Socket通信

  • 前面的工程是一个仅存在服务端的 HTTP 请求的服务器,而 Netty 中最为最见的是 C/S 构架的 Socket 代码。所以下面我们就来看一个 Netty 的 Socket 通信代码。
  • 需求:客户端连接上服务端后,其马上会向服务端发送一个数据。服务端在接收到数据后,会马上向客户端也回复一个数据。客户端每收到服务端的一个数据后,便会再向服务端发送一个数据。而服务端每收到客户端的一个数据后,便会再向客户端发送一个数据。如此反复,无穷匮也。
  • 定义服务端启动类和处理器类:
public class SomeServer {
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup parentGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup childGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(parentGroup, childGroup)
                    .channel(NioServerSocketChannel.class)
//                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel channel) throws Exception {
                            ChannelPipeline pipeline = channel.pipeline();
                            // 服务端需要接收客户端数据,所以这里要添加解码器
                            pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
                            // 服务端需要想客户端发送数据,所以这里要添加编码器
                            pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
                            pipeline.addLast(new SomeServerHandler());
                        }
                    });
            ChannelFuture future = bootstrap.bind(8888).sync();
            System.out.println("==>> 服务端启动成功~~");
            future.channel().closeFuture().sync();
        } finally {
            parentGroup.shutdownGracefully();
            childGroup.shutdownGracefully();
        }
    }
}
public class SomeServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 显示客户端发送来的数据
        System.out.println(ctx.channel().remoteAddress() + ", " + msg);
        // 给客户端发送数据
        ctx.channel().writeAndFlush("From Server : " + UUID.randomUUID());
        TimeUnit.SECONDS.sleep(1);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
  • 定义客户端启动类和处理器类:
public class SomeClient {
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap()
                    .group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel channel) throws Exception {
                            ChannelPipeline pipeline = channel.pipeline();
                            pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
                            pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
                            pipeline.addLast(new SomeClientHandler());
                        }
                    });
            ChannelFuture future = bootstrap.connect("localhost", 8888).sync();
            future.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}
public class SomeClientHandler extends SimpleChannelInboundHandler<String> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println(ctx.channel().remoteAddress() + ", " + msg);
        ctx.channel().writeAndFlush("From Client: " + LocalDateTime.now());
        TimeUnit.SECONDS.sleep(2);
    }

    /**
     * 当前Channel被激活后触发该方法的执行
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.channel().writeAndFlush("From client: begin talking");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
  • 启动服务端和客户端:

在这里插入图片描述

两个处理器的区别

  • SimpleChannelInboundHandler中的 channelRead()方法会自动释放接收到的来自于对方的 msg 所占有的所有资源。
  • ChannelInboundHandlerAdapter中的 channelRead()方法不会自动释放接收到的来自于对方的msg。
  • 若对方没有向自己发送数据,则自定义处理器建议继承自 ChannelInboundHandlerAdapter。因为若继承自 SimpleChannelInboundHandler 需要重写channelRead0()方法。而重写该方法的目的是对来自于对方的数据进行处理。因为对方根本就没有发送数据,所以也就没有必要重写 channelRead0()方法。
  • 若对方向自己发送了数据,而自己又需要将该数据再发送给对方,则自定义处理器建议继承自 ChannelInboundHandlerAdapter。因为 write() 方法的执行是异步的,且 SimpleChannelInboundHandler 中的 channelRead()方法会自动释放掉来自于对方的 msg。若 write()方法中正在处理 msg,而此时 SimpleChannelInboundHandler 中的 channelRead() 方法执行完毕了,将 msg 给释放了,此时就会报错。

TCP的拆包与粘包

拆包粘包简介

  • Netty 在基于 TCP 协议的网络通信中,存在拆包与粘包情况。拆包与粘包同时发生在数据的发送方与接收方两方。
  • 发送方通过网络每发送一批二进制数据包,那么这次所发送的数据包就称为一帧,即Frame。在进行基于 TCP 的网络传输时,TCP 协议会将用户真正要发送的数据根据当前缓存的实际情况对其进行拆分或重组,变为用于网络传输的 Frame。在 Netty 中就是将 ByteBuf 中的数据拆分或重组为二进制的 Frame。而接收方则需要将接收到的 Frame 中的数据进行重组或拆分,重新恢复为发送方发送时的 ByteBuf 数据。
  • 具体来说有以下两种情况:
    • ① 发送方发送的ByteBuf较大:在进行网络传输之前会被TCP底层拆分为多个Frame进行发送。这个过程称为发送方拆包;接收方在接收到后需要将这些 Frame 进行合并,这个合并的过程称为接收方粘包
    • ② 发送方发送的ByteBuf很小:无法形成一个Frame,此时TCP底层会将很多这样的小ByteBuf 合并为一个 Frame 进行传递,这个合并的过程称为发送方粘包;接收方在接收到后需要将这个 Frame 进行拆分,拆分出很多原来的 ByteBuf,这个拆分的过程称为接收方拆包
    • 当一个 Frame 无法放入整数倍个 ByteBuf 时,最后一个 ByteBuf 会发生拆包。这个 ByteBuf 中的一部分放入到了一个 Frame 中,另一部分被放入到了另一个 Frame 中,这个过程就是发送方拆包。但对于将这些 ByteBuf 放入到一个 Frame 的过程,就是发送方粘包;当接收方在接收到两个 Frame 后,对于第一个 Frame 的最后部分,与第二个 Frame 的最前部分会进行合并,这个合并的过程就是接收方粘包。但在将 Frame 中的各个 ByteBuf 拆分出来的过程,就是接收方拆包

定义启动类

  • 服务端启动类:
public class SomeServer {
    public static void main(String[] args) throws Exception {
        NioEventLoopGroup parentGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup childGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(parentGroup, childGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
                            pipeline.addLast(new SomeServerHandler());
                        }
                    });
            ChannelFuture future = bootstrap.bind(8888).sync();
            System.out.println("==>> 服务端启动成功~~");
            future.channel().closeFuture().sync();
        } finally {
            parentGroup.shutdownGracefully();
            childGroup.shutdownGracefully();
        }
    }
}
  • 客户端启动类:
public class SomeClient {
    public static void main(String[] args) throws Exception {
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap()
                    .group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
                            pipeline.addLast(new SomeClientHandler());
                        }
                    });
            ChannelFuture future = bootstrap.connect("localhost", 8888).sync();
            future.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

发送方拆包&粘包

定义服务端处理器
  • 服务端作为接收方,直接将接收到的 Frame 解码为 String 后进行显示,不对这些 Frame 进行粘包与拆包。
public class SomeServerHandler extends SimpleChannelInboundHandler<String> {
    private int counter;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        // 不用给客户端发送数据,用SimpleChannelInboundHandler比较合适,接收数据后就可以释放msg
        System.out.printf("Server端接收到的第 [%d] 个数据包: %s %n", counter++, msg);
    }
}
定义客户端处理器
  • 发送方拆包:客户端作为发送方,向服务端发送两个大的 ByteBuf 数据包,这两个数据包会被拆分为若干个 Frame 进行发送。这个过程中会发生拆包与粘包。
  • 发送方粘包:客户端作为发送方,向服务端发送 100 个小的 ByteBuf 数据包,这 100 个数据包会被合并为若干个 Frame 进行发送。这个过程中会发生粘包与拆包。

在这里插入图片描述

测试

在这里插入图片描述

客户端自己实现编码
  • 前面 SomeClient 使用了 StringEncoder,其实可以不用 StringEncoder,而使用自己的编码逻辑:
@Override
 public void channelActive(ChannelHandlerContext ctx) throws Exception {
     byte[] bytes = message.getBytes();
     ByteBuf buffer;
     for (int i = 0; i < 2; i++) {
         // 申请缓存空间
         buffer = Unpooled.buffer(bytes.length);
         // 将数据写入到缓存
         buffer.writeBytes(bytes);
         // 将缓存中的数据写入到Channel
         ctx.writeAndFlush(buffer);
     }
 }

接收方粘包&拆包

  • 为了解决接收方接收到的数据的混乱性,接收方也可以对接收到的 Frame 包进行粘包与拆包。Netty 中已经定义好了很多的接收方粘包拆包解决方案,我们可以直接使用。下面就介绍几个最常用的解决方案。
  • 接收方的粘包拆包实际在做的工作是解码工作。这个解码基本思想是:发送方在发送数据中添加一个分隔标记,并告诉接收方该标记是什么。这样在接收方接收到 Frame 后,其会根据事先约定好的分隔标记,将数据进行拆分或合并,产生相应的 ByteBuf 数据。这个拆分或合并的过程,称为接收方的拆包与粘包。
LineBasedFrameDecoder
  • 基于行分隔符的帧解码器,即会按照行分隔符对数据进行拆包粘包,解码出 Frame
  • 客户端发送数据,在数据最后添加一个行分割符。
System.getProperty("line.separator");

在这里插入图片描述

  • 服务端启动类添加基于行分隔符的帧解码器:
pipeline.addLast(new LineBasedFrameDecoder(5000));

在这里插入图片描述

  • 需要注意的是:这个解码器需要需要设置在 StringDecoder 之前,且确保每一帧的最大长度要大于发送方每一份分割数据的长度,否则会报错 io.netty.handler.codec.TooLongFrameException

在这里插入图片描述

DelimiterBasedFrameDecoder
  • 基于分隔符的帧解码器,即会按照指定分隔符对数据进行拆包粘包,解码出 ByteBuf
  • 客户端发送数据添加自定义的分隔符###--###

在这里插入图片描述

  • 修改服务端的解码器:
ByteBuf delimiter = Unpooled.copiedBuffer("###--###".getBytes());
pipeline.addLast(new DelimiterBasedFrameDecoder(6000, delimiter));

在这里插入图片描述

FixedLengthFrameDecoder
  • 固定长度帧解码器,即会按照指定的长度对 Frame 中的数据进行拆粘包。客户端发送 100 个 12 字符长度的 Hello Netty!,修改服务端解码器:
pipeline.addLast(new FixedLengthFrameDecoder(12));

在这里插入图片描述

LengthFieldBasedFrameDecoder
  • 基于长度域的帧解码器,用于对 LengthFieldPrepender 编码器编码后的数据进行解码的。所以,首先要清楚 LengthFieldPrepender 编码器的编码原理。
  • LengthFieldPrepender 长度域预设:

在这里插入图片描述

  • LengthFieldBasedFrameDecoder 基于长度域的帧解码器:
public LengthFieldBasedFrameDecoder(
        int maxFrameLength,
        int lengthFieldOffset, int lengthFieldLength,
        int lengthAdjustment, int initialBytesToStrip) {
    this(
            maxFrameLength,
            lengthFieldOffset, lengthFieldLength, lengthAdjustment,
            initialBytesToStrip, true);
}
// maxFrameLength:要解码的 Frame 的最大长度。
// lengthFieldOffset:长度域的偏移量。
// lengthFieldLength:长度域的长度。
// lengthAdjustment:要添加到长度域值中的补偿值,长度矫正值。 
// initialBytesToStrip:从解码帧中要剥去的前面字节。
  • 举几个例子:
1、长度域 2字节、偏移量0,不剥离头部,长度矫正值0
     lengthFieldOffset   = 0
     lengthFieldLength   = 2
     lengthAdjustment    = 0
     initialBytesToStrip = 0 (= do not strip header)

 BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
 +--------+----------------+      +--------+----------------+
 | Length | Actual Content |----->| Length | Actual Content |
 | 0x000C | "HELLO, WORLD" |      | 0x000C | "HELLO, WORLD" |
 +--------+----------------+      +--------+----------------+
 
2、长度域 2字节、偏移量0,剥离头部,长度矫正值0
     lengthFieldOffset   = 0
     lengthFieldLength   = 2
     lengthAdjustment    = 0
     initialBytesToStrip = 2 (= the length of the Length field)

 BEFORE DECODE (14 bytes)         AFTER DECODE (12 bytes)
 +--------+----------------+      +----------------+
 | Length | Actual Content |----->| Actual Content |
 | 0x000C | "HELLO, WORLD" |      | "HELLO, WORLD" |
 +--------+----------------+      +----------------+
 
3、长度域 2字节、偏移量0,不剥离头部,有长度矫正值
     lengthFieldOffset   =  0
     lengthFieldLength   =  2
     lengthAdjustment    = -2 (= the length of the Length field)
     initialBytesToStrip =  0

 BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
 +--------+----------------+      +--------+----------------+
 | Length | Actual Content |----->| Length | Actual Content |
 | 0x000E | "HELLO, WORLD" |      | 0x000E | "HELLO, WORLD" |
 +--------+----------------+      +--------+----------------+
 
4、长度域 3字节、偏移量2,不剥离头部,长度矫值0
     lengthFieldOffset   = 2 (= the length of Header 1)
     lengthFieldLength   = 3
     lengthAdjustment    = 0
     initialBytesToStrip = 0

 BEFORE DECODE (17 bytes)                      AFTER DECODE (17 bytes)
 +----------+----------+----------------+      +----------+----------+----------------+
 | Header 1 |  Length  | Actual Content |----->| Header 1 |  Length  | Actual Content |
 |  0xCAFE  | 0x00000C | "HELLO, WORLD" |      |  0xCAFE  | 0x00000C | "HELLO, WORLD" |
 +----------+----------+----------------+      +----------+----------+----------------+
 
5、长度域 3字节、偏移量0,不剥离头部,长度矫值2
     lengthFieldOffset   = 0
     lengthFieldLength   = 3
     lengthAdjustment    = 2 (= the length of Header 1)
     initialBytesToStrip = 0

 BEFORE DECODE (17 bytes)                      AFTER DECODE (17 bytes)
 +----------+----------+----------------+      +----------+----------+----------------+
 |  Length  | Header 1 | Actual Content |----->|  Length  | Header 1 | Actual Content |
 | 0x00000C |  0xCAFE  | "HELLO, WORLD" |      | 0x00000C |  0xCAFE  | "HELLO, WORLD" |
 +----------+----------+----------------+      +----------+----------+----------------+
 
6、长度域 2字节、偏移量1,剥离头部3,长度矫值1
     lengthFieldOffset   = 1 (= the length of HDR1)
     lengthFieldLength   = 2
     lengthAdjustment    = 1 (= the length of HDR2)
     initialBytesToStrip = 3 (= the length of HDR1 + LEN)

 BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)
 +------+--------+------+----------------+      +------+----------------+
 | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
 | 0xCA | 0x000C | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |
 +------+--------+------+----------------+      +------+----------------+
 
7、长度域 2字节、偏移量1,剥离头部3,长度矫值-3
     lengthFieldOffset   =  1
     lengthFieldLength   =  2
     lengthAdjustment    = -3 (= the length of HDR1 + LEN, negative)
     initialBytesToStrip =  3

 BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)
 +------+--------+------+----------------+      +------+----------------+
 | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
 | 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |
 +------+--------+------+----------------+      +------+----------------+

在这里插入图片描述

Netty应用篇

基础应用

WebSocket长连接

  • WebSocket 是 HTML5 中的协议,是构建在 HTTP 协议之上的一个网络通信协议,其以长连接的方式实现了客户端与服务端的全双工通信
  • HTTP/1.1 版本协议中具有 keep alive 属性,实现的是半双工通信。
  • WebSocket 握手原理:

在这里插入图片描述

  • 需求:在页面上有两个左右并排的文本域,它们的中间有一个“发送”按钮。在左侧文本域中输入文本内容后,单击发送按钮,会显示到右侧文本域中。
  • 定义客户端页面:在 src/main 下定义一个目录 webapp,在其中定义 index.html 页面。

在这里插入图片描述

  • Netty 服务端给 pipeline 添加处理器:
	// ... 略
	.childHandler(new ChannelInitializer<SocketChannel>() {
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();
            // 添加Http编解码器
            pipeline.addLast(new HttpServerCodec());
            // 添加大块数据Chunk处理器
            pipeline.addLast(new ChunkedWriteHandler());
            // 添加Chunk聚合处理器
            pipeline.addLast(new HttpObjectAggregator(4096));
            // 添加WebSocket协议转换处理器
            pipeline.addLast(new WebSocketServerProtocolHandler("/some"));
            // 添加自定义处理器
            pipeline.addLast(new TextWebSocketServerHandler());
        }
    });
  • 服务端自定义处理器:
public class TextWebSocketServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String text = ((TextWebSocketFrame) msg).text();
        // 将接收到客户端的消息再发送给客户端
        ctx.channel().writeAndFlush(new TextWebSocketFrame("From Client: " + text));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
  • 启动服务端,并打开 index.html 页面进行测试:

在这里插入图片描述

网络聊天

  • 该工程是对 socket 编程的一个应用,需求是实现一个网络群聊工具,参与聊天的客户端消息是通过服务端进行广播的。
  • Netty 服务端给 pipeline 添加处理器:
	// ... 略
	.childHandler(new ChannelInitializer<SocketChannel>() {
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();
            pipeline.addLast(new LineBasedFrameDecoder(2048));
            pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
            pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
            pipeline.addLast(new WebChatServerHandler());
        }
    });
  • 服务端处理器类:
public class WebChatServerHandler extends ChannelInboundHandlerAdapter {
    /**
     * 创建一个ChannelGroup,其是一个线程安全的集合,其中存放着与当前服务器相连接的所有 Active 状态的Channel
     * GlobalEventExecutor 是一个单例、单线程的EventExecutor,是为了保证对当前group中的所有Channel的处理线程是同一个线程
     */
    private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * 只要有客户端Channel给当前的服务端发送了消息,那么就会触发该方法的执行
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        Channel channel = ctx.channel();
        // 这里要实现将消息广播给所有group中的客户端Channel
        // 发送给自己的消息与发送给大家的消息是不一样的
        for (Channel ch : channelGroup) {
            if (channel != ch) {
                ch.writeAndFlush(channel.remoteAddress() + " : " + msg + "\n");
            } else {
                channel.writeAndFlush("Me: " + msg + "\n");
            }
        }
    }

    /**
     * 只要有客户端Channel与服务端连接成功就会执行这个方法
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        SocketAddress address = channel.remoteAddress();
        System.out.println(address + "---上线");

        channelGroup.writeAndFlush(address + "上线\n");
        channelGroup.add(channel);
    }

    /**
     * 只要有客户端Channel断开与服务端的连接就会执行这个方法
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        SocketAddress address = channel.remoteAddress();
        System.out.println(address + "---下线");
        channelGroup.writeAndFlush(address + "下线,在线人数:" + channelGroup.size() + "\n");

        // group中存放的都是Active状态的Channel,一旦某Channel的状态不再是Active,group会自动将其从集合中踢出,
        // 所以,下面的语句不用写。remove()方法的应用场景是,将一个Active状态的channel移出group时使用
        // group.remove(channel);
    }

    // ...略
}
  • 客户端启动类:

在这里插入图片描述

  • 测试:

在这里插入图片描述

  • 若127.0.0.1:57001下线:

在这里插入图片描述

读写空闲检测

  • 当客户端与服务端的连接建立好后,它们之间就可以进行通信了。但是,若某客户端与服务端间长时间没有进行通信,而 Channel 却被长时间占用,就会形成资源浪费。 Netty 提供了专门用于进行读写操作空闲检测的处理器可供使用。
  • 服务端启动类 pipeline 添加处理器IdleStateHandler
// 若在3秒内当前服务器没有发生读操作,则会触发读操作空闲事件
// 若在5秒内当前服务器没有发生写操作,则会触发写操作空闲事件
// pipeline.addLast(new IdleStateHandler(3, 5, 0));

// 若在5秒内同时即发生了读又发生了写操作才不会触发all操作空闲事件
// 若在5秒内读与写操作有任何一项没有发生,都会触发all操作空闲事件
pipeline.addLast(new IdleStateHandler(0, 0, 5));
  • 服务端处理器重写 userEventTriggered 方法:
/**
* 所有“规定动作”之外的所有事件都可以通过以下方法触发
 */
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    if (evt instanceof IdleStateEvent) {
        IdleStateEvent event = (IdleStateEvent) evt;
        String eventDes = null;
        switch (event.state()) {
            case READER_IDLE:
                eventDes = "读空闲超时";
                break;
            case WRITER_IDLE:
                eventDes = "写空闲超时";
                break;
            case ALL_IDLE:
                eventDes = "读和写都空闲超时";
                break;
        }
        System.out.println(eventDes);
    } else {
        // 其它事件触发
        super.userEventTriggered(ctx, evt);
    }
}

心跳机制

  • 所谓心跳,即在 TCP 长连接中,客户端和服务器之间定期发送的一种特殊的数据包 , 通知对方自己还“活着”, 以确保 TCP 连接的有效性。
  • 需求:Client 端连接到 Server 端后,会循环执行一个定时任务:随机等待几秒,然后 ping 一下 Server 端,即发送一个心跳。当 Server 端在等待了指定时间后没有读取到 Client 端发送的心跳,Server 端会主动断开连接。
定义服务端
  • 服务端启动类 pipeline 添加处理器:
	ChannelPipeline pipeline = ch.pipeline();
    pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
    pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
    pipeline.addLast(new IdleStateHandler(5, 0, 0));
    pipeline.addLast(new HeartBeatServerHandler());
  • 服务端处理器重写 userEventTriggered 方法:
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    if (evt instanceof IdleStateEvent) {
        IdleState state = ((IdleStateEvent) evt).state();
        if (state == IdleState.READER_IDLE) {
            System.out.println("将要断开连接");
            ctx.close();
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }
}
定义客户端
  • 客户端心跳检测处理:一旦连接被关闭,则将监听器移除

在这里插入图片描述

  • 测试看下效果:

在这里插入图片描述

  • 客户端连接断开后实现重连:

在这里插入图片描述

服务端监听多端口

修改服务端
public class SomeServer {
    private NioEventLoopGroup parentGroup = new NioEventLoopGroup(1);
    private NioEventLoopGroup childGroup = new NioEventLoopGroup();
    private List<ChannelFuture> futures = new ArrayList<>();

    public void start(List<Integer> ports) throws Exception {
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(parentGroup, childGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        // 服务端需要接收客户端数据,所以这里要添加解码器
                        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
                        // 服务端需要想客户端发送数据,所以这里要添加编码器
                        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
                        pipeline.addLast(new SomeServerHandler());
                    }
                });
        for (Integer port : ports) {
            // 生成一个future
            ChannelFuture future = bootstrap.bind(port).sync();
            System.out.println("服务器正在启动中。。。");
            future.addListener(f -> {
                if (f.isSuccess()) {
                    System.out.println("服务器已启动,监听的端口为:" + port);
                }
            });
            // 将所有生成的future添加到集合中
            futures.add(future);
        }
    }

    /**
     * 关闭所有Channel
     */
    public void closeAllChannel() {
        System.out.println("所有Channel已经全部关闭");
        for (ChannelFuture future : futures) {
            future.channel().close();
        }
        parentGroup.shutdownGracefully();
        childGroup.shutdownGracefully();
    }
}
  • 定义服务启动类:
public class ServerStarter {
    public static void main(String[] args) throws Exception {
        List<Integer> ports = new ArrayList<>();
        ports.add(7777);
        ports.add(8888);
        ports.add(9999);

        SomeServer server = new SomeServer();
        // 启动服务器,按照指定端口号进行监听
        server.start(ports);

        // 30秒后关闭所有channel
        TimeUnit.SECONDS.sleep(60);
        server.closeAllChannel();
    }
}
测试

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

讲文明的喜羊羊拒绝pua

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值