Linux下非阻塞socket编程常用的有两种实现:select和epoll。但是select受限于描述符数目限制,实际使用的时候人们大多数都会采用epoll形式来实现(如果在windows下没有epoll原生支持,可以使用select方式)。
Epoll主要有以下三个接口:
1 |
|
epoll_create
主要用于创建一个epoll的句柄,参数是要监听的文件描述符的最大个数。epoll_ctl
主要用于添加,删除和修改监听事件。epoll_wait
类似select
查看是否有文件描述符对应事件已经产生。三个函数接口的详细说明可以参考Linux manual page,这里要说一下,epoll_wait的最后一个参数timeout用来设置等待的时间,若设置为1,则epoll阻塞等待的时间不确定(官方文档描述);若设置为0则是完全非阻塞状态,会立即返回;若设置为其他时间则是规定时间内没有事件触发则超时后返回。
一般情况下,我们在网络编程的服务端常用到epoll来非阻塞地监听客户端发来的连接,下面是我测试通过的epoll服务端的简单示例代码,仅供参考。
1 |
|
此外,这里还需要交待一个可能有些人并没有想清楚的问题:epoll == 非阻塞socket?
其实并不是,首先,socket只是众多描述符中的一种,还有很多非阻塞IO都可以通过epoll来实现。另外,非阻塞socket的实现方式也不单单是epoll和socket,其实socket本身的recv函数就自带一个参数来控制是阻塞还是非阻塞状态方式接收。
1 | ssize_t recv(int sockfd, void *buf, size_t len, int flags); |
一般我们自己写的recv通常会把第四个参数flags置为0,但是当把它设置为MSG_DONTWAIT
时,此时,若接收缓冲区没有数据可读时,recv函数并不会阻塞等待,而是会返回一个EWOULDBLOCK
的错误表示缓冲区当前实际已经阻塞,这一点类似于send函数的返回值,当发送缓冲区满后,send函数也会返回一个EWOULDBLOCK
表示当前发送缓冲已经阻塞。
此外,如果不想在每次recv函数调用时都传参来控制阻塞还是非阻塞接收,可以调用以下函数达到类似的效果:
1 | int on = 1; |
这个函数设置以后,对应的socket就可以通过非阻塞方式工作了。
上述两种介绍的方式来实现非阻塞socket貌似更加直接方便,那是不是epoll就可以不用了呢?
显然不是的,epoll可以同时监听多个描述符是否可读写,而这对于socket网络编程中的服务端编写是必不可少的,实际的网络场景中,服务端需要通过非阻塞的方式来监听每一个客户端发来的连接请求同时还要以非阻塞的方式来与对端进行数据收发,因此epoll的使用也尤为重要。
除此之外,对于一个服务端socket程序,ioctlsocket
和epoll同时使用会更加高效,可以参考以下代码:
1 | int on = 1; |
如果在程序一开始不使用ioctlsocket将socket设置为非阻塞状态,当epoll_wait检测到socket可读的时候,执行上述代码可能会造成阻塞!原因就在于最外层for循环,循环的目的在于socket缓冲区可能一次性接收到很多字节,而BUFFER_SZ不一定能够把所有的都读取出来,因此通过for循环几次可以比较方便高效地在检测到一次epoll事件可读时将更多的字节读取出来,但由于socket是非阻塞工作模式,因此如果缓冲区已经空了,再调用recv函数,就会阻塞等待。如果不想设置套接字为非阻塞,单纯用epoll来实现,就需要在每次调用recv之前都要先通过epoll_wait判断套接字是否可读,即两者必须成对调用,才能保证程序接收不会发生阻塞。