为什么需要 EPOLLONESHOT?
epoll
有两种触发的方式:LT(水平触发)和ET(边缘触发)。对于前者,只要存在着事件就会不断的触发,直到处理完成;而后者只触发一次相同事件,或者说只在从非触发到触发两个状态转换的时候才触发。
想象这样一种情况,当前是多线程在处理,一个 socket
事件到来,数据开始解析。这时候这个 socket
又来了同样一个事件,而你的数据解析尚未完成,那么程序会自动调度另外一个线程或者进程来处理新的事件。这就会造成一个很严重的问题:不同的线程或者进程在处理同一个 socket
的事件,这会使程序的健壮性大大降低,即使在ET模式下也有可能出现这种情况。
解决这种情况有两种方法:一是在单独的线程或进程里解析数据,也就是说,接收数据的线程立刻将数据转移至另外的线程;二就是利用 EPOLLONESHOT
。
对于注册了 EPOLLONESHOT
事件的文件描述符,操作系统最多只触发其上注册的一个事件,且仅触发一次。这意味着,当一个线程处理该 socket
时,其他线程不会再收到该 socket
的事件通知。
如果需要让该 socket
在下次可读时仍然触发事件,必须在处理完毕后,使用 epoll_ctl
重新注册 EPOLLONESHOT
事件,以便 socket
在下一次有数据可读时能再次被触发,从而让其他线程有机会继续处理该 socket
。
两种事件处理模型:Reactor/Proactor
二者的主要区别在于 谁负责完成 I/O 操作:
- Reactor:I/O 操作由 工作线程 完成(同步I/O)
- Proactor:I/O 操作由 内核或主线程 完成,工作线程只处理逻辑(异步I/O)
Reactor
- 主要特点:
- 主线程(I/O 线程) 负责监听 socket 是否可读/可写
- 当 socket 可读/可写 时,主线程通知 工作线程(逻辑线程) 进行数据读取或写入,并处理业务逻辑。
- 处理流程(以
read()
为例):- 主线程(I/O 线程) 监听 socket 事件(epoll、select、poll)
- 当 socket 可读 时,通知 工作线程 处理
- 工作线程 调用
read()
读取数据,然后进行业务处理 - 处理完成后,工作线程 可能会向 socket 写入 响应数据
- 若 socket 可写,主线程 再次通知 工作线程 处理写操作
- 优点:高并发性能,因为 I/O 线程只负责事件通知,不做繁重的 I/O 处理
- 缺点:数据读取和写入由工作线程完成,可能会导致 I/O 阻塞,降低性能
Proactor
- 主要特点:
- 主线程(I/O 线程)+ 内核 负责完成 I/O 操作(读/写)
- 工作线程 只需要处理业务逻辑,而不涉及 I/O 读写操作
- 处理流程(以
read()
为例):- 主线程(I/O 线程) 通过
aio_read()
等异步 I/O 操作向 内核 发起 I/O 请求 - 内核 负责异步完成
read()
操作,并将读取的数据放入缓冲区 - 当内核完成 I/O 操作,通知 主线程
- 主线程 将任务交给 工作线程 进行业务逻辑处理(如
pool->append(users + sockfd)
) - 若需要写入数据,主线程 调用
aio_write()
,让 内核 异步完成写入
- 主线程(I/O 线程) 通过
- 优点:I/O 线程几乎不做阻塞等待,可以更高效地处理 I/O 请求,更适合高并发场景
- 缺点:依赖操作系统支持异步 I/O,实现复杂度较高
Reactor 是非阻塞同步网络模式,感知的是就绪可读写事件。在每次感知到有事件发生(比如可读就绪事件)后,就需要应用进程主动调用 read 方法来完成数据的读取,也就是要应用进程主动将 socket 接收缓存中的数据读到应用进程内存中,这个过程是同步的,读取完数据后应用进程才能处理数据。
Proactor 是异步网络模式,感知的是已完成的读写事件。在发起异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做,并不需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据。
因此,Reactor 可以理解为来了事件操作系统通知应用进程,让应用进程来处理,而 Proactor 可以理解为来了事件操作系统来处理,处理完再通知应用进程。这里的事件就是有新连接、有数据可读、有数据可写的这些 1/0 事件这里的处理包含从驱动读取到内核以及从内核读取到用户空间。
为什么一般情况下 epoll 性能要优于 select 和 poll?
- 对于 select 和 poll 来说,所有文件描述符都是在用户态被加入其文件描述符集合的,每次调用都需要将整个集合拷贝到内核态;epoll 则将整个文件描述符集合维护在内核态,每次添加文件描述符的时候都需要执行一个系统调用。系统调用的开销是很大的,在有很多短期活跃连接的情况下,由于这些大量的系统调用开销,epoll 可能会慢于 select 和 poll。
- select 使用线性表描述文件描述符集合,文件描述符有上限;poll 使用链表来描述;而 epoll 底层通过红黑树来描述,并且维护一个 ready list,将事件表中已经就绪的事件添加到这里,在使用 epoll_wait 调用时,仅观察这个 list 中有没有数据即可。
- select 和 poll 的最大开销来自内核判断是否有文件描述符就绪这一过程:每次执行 select 或 poll 调用时,它们会采用遍历的方式,遍历整个文件描述符集合去判断各个文件描述符是否有活动;epoll 则不需要去以这种方式检查。当有活动产生时,会自动触发 epoll 回调函数通知 epoll 文件描述符,然后内核将这些就绪的文件描述符放到之前提到的 ready list 中等待 epoll_wait 调用后被处理。
- select 和 poll 都只能工作在相对低效的LT模式下,而 epoll 同时支持LT和ET模式。
综上,当监测的文件描述符数量较小,且都很活跃的情况下,建议使用 select 和 poll;当监听的文件描述符数量较多,且单位时间仅部分活跃的情况下,使用 epoll 会明显提升性能。
GET和POST的区别
- 最直观的区别就是GET把参数包含在URL中,POST 通过 request body 传递参数。
- GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
- GET请求在URL中传送的参数是有长度限制,(大多数)浏览器通常都会限制url长度在2K个字节,而(大多数)服务器最多处理64K大小的url。
- GET产生一个TCP数据包;POST产生两个TCP数据包。对于GET方式的请求,浏览器会把 http header 和 data 一并发送出去,服务器响应 200(返回数据);而对于POST,浏览器先发送 header,服务器响应 100(指示信息—表示请求已接收,继续处理)continue,浏览器再发送 data,服务器响应 200 ok(返回数据)。
举例:假设你已经搭好了你的HTTP服务器,然后你在本地浏览器中键入localhost:9000
,然后回车,这时候你就给你的服务器发送了一个GET请求,什么都没做,然后服务器端就会解析你的这个HTTP请求,然后发现是个GET请求,然后返回给你一个静态HTML页面,也就是项目中的judge.html
页面,那POST请求怎么来的呢?这时你会发现,返回的这个judge
页面中包含着一些新用户
和已有账号
这两个button
元素,当你用鼠标点击这个button
时,你的浏览器就会向你的服务器发送一个POST请求,服务器段通过检查action
来判断你的POST请求类型是什么,进而做出不同的响应。
总结:GET 请求是索取数据,POST 请求是提供数据