Netty
一、初识Netty
Netty是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。
- netty是一个提供了易于使用的api客户端/服务端框架
- 并发高-NIO(非阻塞IO)
- 传输快-零拷贝
1.1 几个概念
- 阻塞与非阻塞
线程访问资源,该资源是否准备就绪的一种处理方式
-
同步和异步
同步和异步是指访问数据的一种机制
1.2 BIO(同步阻塞 IO )
BIO(Blocking-IO)
即同步阻塞模型,这也是最初的IO
模型,也就是当调用内核的read()
函数后,内核在执行数据准备、复制阶段的IO
操作时,应用线程都是阻塞的,所以被称为同步阻塞式IO
思考问题:「当本次IO
操作还在执行时,又出现多个IO
调用,比如多个网络数据到来,此刻该如何处理呢?🤔」
答案:很简单,采用多线程实现,包括最初的IO
模型也的确是这样实现的,也就是当出现一个新的IO
调用时,服务器就会多一条线程去处理
在BIO
这种模型中,为了支持并发请求,通常情况下会采用“请求:线程”1:1
的模型,那此时会带来很大的弊端:
- ①并发过高时会导致创建大量线程,而线程资源是有限的,超出后会导致系统崩溃。
- ②并发过高时,就算创建的线程数未达系统瓶颈,但由于线程数过多也会造成频繁的上下文切换。
但在Java
常用的Tomcat
服务器中,Tomcat7.x
版本以下默认的IO
类型也是BIO
,但似乎并未碰到过:并发请求创建大量线程导致系统崩溃的情况出现呢?这是由于Tomcat
中对BIO
模型稍微进行了优化,通过线程池做了限制:
在Tomcat
中,存在一个处理请求的线程池,该线程池声明了核心线程数以及最大线程数,当并发请求数超出配置的最大线程数时,会将客户端的请求加入请求队列中等待,防止并发过高造成创建大量线程,从而引发系统崩溃。
1.3 NIO(同步非阻塞 IO )
`NIO(Non-Blocking-IO)`同步非阻塞模型,从字面意思上来说就是:调用`read()`函数的线程并不会阻塞,而是可以正常运行。
当应用程序中发起IO
调用后,内核并不阻塞当前线程,而是立马返回一个“数据未就绪”的信息给应用程序,而应用程序这边则一直反复轮询去问内核:数据有没有准备好?直到最终数据准备好了之后,内核返回“数据已就绪”状态,紧接着再由进程去处理数据…
但聪明的你也应该能明显感受到这种所谓的NIO
相对来说较为鸡肋,因此目前大多数的NIO
技术并非采用这种多线程的模型,而是基于单线程的多路复用模型实现的,Java
中支持的NIO
模型亦是如此。
在理解多路复用模型之前,我们先分析一下上述的NIO
模型到底存在什么问题呢?很简单,由于线程在不断的轮询查看数据是否准备就绪,造成CPU
开销较大。既然说是由于大量无效的轮询造成CPU
占用过高,那么等内核中的数据准备好了之后,再去询问数据是否就绪是不是就可以了?答案是Yes
。
那又该如何实现这个功能呢?此时大名鼎鼎的多路复用模型登场了,该模型是基于文件描述符File Descriptor
实现的,在Linux
中提供了select、poll、epoll
等一系列函数实现该模型,结构如下:
在多路复用模型中,内核仅有一条线程负责处理所有连接,所有网络请求/连接(Socket
)都会利用通道Channel
注册到选择器上,然后监听器负责监听所有的连接,过程如下:
当出现一个IO
操作时,会通过调用内核提供的多路复用函数,将当前连接注册到监听器上,当监听器发现该连接的数据准备就绪后,会返回一个可读条件给用户进程,然后用户进程拷贝内核准备好的数据进行处理(这里实际是读取Socket
缓冲区中的数据)。
1.4 Reacotr线程模型
1.4.1 单线程模型
所有的IO
操作都会由同一个NIO
线程处理
1.4.2 多线程模型
由一个 NIO
线程处理客户端连接,一组 NIO
线程池处理 IO
操作
1.4.3 主从线程模型 (Netty推荐线程模型)
一组线程池接受请求,一组线程池处理 IO
二、Netty的初步上手
本节将通过简单代码对Netty进行实操,想想就有点小激动呢
2.1 Hello Netty服务器
-
构建一对主从线程池
-
定义服务器启动类
-
为服务器设置Channel
-
设置处理从线程池的助手类初始化器
每一个channel有多个handler共同组成管道(pipeline),可以理解为过滤器
-
监听启动和关闭服务器
<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 生命周期的主要阶段:
状态 | 描述 |
---|---|
ChannelUnregistered | Channel 已经被创建,但还未注册到 EventLoop |
ChannelRegistered | Channel 已经被注册到 EventLoop |
ChannelActive | Channel 处于活动状态(已经连接到它的远程节点),可以接收和发送数据 |
ChannelInactive | Channel 没有连接到远程节点 |
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 将数据写到远程节点时被调用 |
/**
* 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:
- Ajax 轮询(Ajax Polling)
Ajax 轮询是一种最早用于实现实时通信的方法之一。它基于客户端定时发送 HTTP 请求来询问服务器是否有新的数据可用,服务器会在数据更新时,回复一个响应给客户端,客户端在收到响应后解析数据并进行相应的处理。
-
优点:
-
支持在大多数浏览器上使用。
-
对于简单的应用场景来说,实现相对简单。
-
-
缺点:
-
频繁的 HTTP 请求可能会导致高延迟和高网络开销。
-
服务器必须处理大量的请求,增加了服务器负担。
-
不适用于高实时性的应用,因为轮询间隔存在一定延迟。
-
- 长轮询(Long Polling)
长轮询是对传统 Ajax 轮询的改进。在长轮询中,客户端发送一个 HTTP 请求到服务器,而服务器将保持请求打开,直到有新数据可用或超时。当数据更新后,服务器会响应请求,然后客户端在收到响应后重新发起新的请求。
-
优点:
-
较传统的轮询方式减少了请求次数,降低了网络开销。
-
可以实现更高的实时性,因为服务器可以在数据准备好时立即响应。
-
-
缺点:
-
仍然需要频繁的建立和断开连接,可能造成资源浪费。
-
服务器需要在请求保持打开期间分配资源,导致服务器负担。
-
- WebSocket
WebSocket 是一种全双工的通信协议,它提供了持久连接以支持双向通信。通过 WebSocket,客户端和服务器可以在一个连接上实时地交换消息,而无需频繁地发起新的连接。
-
优点:
-
实现了真正的双向通信,适用于高实时性的应用场景。
-
建立一次连接后,可以持续地交换数据,减少了连接和断开的开销。
-
较低的延迟,更适合实时应用。
-
-
缺点:
-
需要浏览器和服务器端都支持 WebSocket 协议。
-
需要特定的服务器支持,不同于传统的 HTTP 服务器。
-
总的来说,Ajax 轮询、长轮询和 WebSocket 都有各自的优缺点。选择合适的方法取决于你的应用需求和技术栈。如果需要更高的实时性和效率,WebSocket 是一个更为先进和适用的选择。如果需要向后兼容老版本浏览器或者简单的实时通信场景,Ajax 轮询或长轮询可以考虑。
3.2 WebSocket的初步上手
3.2.1 后端代码
Netty服务器相同,我们创建一个WebSocketServer
-
构建一对主从线程池
-
定义服务器启动类
-
为服务器设置Channel
-
设置处理从线程池的助手类初始化器
每一个channel有多个handler共同组成管道(pipeline),可以理解为过滤器
-
监听启动和关闭服务器
/**
* 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());
}
}