翻译:使用Libevent的快速可移植非阻塞网络编程:异步IO简介

原文出处:http://www.wangafu.net/~nickm/libevent-book/01_intro.html

大多数程序员从阻塞IO调用开始学习。如果调用在操作完成之前,或者足够的时间已经流逝使得网络栈放弃操作之前,不会返回,那么就是异步的。比如说,在TCP连接上调用connect()时,操作系统将一个SYN分组排队到TCP连接的另一端主机中。在收到来自对方主机的SYNACK分组之前,或者直到足够的时间已经流逝而决定放弃操作之前,控制不会返回到应用程序。

这里有一个使用阻塞网络调用的简单客户端示例。它打开到www.google.com的连接,发送一个简单的HTTP请求,将响应打印到stdout

…… 省略示例代码 ……

上述代码中的所有网络调用都是阻塞的:在成功解析www.google.com,或者解析失败之前,gethostbyname不会返回;连接建立之前connect不会返回;收到数据或者关闭之前recv调用不会返回;至少在清空输出缓冲区到内核的写缓冲区之前,send调用不会返回。

这里,阻塞IO没有什么不好的。如果没有其他事情需要同时进行,阻塞IO会工作得很好。但是考虑需要同时处理多个连接的情形。考虑一个具体的例子:需要从两个连接读取输入,但是不知道哪个连接将先收到输入。程序可能是这样的:


即使fd[2]上最先有数据到达,对fd[0]fd[1]的读取操作取得一些数据并且完成之前,程序不会试图从fd[2]进行读取。

有时候用多线程或者多进程服务器来解决此问题。最简单的方式是用一个单独的进程(或者线程)处理每个连接。因为每个连接拥有独立的进程,一个连接上阻塞的IO调用不会阻塞其他任何连接的进程。

这里有另一个示例程序。它是一个简单的服务器,在端口47013上监听TCP连接,每次从其输入缓冲区读取一行,写回其ROT13混淆结果。程序使用fork()调用为每个进入的连接创建一个新的进程。


是否有同时处理多个连接的完美解决方案?我可以停止编写本书,去做其他事情吗?不可以。首先,一些平台上进程创建(甚至线程创建)的开销是很大的。现实中你可能想用线程池代替创建新进程。然而,线程的扩展性根本达不到期望。如果需要同时处理成千上万个连接,处理上万个线程的效率并不比在每个CPU上使用少量线程高。

如果线程不是处理多个连接的答案,那么什么是呢?在Unix世界中,可以使用非阻塞套接字:

fcntl(fd, F_SETFL, o_NONBLOCK);

这里fd是套接字的文件描述符。将fd(套接字)设置为非阻塞之后,对fd进行网络调用时,调用要么立即完成操作,要么返回一个特定的错误号,指示“现在不能进行操作,请重试”。这样,示例程序可以写作:


使用非阻塞套接字,上述代码可以工作,但只是在很少的情况下。程序性能将很糟糕,原因有两个。首先,如果任何连接上都没有数据可读,循环还是会无限进行,消耗CPU时间。第二,如果用这种方式处理多于一两个连接,程序将为每个连接进行内核调用,不论连接上是否有数据。我们需要的是一种可以告诉内核“等待这些套接字中的某一个有数据可读,并且告知是哪一个”。

对于此问题,现在仍然使用的最老的解决方案是select()。select()调用要求三个fd集合(作为位数组实现):一个用于读取,一个用于写入,一个用于异常。select()将等待集合中的某个套接字就绪,并且修改集合,使之仅包含已经就绪的套接字。

这是使用select的相同示例:


这里是使用select重新实现的ROT13服务器:

…… 省略示例代码 ……

事情还没完。因为生成和读取select位数组所需的时间与用于select的最大fd成比例,所以当套接字个数增加时,select调用的开销将急剧增加。

不同的操作系统为select提供了不同的替代功能,包括pollepollkqueueevports/dev/poll。这些函数的性能都比select高,而且除了poll之外,添加、删除套接字和通知套接字已经准备好IO的性能都是O1)。

不幸的是,这些接口都不是标准的。LinuxepollBSD(包括Darwin)有kqueueSolarisevports/dev/poll……,然而没有哪个操作系统有其他系统所拥有的调用。所以,如果想编写可移植的高性能异步应用,就需要一个封装所有这些接口的抽象,提供这些调用中性能最高的一个供使用。

这就是LibeventAPI最底层所做的事情。Libevent为各种select替代提供了一致的接口,使用所运行在的计算机上的最高效版本。

下面是另一个版本的异步ROT13服务器。这次用Libevent2代替了select。注意fd_sets已经被抛弃:替代的是,将事件与结构体event_base关联或者断开关联,这可能是用selectpollepoll或者kqueue实现的。

…… 省略示例代码 ……

(代码需要注意的其他地方:使用evutil_socket_t代替int来代表套接字;调用evutil_make_socket_nonblocking来将套接字设置为异步的,而不是调用fcntlO_NONBLOCK)。这使得代码兼容于Win32网络API

使用是否便捷?(还有Windows呢?)

你可能注意到代码效率更高了,但是也更复杂了。使用fork的时候,(1)不需要为每个连接管理缓冲区:仅对每个进程使用一个单独的在栈上分配的缓冲区。(2)不需要显式跟踪每个套接字是否在读取或者写入:这隐藏在代码中了。(3)也不需要跟踪每个操作是否完成的结构体:只需要循环和栈变量。

此外,如果对Windows网络有很深的体验,你将认识到用于上述示例的时候,Libevent并不能取得优化的性能。在Windows上进行快速异步IO的方法不是使用select接口:而是使用IOCP。与其他快速网络API不同的是,IOCP不是在套接字已经准备好某种操作时通知程序,然后程序可以进行相应的操作。替代的是,程序告知Windows网络栈启动某网络操作,IOCP在操作完成时通知程序。

幸运的是,Libevent2 的“bufferevent”接口解决了所有这些问题:它提供了让LibeventWindowsUnix上都能够有效实现的接口,让程序编写更简单。

这是最后一个版本的ROT13,使用buffereventAPI

…… 省略示例代码 ……

这些真的很高效吗?

XXXX将在此写有效的一节。libevent网页上的benchmarks数据真的过期了。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值