从零手写实现 nginx-12-keepalive HTTP 持久连接或连接复用
前言
大家好,我是老马。很高兴遇到你。
我们为 java 开发者实现了 java 版本的 nginx
如果你想知道 servlet 如何处理的,可以参考我的另一个项目:
手写从零实现简易版 tomcat minicat
手写 nginx 系列
如果你对 nginx 原理感兴趣,可以阅读:
从零手写实现 nginx-01-为什么不能有 java 版本的 nginx?
从零手写实现 nginx-03-nginx 基于 Netty 实现
从零手写实现 nginx-04-基于 netty http 出入参优化处理
从零手写实现 nginx-05-MIME类型(Multipurpose Internet Mail Extensions,多用途互联网邮件扩展类型)
从零手写实现 nginx-12-keep-alive 连接复用
从零手写实现 nginx-13-nginx.conf 配置文件介绍
从零手写实现 nginx-14-nginx.conf 和 hocon 格式有关系吗?
从零手写实现 nginx-15-nginx.conf 如何通过 java 解析处理?
从零手写实现 nginx-16-nginx 支持配置多个 server
从零手写实现 nginx-18-nginx 请求头+响应头操作
从零手写实现 nginx-20-nginx 占位符 placeholder
从零手写实现 nginx-21-nginx modules 模块信息概览
从零手写实现 nginx-22-nginx modules 分模块加载优化
从零手写实现 nginx-23-nginx cookie 的操作处理
从零手写实现 nginx-26-nginx rewrite 指令
从零手写实现 nginx-27-nginx return 指令
从零手写实现 nginx-28-nginx error_pages 指令
从零手写实现 nginx-29-nginx try_files 指令
从零手写实现 nginx-30-nginx proxy_pass upstream 指令
从零手写实现 nginx-31-nginx load-balance 负载均衡
从零手写实现 nginx-32-nginx load-balance 算法 java 实现
从零手写实现 nginx-33-nginx http proxy_pass 测试验证
从零手写实现 nginx-34-proxy_pass 配置加载处理
从零手写实现 nginx-35-proxy_pass netty 如何实现?
3次握手+4次挥手
3次握手
TCP三次握手过程:
客户端 服务器
(SYN=1, seq=x) ---->
(SYN=1, ACK=1, seq=y, ack=x+1)
(ACK=1, seq=v, ack=u+1)
() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
// 设置读写超时
pipeline.addLast(new ReadTimeoutHandler(30, TimeUnit.SECONDS));
pipeline.addLast(new WriteTimeoutHandler(30, TimeUnit.SECONDS));
// 设置空闲检测
pipeline.addLast(new IdleStateHandler(60, 30, 0, TimeUnit.SECONDS));
pipeline.addLast(new HttpServerHandler());
}
});
在Netty中,ReadTimeoutHandler
、WriteTimeoutHandler
和IdleStateHandler
是用于处理超时和空闲检测的处理器(Handler),它们可以帮助开发者管理连接的生命周期,确保资源的有效利用并防止资源泄漏。
下面是这三个类的详细介绍:
ReadTimeoutHandler
ReadTimeoutHandler
用于设置读超时。当连接上的读取操作在指定的时间内没有数据到达时,会触发一个超时事件。
这通常用于检测和处理半开连接(即一方已经关闭连接,而另一方仍然认为连接是打开的)。
- 触发事件:
- 当指定的时间内没有读取到任何数据时,会触发一个
ReadTimeoutException
。
- 当指定的时间内没有读取到任何数据时,会触发一个
- 触发事件:
WriteTimeoutHandler
WriteTimeoutHandler
用于设置写超时。当连接上的写操作在指定的时间内没有完成时,会触发一个超时事件。这通常用于确保数据能够在合理的时间内被发送出去。
- 触发事件:
- 当指定的时间内写操作没有完成时,会触发一个
WriteTimeoutException
。
- 当指定的时间内写操作没有完成时,会触发一个
- 触发事件:
IdleStateHandler
IdleStateHandler
用于检测连接的空闲状态。它可以设置读空闲、写空闲和所有空闲(既没有读也没有写)的超时时间。当连接在指定的时间内没有任何读或写活动时,可以触发相应的事件。
- 参数:
readerIdleTime:读空闲超时时间,单位为秒。如果设置为0,则表示不检测读空闲。
writerIdleTime:写空闲超时时间,单位为秒。如果设置为0,则表示不检测写空闲。
allIdleTime:所有空闲(既没有读也没有写)的超时时间,单位为秒。如果设置为0,则表示不检测所有空闲。
unit:时间单位,例如TimeUnit.SECONDS。
触发事件:
当连接在指定的时间内没有读活动时,会触发
IdleStateEvent.READER_IDLE
事件。当连接在指定的时间内没有写活动时,会触发
IdleStateEvent.WRITER_IDLE
事件。当连接在指定的时间内既没有读也没有写活动时,会触发
IdleStateEvent.ALL_IDLE
事件。
在Netty的ChannelPipeline
中添加这些处理器,可以使得你的网络应用更加健壮和可靠。
通过设置合适的超时和空闲检测,可以有效地管理连接的生命周期,防止资源浪费,并提高应用的稳定性和性能。
小结
本节我们实现了文件的压缩处理,这个对于文件的传输性能提升比较大。
当然,压缩+解压本身也是对性能有损耗的。要结合具体的压缩比等考虑。
下一节,我们考虑实现一下 cors 的支持。
我是老马,期待与你的下次重逢。
开源地址
为了便于大家学习,已经将 nginx 开源
chat
netty 实现核心思路
1)netty 如何实现 http 请求处理的 keep-alive
Netty 是一个基于 Java 的异步事件驱动的网络应用框架,用于快速开发可扩展的高性能协议服务器和客户端。要实现 HTTP 请求处理中的 Keep-Alive,Netty 提供了一些内置的功能和处理器。以下是实现 HTTP Keep-Alive 的步骤:
引入必要的依赖:确保你的项目中包含 Netty 的相关依赖,例如
netty-all
或者根据需要引入特定模块的依赖。编写处理器类:实现一个简单的
HttpServerHandler
来处理 HTTP 请求。配置管道:在 Netty 的
ChannelInitializer
中配置管道(pipeline),添加 HTTP 服务器编解码器和自定义处理器。处理 Keep-Alive 逻辑:在自定义处理器中根据 HTTP 请求头中的
Connection
字段来决定是否保持连接。
以下是一个示例代码,演示如何在 Netty 中实现 HTTP 请求处理的 Keep-Alive:
示例代码
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
public class HttpServer {
private final int port;
public HttpServer(int port) {
this.port = port;
}
public void start() throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
pipeline.addLast(new HttpServerHandler());
}
});
ChannelFuture future = bootstrap.bind(port).sync();
System.out.println("HTTP server started on port " + port);
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
int port = 8080;
new HttpServer(port).start();
}
}
class HttpServerHandler extends SimpleChannelInboundHandler {
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
boolean keepAlive = HttpUtil.isKeepAlive(request);
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
response.content().writeBytes("Hello, World!".getBytes());
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
if (keepAlive) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
ctx.writeAndFlush(response);
} else {
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
代码说明
HttpServer 类:
- 创建了一个 Netty 服务器,监听指定的端口。
- 配置了
HttpServerCodec
和HttpObjectAggregator
处理器,这些处理器用于处理 HTTP 编码和聚合 HTTP 消息。 - 添加了自定义的
HttpServerHandler
来处理 HTTP 请求。
HttpServerHandler 类:
- 继承自
SimpleChannelInboundHandler
,用于处理完整的 HTTP 请求。 - 在
channelRead0
方法中检查请求的Connection
头是否为keep-alive
。 - 如果是 Keep-Alive 请求,则设置响应的
Connection
头为keep-alive
并发送响应而不关闭连接。 - 否则,发送响应并关闭连接。
- 继承自
通过这种方式,你可以实现 Netty 中的 HTTP Keep-Alive 功能,从而提高 HTTP 请求的处理效率。
2) netty 如何正确关闭 keep-alive 对应的链接?
在 Netty 中正确关闭 Keep-Alive 对应的连接涉及到几个步骤。通常情况下,HTTP 服务器会根据客户端的请求头和服务器的响应头来决定是否保持连接以及何时关闭连接。如果要正确关闭一个 Keep-Alive 的连接,可以采取以下步骤:
检查请求头中的
Connection
字段:客户端通过请求头中的Connection
字段告知服务器是否希望保持连接。如果字段值为close
,服务器应关闭连接。设置响应头中的
Connection
字段:如果服务器决定关闭连接,应在响应头中设置Connection
字段为close
。调用关闭方法:在发送完响应后,调用 Netty 提供的方法来关闭连接。
以下是一个示例代码,演示如何在 Netty 中正确关闭 Keep-Alive 的连接:
示例代码
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
public class HttpServer {
private final int port;
public HttpServer(int port) {
this.port = port;
}
public void start() throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
pipeline.addLast(new HttpServerHandler());
}
});
ChannelFuture future = bootstrap.bind(port).sync();
System.out.println("HTTP server started on port " + port);
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
int port = 8080;
new HttpServer(port).start();
}
}
class HttpServerHandler extends SimpleChannelInboundHandler {
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
boolean keepAlive = HttpUtil.isKeepAlive(request);
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
response.content().writeBytes("Hello, World!".getBytes());
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
if (keepAlive) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
} else {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
}
ChannelFuture future = ctx.writeAndFlush(response);
// Close the connection if the request was not keep-alive
if (!keepAlive) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
代码说明
- HttpServerHandler 类:
- 在
channelRead0
方法中,通过HttpUtil.isKeepAlive(request)
检查请求头是否要求保持连接。 - 构建响应时,根据
keepAlive
变量设置响应头Connection
为keep-alive
或close
。 - 发送响应后,如果
keepAlive
为false
,则添加ChannelFutureListener.CLOSE
监听器,确保在响应发送完毕后关闭连接。
- 在
通过这种方式,Netty 可以正确处理 HTTP Keep-Alive 连接,并在需要时关闭连接。
这不仅符合 HTTP/1.1 协议规范,也能确保服务器资源得到合理使用。
3)netty 如果正确处理一些可能没有正确关闭的链接?比如设置超时等
在 Netty 中,为了正确处理可能没有正确关闭的连接,可以设置连接的超时时间。这样,当连接闲置一定时间后,会自动关闭,以防止资源泄露和无效连接占用系统资源。可以通过以下几个步骤实现:
- 设置读取和写入超时:可以使用 Netty 提供的
ReadTimeoutHandler
和WriteTimeoutHandler
。 - 设置空闲超时:可以使用
IdleStateHandler
来检测连接是否在指定时间内没有读写操作,从而关闭空闲连接。
示例代码
以下代码演示了如何在 Netty 中设置连接超时和处理空闲连接:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import java.util.concurrent.TimeUnit;
public class HttpServer {
private final int port;
public HttpServer(int port) {
this.port = port;
}
public void start() throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
// 设置读写超时
pipeline.addLast(new ReadTimeoutHandler(30, TimeUnit.SECONDS));
pipeline.addLast(new WriteTimeoutHandler(30, TimeUnit.SECONDS));
// 设置空闲检测
pipeline.addLast(new IdleStateHandler(60, 30, 0, TimeUnit.SECONDS));
pipeline.addLast(new HttpServerHandler());
}
});
ChannelFuture future = bootstrap.bind(port).sync();
System.out.println("HTTP server started on port " + port);
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
int port = 8080;
new HttpServer(port).start();
}
}
class HttpServerHandler extends SimpleChannelInboundHandler {
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
boolean keepAlive = HttpUtil.isKeepAlive(request);
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
response.content().writeBytes("Hello, World!".getBytes());
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
if (keepAlive) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
} else {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
}
ChannelFuture future = ctx.writeAndFlush(response);
if (!keepAlive) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateHandler) {
// 处理空闲连接
ctx.close();
} else {
super.userEventTriggered(ctx, evt);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
代码说明
ReadTimeoutHandler 和 WriteTimeoutHandler:在
ChannelPipeline
中添加了ReadTimeoutHandler
和WriteTimeoutHandler
,分别设置了读取和写入的超时时间为 30 秒。如果在指定时间内没有读取或写入操作,连接将被关闭。IdleStateHandler:在
ChannelPipeline
中添加了IdleStateHandler
,该处理器会监测读和写的空闲时间。在这个例子中,读空闲时间设置为 60 秒,写空闲时间设置为 30 秒。当连接在指定时间内没有读或写操作时,会触发userEventTriggered
方法。HttpServerHandler:
- 在
userEventTriggered
方法中处理空闲事件。如果IdleStateHandler
触发了空闲事件,则关闭连接。 - 在
channelRead0
方法中,处理 HTTP 请求和响应。如果请求不需要保持连接,则在响应发送后关闭连接。
- 在
通过这种方式,可以确保在 Netty 中正确处理和关闭空闲连接,从而避免资源泄露和无效连接占用系统资源。
小结
keep-alive 利用链接复用的特性,大大缩短请求链接的创建时间,提升请求性能。
不过需要注意使用的场景。
下一节,我们考虑实现拓展一些压缩算法, 引入 zlib。
我是老马,期待与你的下次重逢。
开源地址
为了便于大家学习,已经将 nginx 开源