UNIX网络编程之epoll的 accept , read , write

本文转载自:http://www.it165.net/os/html/201308/5868.html

非阻塞模式下的网络编程,非阻塞模式常常需要不停地进行轮询,大量耗费CPU资源,这种方式并不可取。

在一个非阻塞的socket上调用read/write函数,返回EAGAIN或者EWOULDBLOCK(注:EAGAIN就是EWOULDBLOCK)。

从字面上看,意思是:

  • EAGAIN: 再试一次
  • EWOULDBLOCK:如果这是一个阻塞socket, 操作将被block
  • perror输出:Resource temporarily unavailable

总结:

这个错误表示资源暂时不够,可能read时, 读缓冲区没有数据, 或者write时,写缓冲区满了。

遇到这种情况,如果是阻塞socket、 read/write就要阻塞掉。而如果是非阻塞socket、 read/write立即返回-1, 同 时errno设置为EAGAIN。

所以对于阻塞socket、 read/write返回-1代表网络出错了。但对于非阻塞socket、read/write返回-1不一定网络真的出错了。可能是Resource temporarily unavailable。这时你应该再试,直到Resource available。

综上, 对于non-blocking的socket,正确的读写操作为:

  • 读: 忽略掉errno = EAGAIN的错误,下次继续读 
  • 写:忽略掉errno = EAGAIN的错误,下次继续写 

对于select和epoll的LT模式,这种读写方式是没有问题的。 但对于epoll的ET模式,这种方式还有漏洞。

epoll的两种模式 LT 和 ET

二者的差异在于 level-trigger 模式下只要某个 socket 处于 readable/writable 状态,无论什么时候进行 epoll_wait 都会返回该 socket;而 edge-trigger 模式下只有某个 socket 从 unreadable 变为 readable 或从unwritable 变为 writable 时,epoll_wait 才会返回该 socket。如下两个示意图:

从socket读数据:

往socket写数据:

所以在epoll的ET模式下,正确的读写方式为:

  • 读: 只要可读, 就一直读,直到返回0,或者 errno = EAGAIN
  • 写:只要可写, 就一直写,直到数据发送完,或者 errno = EAGAIN

正确的读:

1.n = 0; 

2.while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) { 

3.    n += nread; 

4.

5.if (nread == -1 && errno != EAGAIN) { 

6.    perror("read error"); 

7.}

正确的写:

01.int nwrite, data_size = strlen(buf); 

02.n = data_size; 

03.while (n > 0) { 

04.    nwrite = write(fd, buf + data_size - n, n); 

05.    if (nwrite < n) { 

06.        if (nwrite == -1 && errno != EAGAIN) { 

07.            perror("write error"); 

08.       

09.        break

10.   

11.    n -= nwrite; 

12.}

正确的accept,accept 要考虑 2 个问题:参考<<UNIX网络编程——epoll的 et,lt关注点>>讲解的更加详细

(1) LT模式下或ET模式下,阻塞的监听socket, accept 存在的问题

accept每次都是从已经完成三次握手的tcp队列中取出一个连接,考虑这种情况: TCP 连接被客户端夭折,即在服务器调用 accept 之前,客户端主动发送 RST 终止连接,导致刚刚建立的连接从就绪队列中移出,如果套接口被设置成阻塞模式,服务器就会一直阻塞在 accept 调用上,直到其他某个客户建立一个新的连接为止。但是在此期间,服务器单纯地阻塞在accept 调用上,就绪队列中的其他描述符都得不到处理

解决办法是:把监听套接口设置为非阻塞,当客户在服务器调用 accept 之前中止某个连接时,accept 调用可以立即返回 -1, 这时源自 Berkeley 的实现会在内核中处理该事件,并不会将该事件通知给 epool,而其他实现把 errno 设置为 ECONNABORTED 或者 EPROTO 错误,我们应该忽略这两个错误。

(2) ET 模式下 accept 存在的问题

考虑这种情况:多个连接同时到达,服务器的 TCP 就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll 只会通知一次,accept 只处理一个连接,导致 TCP 就绪队列中剩下的连接都得不到处理

解决办法是将监听套接字设置为非阻塞模式,用 while 循环抱住 accept 调用,处理完 TCP 就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢? accept 返回 -1 并且 errno 设置为 EAGAIN 就表示所有连接都处理完

综合以上两种情况,服务器应该使用非阻塞地 accept, accept 在 ET 模式下 的正确使用方式为:

01.while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote,  

02.                (size_t *)&addrlen)) > 0) { 

03.    handle_client(conn_sock); 

04.

05.if (conn_sock == -1) { 

06.    if (errno != EAGAIN && errno != ECONNABORTED  

07.            && errno != EPROTO && errno != EINTR)  

08.        perror("accept"); 

09.}

一道腾讯后台开发的面试题:

使用Linux epoll模型,水平触发模式;当socket可写时,会不停的触发 socket 可写的事件,如何处理?

  • 第一种最普遍的方式:

需要向 socket 写数据的时候才把 socket 加入 epoll ,等待可写事件。接受到可写事件后,调用 write 或者 send 发送数据。当所有数据都写完后,把 socket 移出 epoll。

这种方式的缺点是,即使发送很少的数据,也要把 socket 加入 epoll,写完后在移出 epoll,有一定操作代价。

  • 一种改进的方式:

开始不把 socket 加入 epoll,需要向 socket 写数据的时候,直接调用 write 或者 send 发送数据。如果返回 EAGAIN,把 socket 加入 epoll,在 epoll 的驱动下写数据,全部数据发送完毕后,再移出 epoll。

这种方式的优点是:数据不多的时候可以避免 epoll 的事件处理,提高效率。

最后贴一个使用epoll,ET模式的简单HTTP服务器代码:

001.#include <sys/socket.h>   

002.#include <sys/wait.h>   

003.#include <netinet/in.h>   

004.#include <netinet/tcp.h>   

005.#include <sys/epoll.h>   

006.#include <sys/sendfile.h>   

007.#include <sys/stat.h>   

008.#include <unistd.h>   

009.#include <stdio.h>   

010.#include <stdlib.h>   

011.#include <string.h>   

012.#include <strings.h>   

013.#include <fcntl.h>   

014.#include <errno.h>    

015.#define MAX_EVENTS 10   

016.#define PORT 8080   

017.//设置socket连接为非阻塞模式   

018.void setnonblocking(int sockfd) {   

019.    int opts;   

020.  

021.   opts = fcntl(sockfd, F_GETFL);   

022.    if(opts < 0) {   

023.        perror("fcntl(F_GETFL)\n");   

024.        exit(1);   

025.    }   

026.    opts = (opts | O_NONBLOCK);   

027.    if(fcntl(sockfd, F_SETFL, opts) < 0) {   

028.        perror("fcntl(F_SETFL)\n");   

029.        exit(1);   

030.    }   

031.}   

032.   

033.int main(){   

034.    struct epoll_event ev, events[MAX_EVENTS];   

035.    int addrlen, listenfd, conn_sock, nfds, epfd, fd, i, nread, n;   

036.    struct sockaddr_in local, remote;   

037.    char buf[BUFSIZ];   

038.   

039.    //创建listen socket   

040.    if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {   

041.        perror("sockfd\n");   

042.        exit(1);   

043.    }   

044.    setnonblocking(listenfd);   

045.    bzero(&local, sizeof(local));   

046.    local.sin_family = AF_INET;   

047.    local.sin_addr.s_addr = htonl(INADDR_ANY);;   

048.    local.sin_port = htons(PORT);   

049.    if( bind(listenfd, (struct sockaddr *) &local, sizeof(local)) < 0) {   

050.        perror("bind\n");   

051.        exit(1);   

052.    }   

053.    listen(listenfd, 20);   

054.   

055.    epfd = epoll_create(MAX_EVENTS);   

056.    if (epfd == -1) {   

057.        perror("epoll_create");   

058.        exit(EXIT_FAILURE);   

059.    }     

060.    ev.events = EPOLLIN;   

061.    ev.data.fd = listenfd;   

062.    if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev) == -1) {   

063.        perror("epoll_ctl: listen_sock");   

064.        exit(EXIT_FAILURE);   

065.    }   

066.   

067.    for (;;) {   

068.        nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);   

069.       if (nfds == -1) {   

070.            perror("epoll_pwait");   

071.            exit(EXIT_FAILURE);   

072.        }   

073.   

074.        for (i = 0; i < nfds; ++i) {   

075.            fd = events[i].data.fd;   

076.            if (fd == listenfd) {   

077.                while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote,(size_t *)&addrlen)) > 0) {   

078.                    setnonblocking(conn_sock); //设置连接socket为非阻塞  

079.                    ev.events = EPOLLIN | EPOLLET; //边沿触发要求套接字为非阻塞模式;水平触发可以是阻塞或非阻塞模式  

080.                    ev.data.fd = conn_sock;   

081.                    if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sock,&ev) == -1) {   

082.                        perror("epoll_ctl: add");   

083.                        exit(EXIT_FAILURE);   

084.                    }   

085.                }   

086.                if (conn_sock == -1) {   

087.                    if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR)    

088.                        perror("accept");   

089.                }   

090.                continue;   

091.            }     

092.            if (events[i].events & EPOLLIN) {   

093.                n = 0;   

094.                while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) {   

095.                    n += nread;   

096.                }   

097.                if (nread == -1 && errno != EAGAIN) {   

098.                    perror("read error");   

099.                }   

100.                ev.data.fd = fd;   

101.                ev.events = events[i].events | EPOLLOUT;   

102.                if (epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev) == -1) {   

103.                    perror("epoll_ctl: mod");   

104.                }   

105.            }   

106.            if (events[i].events & EPOLLOUT) {   

107.                sprintf(buf, "HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\nHello World", 11);   

108.                int nwrite, data_size = strlen(buf);   

109.                n = data_size;   

110.                while (n > 0) {   

111.                    nwrite = write(fd, buf + data_size - n, n);   

112.                    if (nwrite < n) {   

113.                        if (nwrite == -1 && errno != EAGAIN) {   

114.                            perror("write error");   

115.                        }   

116.                        break;   

117.                    }   

118.                    n -= nwrite;   

119.                }   

120.                close(fd);   

121.            }   

122.        }   

123.    }

124.    close(epfd);

125.    close(listenfd);   

126.    return 0;   

127.}

时间: 2024-10-09 02:56:51

UNIX网络编程之epoll的 accept , read , write的相关文章

linux/unix网络编程之epoll

转载自 Linux epoll模型 ,这篇文章讲的非常详细! 定义: epoll是Linux内核为处理大批句柄而作改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著的减少程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率.因为它会复用文件描述符集合来传递结果而不是迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一个原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符

Unix网络编程之IO复用

上篇存在的问题 在上一篇TCP套接字中,还存在着一些问题. 当客户端连接上服务器后,阻塞于从标准输入读入信息的状态,若此时服务器进程被杀死,即使给客户TCP发来一个 FIN结束分节,但是由于客户处于阻塞状态,它将看不到这个EOF,直到读取之后,此时可能已经过去了很长时间. 因此进程需要一种能力,让内核同时检测多个IO口是否就绪,这个能力就称为IO复用.这是由select和poll两个函数 支持的. select函数 作用 允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或

linux/unix网络编程之 select

转自http://www.cnblogs.com/zhuwbox/p/4221934.html linux 下的 select 知识点 unp 的第六章已经描述的很清楚,我们这里简单的说下 select 的作用,并给出 select 的客户端实例.我们知道 select 是IO 多路复用的一个最简单支持,poll 和 epoll 是 select 的升级版.在 UNIX 网络编程第五章读书笔记 我们遇到这样一个问题:当客户端阻塞在 fgets() 等待客户输入的时候,服务器端断开连接.而客户端却

unix网络编程之listen()详解

Unix网络编程描述如下: #include <sys/socket.h> int listen(int sockfd,  int backlog); 返回:若成功则为0,  若出错则为-1: 本函数通常应该在调用socket和bind这两个函数之后,并在调用accept函数之前调用: 为了理解其中的backlog参数,我们必须认识到内核为任何一个给定的监听套接字维护两个队列: (1)未完成连接队列(incomplete connection queue),

Unix网络编程之UDP常见缺陷与实例

UDP与TCP相比,各有优缺点,下来来列举一下UDP的缺点: 1.UDP是一种不可靠的协议(缺乏流量控制) 实例代码: //server.c #include <stdlib.h> #include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> int main() { int sockfd; struct sockaddr_in ser

Linux-C网络编程之epoll函数

上文中说到如果从100的不同的地方取外卖,那么epoll相当于一部手机,当外卖到达后,送货员可以通知你,从而达到每去必得,少走很多路. 它是如何实现这些作用的呢? epoll的功能 epoll是select/poll的强化版,同是多路复用的函数,epoll有了很大的改进. 支持监听大数目的socket描述符* 一个进程内,select能打开的fd是有限制的,由宏FD_SETSIZE设置,默认值是1024.在某些时候,这个数值是远远不够用的.解决办法有两种,一是修改宏然后重新编译内核,但与此同时会

高并发网络编程之epoll详解

select.poll和epoll的区别 在linux没有实现epoll事件驱动机制之前,我们一般选择用select或者poll等IO多路复用的方法来实现并发服务程序.在大数据.高并发.集群等一些名词唱的火热之年代,select和poll的用武之地越来越有限了,风头已经被epoll占尽. select()和poll() IO多路复用模型 select的缺点: 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述

Linux网络编程之epoll知识点备忘

首先是关于IO多路复用的基础概念: select,poll,epoll都是IO多路复用的机制.I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作.但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间. 关键要了解阻塞非阻塞.同步异步之间的关系与区别,然后对

linux/unix网络编程之 poll

转自http://www.cnblogs.com/zhuwbox/p/4222382.html poll 与 select 很类似,都是对描述符进行遍历,查看是否有描述符就绪.如果有就返回就绪文件描述符的个数将.poll 函数如下: #include <poll.h> int poll(struct pollfd *fdarray, unsigned long nfds, int timeout) 第一个参数指向结构数组第一个元素的指针,每个数组都是一个 pollfd 结构,用于指定测试某个给