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头部和数据链路层头部,然后发送给交换机,当到了目标主机之后,也是一层一层剥离,最后拿到原封不动的消息。
网络分层层面
-
浏览器从我们的url中首先解析出域名,然后根据域名去查询dns,获取到域名对应的ip地址。
-
寻址过程
现在我们访问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地址
-
这个http get请求到了传输层,浏览器会打开一个端口,从windows的任务管理器中可以看到这一点,然后会把这个端口记录到tcp报文中,然后把nginx的端口比如80或者443也会在tcp报文中记录下来。
-
在网络层会记录下我们主机的ip,和nginx服务器的主机的公网ip
-
在链路层通过以太网到达家里的路由器,家里的路由器会记录下我们的运营商的下一段的ip,经过广域网后,最终跳转到主机B所在的机器上,经过链路层、网络层、传输层,
-
在传输层上,操作系统就知道是给那个打开了80或443端口的进程nginx
-
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段。
拆分的依据是什么呢?
-
如果tcp层不分段,那么ip一定会分段,我们要避免ip层分段,因为ip层基于MTU的分段没有效率
默认 MSS:536 字节(默认 MTU576 字节,20 字节 IP 头部,20 字节 TCP 头部)
-
流控,比如说接收端现在只能接收3个字节了,server很繁忙,需要流控
1. 三次握手建立连接阶段
握手的目标
-
同步sequence number
client和server端的ISN是不同的,所以需要三次握手来交换ISN (initial sequence number),同步双方的序列号sequence number
-
交换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的时候,报文充分,直接读取
-
网卡接收到了报文sequence s1~s2,放入到了内核的receive队列中
-
接收到了失序的报文sequence s3~s4,插入out_of_order队列
-
接收到了报文sequence s2~s3,放入到内核的receive队列中
-
遍历out_of_order队列取出s3~s4,放入receive队列中
-
nginx调用内核提供的recv方法,接收tcp消息
-
内核调用tcp_recvmsg方法,锁住socket,获取最低接收阈值
-
处理receive队列中已排序的报文
-
8,9,10 将三段报文从内核态复制到用户态进程中分配的buffer
-
同上
-
同上
-
tcp_recvmsg方法返回,检查是否接收到超过最低阈值的tcp消息长度
-
判断已经拷贝的字节数是否超过最低接收阈值,而且backlog队列为空(全部读取完成了)
-
recv方法返回已经拷贝的s4~s1字节数
注意其默认接收阈值为1,即只要有报文,就拷贝到用户态进程buffer中
情况2:read的时候没有报文
-
现在调用recv方法接收阻塞socket,实际上我们调用的时候,操作系统内核还没有收到消息
-
内核调用tcp_recvmsg方法,锁住socket,获取最低接收阈值
-
处理receive队列中已排序好的报文
-
各队列都为空,于是就让这个进程休眠了,进入sleep状态,用ps可以看到这个状态
-
从网卡中接收到一个报文s1~s2,插入prequeue队列
-
激活休眠的用户进程,激活这个进程,就置为Runnable,当CPU调度到这个进程,就为Running,发现已拷贝字节数未达到最低阈值,进程休眠,等待超时或receive队列不为空
-
等到字节数超过了最低阈值,就会唤醒这个进程
-
将报文复制到用户态buffer中
-
检查是返回?还是休眠?还是处理其他队列?
-
检查是否接收到超过最低阈值的tcp消息长度,如果已经拷贝的字节数超过最低接收阈值,而且backlog队列为空,就返回
-
recv方法返回已拷贝的字节数
nginx是基于epoll的方式,属于非阻塞方式,应该不会发生情况2?情况2是阻塞的方式
情况3:read时有新报文到达
-
网卡生成报文s1~s2并加入receive队列
-
调用recv方法接收阻塞socket
-
内核调用tcp_recvmsg方法,锁住socket,获取最低接收阈值
-
拷贝各队列中收到的报文
-
将报文s1~s2复制到用户态
-
发现socket正在使用,加入backlog队列,s3~s4的报文
-
在进程休眠前将s3~s4报文从backlog队列移动到out_of_order队列中
-
已拷贝字节未达到最低阈值,进程休眠
-
s2~s3报文到达并处理
-
用户进程正休眠等待,顺序正确的报文,直接复制到用户态buffer中
-
复制out_of_order队列的s3~s4报文
-
进程唤醒
-
返回,检查是否接收到超过最低阈值的tcp消息长度
-
检查是否接收到超过最低阈值的tcp消息长度,如果已经拷贝的字节数超过最低接收阈值,而且backlog队列为空,就返回
-
recv方法返回已拷贝的字节数
在上面的三种情况中可以看出,nginx应该会出现情况1和情况3,nginx接收和发送报文都是与内核中的缓冲区有关,而内核中的缓冲区是与滑动窗口有关的
4. 请求的处理过程
-
处理请求的时候,可能需要做大量的上下文分析,去分析其http协议,分析每一个header,所以此时要分配一个请求内存池。request_pool_size:4k,基本上是connection_pool_size的8倍,为什么要这么大呢?因为请求的上下文涉及到业务,通常4k是比较合适的值。如果分配的过小,那么请求内存池要不断扩充,性能是会下降的。
-
使用状态机解析请求行。包括method url http1.1/r/n
-
解析请求行的过程中,可能发现有的url特别大,已经超过了1k的内存了,此时就会分配大内存large_client_header_buffers:4 8k(这里要注意是否有过大的请求头部)
先分配8k,然后把前面的1k拷贝到8k中,使用7k再接收,最多32k
-
状态机再次解析请求行,直至解析完成
-
就可以标识URL了
nginx有很多变量,这些变量并不是真的会复制一份数据,而是一个指针。这里就是使用指针来标识URL
-
状态机解析header
header的内容很多,比如cookie、host等等字段,header是很可能超过1k的
-
分配大内存,复用3中的最大32k的内存
-
标识header
除了指针操作外,还包括确定哪个server块来处理这个请求(根据host header)
-
当接收到全部的header之后,就要移除超时定时器了client_header_timeout:60s
-
接下来开始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阶段生效。
最后结束就是在关闭或复用连接部分(图中右下角)。
-
查看cache是否命中
根据key判断,如果cache命中,就不需要向上游发送请求了,直接发送响应就可以了。
-
如果没有命中,先生成发往上游的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是我们与上游服务器建立连接的方法。
-
接下来根据负载均衡策略来选择服务器,根据参数连接上游服务器,发送请求
-
发送请求完成,tomcat就会发送其响应给nginx,nginx需要先接收响应头部
处理响应头部,有很多的指令控制它
-
nginx处理完成响应头部,通过proxy_buffering 控制的一个分支,表示我们应该怎样看待上下游之间的网速。
proxy_buffering on,就会接收完整的响应包体,再向下游发放。也就是我们会用临时文件存储上游发送的body,这样做有一个好处:我们上游往往与nginx处于内网中,内网的网速往往是很快的,而客户端处于公网,网速往往是很慢的,如果采用边读包体边发的方式,可能会发送很长时间,仍然会导致nginx与tomcat之间建立的这个连接的时间很长,影响tomcat的并发能力。
-
之后是发送响应头部,以及发送响应包体
-
判断是否打开了cache,如果这个响应应该被cache,那么就cache
7. nginx发送消息
注意:nginx发送tcp消息的时候,nginx是没有办法知道这个消息是否发出去了,其只是把这些消息发送到了linux内核中,至于linux内核什么时候能够把这些消息发送出去,nginx是不知道的。
看一下这个流程,这个流程搞清楚了,我们就知道如何配置缓存的大小。
-
比如现在是nginx的content阶段,生成了一个200的响应,所有待发送的内容都在待发送字符流里面。
-
调用send方法去发送,这个send方法是内核提供的
-
内核调用了tcp_sendmsg方法
-
调用完这个方法之后,需要把用户态的字符流复制到内核态
-
继续复制到内核态
-
假如说我们内核区的缓存不足了,内核区为什么会缓存不足呢?可以看到内核区的发送缓存长度由/proc/sys/net/core/wmem/default来控制,缓存不足了,就会导致这个send方法被阻塞(当然我们如果使用非阻塞的方法就没有这个问题了)。
-
缓存不足了,我们就等待,等到tcp发送队列中有报文发送出去了,有空闲缓存了/超时
-
我们继续复制剩下的到内核态,循环将待发送字符流分成MSS(max segment size)分组拷贝到内核态
-
执行方法tcp_push,按Nagle、慢启动等算法发送报文,发送的时候会从tcp发送队列中取出字符流,按照MSS分组发送
Nagle算法:没有已发送未确认报文段时,立刻发送数据
存在未确认报文段时,直到:1-没有已发送未确认报文段,或者 2-数据长度达到 MSS 时再发送 -
第2步调用的tcp_sendmsg方法返回了
-
第1步调用的send方法也返回了,表示全部发送或部分发送
8. 给windows浏览器发送响应
当服务器收到这个请求之后,完成资源的表述,把html页面作为包体返回给浏览器。
浏览器在渲染引擎中解析这个html响应,根据这个响应中的超链接内容,可能是css,可能是javascript,也可能是图片,根据这些超链接,就会发起新的网络请求。比如一个javascript请求,那么就再通过网络模块发起javascript的网络请求。javascript网络请求回来之后,再调用JS解释器,来解析这个js文件。
9. 四次挥手关闭连接
-
Client发送FIN+ACK包后,进入FIN-WAIT-1状态
-
当Server端收到Client端的FIN包后,会进入CLOSE-WAIT状态,在操作系统kernel中会自动回复一个FIN ACK包。
-
Client接收到FIN-ACK后,进入FIN-WAIT-2状态
注意FIN包和FIN-ACK包,都符合之前所说的重发确认
-
当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消息可能丢失了。
-
当这个FIN包到达Client之后,Client就进入到TIME-WAIT状态,其会回送一个ACK。
这条ACK也可能是会丢失的,如果丢失了,那么client就会继续收到server发送的FIN消息,client也就知道了之前自己发送的ACK丢失了,其就会重新发送ACK消息。
-
server端接收到ACK之后,就会进入CLOSE状态
-
client端发送ACK完成后,要等两个MSL(maximum segment life)的时间,也就是两倍的我们的tcp报文端在网络中存活的最大时间之后,也会进入到CLOSE状态
为什么是2MSL呢?
client并不知道server是否收到自己的ACK,client是这样想的:
- 如果server没有收到自己的ACK,那么server会超时(1个MSL)重传FIN,那么就再发送ACK
- 如果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,关联在一起的?
-
请求建立tcp连接事件
其实是发送了一个tcp的报文,通过上面的发送报文的流程,到达了nginx,所以这实际上是一个读事件,对nginx来说,我读取到了一个报文,所以就是accept接口 对应 建立连接的事件
-
tcp连接可读事件
比如我们发送了一个消息,对nginx来说,也是一个读事件,就是read接口 对应 tcp连接可读事件
-
tcp连接关闭事件
比如我们浏览器主动关闭了这个连接,相当于windows操作系统发送了一个关闭连接的事件,对于nginx来说,仍旧是一个读事件
-
当nginx需要向浏览器发送一个响应的时候,我们需要把消息写到操作系统中,要求操作系统发送到网络中,这就是一个写事件,对应write接口
网络中的读写事件,通常在nginx中,或者在任何一个异步事件的处理框架中,都有一个事件收集、分发器。我们会定义每类事件处理的消费者,也就是说生产者生产一个事件,通过网络自动生产到我们的nginx中的,我们对每种事件都建立一个消费者
比如连接建立事件的消费者,就是我们的accept接口的调用,nginx的http模块就会去建立一个新的连接,
比如读事件的消费者,写事件的消费者,在http状态机中,不同的时间段,我们会调用不同的方法,也就是每一个消费者去处理
包括像AIO异步读写磁盘的事件,也对应异步读写磁盘的消费者
还有我们的定时器事件,比如说我们是否超时,像worker shutdown timeout,都是定时器事件,对应定时器事件的消费者
网络事件 网络事件生产者 网络事件消费者 建立连接的事件 操作系统 accept接口的调用 tcp连接可读事件 操作系统 read接口的调用 关闭连接的事件(仍旧是一个读事件) 操作系统 read接口的调用 发送响应是一个写事件 write接口的调用 操作系统 AIO异步读磁盘的事件 操作系统 应用程序 AIO异步写磁盘的事件 应用程序 操作系统 定时器事件(如是否超时) 操作系统 应用程序