网络编程深层剖析

为什么需要 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实现复杂度较高

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 请求是提供数据


不准投币喔 👆

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇