小白也能看懂的 Netty 框架——实战(二)
一、任务(Task)介绍
在处理高并发网络请求时,Netty 提供了一种高效的任务调度机制,避免了阻塞主线程或导致性能瓶颈。对于一些耗时任务,Netty 使用了任务队列(task queue)来异步处理,确保系统能够继续处理其他请求。
1. 常见的任务类型
Netty 支持三种常见的任务类型:普通任务、定时任务以及跨线程调用任务。接下来,我们会详细介绍这三种任务类型及其实现方式。
2. 用户自定义普通任务
普通任务通常用于处理不需要定时触发的耗时操作。例如,我们可能会在收到客户端请求后,执行一些后台任务(如数据处理、文件操作等),然后向客户端返回结果。此类任务会被提交到 taskQueue
中异步执行。
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
// 模拟耗时操作
Thread.sleep(10 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello client : miaowu2", CharsetUtil.UTF_8));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
在上面的代码中,我们将任务提交给 eventLoop
,该任务会被放入 taskQueue
中异步执行,避免了阻塞当前线程。
3. 用户自定义定时任务
定时任务用于处理那些需要延迟执行的操作。例如,定时推送消息、定时清理缓存等任务。这类任务会被提交到 scheduleTaskQueue
中,由 Netty 的事件循环线程池按时间顺序调度执行。
ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
try {
// 模拟耗时操作
Thread.sleep(10 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello client : miaowu2", CharsetUtil.UTF_8));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, 5, TimeUnit.SECONDS);
在这个例子中,我们使用 schedule
方法定义了一个 5 秒后执行的任务,任务会被放入 scheduleTaskQueue
,然后由 eventLoop
异步执行。
4. 跨线程调用 Channel 的方法
在一些复杂的应用场景中,可能需要在非当前 Reactor
线程中调用 Channel 的各种方法。例如,在一个推送系统中,服务器可能需要根据用户标识推送消息,而这些推送操作通常是由非 Reactor
线程发起的。
解决方案
我们可以将每个用户的 Channel
存储到一个集合中,然后在推送消息时将任务提交到该用户对应的 eventLoop
的 taskQueue
或 scheduleTaskQueue
中。
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyServerHandler());
}
通过 NettyServerHandler
处理器,Netty 会管理每个连接的 Channel
,并确保推送消息时不会发生阻塞。
二、Netty 异步模型
1. Netty 的异步 IO 操作
Netty 使用异步非阻塞 IO 操作来提高性能和吞吐量。常见的操作,如 bind
、write
、connect
等,都会异步执行。调用者无法立即获得执行结果,而是通过 future-listener
机制来获取操作的结果。
2. Future
和 ChannelFuture
在 Netty 中,Future
是一个接口,表示某个任务的执行结果。通过 ChannelFuture
,用户可以在任务完成时被通知,避免了阻塞调用。
示例:绑定端口并监听结果
ChannelFuture cf = bootstrap.bind(6668).sync(); // 绑定端口并同步等待
// 添加监听器,异步处理结果
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (cf.isSuccess()) {
System.out.println("监听端口6668绑定成功");
} else {
System.out.println("监听失败");
}
}
});
// 监听通道关闭事件
cf.channel().closeFuture().sync();
在上面的代码中,ChannelFuture
通过监听器回调来处理端口绑定是否成功。通过异步模型,Netty 可以避免阻塞,允许程序继续执行其他任务。
3. 异步模型的优势
使用 Netty 的异步模型,能够避免阻塞操作,提高系统的并发处理能力。通过 future
机制,调用者不必等待结果的返回,而是可以在结果准备好时通知回调。这使得 Netty 能够高效地处理大量并发请求。
三、每个客户端独立的 Pipeline 和 Handler
在 Netty 中,每个客户端连接都会有独立的 Pipeline
和 Handler
,这使得每个连接的处理逻辑相互独立,互不干扰。这样设计的好处是可以高效地处理大量并发连接,而不会导致线程安全问题。
- Pipeline:每个
Channel
会有一个Pipeline
,它是一个责任链模式,用来处理数据的传输和处理。 - Handler:
Handler
负责执行具体的业务逻辑处理。
Netty 会根据每个 Channel
独立管理其数据流,确保不同连接之间的处理互不干扰,从而提升系统的扩展性和稳定性。
四、实战:编写一个简单的 HTTP 服务
在本节中,我们将通过一个简单的 HTTP 服务器实例,演示如何使用 Netty 框架构建一个支持基本 HTTP 请求和响应的服务。我们将实现三部分代码:HTTP 请求的处理器(TestHttpServerHandler
)、服务器启动类(TestServer
)和服务器的初始化类(TestserverInitializer
)。
1. HTTP 请求处理器 (TestHttpServerHandler
)
该处理器用于接收和处理来自客户端的 HTTP 请求。我们将处理两类请求:
- 预检请求(OPTIONS):用于处理跨域请求。
- 其他 HTTP 请求(如 GET 请求):用于处理常规的 GET 请求,并返回响应。
package netty.http;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import java.net.URI;
/**
* HTTP 请求处理器,用于处理客户端发来的 HTTP 请求。
*/
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
// 处理预检请求 (OPTIONS),用于跨域资源共享(CORS)
if (msg instanceof HttpRequest && ((HttpRequest) msg).method() == HttpMethod.OPTIONS) {
// 构建一个空的响应,表示允许跨域请求
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
// 设置 CORS 响应头
response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS");
response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type, Authorization");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
ctx.writeAndFlush(response);
return;
}
// 处理其他 HTTP 请求(如 GET 请求)
if (msg instanceof HttpRequest) {
System.out.println(msg.getClass());
System.out.println(ctx.channel().remoteAddress());
HttpRequest httpRequest = (HttpRequest) msg;
URI uri = new URI(httpRequest.uri());
// 忽略 favicon.ico 请求
if ("/favicon.ico".equals(uri.getPath())) {
System.out.println("请求了 favicon.ico");
return;
}
// 构建响应内容
ByteBuf byteBuf = Unpooled.copiedBuffer("hello, I'm server", CharsetUtil.UTF_8);
// 构建 HTTP 响应
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, byteBuf);
// 设置响应头
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());
// 发送响应到客户端
ctx.writeAndFlush(response);
}
}
}
2. 服务器启动类 (TestServer
)
该类负责启动 HTTP 服务器,并绑定到指定端口(在此示例中为 8084
)。它配置了 EventLoopGroup
来处理所有的 IO 操作,并设置 ServerBootstrap
来启动服务器。
package netty.http;
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;
/**
* 启动 HTTP 服务器
*/
public class TestServer {
public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup(1); // 处理连接请求
EventLoopGroup workergroup = new NioEventLoopGroup(); // 处理连接之后的任务
try {
// 创建并配置 ServerBootstrap
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workergroup)
.channel(NioServerSocketChannel.class) // 设置使用 NioServerSocketChannel 作为通信方式
.childHandler(new TestserverInitializer()); // 初始化服务器处理逻辑
// 绑定端口并启动服务器
ChannelFuture sync = bootstrap.bind(8084).sync();
sync.channel().closeFuture().sync(); // 等待服务器关闭
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
bossGroup.shutdownGracefully(); // 优雅地关闭 EventLoopGroup
workergroup.shutdownGracefully();
}
}
}
3. 服务器初始化类 (TestserverInitializer
)
此类用于设置服务器的 Pipeline
,并添加必要的编解码器和自定义处理器。我们使用 Netty 提供的 HttpServerCodec
来处理 HTTP 编解码,并将自定义的 TestHttpServerHandler
添加到 Pipeline
中。
package netty.http;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;
/**
* 初始化服务器的处理管道
*/
public class TestserverInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 获取管道
ChannelPipeline pipeline = ch.pipeline();
// 添加 HTTP 编解码器(HttpServerCodec 会自动处理 HTTP 的解码和编码)
pipeline.addLast("MyHttpServerCodec", new HttpServerCodec());
// 添加自定义的 HTTP 请求处理器
pipeline.addLast("MyHttpServerHandler", new TestHttpServerHandler());
}
}
4. 测试与运行
5. 说明
- CORS(跨域资源共享)处理: 服务器支持处理
OPTIONS
请求,这是跨域请求时浏览器发送的预检请求。服务器会返回合适的 CORS 响应头,允许跨域访问。 - 简单的 HTTP 处理: 服务器会对其他 HTTP 请求(如 GET 请求)做出响应,返回一条简单的消息
hello, I'm server
。 - Netty 的事件驱动模型: 通过
ChannelPipeline
和ChannelHandler
,Netty 可以高效地处理 HTTP 请求和响应。这种设计允许 Netty 在不阻塞的情况下,快速处理大量并发连接。
总结
- 任务队列:Netty 提供了三种常见的任务类型:普通任务、定时任务和跨线程任务。这些任务通过
taskQueue
或scheduleTaskQueue
异步执行,避免了线程阻塞。 - 异步 IO:Netty 的异步模型使得 IO 操作不会阻塞调用线程。通过
future-listener
机制,用户可以获取操作结果并进一步处理。 - 独立的 Pipeline 和 Handler:每个客户端的连接都会有独立的
Pipeline
和Handler
,从而避免了不同连接之间的干扰,提升了系统的并发处理能力。