Netty

Netty

一、初识Netty


​ Netty是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。

  • netty是一个提供了易于使用的api客户端/服务端框架
  • 并发高-NIO(非阻塞IO)
  • 传输快-零拷贝

1.1 几个概念

  • 阻塞与非阻塞

​ 线程访问资源,该资源是否准备就绪的一种处理方式

image-20230810220803288

  • 同步和异步

    同步和异步是指访问数据的一种机制

image-20230810220954703

1.2 BIO(同步阻塞 IO )

BIO(Blocking-IO)即同步阻塞模型,这也是最初的IO模型,也就是当调用内核的read()函数后,内核在执行数据准备、复制阶段的IO操作时,应用线程都是阻塞的,所以被称为同步阻塞式IO

image-20230810221119912

​ 思考问题:「当本次IO操作还在执行时,又出现多个IO调用,比如多个网络数据到来,此刻该如何处理呢?🤔

​ 答案:很简单,采用多线程实现,包括最初的IO模型也的确是这样实现的,也就是当出现一个新的IO调用时,服务器就会多一条线程去处理

image-20230810221601809

BIO这种模型中,为了支持并发请求,通常情况下会采用“请求:线程”1:1的模型,那此时会带来很大的弊端:

  • ①并发过高时会导致创建大量线程,而线程资源是有限的,超出后会导致系统崩溃。
  • ②并发过高时,就算创建的线程数未达系统瓶颈,但由于线程数过多也会造成频繁的上下文切换。

但在Java常用的Tomcat服务器中,Tomcat7.x版本以下默认的IO类型也是BIO,但似乎并未碰到过:并发请求创建大量线程导致系统崩溃的情况出现呢?这是由于Tomcat中对BIO模型稍微进行了优化,通过线程池做了限制:

tomcat中的BIO

​ 在Tomcat中,存在一个处理请求的线程池,该线程池声明了核心线程数以及最大线程数,当并发请求数超出配置的最大线程数时,会将客户端的请求加入请求队列中等待,防止并发过高造成创建大量线程,从而引发系统崩溃。

1.3 NIO(同步非阻塞 IO )

`NIO(Non-Blocking-IO)`同步非阻塞模型,从字面意思上来说就是:调用`read()`函数的线程并不会阻塞,而是可以正常运行。

​ 当应用程序中发起IO调用后,内核并不阻塞当前线程,而是立马返回一个“数据未就绪”的信息给应用程序,而应用程序这边则一直反复轮询去问内核:数据有没有准备好?直到最终数据准备好了之后,内核返回“数据已就绪”状态,紧接着再由进程去处理数据…

​ 但聪明的你也应该能明显感受到这种所谓的NIO相对来说较为鸡肋,因此目前大多数的NIO技术并非采用这种多线程的模型,而是基于单线程的多路复用模型实现的,Java中支持的NIO模型亦是如此。

image-20230810222107401

​ 在理解多路复用模型之前,我们先分析一下上述的NIO模型到底存在什么问题呢?很简单,由于线程在不断的轮询查看数据是否准备就绪,造成CPU开销较大。既然说是由于大量无效的轮询造成CPU占用过高,那么等内核中的数据准备好了之后,再去询问数据是否就绪是不是就可以了?答案是Yes

​ 那又该如何实现这个功能呢?此时大名鼎鼎的多路复用模型登场了,该模型是基于文件描述符File Descriptor实现的,在Linux中提供了select、poll、epoll等一系列函数实现该模型,结构如下:

image-20230810222349334

​ 在多路复用模型中,内核仅有一条线程负责处理所有连接,所有网络请求/连接(Socket)都会利用通道Channel注册到选择器上,然后监听器负责监听所有的连接,过程如下:

​ 当出现一个IO操作时,会通过调用内核提供的多路复用函数,将当前连接注册到监听器上,当监听器发现该连接的数据准备就绪后,会返回一个可读条件给用户进程,然后用户进程拷贝内核准备好的数据进行处理(这里实际是读取Socket缓冲区中的数据)。

1.4 Reacotr线程模型

1.4.1 单线程模型

​ 所有的IO操作都会由同一个NIO线程处理

image-20230810222929393

1.4.2 多线程模型

​ 由一个 NIO 线程处理客户端连接,一组 NIO 线程池处理 IO 操作

image-20230810223210665

1.4.3 主从线程模型 (Netty推荐线程模型)

​ 一组线程池接受请求,一组线程池处理 IO

image-20230810223419040

二、Netty的初步上手


​ 本节将通过简单代码对Netty进行实操,想想就有点小激动呢

2.1 Hello Netty服务器

  1. 构建一对主从线程池

  2. 定义服务器启动类

  3. 为服务器设置Channel

  4. 设置处理从线程池的助手类初始化器

    每一个channel有多个handler共同组成管道(pipeline),可以理解为过滤器

    image-20230810225233523
  5. 监听启动和关闭服务器

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.43.Final</version>
</dependency>
package com.netty.nettyspringboot;

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

/**
 *
 * 实现服务端发送一个请求,服务器会返回Hello netty
 * @author St_up
 * @date 2023/8/10
 */
public class HelloServer {
    public static void main(String[] args) throws InterruptedException {
        // 1. 定义一对线程组
        // 主线程组,用于接收客户端连接,不做任何处理
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        // 从线程组,主线程组会把任务丢给从线程组,让从线程组去做任务
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // 2. netty服务器的创建,ServerBootstrap是一个启动类
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                // 3. 设置NIO的双向通道
                .channel(NioServerSocketChannel.class)
                // 4. 助手类,子处理器,用于处理workerGroup
                .childHandler(new HelloServerInitializer());

            // 5. 启动server,并且设置9874为启动的端口号,同时启动方式为同步
            ChannelFuture channelFuture = serverBootstrap.bind(9874).sync();

            // 监听关闭的channel,设置为同步方式
            channelFuture.channel().closeFuture().sync();
        }finally {
            // 优雅关闭
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}


/*==============================================================================================*/

/**
 * 初始化器,channel注册后,会执行里面的相应的初始化方法
 * @author St_up
 * @date 2023/8/10
 */
public class HelloServerInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel channel) throws Exception {
        // 通过SocketChannel去获得对应的管道
        ChannelPipeline pipeline = channel.pipeline();

        // HttpServerCodec是由netty自己提供的助手类,可以理解为拦截器
        // 当请求到服务端,我们需要做解码,响应到客户端做编码
        pipeline.addLast("HttpServerCodec", new HttpServerCodec());

        // 管道里面有很多handler,类似于拦截器,HelloServerHandler是我们自己定义的,返回"Hello netty"
        pipeline.addLast("helloServerHandlerA", new HelloServerHandlerA());

    }
}

/*==============================================================================================*/

/**
 * 自定义助手类,返回"Hello netty"
 *
 * SimpleChannelInboundHandler:对于请求来讲,相当于[入站,入境]
 * @author St_up
 * @date 2023/8/10
 */
public class HelloServerHandlerA extends SimpleChannelInboundHandler<HttpObject>{
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
        // 获取channel
        Channel channel = ctx.channel();
        // 判断msg是不是httpRequest请求
        if(msg instanceof HttpRequest){
            System.out.format("remote address: %s\n", channel.remoteAddress());
            // 定义发送的数据消息
            ByteBuf content = Unpooled.copiedBuffer("Hello netty", StandardCharsets.UTF_8);

            // 构建一个http response
            FullHttpResponse response =
                new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
            // 为响应增加数据类型和长度
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());


            // 把响应刷到客户端
            ctx.writeAndFlush(response);
        }
    }
}

2.2 Channel的生命周期

2.2.1 Channel生命周期

​ Channel 是 Netty 中用于实现网络通信的核心抽象。每当建立一个连接时,Netty 会为该连接创建一个 Channel,通过该 Channel 可以进行数据的读取、写入和其他操作。Channel 生命周期涵盖了从创建到关闭的整个过程

​ 以下是 Channel 生命周期的主要阶段:

状态描述
ChannelUnregisteredChannel 已经被创建,但还未注册到 EventLoop
ChannelRegisteredChannel 已经被注册到 EventLoop
ChannelActiveChannel 处于活动状态(已经连接到它的远程节点),可以接收和发送数据
ChannelInactiveChannel 没有连接到远程节点

2.2.2 ChannelHandler 生命周期

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

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

2.2.3 ChannelInboundHandler 接口

ChannelInboundHandler 是 ChannelHandler 接口的子接口,处理入站数据以及各种状态变化。下表列出 ChannelInboundHandler 的生命周期方法,这些方法将会在数据被接收时或者与其对应的 Channel 状态发生改变时被调用

类型描述
channelRegistered当 Channel 已经注册到它的 EventLoop 并且能够处理 IO 时被调用
channelUnregistered当 Channel 从它的 EventLoop 注销并且无法处理任何 IO 时被调用
channelActive当 Channel 处于活动状态时被调用,Channel 已经连接/绑定并且就绪
channelInactive当 Channel 离开活动状态并且不再连接它的远程节点时被调用
channelReadComplete当 Channel 上的一个读操作完成时被调用
channelRead当从 Channel 读取数据时被调用
ChannelWritabilityChanged当 Channel 的可写状态发生改变时被调用
useEventTriggered当 ChannelInboundHandler.fireUserEventTriggered() 方法被调用时被调用,因为一个 POJO 流经 ChannelPipeline

2.2.4 ChannelOutboundHandler 接口

出站操作和数据由 ChannelOutboundHandler 处理,它的方法将被 Channel、ChannelPipeline 以及 ChannelHandlerContext 调用

ChannelOutboundHandler 的一个强大的功能是可以按需推迟操作或者事件,使得可以通过一些复杂的方法来处理请求,例如,如果到远程节点的写入被暂停了,那么你可以推迟冲刷操作并在稍后继续,下表显示了所有由 ChannelOutboundHandler 本身所定义的方法:

类型描述
bind(ChannelHandlerContext, SocketAddress, ChannelPromise)当请求将 Channel 绑定到本地地址时被调用
connect(ChannelHandlerContext, SocketAddress, ChannelPromise)当请求将 Channel 连接到远程节点时被调用
disconnect(ChannelHandlerContext, ChannelPromise)当请求将 Channel 从远程节点断开时被调用
close(ChannelHandlerContext, ChannelPromise)当请求关闭 Channel 时被调用
deregister(ChannelHandlerContext, ChannelPromise)当请求将 Channel 从它的 EventLoop 注销时被调用
read(ChannelHandlerContext)当请求从 Channel 读取更多的数据时被调用
flush(ChannelHandlerContext)当请求通过 Channel 将入队数据冲刷到远程节点时被调用
write(ChannelHandlerContext, Object, ChannelPromise)当请求通过 Channel 将数据写到远程节点时被调用

image-20230810234155786

/**
 * Channel的生命周期示例
 * @author St_up
 * @date 2023/8/10
 */
public class HelloServerHandlerB extends SimpleChannelInboundHandler<HttpObject> {

    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println("Channel registered: " + ctx.channel());
        super.channelRegistered(ctx);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("Channel active: " + ctx.channel());
        super.channelActive(ctx);
    }


    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("Channel inactive: " + ctx.channel());
        super.channelInactive(ctx);
    }

    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println("Channel unregistered: " + ctx.channel());
        super.channelUnregistered(ctx);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("Channel read: " + ctx.channel());
        super.channelRead(ctx, msg);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
        // Handle incoming data
        System.out.println("Channel channelRead0: " + ctx.channel());
        // ... handle the data ...
    }
}



// channelRead 中调用了 channelRead0,其会先做消息类型检查,判断当前message 是否需要传递到下一个handler。



//表示 Channel 被注册到 EventLoop 上
Channel registered:  [id: 0x181da415, L:/127.0.0.1:9874 - R:/127.0.0.1:55934]
// 表示 Channel 已经激活并准备好进行数据传输
Channel active:  [id: 0x181da415, L:/127.0.0.1:9874 - R:/127.0.0.1:55934]
// 表示接收到了数据
Channel read: [id: 0x181da415, L:/127.0.0.1:9874 - R:/127.0.0.1:55934]
Channel channelRead0: [id: 0x181da415, L:/127.0.0.1:9874 - R:/127.0.0.1:55934]
// 表示 Channel 不再活跃,即连接已关闭
Channel inactive:  [id: 0x181da415, L:/127.0.0.1:9874 - R:/127.0.0.1:55934]
// 表示 Channel 从 EventLoop 上注销
Channel unregistered: [id: 0x181da415, L:/127.0.0.1:9874 - R:/127.0.0.1:55934]

三、 实时通信

3.1 实时通信概述

当涉及实时通信时,有几种常见的技术方法可供选择,包括 Ajax 轮询、长轮询(Long Polling)和 WebSocket

  1. Ajax 轮询(Ajax Polling)

Ajax 轮询是一种最早用于实现实时通信的方法之一。它基于客户端定时发送 HTTP 请求来询问服务器是否有新的数据可用,服务器会在数据更新时,回复一个响应给客户端,客户端在收到响应后解析数据并进行相应的处理。

  • 优点

    • 支持在大多数浏览器上使用。

    • 对于简单的应用场景来说,实现相对简单。

  • 缺点

    • 频繁的 HTTP 请求可能会导致高延迟和高网络开销。

    • 服务器必须处理大量的请求,增加了服务器负担。

    • 不适用于高实时性的应用,因为轮询间隔存在一定延迟。

  1. 长轮询(Long Polling)

长轮询是对传统 Ajax 轮询的改进。在长轮询中,客户端发送一个 HTTP 请求到服务器,而服务器将保持请求打开,直到有新数据可用或超时。当数据更新后,服务器会响应请求,然后客户端在收到响应后重新发起新的请求。

  • 优点

    • 较传统的轮询方式减少了请求次数,降低了网络开销。

    • 可以实现更高的实时性,因为服务器可以在数据准备好时立即响应。

  • 缺点

    • 仍然需要频繁的建立和断开连接,可能造成资源浪费。

    • 服务器需要在请求保持打开期间分配资源,导致服务器负担。

  1. WebSocket

WebSocket 是一种全双工的通信协议,它提供了持久连接以支持双向通信。通过 WebSocket,客户端和服务器可以在一个连接上实时地交换消息,而无需频繁地发起新的连接。

  • 优点

    • 实现了真正的双向通信,适用于高实时性的应用场景。

    • 建立一次连接后,可以持续地交换数据,减少了连接和断开的开销。

    • 较低的延迟,更适合实时应用。

  • 缺点

    • 需要浏览器和服务器端都支持 WebSocket 协议。

    • 需要特定的服务器支持,不同于传统的 HTTP 服务器。

​ 总的来说,Ajax 轮询、长轮询和 WebSocket 都有各自的优缺点。选择合适的方法取决于你的应用需求和技术栈。如果需要更高的实时性和效率,WebSocket 是一个更为先进和适用的选择。如果需要向后兼容老版本浏览器或者简单的实时通信场景,Ajax 轮询或长轮询可以考虑。

3.2 WebSocket的初步上手

3.2.1 后端代码

​ Netty服务器相同,我们创建一个WebSocketServer

  1. 构建一对主从线程池

  2. 定义服务器启动类

  3. 为服务器设置Channel

  4. 设置处理从线程池的助手类初始化器

    每一个channel有多个handler共同组成管道(pipeline),可以理解为过滤器

  5. 监听启动和关闭服务器

/**
 * WebSocketServer
 * @author St_up
 * @date 2023/8/10
 */
public class WebSocketServer {
    public static void main(String[] args) throws InterruptedException {

        NioEventLoopGroup mainGroup = new NioEventLoopGroup();
        NioEventLoopGroup subGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap server = new ServerBootstrap();
            server.group(mainGroup, subGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new WebSocketInitializer());

            ChannelFuture future = server.bind(9874).sync();
            future.channel().closeFuture().sync();

        } finally {
            mainGroup.shutdownGracefully();
            subGroup.shutdownGracefully();
        }
    }
}


/*==============================================================================================*/


/**
 * @author St_up
 * @date 2023/8/11
 */
public class WebSocketInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel channel) throws Exception {
        ChannelPipeline pipeline = channel.pipeline();

        // websocket基于http协议,所以要有http编解码器
        pipeline.addLast(new HttpServerCodec());

        // 对写大数据流的支持,不加入这个handler,大数据发送时会发生写半包的错误,因为Netty对http数据流写的时候是分段写的
        pipeline.addLast(new ChunkedWriteHandler());

        // 对httpMessage进行聚合,聚合成FullHttpRequest或FullHttpResponse,几乎在netty中的编程,都会使用到此handler
        pipeline.addLast(new HttpObjectAggregator(1024 * 64));

        // ====================== 以上是用于支持http协议    ======================

        // ====================== 以下是支持httpWebsocket ======================

        // websocket服务器处理的协议,用于指定给客户端连接访问的路由:/ws
        // 本handler会帮你处理一些繁重的复杂的事,会帮你处理握手动作:handshaking(close,ping,pong) ping + pong = 心跳
        // 对于websocket来讲,都是以frames进行传输的,不同的数据类型对应的frames也不同
        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));


        // 添加自定义的webSocket助手类
        pipeline.addLast(new WebSocketHandler());
    }
}

/*==============================================================================================*/

/**
 * 处理消息的handler
 * TextWebSocketFrame:在netty中,是用于为websocket专门处理文本的对象,frame是消息的载体
 * @author St_up
 * @date 2023/8/11
 */
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    /** 用于记录和管理所有客户端的channel */
    private static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        // 获取客户端传输过来的消息
        String text = msg.text();
        System.out.println("接收到的消息:" + text);

        // 将消息转发到所有客户端
        for (Channel client : clients) {
            client.writeAndFlush(new TextWebSocketFrame("服务器接收到消息:" + text));
        }

        // 下面这行代码和上面的for循环是等价的
        clients.writeAndFlush(new TextWebSocketFrame("服务器接收到消息:" + text));
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        // 当客户端连接服务端之后, 获取客户端的channel,并且放到ChannelGroup中去进行管理
        Channel channel = ctx.channel();
        clients.add(channel);
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        // 当触发handlerRemoved,ChannelGroup会自动移除对应客户端的channel
        // clients.remove(ctx.channel()); // 不需要手动移除

        System.out.println("客户端断开,channel对应的长id为:" + ctx.channel().id().asLongText());
        System.out.println("客户端断开,channel对应的短id为:" + ctx.channel().id().asShortText());
    }
}


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

St_up

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

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

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

打赏作者

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

抵扣说明:

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

余额充值