大家好,我是老马。很高兴遇到你。
我们为 java 开发者实现了 java 版本的 nginx
如果你想知道 servlet 如何处理的,可以参考我的另一个项目:
手写从零实现简易版 tomcat minicat
如果你对 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 请求头响应头的处理
TCP三次握手过程:
客户端 服务器
(SYN=1, seq=x) ---->
(SYN=1, ACK=1, seq=y, ack=x+1)
<---- (SYN=0, ACK=1, seq=y+1, ack=x+1)
(SYN=0, ACK=1, seq=x+1, ack=y+2)
----> <----
这个ASCII图示说明了三次握手的每个步骤:
第一步:客户端随机生成一个序列号x
,并将SYN标志位设为1,表示开始建立连接,然后将这个TCP段发送给服务器。
第二步:服务器收到客户端的SYN后,会生成自己的序列号y
,并将SYN和ACK标志位都设为1,确认号ack
设为x+1
,表示已经接收到客户端的SYN,然后将这个TCP段发送回客户端。
第三步:客户端收到服务器的SYN-ACK后,会发送一个ACK确认包,ACK标志位设为1,序列号seq
设为x+1
,确认号ack
设为y+1
,表示已经接收到服务器的SYN-ACK,至此,三次握手完成,TCP连接建立。
请注意,这个图示是简化的,实际的TCP段中还会包含其他信息,如源端口号、目的端口号、窗口大小等。
此外,序列号和确认号在实际中是32位的数字,这里为了简化表示只用了单个字符。
TCP四次挥手过程:
客户端 服务器
(FIN=1, seq=u) ---->
(ACK=1, seq=v, ack=u+1)
<---- (FIN=1, ACK=1, seq=w, ack=u+1)
(ACK=1, seq=u+1, ack=w+1)
----> <----
这个ASCII图示说明了TCP四次挥手的每个步骤:
第一步:客户端决定关闭连接,随机生成一个序列号u
,并将FIN标志位设为1,表示客户端已经完成发送数据,准备关闭连接,然后将这个TCP段发送给服务器。
第二步:服务器收到客户端的FIN后,会发送一个ACK确认包,序列号v
,确认号ack
设为u+1
,表示已经接收到客户端的FIN请求,但此时服务器可能还有数据要发送,所以连接并未立即关闭。
第三步:服务器完成数据发送后,随机生成一个序列号w
,并将FIN标志位设为1,表示服务器也准备关闭连接,然后将这个TCP段发送给客户端。
第四步:客户端收到服务器的FIN后,会发送一个ACK确认包,序列号u+1
,确认号ack
设为w+1
,表示已经接收到服务器的FIN请求,至此,四次挥手完成,TCP连接关闭。
TCP连接的建立需要三次握手,而断开连接需要四次挥手,这是由TCP协议的设计和其确保数据可靠传输的特性决定的。
下面是创建连接和断开连接过程中差异的原因:
同步连接状态:三次握手的目的是确保双方的接收和发送通道都处于工作状态。客户端发送SYN开始连接建立,服务器回复SYN-ACK确认接收到客户端的连接请求,客户端再回复ACK确认接收到服务器的确认。
防止已失效的连接请求突然传输:如果一个连接请求在网络中延迟了,并且在连接释放后到达服务器,服务器可能会错误地认为客户端想要建立新连接。三次握手通过序列号机制避免了这种情况。
交换初始序列号:三次握手允许客户端和服务器交换各自的初始序列号,这是TCP连接中数据传输的基础。
不同时结束:TCP连接是全双工的,这意味着数据可以在两个方向上独立发送。因此,每个方向上的连接结束都需要单独的FIN和ACK。
确保数据传输完成:在客户端或服务器想要关闭连接时,可能还有数据在传输中。四次挥手允许双方在关闭连接之前发送完所有数据,并确认对方已经接收到所有数据。
有序关闭:四次挥手允许一方在另一方准备好关闭连接之前,先发送FIN来关闭其发送方向的连接。接收方可以继续发送数据,直到它也准备好关闭连接。
确认关闭:由于TCP连接的全双工特性,每个方向的关闭都需要单独的确认。当一方收到FIN时,它知道对方已经没有数据要发送了,但它可能还有数据要发送给对方。因此,它发送ACK确认收到FIN,但在发送完自己的数据后才发送自己的FIN。
防止延迟的ACK:如果使用三次挥手关闭连接,一个延迟的ACK可能会被误解为一个新的连接请求,导致混乱。四次挥手通过确保每个方向的关闭都有单独的确认,避免了这个问题。
总结来说,三次握手是为了建立可靠的连接,而四次挥手是为了确保连接的有序、安全和可靠的关闭。
这种设计允许TCP协议在复杂的网络环境中提供稳定和可靠的服务。
好的,让我们用一个接地气的例子来通俗解释TCP的三次握手和四次挥手:
想象一下,你和你的朋友想要通过电话来一场远程的合作项目。
现在项目完成了,你和你的朋友需要结束通话。
通过这个例子,我们可以看到TCP三次握手和四次挥手的过程,其实就像是两个人通过电话进行合作和结束通话的过程,确保了双方都能够清晰、有序地开始和结束他们的通信。
普通的HTTP连接建立过程遵循的是TCP/IP模型中的TCP(传输控制协议)三次握手过程。
以下是详细的步骤:
Connection: keep-alive
。Connection: close
。通过这个过程,客户端和服务器之间建立了一条可靠的通信通道,可以开始进行HTTP数据的交换。
HTTP Keep-Alive 是一种技术,它允许在单个TCP连接上发送多个HTTP请求和响应,而不是为每个请求和响应创建一个新的连接。这项技术可以显著提高Web页面的加载速度,因为它减少了连接建立和关闭的开销。
以下是HTTP Keep-Alive的一些关键点:
减少连接开销:在没有Keep-Alive的情况下,每个HTTP请求都会创建一个新的TCP连接,这包括一个完整的三次握手过程。使用Keep-Alive,多个请求可以复用同一个TCP连接,从而减少了连接建立和关闭的开销。
提高性能:由于减少了连接建立和关闭的次数,Keep-Alive可以提高Web应用程序的性能,尤其是在高流量的环境下。
配置选项:Keep-Alive可以通过HTTP头信息中的Connection
字段来配置。如果发送的请求中包含Connection: keep-alive
,则客户端希望服务器保持连接打开状态,以便发送后续请求。
超时和限制:服务器可以设置Keep-Alive连接的超时时间,以及允许的最大连接数。如果超过这些限制,连接将被关闭。
HTTP/1.1 默认启用:在HTTP/1.1协议中,Keep-Alive是默认启用的。而在HTTP/1.0中,需要显式地在请求头中设置Connection: keep-alive
来启用。
安全性:虽然Keep-Alive提高了性能,但它也可能引入一些安全问题,比如HTTP劫持。因此,在使用Keep-Alive时,还需要考虑使用HTTPS等安全措施。
与HTTP/2的关系:HTTP/2协议进一步改进了连接的复用,通过多路复用技术,允许在单个TCP连接上并行发送多个请求和响应,从而进一步提高了性能。
浏览器和服务器支持:大多数现代浏览器和服务器都支持Keep-Alive。服务器端的配置(如Apache、Nginx等)通常允许管理员根据需要调整Keep-Alive的相关设置。
HTTP Keep-Alive(持久连接)是一种网络协议特性,它允许多个HTTP请求和响应复用同一个TCP连接,从而提高网络传输效率。
以下是HTTP Keep-Alive的一些优缺点:
减少连接建立和关闭的开销:通过复用TCP连接,Keep-Alive减少了频繁建立和关闭连接的需要,从而节省了时间。
提高性能:由于减少了连接建立的时间,页面加载速度会更快,用户体验得到提升。
降低服务器负载:减少了连接的建立和关闭次数,可以减轻服务器处理连接请求的负担。
减少网络延迟:TCP连接的复用减少了每次通信所需的时间,因为不需要等待TCP三次握手。
提高资源利用率:由于TCP连接被多个请求共享,网络资源得到了更有效的利用。
支持流水线技术:在Keep-Alive连接上,可以实施流水线技术,即在等待一个响应的同时发送下一个请求,进一步提高效率。
资源占用:Keep-Alive连接会持续占用服务器的资源,如果有很多空闲连接长时间不被关闭,可能会导致资源浪费。
连接超时管理:需要合理配置Keep-Alive超时时间,如果设置不当,可能会导致连接长时间空闲,浪费服务器资源。
并发连接限制:由于操作系统和硬件的限制,服务器能够同时维持的TCP连接数是有限的。如果Keep-Alive连接过多,可能会影响新连接的建立。
不适合短连接:对于偶尔的、一次性的HTTP请求,使用Keep-Alive可能不会带来太大的性能提升,反而可能增加管理上的复杂性。
可能的内存泄漏:如果应用程序没有正确管理Keep-Alive连接,可能会导致内存泄漏或其他资源管理问题。
兼容性问题:虽然现代浏览器和服务器都支持Keep-Alive,但在一些特殊情况下,可能需要考虑不同实现之间的兼容性问题。
安全考虑:持久连接可能会增加某些类型的安全风险,例如,如果攻击者能够利用一个持久连接来持续发送请求,可能会对服务器造成拒绝服务攻击(DoS)。
总的来说,HTTP Keep-Alive在提高网络通信效率和性能方面具有明显优势,但在实际应用中需要合理配置和管理,以避免潜在的资源浪费和性能问题。
在Netty中实现完整的HTTP Keep-Alive处理,需要考虑几个关键点,包括HTTP协议的版本、连接的配置、以及如何处理连接的生命周期。以下是如何在Netty中实现Keep-Alive的详细步骤:
重点考虑下面几个问题:
boolean keepAlive = HttpUtil.isKeepAlive(request);
if (keepAlive) {
// 如果是 keep-alive
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
ctx.writeAndFlush(response);
}
boolean keepAlive = HttpUtil.isKeepAlive(request);
if (keepAlive) {
// 否则,立刻关闭
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
和常见的处理方法一样,我们可以设置对应的超时时间。
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());
}
});
在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 开源