0. 网络传输阶段

比如说主机A是家里windows的一台笔记本电脑,主机B是linux服务器上的一个nginx,其监听80或443等web端口。

在笔记本的浏览器发送了一个http get请求,其数据流是怎样的?

物理网络层面


有三块网络。一个是用户所在的客户端的网络(比如是基于wifi的),中间是一个广域网(简单理解为是一个骨干网,可能是通过光纤、海底电缆等),第三个网络则是企业的IDC内部的一个网络。

首先是在数据链路层,通过Client Host上配置的默认网关,找到10.2.14.1这个路由器,这是link#1,由这台路由器开始进入骨干网,在这个网络中可以看到有许多条路径,会选择一条最短的路径,通过link3,4,5,到达了internet server所在的企业的内网,通过路由器找到这台server。

请求及响应的过程:

客户端的浏览器电脑上发起了一个get请求,这是一个http request,通过蓝色的线,先由数据链路层将其发送到了路由器上,根据ip层选定最短的一个路径,而tcp层则是把我们这样一个不定长度的tcp请求切分成tcp认为合适的segment段。开始发送到目的的server上。在中间的任意一个节点中,这个报文都有可能被丢掉,而且路径也有可能发生变换。tcp必须保证每一个segment都能到达server。通常tcp层都是由操作系统的内核实现的,server接收到http request之后,操作系统的kernel按照相同的顺序把http request丢给我们上面的tomat,tomcat处理之后生成了一个web页面,再通过相同的路径发送给我们的客户端。

在上面这个过程中,如何选路是由ip层决定的,根据IP地址穿越网络传送数据;如何构造请求和响应是由应用层实现的。而如何可靠的发送,如何保证顺序,是tcp层决定的。

在应用层构造好一个http get消息,在linux kernel中就会将http消息拆分成很多的tcp segment,然后由网络层添加20个字节的ip头部,然后到数据链路层加上相应的头部后进行传输。经过路由器,路由器会一层一层将数据链路层和ip层的头部剥离,其知道要发给谁之后,知道了对方的网络特性,又会添加新的ip头部和数据链路层头部,然后发送给交换机,当到了目标主机之后,也是一层一层剥离,最后拿到原封不动的消息。

网络分层层面

  1. 浏览器从我们的url中首先解析出域名,然后根据域名去查询dns,获取到域名对应的ip地址。

  2. 寻址过程
    现在我们访问www.freesoft.org,其ip地址是208.130.29.33

    其寻址历程是:

    首先是80年代末,北美的IRIN将208.128.0.0/11这样一个200多万的ip地址的地址段分配给了MCI

    所以我们先寻址到了MCI

    MCI将 208.130.28.0/22 分配给ARS这家公司

    ARS这家公司 将 208.130.29.0/22 分配给其一个公共服务器集群Public Servers 使用

    到这个公共服务器集群中,找到了208.130.29.33这个ip地址

  3. 这个http get请求到了传输层,浏览器会打开一个端口,从windows的任务管理器中可以看到这一点,然后会把这个端口记录到tcp报文中,然后把nginx的端口比如80或者443也会在tcp报文中记录下来。

  4. 在网络层会记录下我们主机的ip,和nginx服务器的主机的公网ip

  5. 在链路层通过以太网到达家里的路由器,家里的路由器会记录下我们的运营商的下一段的ip,经过广域网后,最终跳转到主机B所在的机器上,经过链路层、网络层、传输层,

  6. 在传输层上,操作系统就知道是给那个打开了80或443端口的进程nginx

  7. nginx会在其http的状态机中处理这个请求

我们发送的这个http请求,会切割成一个一个的小报文,在网络层会切割为MTU,每个MTU是1500个字节,在tcp层,会考虑整个传输中最大的MTU值,此时每个报文可能只有几百个字节,这个报文大小称为MSS。

所以,每收到一个MSS这么大的一个报文时,就是一个网络事件。

数据传输层面


比如说有一个应用程序client,向另一个应用程序server发送了22个字节。前三个字节1 2 3发送到了server。4 5 6 7目前正在server的操作系统的tcp栈中。8 9 还在网络中传输。10 11 12 13可能刚刚从ip层下去,正在数据链路层中进行处理。而14 15 16 17 18还在client的操作系统的tcp栈中。而19 20 21 22还在应用层中。我们的应用程序是直接发送这22个字节的。也就是我们实际上是一个流,但是拆分成很多个segment段。

拆分的依据是什么呢?

  1. 如果tcp层不分段,那么ip一定会分段,我们要避免ip层分段,因为ip层基于MTU的分段没有效率

    默认 MSS:536 字节(默认 MTU576 字节,20 字节 IP 头部,20 字节 TCP 头部)

  2. 流控,比如说接收端现在只能接收3个字节了,server很繁忙,需要流控

1. 三次握手建立连接阶段


握手的目标

  1. 同步sequence number

    client和server端的ISN是不同的,所以需要三次握手来交换ISN (initial sequence number),同步双方的序列号sequence number

  2. 交换tcp通讯参数

    如MSS、窗口比例因子等

三次握手就是确定了全双工是可以的

sequence number和acknowledge number用来解决应用层字节流的可靠发送

  • sequence number唯一表示一个tcp报文:分为relative相对的 和 绝对的
  • ack number用于确认报文,保证顺序的可达性

序列号的值针对的是字节而不是报文

sequence number存在一个复用的问题,也就是2的32次方(4个字节=32bit) 也就是4G多的数据后,这个连接如果继续使用,就要复用原来的ISN了(采用timestamp防止序列号回绕造成问题)。

ISN是不可预测的,如果C可以预测A和B的ISN,通过C reset A和B之间的connection,或者可以向A和B的流中插入数据

If C is in the same network as of A & B & if it can sniff packets, it can easily see the ISN by just sniffing the packets. Random sequence number does not prevent this. This prevents attack if you are on a different network than A & B.

操作系统内核阶段

三次握手内核中的处理

linux内核作为服务器端收到了syn,会插入到syn队列(半连接队列)中,同时发送syn+ack,处于半连接状态。

当收到ack,就会放入到accept队列中,通知nginx,这里有一个读请求事件,nginx就会从epoll.wait()中唤醒,执行accept方法,从accept队列中取出

比如,我在家里的windows发送syn包,nginx所在的linux操作系统回复syn及ack包,实际上此时nginx是无感知的,连接处于半打开状态(即我家里这一侧establish了,进入到了打开状态)
直到windows发送ack包到nginx所在的linux之上时,当内核收到客户端发送的ack包时,内核根据操作系统的负载均衡算法,选择一个worker进程来处理请求,告诉nginx,我们收到了一个读事件,这个读事件对应的是建立一个新连接,所以nginx此时会调用accept这个方法,去建立一个新的连接。

nginx事件模块

这个worker进程处于epoll_wait方法,被唤醒后拿到刚刚建立好这个连接的句柄,这个读事件对应的是建立一个新连接,所以nginx此时会调用accept这个方法,调用accept这个方法的时候,会去分配连接内存池,connection_pool_size,默认值是512,意思是nginx会为这个新的连接分配512个字节的内存池。
分配好内存池,建立好连接之后,进入Http模块的处理。

Http模块

处理连接

http模块在启动的时候,就会定义好一个方法ngx_http_init_connection这样的一个回调方法,当建立一个新连接的时候,这个方法就会被回调,这个时候要把新建连接这个读事件通过epoll_ctl(epoll controll)函数添加到epoll中,然后还要加一个定时器client_header_timeout:60s,表示如果60秒内我没有收到请求,就超时。

这个流程做好了之后,可能nginx模块就切换到其他的fd去处理了。

处理请求

之后看下面的DATA部分
当一个http get请求发送过来了,发送Data过来,在操作系统内核会回复一个ACK。

然后内核告诉nginx,我们收到了一个读事件,worker进程会通过epoll_wait方法,拿到这个连接的句柄,这个请求的回调方法是ngx_http_wait_request_handler方法(此方法也是在前面设置好的),于是从事件模块进入到HTTP模块,在这个方法中,需要把内核中的DATA读到nginx的用户态中,要读到用户态中是要分配内存的,这一段内存要分配多大呢?我是从哪里分配出来的呢?是从连接内存池中分的,client_header_buffer_size:1k,相当于在原有512字节的连接内存池中扩展了1K。

处理连接与处理请求是不同的,处理连接只要将其收到我的nginx内存中,就可以了。

2. nginx event loop及epoll操作

epoll操作

epoll有三个API,epoll_create,epoll_ctl,epoll_wait

epoll是在linux 2.5.44中出现的,epoll的功能是在进程内同时刻找到缓冲区或者连接状态有变化(例如从established进入到close-wait状态)的所有tcp连接,然后返回给我们的进程,这样我们的进程就可以基于非阻塞socket,快速地处理所有这样的连接

看一下上图,其所有的accept都在epoll_ctl控制中,当event发生变化,如send or receive,那么就将其放到epoll_wait的控制中

epoll为何如此高效呢?

比如我们同时处理100w个连接,同一时刻其活跃连接可能只有几千个,epoll在其实现中有两个核心的数据结构,一个是红黑树,红黑树中存放了所有的连接,但是当读写缓冲区发生变化,或者连接状态发生变化,当然变化是由于事件的触发,对于发生变化的这些tcp连接,就放到一个队列中,调用epoll_wait()的时候只返回这个队列中的tcp连接

使用了epoll及非阻塞socket,会导致我们的编程非常的繁琐,目前主要使用协程Coroutine来实现

当我们使用非阻塞socket读取数据的时候,我们不需要等待操作系统内核缓冲区中一定有数据,或者说等一个超时时间,然后返回,而是立刻返回。如果有数据,就马上拷贝数据。
对write写函数也是一样的,如果我们的写缓冲区或者可用的发送窗口为0,那么write方法立即返回,说明这次没有写进去;如果可以写,那么就能写多少就写多少。这些就是非阻塞socket上的read和write

非阻塞socket+epoll+同步编程=协程
协程也是采用“异步回调”来避免阻塞这个思路,并且它的网络通信也是通过epoll来实现非阻塞的,只不过它向开发者提供了“同步阻塞”式的编程API,另外协程的上下文切换开销也比线程小,因为它将“函数调用上下文”保存在应用层面,内核感觉不到,但是这需要额外的内存、调度和管理开销。

nginx event loop


当我们nginx刚刚启动时,实际上nginx处于上图中的 wait for events on connections,也就是说我们打开80或443端口,在等待新的事件进来。比如新的客户端向我们的nginx发起连接事件,这里对应着epoll中的epoll.wait方法,此时nginx是处于sleep进程状态的。

当操作系统收到了一个建立tcp连接的握手报文,并且处理完握手流程以后,操作系统就会通知epoll.wait这个阻塞方法,告诉其可以向下执行了,同时唤醒nginx进程。

epoll.wait向下执行,就到了receive a queue of new events,那么就去找操作系统去要事件,上图的kernel就是操作系统内核,操作系统会把准备好的事件放到事件队列中,从这个事件队列中我们就可以获取到一个一个要处理的事件,比如建立连接,比如收到一个tcp请求报文。取出来之后,就进入处理事件的循环process this events queue in cycle,处理事件的循环见下图

发现内核中的事件队列不为空,就把事件取出来,处理事件。
在处理事件的过程中,可能会生成新事件,比如说nginx发现一个连接新建立了,那么nginx要为这个连接设置一个超时时间,比如说60秒,比如60秒之内,如果浏览器不向我发送请求,nginx就会把这个连接关掉(client_body_timeout)。
又比如说nginx已经可以生成http响应了,而生成响应是需要把响应写入到操作系统的写缓冲区里,要求操作系统尽快将这段响应内容发送到浏览器上,实际上此时我期待一个写事件等等。

实际上在nginx中,还是把这多种情况抽象为读事件和写事件,比如超时时间,设置的是读事件中的定时器。
这个新事件就是nginx event loop图中的下面红框中的队列
当所有的事件都处理完毕后,又会返回到wait for event on connections状态中,以上的循环过程就是nginx event loop

所以nginx是不能容忍其中的第三方模块做一些大量占用cpu操作的行为,因为这样会导致事件得不到处理,实际上nginx中gzip等也是分段来处理的,不会长时间占用cpu

3. 应用服务接收消息的三种情况

情况1:read的时候,报文充分,直接读取

  1. 网卡接收到了报文sequence s1~s2,放入到了内核的receive队列中

  2. 接收到了失序的报文sequence s3~s4,插入out_of_order队列

  3. 接收到了报文sequence s2~s3,放入到内核的receive队列中

  4. 遍历out_of_order队列取出s3~s4,放入receive队列中

  5. nginx调用内核提供的recv方法,接收tcp消息

  6. 内核调用tcp_recvmsg方法,锁住socket,获取最低接收阈值

  7. 处理receive队列中已排序的报文

  8. 8,9,10 将三段报文从内核态复制到用户态进程中分配的buffer

  9. 同上

  10. 同上

  11. tcp_recvmsg方法返回,检查是否接收到超过最低阈值的tcp消息长度

  12. 判断已经拷贝的字节数是否超过最低接收阈值,而且backlog队列为空(全部读取完成了)

  13. recv方法返回已经拷贝的s4~s1字节数

注意其默认接收阈值为1,即只要有报文,就拷贝到用户态进程buffer中

情况2:read的时候没有报文

  1. 现在调用recv方法接收阻塞socket,实际上我们调用的时候,操作系统内核还没有收到消息

  2. 内核调用tcp_recvmsg方法,锁住socket,获取最低接收阈值

  3. 处理receive队列中已排序好的报文

  4. 各队列都为空,于是就让这个进程休眠了,进入sleep状态,用ps可以看到这个状态

  5. 从网卡中接收到一个报文s1~s2,插入prequeue队列

  6. 激活休眠的用户进程,激活这个进程,就置为Runnable,当CPU调度到这个进程,就为Running,发现已拷贝字节数未达到最低阈值,进程休眠,等待超时或receive队列不为空

  7. 等到字节数超过了最低阈值,就会唤醒这个进程

  8. 将报文复制到用户态buffer中

  9. 检查是返回?还是休眠?还是处理其他队列?

  10. 检查是否接收到超过最低阈值的tcp消息长度,如果已经拷贝的字节数超过最低接收阈值,而且backlog队列为空,就返回

  11. recv方法返回已拷贝的字节数

nginx是基于epoll的方式,属于非阻塞方式,应该不会发生情况2?情况2是阻塞的方式

情况3:read时有新报文到达

  1. 网卡生成报文s1~s2并加入receive队列

  2. 调用recv方法接收阻塞socket

  3. 内核调用tcp_recvmsg方法,锁住socket,获取最低接收阈值

  4. 拷贝各队列中收到的报文

  5. 将报文s1~s2复制到用户态

  6. 发现socket正在使用,加入backlog队列,s3~s4的报文

  7. 在进程休眠前将s3~s4报文从backlog队列移动到out_of_order队列中

  8. 已拷贝字节未达到最低阈值,进程休眠

  9. s2~s3报文到达并处理

  10. 用户进程正休眠等待,顺序正确的报文,直接复制到用户态buffer中

  11. 复制out_of_order队列的s3~s4报文

  12. 进程唤醒

  13. 返回,检查是否接收到超过最低阈值的tcp消息长度

  14. 检查是否接收到超过最低阈值的tcp消息长度,如果已经拷贝的字节数超过最低接收阈值,而且backlog队列为空,就返回

  15. recv方法返回已拷贝的字节数

在上面的三种情况中可以看出,nginx应该会出现情况1和情况3,nginx接收和发送报文都是与内核中的缓冲区有关,而内核中的缓冲区是与滑动窗口有关的

4. 请求的处理过程

  1. 处理请求的时候,可能需要做大量的上下文分析,去分析其http协议,分析每一个header,所以此时要分配一个请求内存池。request_pool_size:4k,基本上是connection_pool_size的8倍,为什么要这么大呢?因为请求的上下文涉及到业务,通常4k是比较合适的值。如果分配的过小,那么请求内存池要不断扩充,性能是会下降的。

  2. 使用状态机解析请求行。包括method url http1.1/r/n

  3. 解析请求行的过程中,可能发现有的url特别大,已经超过了1k的内存了,此时就会分配大内存large_client_header_buffers:4 8k(这里要注意是否有过大的请求头部)

    先分配8k,然后把前面的1k拷贝到8k中,使用7k再接收,最多32k

  4. 状态机再次解析请求行,直至解析完成

  5. 就可以标识URL了

    nginx有很多变量,这些变量并不是真的会复制一份数据,而是一个指针。这里就是使用指针来标识URL

  6. 状态机解析header

    header的内容很多,比如cookie、host等等字段,header是很可能超过1k的

  7. 分配大内存,复用3中的最大32k的内存

  8. 标识header

    除了指针操作外,还包括确定哪个server块来处理这个请求(根据host header)

  9. 当接收到全部的header之后,就要移除超时定时器了client_header_timeout:60s

  10. 接下来开始nginx中的11个阶段的http请求处理

5. nginx处理请求的11个阶段


当请求进入到nginx中

  • Read Request Headers,来决定哪一个server块来处理这个请求

  • Identify Configuration Block,寻找哪个location是生效的

  • Apply Rate Limits,来决定是否要对其进行限速

  • Perform Authentication,来做一些验证,比如说根据referer等字段,判断是否盗链,或者用auth basic这样的协议验证用户的权限(http_auth_basic_module)

  • Generate Content,返回给用户的响应,为了生成这个响应,需要与上游服务进行交互

  • Upstream Services,得到Upstream Services的返回值

  • 中间的internal redirects and subrequests,在以上这个过程中,可能会产生一些子请求或者重定向,会重复走一遍这样的请求

  • Response Filters,在向用户返回请求的时候,需要经过过滤模块,比如说使用gzip对response进行压缩

  • Log Access and session log,最后记录日志

11个阶段的顺序处理

6. content阶段


11个阶段中的content阶段,也就是处理请求体,生成发给用户的响应。反向代理要在content阶段生效。

最后结束就是在关闭或复用连接部分(图中右下角)。

  1. 查看cache是否命中

    根据key判断,如果cache命中,就不需要向上游发送请求了,直接发送响应就可以了。

  2. 如果没有命中,先生成发往上游的http头部及包体,而不是与上游服务器建立连接。这是为什么呢?因为与上游服务器建立了连接,我们就对上游的tomcat这些并发能力并不是很强的服务造成了影响。

    在这一步有很多proxy提供的指令,让我们来控制header和body的内容。因为content阶段我们只是收到了客户端的header,如果客户端是POST,那么很可能还有body,这个body我们还没有收,这个时候我们需要一个选择,proxy_request_buffering on,当打开buffering,那么就是说我们会先读取请求的完整包体。

    如果我们proxy_request_buffering off,那么其会采用边读包体,边转发的方式,会有一个问题,客户端是公网,网速很慢,nginx并发能力强,很快,tomcat并发能力差,就是如果这个body很长,客户端的网速很慢,比如10k/s,就会导致nginx与tomcat之间建立的这个连接的时间很长,而tomcat的并发连接数是非常有限的,所以我们通常要求nginx先把用户发来的body全部缓存到磁盘上。

    nginx做反向代理的时候有一个特点,其会考虑到上游服务的处理能力相对是不足的,所以如果是一个有body的http请求,nginx会先把body接收完,再去向上游服务器发起连接(proxy_request_buffering)。

    当我们收到header之后,我们就知道需要向上游哪台服务器发起反向代理来处理连接了,但是其会先读取body,当执行完read_reqeust_body之后,再去回调post_handler这个方法,ngx_http_upstream_init是我们与上游服务器建立连接的方法。

  3. 接下来根据负载均衡策略来选择服务器,根据参数连接上游服务器,发送请求

  4. 发送请求完成,tomcat就会发送其响应给nginx,nginx需要先接收响应头部

    处理响应头部,有很多的指令控制它

  5. nginx处理完成响应头部,通过proxy_buffering 控制的一个分支,表示我们应该怎样看待上下游之间的网速。

    proxy_buffering on,就会接收完整的响应包体,再向下游发放。也就是我们会用临时文件存储上游发送的body,这样做有一个好处:我们上游往往与nginx处于内网中,内网的网速往往是很快的,而客户端处于公网,网速往往是很慢的,如果采用边读包体边发的方式,可能会发送很长时间,仍然会导致nginx与tomcat之间建立的这个连接的时间很长,影响tomcat的并发能力。

  6. 之后是发送响应头部,以及发送响应包体

  7. 判断是否打开了cache,如果这个响应应该被cache,那么就cache

7. nginx发送消息


注意:nginx发送tcp消息的时候,nginx是没有办法知道这个消息是否发出去了,其只是把这些消息发送到了linux内核中,至于linux内核什么时候能够把这些消息发送出去,nginx是不知道的。

看一下这个流程,这个流程搞清楚了,我们就知道如何配置缓存的大小。

  1. 比如现在是nginx的content阶段,生成了一个200的响应,所有待发送的内容都在待发送字符流里面。

  2. 调用send方法去发送,这个send方法是内核提供的

  3. 内核调用了tcp_sendmsg方法

  4. 调用完这个方法之后,需要把用户态的字符流复制到内核态

  5. 继续复制到内核态

  6. 假如说我们内核区的缓存不足了,内核区为什么会缓存不足呢?可以看到内核区的发送缓存长度由/proc/sys/net/core/wmem/default来控制,缓存不足了,就会导致这个send方法被阻塞(当然我们如果使用非阻塞的方法就没有这个问题了)。

  7. 缓存不足了,我们就等待,等到tcp发送队列中有报文发送出去了,有空闲缓存了/超时

  8. 我们继续复制剩下的到内核态,循环将待发送字符流分成MSS(max segment size)分组拷贝到内核态

  9. 执行方法tcp_push,按Nagle、慢启动等算法发送报文,发送的时候会从tcp发送队列中取出字符流,按照MSS分组发送

    Nagle算法:没有已发送未确认报文段时,立刻发送数据
    存在未确认报文段时,直到:1-没有已发送未确认报文段,或者 2-数据长度达到 MSS 时再发送

  10. 第2步调用的tcp_sendmsg方法返回了

  11. 第1步调用的send方法也返回了,表示全部发送或部分发送

8. 给windows浏览器发送响应

当服务器收到这个请求之后,完成资源的表述,把html页面作为包体返回给浏览器。

浏览器在渲染引擎中解析这个html响应,根据这个响应中的超链接内容,可能是css,可能是javascript,也可能是图片,根据这些超链接,就会发起新的网络请求。比如一个javascript请求,那么就再通过网络模块发起javascript的网络请求。javascript网络请求回来之后,再调用JS解释器,来解析这个js文件。

9. 四次挥手关闭连接

  1. Client发送FIN+ACK包后,进入FIN-WAIT-1状态

  2. 当Server端收到Client端的FIN包后,会进入CLOSE-WAIT状态,在操作系统kernel中会自动回复一个FIN ACK包。

  3. Client接收到FIN-ACK后,进入FIN-WAIT-2状态

    注意FIN包和FIN-ACK包,都符合之前所说的重发确认

  4. 当server进入到CLOSE-WAIT状态,server上的应用程序没有处理这样一个状态,那么我们这个连接的状态将永远保持在CLOSE-WAIT状态。当我们有些web应用发生bug时,使用netstat就很容易观察到CLOSE-WAIT状态。当server上的应用程序通过read()函数接收到0的时候,就应该关闭这个连接了,当其调用close这个socket之后,就会发送一个FIN包给client,之后Server连接状态进入到LAST-ACK状态。

    进入到LAST-ACK状态后,其会以一定间隔时间持续向client发送FIN命令,这个间隔时间一般是一个数据片所送达的最长时间,也就是MSL(Maximum Segment Lifetime:任何报文段被丢弃前在网络内的最长时间),默认是2分钟,在linux系统中将这个值改为了30秒,认为30秒已经是网络上一个非常可靠的时间。如果30秒client没有收到server发送的FIN消息,那么认为这条FIN消息可能丢失了。

  5. 当这个FIN包到达Client之后,Client就进入到TIME-WAIT状态,其会回送一个ACK。

    这条ACK也可能是会丢失的,如果丢失了,那么client就会继续收到server发送的FIN消息,client也就知道了之前自己发送的ACK丢失了,其就会重新发送ACK消息。

  6. server端接收到ACK之后,就会进入CLOSE状态

  7. client端发送ACK完成后,要等两个MSL(maximum segment life)的时间,也就是两倍的我们的tcp报文端在网络中存活的最大时间之后,也会进入到CLOSE状态

为什么是2MSL呢?

参考:为什么TCP4次挥手时等待为2MSL?

client并不知道server是否收到自己的ACK,client是这样想的:

  1. 如果server没有收到自己的ACK,那么server会超时(1个MSL)重传FIN,那么就再发送ACK
  2. 如果server收到了自己的ACK,就不会发送任何消息了

无论情况1还是情况2,A都要等待,最坏的情况就是:去向Server的ACK消息的最大存活时间(1 MSL)+ Server去向Client的FIN消息的最大存活时间(1 MSL)

特殊情况1:如果server 一直收不到client的最后一个ACK,是不是server会一直处于last ack?

答:server重传FIN的次数有上限,所以超过了上限server就会reset连接

补充

tcp协议中许多事件是怎样和我们日常调用一些linux的系统调用接口,比如accept,read,write,close,关联在一起的?

  1. 请求建立tcp连接事件

    其实是发送了一个tcp的报文,通过上面的发送报文的流程,到达了nginx,所以这实际上是一个读事件,对nginx来说,我读取到了一个报文,所以就是accept接口 对应 建立连接的事件

  2. tcp连接可读事件

    比如我们发送了一个消息,对nginx来说,也是一个读事件,就是read接口 对应 tcp连接可读事件

  3. tcp连接关闭事件

    比如我们浏览器主动关闭了这个连接,相当于windows操作系统发送了一个关闭连接的事件,对于nginx来说,仍旧是一个读事件

  4. 当nginx需要向浏览器发送一个响应的时候,我们需要把消息写到操作系统中,要求操作系统发送到网络中,这就是一个写事件,对应write接口

    网络中的读写事件,通常在nginx中,或者在任何一个异步事件的处理框架中,都有一个事件收集、分发器。我们会定义每类事件处理的消费者,也就是说生产者生产一个事件,通过网络自动生产到我们的nginx中的,我们对每种事件都建立一个消费者

    比如连接建立事件的消费者,就是我们的accept接口的调用,nginx的http模块就会去建立一个新的连接,

    比如读事件的消费者,写事件的消费者,在http状态机中,不同的时间段,我们会调用不同的方法,也就是每一个消费者去处理

    包括像AIO异步读写磁盘的事件,也对应异步读写磁盘的消费者

    还有我们的定时器事件,比如说我们是否超时,像worker shutdown timeout,都是定时器事件,对应定时器事件的消费者

    网络事件 网络事件生产者 网络事件消费者
    建立连接的事件 操作系统 accept接口的调用
    tcp连接可读事件 操作系统 read接口的调用
    关闭连接的事件(仍旧是一个读事件) 操作系统 read接口的调用
    发送响应是一个写事件 write接口的调用 操作系统
    AIO异步读磁盘的事件 操作系统 应用程序
    AIO异步写磁盘的事件 应用程序 操作系统
    定时器事件(如是否超时) 操作系统 应用程序

深入操作系统底层分析nginx网络请求及响应过程相关推荐

  1. Android中使用logger打印完整的okhttp网络请求和响应的所有相关信息(请求行、请求头、请求体、响应行、响应行、响应头、响应体)

    如果你的项目中的网络请求库是Retrofit的话,他的底层封装的是OkHttp,通常调试网络接口时都会将网络请求和响应相关数据通过日志的形式打印出来.OkHttp也提供了一个网络拦截器okhttp-l ...

  2. HTTP请求和响应过程

    HTTP请求和响应过程 1.HTTP协议 Internate的基本协议是TCP/IP(传输控制协议和网际协议).而目前使用的FTP,HTTP都是建立在TCP/IP上的应用层协议.不同的协议对应不同的应 ...

  3. tp5源码分析之网络请求

    1 网络请求 网络请求 对客户端而言,指服务器发起的请求操作. 对服务器端而言,指客户端发起的请求信息. 服务器端主要用来对客户端发起的网络请求进行处理. 2 请求信息 2-1 Url相关 Reque ...

  4. HTTP HTTPS 及网络请求与响应

    HTTP URI的全称为 Uniform Resource Identifier,即统一资源标志符 URL的全称为 Universal Resource Locator,即统一资源定位符 URL是UR ...

  5. HTTP网页从请求到响应过程详解

    引言 本文针对于<计算机网络>课程内容的总结和扩展,主要是讲解浏览器从键入URL网址后,到网页显示,其间的过程. 首先,浏览器的第一步工作就是对 URL 进行解析,然后给向Web 服务器发 ...

  6. Android ANR日志分析指南,android查看网络请求和响应

    既然不是CPU负载的原因,那么到底是什么原因呢? 这时就要看我们的终极大杀器--traces.txt. 二. traces.txt 日志分析 当APP不响应.响应慢了.或者WatchDog的监视没有得 ...

  7. nginx 子请求接收响应_Nginx详解其原理

    Nginx进程模型分析  在介绍Nginx的进程模型之前我们先来给大家解释下一些常见的名词,这能辅助我们更好的了解Nginx的进程模型.作为Web服务器,设计的初衷就是为了能够处理更多的客户端的请求, ...

  8. Ril分析三——客户端请求和响应处理与modem交互

    Ril与modem的交互 一 客户端的请求处理 客户端请求从EventLoop通过dispatch传递到reference-ril.c中调用onRequest接口. 处理客户端请求: static v ...

  9. python爬虫 - python requests网络请求简洁之道

    转自:python爬虫 - python requests网络请求简洁之道 requests简介 requests是一个很实用的Python HTTP客户端库,编写爬虫和测试服务器响应数据时经常会用到 ...

最新文章

  1. Python - 输出格式 (学习小结)
  2. 每日一皮:资深程序员调试代码的样子...
  3. url、base64、blob,三者之间的转化
  4. 开源的pop3和smtp组件(支持中文及SSL)
  5. 小程序开发(3)-之wx.request封装
  6. [算法总结] 13 道题搞定 BAT 面试——字符串
  7. 用JavaScript中的示例进行fill()函数
  8. jvm入门到详解-1
  9. JavaScript学习(七十六)—this的指向问题
  10. C++ 的门门道道 | 技术头条
  11. Cesium 环境配置笔记(使用node.js 或者WampServer服务器)
  12. 码云git上传下载代码
  13. java天津与深圳,国内最可惜的城市:GDP曾是深圳的38倍,如今GDP被反超万亿!
  14. C++析构函数定义和使用
  15. 1G、2G、3G、4G和5G有什么区别?5G的原理是什么?
  16. 什么句型可以 让我说出 悲伤的文法
  17. 2021小结暨2022打脸计划
  18. python中sub的用法_Python Pandas Series.sub()用法及代码示例
  19. kernel下HDMI调试记录
  20. CTF -bugku-misc(持续更新直到全部刷完)

热门文章

  1. 病毒---手动删除Trojan.Miner.gbq病毒
  2. 2021年11月软考网络规划设计师下午真题及答案解析
  3. 计算机毕业设计SSM大学生健康管理系统的设计与实现【附源码数据库】
  4. Flutter实现天气查询App
  5. Python做数据处理(二):贷款风险预测
  6. 13、【易混淆概念集】-第七章 质量成本 项目预算 成本基准 应急储备 VS 管理储备 挣值分析
  7. vue中methods、mounted等使用方法整理
  8. 摘抄各类语言设计模式(仅做笔记)
  9. 应聘经历-百度华为移动
  10. 利用dpabi和restplus提取ROI灰质体积学习笔记