网络编程(三):从libevent到事件通知机制

blog

由于POSIX标准的滞后性,事件通知API的混乱一直保持到现在, 所有就有libevent、libev甚至后面的libuv的出现为跨平台编程扫清障碍。

下面是WikiPedia对于libevent的介绍:

libevent是一个异步事件处理软件函式库,以BSD许可证发布。 libevent提供了一组应用程序编程接口(API),让程序员可以设定某些事件发生时所执行的函式,也就是说,libevent可以用来取代网络服务器所使用的事件循环检查框架。

由于可以省去对网络的处理,且拥有不错的效能, 有些软件使用libevent作为网络底层的函式库,如:Chromium(Chrome的开源版)、 memcached、Tor。

按照libevent的官方网站,libevent库提供了以下功能:当一个文件描述符的特定事件 (如可读,可写或出错)发生了,或一个定时事件发生了, libevent就会自动执行用户指定的回调函数,来处理事件。

目前,libevent已支持以下接口/dev/poll, kqueue(2), event ports, select(2), poll(2) 和epoll(4)。

libevent的内部事件机制完全是基于所使用的接口的。因此libevent非常容易移植, 也使它的扩展性非常容易。目前,libevent已在以下操作系统中编译通过: Linux,BSD,Mac OS X,Solaris和Windows。

libevent的高明之处还在于,它把fd读写、信号、DNS、定时器甚至idle(空闲) 都抽象化成了event(事件)。

我们可以简单看一下一个简单的基于libevent的网络server, 这有助于我们理解event-driven programming(事件驱动编程), 也为我们后续的实操做准备。

我在代码中增加了详细的注释,希望大家能大致明白event-driven programming 的一半方法:

可以看出,用这种方式写出来的异步非阻塞server的逻辑还是比较容易理解的。

和协程的实现方式相比,这种方式完全避免了“手工”的上线文切换, 有利于CPU的分支预测的成功率,能发挥CPU处理网络连接的的最大潜能。

水平触发LT & 边沿触发ET

在struct epoll_event里有连个Flag:EPOLLET和EPOLLLT让初学者很难以理解

以下是一段关于epoll的man文档:

epoll is a variant of poll(2) that can be used either as an edge-triggered or a level-triggered interface and scales well to large numbers of watched file descriptors. The following system calls are pro- vided to create and manage an epoll instance:

  • An epoll instance created by epoll_create(2), which returns a file descriptor referring to the epoll instance. (The more recent epoll_create1(2) extends the functionality of epoll_create(2).)
  • Interest in particular file descriptors is then registered via epoll_ctl(2). The set of file descriptors currently registered on an epoll instance is sometimes called an epoll set.
  • Finally, the actual wait is started by epoll_wait(2).

Level-Triggered and Edge-Triggered The epoll event distribution interface is able to behave both as edge-triggered (ET) and as level-triggered (LT). The difference between the two mechanisms can be described as follows. Suppose that this scenario happens:

  1. The file descriptor that represents the read side of a pipe (rfd) is registered on the epoll instance.
  2. A pipe writer writes 2 kB of data on the write side of the pipe.
  3. A call to epoll_wait(2) is done that will return rfd as a ready file descriptor.
  4. The pipe reader reads 1 kB of data from rfd.
  5. A call to epoll_wait(2) is done.

If the rfd file descriptor has been added to the epoll interface using the EPOLLET (edge-triggered) flag, the call to epoll_wait(2) done in step 5 will probably hang despite the available data still present in the file input buffer; meanwhile the remote peer might be expecting a response based on the data it already sent. The reason for this is that edge-triggered mode only delivers events when changes occur on the monitored file descriptor. So, in step 5 the caller might end up waiting for some data that is already present inside the input buffer. In the above example, an event on rfd will be generated because of the write done in 2 and the event is consumed in 3. Since the read operation done in 4 does not consume the whole buffer data, the call to epoll_wait(2) done in step 5 might block indefinitely.

An application that employs the EPOLLET flag should use non-blocking file descriptors to avoid having a blocking read or write starve a task that is handling multiple file descriptors. The suggested way to use epoll as an edge-triggered (EPOLLET) interface is as follows:

i with non-blocking file descriptors; and

ii by waiting for an event only after read(2) or write(2) return EAGAIN.

By contrast, when used as a level-triggered interface (the default, when EPOLLET is not specified), epoll is simply a faster poll(2), and can be used wherever the latter is used since it shares the same semantics.

Since even with edge-triggered epoll, multiple events can be generated upon receipt of multiple chunks of data, the caller has the option to specify the EPOLLONESHOT flag, to tell epoll to disable the associated file descriptor after the receipt of an event with epoll_wait(2). When the EPOLLONESHOT flag is specified, it is the caller’s responsibility to rearm the file descriptor using epoll_ctl(2) with EPOLL_CTL_MOD.

在epoll的man文档里,我们会看到一个花费大量篇幅描述的两个概念:

LT(Level Triggered,水平触发) 和 ET(Edge Triggered,边沿触发)

作者当年花费了九牛二虎之力也没能领悟这段“经文”。后来一个偶然的机会, 一个做电子设计的朋友给我讲明白了其中的道道。

为了弄明白LT(Level Triggered,水平触发) 和 ET(Edge Triggered,边沿触发), 我们先要了解,这个Level和Edge是什么涵义,Level翻译成中文这里准确的涵义应该是电平; Edge是边沿。

这两个词曾经是电子信号领域的一个专有名词。如果,用时序图来标示一个数字电信号“010”, 应该是类似下图所示:

cc7946c9424291a2d1043a2ecad59f23_b

  • 低电平表示0。
  • 高电平表示1。
  • 0向1变化的竖线就是上升沿。
  • 1向0变化的竖线就是下降沿。
  • 在0或者1的情况下触发的信号就是LT(Level Triggered,水平触发)
  • 在0向1、1向0变化的过程中触发的信号就是 和 ET(Edge Triggered,边沿触发)

0或1都是一个状态,而0向1、1向0变化则只是一个事件。

我们很直观的就可以得出结论,LT是一个持续的状态,ET是个事件性的一次性状态。

二者的差异在于Level Triggered模式下只要某个socket处于readable/writable状态, 无论什么时候进行epoll_wait都会返回该socket;

而Edge Triggered模式下只有某个socket从unreadable变为readable或 从unwritable变为writable时,epoll_wait才会返回该socket。

虽然有很多资料表明ET模式的销量会比LT稍高, 但ET模式的编程由于事件只通知一次,很容易犯错误导致程序假死,我们推荐epoll工作于LT模式。 除非你很清楚你选择的是什么。

闲话QQ的通信协议

如果大家研究过早期的腾讯QQ的通信协议,可以发现QQ的通信协议是基于UDP的。 这点从今天的角度看来显得十分的怪异,因为用UDP这种无连接的协议 实现一套保证消息可靠性的聊天服务的难度是非常之高的。

了解过那段历史的同学可能知道,当时UDP的确是QQ的唯一选择。 当年QQ达到百万人同时在线的时候,国外的同行还没有认为C10K是个问题。 想要用TCP承担百万人同时在线,在当时的技术条件下恐怕要付出上千台服务器的代价, 这对于当时的”小企鹅”来说是绝对负担不起的一笔投入。

由于缺乏操作系统对于高性能TCP协议的支持,想要在极为有限的服务器条件下 处理QQ的C1000K问题,UDP的确是当时的腾讯架构师的唯一选择。

文章分类 后端, 技术博客, 运维开发

发表评论

电子邮件地址不会被公开。

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

在线交流

数百位业内高手和同行在等你交流
Reboot运维开发分享