【UNIX网络编程】|【04】I/O模型、select、pselect、poll

1、概述

由上一章节TCP客户同时处理两个输入:标准输入和TCP套接字时,客户阻塞于fgets调用期间,服务器进程会被杀死;服务器给客户发送了一个FIN,但客户阻塞
于标准输入读入的过程,看不到这个EOF,直到从套接字读时为止;这样的进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件
就绪(也就是说输入已准备好被读取,或者描述符已能承接更多的输出),它就通知进程,这个能力称为IO复用,由select和poll支持的;
1.1 应用场景
- 当客户处理多个描述符时,必须使用IO复用;
- 一个客户同时处理多个套接字是可能的;
- 如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字;
- 如果一个服务器即要处理TCP,又要处理UDP;
- 如果一个服务器要处理多个服务或者多个协议;
- I/O复用并非只限于网络编程,许多重要的应用程序也需要使用这项技术。

2、I/O模型

本给给出例子,两个不同阶段:
- 等待数据准备好;
- 从内核向进程复制数据;
第一步通常等待数据从网络中到达,当所等待分组到达时,它被复制到内核中的某个缓冲区;
第二步即把数据从内核缓冲区复制到应用进程缓冲区;
2.1 阻塞式I/O模型
- 最流行的模型;
- 默认下,所有套接字都是阻塞的;

在这里插入图片描述
为什么使用UDP,而不是TCP

UDP数据读取概念简单,数据报要被被接收到,要么就没有;
本节中,进程调用recvfrom,其系统调用知道数据报到达且被复制到应用进程的缓冲区中或发送错误才返回;
【常见的错误】:系统调用被信号中断;
【该函数成功返回】:应用进程开始处理数据报;
2.2 非阻塞式I/O模型
进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操作要把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误;

在这里插入图片描述

前三次调用recvfrom时,没有数据可返回,故内核返回EWOULDLOCK错误;第四次调用recvfrom时已有一个数据报准备好,它被复制到应用进程缓冲区,故成功;
【轮询】:当一个应用进程对非阻塞描述符循环调用recvfrom时;
应用进程持续轮询内核(忙等待),以查看某个操作是否就绪;但该做法会耗费大量CPU时间,加剧响应延迟;
2.3 I/O复用模型
有了I/O复用,即可调用select或poll,阻塞于该两个函数上,而不是阻塞在真正的I/O系统调用上;

在这里插入图片描述

阻塞于select调用,等待数据报套接字变为可读;当select返回套接字可读这一条件时,我们调用recvfrom把所有数据复制到应用进程缓冲区;

当相比于上述用法,该模型并没有优势;事实上使用select需要两个而不是单个系统调用,该模型还稍有劣势;
- 描述符的个数有限定;
- 调用select需要传入所有监听集合,需要频繁拷贝数据;
- 需要遍历集合来判断fd,耗费时间;
2.4 信号驱动式I/O模型
可使用信号,让内核在描述符就绪时发送SIGIO信号通知我们;

在这里插入图片描述

- 我们首先开启套接字的信号驱动式I/O功能,并通过sigaction系统调用安装一个信号处理函数;
- 该系统调用将立即返回,没有被阻塞;
- 当数据报准备好读取时,内核就为该进程产生SIGIO信号;
- 即可在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理,也可立即通知主循环,让它读取数据报;

【优势】:等待数据报到达期间进程不被阻塞;主循环可以继续执行,只要等待信号;
2.5 异步I/O模型
异步I/O由POSIX规范定义;
【实时函数】:告知内核启动某个操作,并让内核在整个操作完成后通知我们;
【与信号驱动模型的主要区别】:信号驱动式I/O是由内核通知我们何时可启动I/O操作,该模型是由内核通知我们I/O操作何时完成;

在这里插入图片描述

调用aio_read函数,给内核传递描述符、缓冲区指针、缓冲区大小和文件偏移,并告诉内核当整个操作完成时如何通知我们;
该系统调用立即返回,而在等待I/O完成期间,我们的进程不被阻塞;

【与信号驱动模型的不同】:信号要直到数据已复制到应用程序缓冲区才产生;
2.6 各种I/O模型的比较
前4种模型主要区别在于第一阶段:数据从内核复制到调用者的缓冲区期间,进程阻塞于recvfrom调用;
异步I/O模型在这两阶段都要处理,不同于其他4模型;
2.7 同步I/O和异步I/O对比
在POSIX:
【同步I/O操作】:导致请求进程阻塞,直到I/O操作完成;
【异步I/O操作】:不导致请求进程阻塞;

在这里插入图片描述

上述中,前4种模型都是同步,由于真正I/O操作将阻塞进程;

3、select函数

#include <sys/select.h>
#include <sys/time.h>

int select(int maxfdp1, fd_set *readset,  fd_set *writeset, fd_set *exceptset, 
		const struct timeval *timeout);
struct timeval {
	long tv_sec;	// 秒
	long tv_usec;	// 微妙
};
/**
@func: 该函数允许进程指示内核等待多个事件中的任何一个发送,并只在一个或多个时间发送或经历一段指定的时间后才唤醒它;
@param maxfdp1: 待测试的描述符个数,最大为FD_SETSIZE=1024,但描述符是从0开始的;
@param timeout: 
	- nullptr:永远等待,直到有一个描述符准备好I/O时才返回;
	- timeval中指定的秒数和微秒数:等待指定秒数,在有一个描述符准备好I/O时返回;
	- 根本不等待:检查描述符好立即返回(轮询)timeval内指为0;
	1、2情况回被进程在等待期间捕获信号中断,并从信号处理函数返回;
中间三个参数指定要让内核测试读、写和异常条件的描述符,支持的异常条件:	
	- 某个套接字的带外数据的到达;
	- 某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息;
	三个参数可被设置为nullpter;
	为值-结果参数;
	函数调用时,我们指定需要的描述符的值,返回时查看指定的描述符是否就绪(FD_ISSET)
*/

/**
select使用描述符集(整数数组),每个整数中的每一位对应一个描述符;相应的函数:
*/
void FD_ZERO(fd_set *fdset);			// 清空
void FD_SET(int fd, fd_set *fdset);		// 设置
void FD_CLR(int fd, fd_set *fdset);		// 清空对应bit
int FD_ISSET(int fd, fd_set *fdset);	// 是否被设置


【返回案例,可告知内核在以下情况返回】:
- 集合{1,4,5}中的任何描述符准备好读;
- 集合{2,7}中的任何描述符准备好写;
- 集合{1,4}中的任何描述符由异常条件待处理;
- 已经历了10.2s;	==> 告知内核对哪些描述符感兴趣以及等待多长时间;

【注意】:
- Berkeley的内核绝不自动重启被中断的select;
- SVR4可以自动重启被中断的select,条件是在安装信号处理函数时指定了SA_RESTART标志,在捕获信号,必须做好select返回EINTR错误的准备;
- 尽管timeout可指定微妙级,单内核往往粗糙得多,舍入10ms倍数,且调用延迟;
- 每次重新调用select函数时,都要把所有描述符集内关心的位置为1

常见错误

- 忘了对最大描述符加1;
- 忘了描述符集是值-结果参数;
- 第二个错误导致调用select时,描述符集内我们认为是1的位却被置为0;
3.1 描述符就绪条件
满足下列任一条件,且一个套接字准备好读;
- 该套接字接收缓冲区中的数据字节数>=套接字接收缓冲区低水位标记的当前大小;
		以上套接字执行读操作不会阻塞并将返回一个大于0的值(即返回准备好读入的数据);可使用SO_RCVLOWAT设置该套接
	字的低水位标记;对于TCP和UDP套接字而言,其默认值为1。
- 该连接的读半部关闭(即接收了FIN的TCP连接);
	以上套接字的读操作将不阻塞并返回0(即返回EOF)。
- 该套接字是一个监听套接字且已完成的连接数不为0;
	以上套接字的accept通常不会阻塞;
- 其上有一个套接字错误待处理;
	以上套接字的读操作将不阻塞并返回-1,同时把errno设置成确切的错误条件;这些待处理错误也可以通过指定SO_ERROR套接
	字选项调用getsockopt获取并清除;

下列四个条件中的任何一个满足时,一个套接字准备好写:
- 该套接字发送缓冲区中的可用空间字节数>=套接字发送缓冲区低水位标记的当前大小,并且或者该套接字已连接,或者该套接字
	不需要连接(如UDP套接字);这意味着如果我们把这样的套接字设置成非阻塞,写操作将不阻塞并返回一个正值(例如由传输
	层接受的字节数)。我们可以使用SO_SNDLOWAT套接字选项来设置该套接字的低水位标记。对于TCP和UDP套接字,默认值为2048。
- 该连接的写半部关闭;
	对这样的套接字的写操作将产生STGPIPE信;
- 使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终;
- 其上有一个套接字错误待处理;
	对这样的套接字的写操作将不阻塞并返回-1,同时把errno设置成确切的错误条件。这些待处理的错误也可以通过指SO_ERROR
	套接字选项调用getsockopt获取并清除。

如果一个套接字存在带外数据或者仍处于带外标记,那么它有异常条件待处理:
【注意】:当某个套接字上发生错误时,它将由select标记为既可读又可写;
【接收低水位标记和发送低水位标记的目的在于】:允许应用进程控制在select返回可读或可写条件之前有多少数据可读或有多大
	空间可用于写;
	【eg】:若知道除非至少存在64个字节的数据,否则进程没有任何有效工作可做,则把接收低水位标记设置为64,以防少
		于64个字节的数据准备好读时select唤醒我们;
任何UDP套接字只要其发送低水位标记<=发送缓冲区大小,总是可写的,由于IDP套接字不需要连接;

在这里插入图片描述

3.2 str_cli函数
可以使用select(替换fgets)重写str_cli函数,当服务器进程一终止,客户就能马上得到通知;

在这里插入图片描述

客户的套接字上的三个条件处理:
- 若对端TCP发送数据,则该套接字变为可读,并且read返回读入数据的字节数;
- 若对端TCP发送一个FIN,则该套接字变为可读,并且read返回0;
- 若对端TCP发送一个RST,则该套接字变为可读,并read返回-1,而errno含有确切的错误码;

调用select

我们只需一个用于检查可读性的描述符集;该集合由FD_ZERO初始化,并用PD_SET打开两位对应文件指针fp和sockfd;
fileno函数把标准IO文件指针转换为对应的描述符。select(和poll)只工作在描述符上。计算出两个描述符中的较大值后,调用select;
在该调用中,写集合指针和异常集合指针都是空指针。最后时间限制参数也是空指针,因为希望本调用阻塞到某个描述符就绪为止;

处理可读套接字

如果在select返回套接字是可读的,则先用readline读入回射文本行,再用fputs输出它;
void str_cli_sel(FILE *fp, int sockfd) {
    int maxFdp1;
    fd_set rset;
    char sendline[MAXLINE], recvline[MAXLINE];

    FD_ZERO(&rset);
    while (1) {
        FD_SET(fileno(fp), &rset);
        FD_SET(sockfd, &rset);
        maxFdp1 = max(fileno(fp), sockfd) + 1;	// 获取最大描述符
        select(maxFdp1, &rset, NULL, NULL, NULL);
		
        if(FD_ISSET(sockfd, &rset)) {
            if(readline(sockfd, recvline, MAXLINE) == 0)
                err_quit("str_cli: server terminated prematurely");
            fputs(recvline, stdout);
        }
		
        if(FD_ISSET(fileno(fp), &rset)) {
            if(fgets(sendline, MAXLINE, fp) == NULL)
                return;
            writen(sockfd, sendline, strlen(sendline));
        }
    }
}

处理可读输入

如果标准输入可读,则先用fget读入一行文本,在用writen写入套接字;
但该版本是由select(不是fgets)调用来驱动的,提高客户程序的健壮性;
3.3 批量输入
若客户与客户端之间的网络作为全双工管道来考虑,请求从客户发生,应答从服务器发生;

插图

客户在时刻0发出请求,时刻4时应答,时刻7时接收到;当把标准输入和输出重定向到文件中运行客户程序,
却发现输出文件总是小于输入文件;

测试批量处理
在这里插入图片描述

- 假设第一个请求后,立即发出下一个,在接着下一个;且客户能以网络可以接受他们的最快速度持续发生请求,
并能够以网络可提供给它们的最快速度处理应答;
- 需要一种关闭tcp连接其中一半的方法,给服务器发生一个FIN(提示数据已发生完成),但仍需保持套接字
	描述符打开以便读取;
【注意】:
- 当我们发生完请求,不能立即关闭连接,由于管道中还有其他的请求和应答;
- 在该方式下,读到EOF并不意味着我们同时也完成了从套接字的读入;

在这里插入图片描述

解决缓冲区问题

根据之前的代码中,为了提高性能引入缓冲机制;使用到fgets或readline,但都增加了引入到复杂性;
3.4 shutdown函数
相比close,两者都是用来终止网络连接到方法,但close由两个限制:
- close把描述符的引用计数减1,但为0时才关闭套接字;
- close终止读和写两个方向的数据传送;

在这里插入图片描述

#include <sys/socket.h>

int shutdown(int sockfd, int howto);

/**
@func: 关闭套接字连接;
@param howto: 
	SHUT_RD: 关闭连接的读这一半——套接字中不再有数据可接收,且套接字接收缓冲区中有现有数据都被丢弃;
		进程不对该套接字调用任何读函数;
	SHUT_WR: 关闭连接的写这一半——对于套接字即半关闭;
		当前留在套接字发生缓冲区的数据将被发送掉,后跟TCP的正常连接终止序列;
		进程不能对该套接字任何写操作;
	SHUT_RDWR: 关闭连接的读半部和写半部;
*/
#include “unp.h”

void str_cli_sel02(FILE *fp, int sockfd) {
    int maxfdp1, stdineof;
    fd_set rset;
    char buf[MAXLINE];
    int n;
    stdineof = 0;

    FD_ZERO(rset);  // 将集合先全部置空
    while (1) {
        /* 设置,描述符 */
        if(stdineof == 0)
            FD_SET(fileno(fp), &rset);
        FD_SET(sockfd, &rset);

        maxfdp1 = max(fileno(fp), sockfd) + 1;  // 获取最大描述符
        select(maxfdp1, &rset, NULL, NULL, NULL);

        if(FD_ISSET(sockfd, &rset)) {
            if((n == Read(sockfd, buf, MAXLINE))==0) {
                if(stdineof == 1) return;
                else err_quit("str_cli: server terminated prematurely");
            }
            writen(fileno(stdout), buf, n);
        }

        if(FD_ISSET(fileno(fp), &rset)) {
            if((n == Read(fileno(fp), buf, MAXLINE))) {
                stdineof = 1;
                shutdown(sockfd, SHUT_WR);
                FD_CLR(fileno(fp), &rset);
                continue;
            }
            writen(sockfd, buf, n);
        }
    }
}
/**
	stdineof初始化为0,每次在主循环中总是select标准输入的可读性;
	当sockfd上读到EOF时,若己在标准输入上遇到EOF即正常终止,故函数返回;
		若没有遇到EOF,则服务器进程已过早终止,该用read、write对缓冲区而不是文本行进行操作;
	当标准输入遇到EOF时,将stdineof设为1,并将第二参数指定为SHUT_WR来调用shutdown以发生FIN;
*/
3.5 TCP回射服务器程序
我们将之前的版本修改成使用select来处理任意个客户的单进程程序,而不是位每个客户派生一个子进程;

跟踪客户的数据结构
在这里插入图片描述

服务器只维护一个读描述符集,假设服务器是在前台启动的,则描述符0、1、2对应设置位标准输入、输出、错误输出;
	- 即第一个可用描述符为3;
cilent数组都初始化为-1,含有每个客户的已连接套接字描述符;

在这里插入图片描述

rset中非0项即表示监听套接字的项,故select的第一个参数为4;
当第一个客户与服务器建立连接时,监听描述符变为可读,服务器调用accept;
在本例中,由accept返回的新连接描述符是4;

在这里插入图片描述

此时,服务器必须在其clent数组中记录该新的已连接描述符,并加到描述符集中;

在这里插入图片描述

第二个客户建立连接;

在这里插入图片描述

新的已连接描述符必须被记录;

在这里插入图片描述

当第一个客户终止连接,该客户的TCP发送一个FIN,使得服务器中的描述符4变为可读;
当服务器读到该已连接套接字时,read将返回0,故关闭套接字并相应的更新数据结构,如下图;
【注意】:但maxfd的值没有改变;

在这里插入图片描述

【总结】:当客户到达时,使用client中的第一个可用项记录已连接套接字,必须将该描述符加到读描述符集中;
maxfd为client最大下标,也是select第一参数;
int main(int argv, char **argc){
    int maxi, maxfd, lfd, cfd, sfd;
    int nready, client[FD_SETSIZE];
    ssize_t n;
    fd_set rset, allset;
    char buf[MAXLINE];
    struct sockaddr_in cliaddr, servaddr;
    socklen_t clen;

    /* 创建套接字 */
    lfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);
    bind(lfd, LISTENQ);

    maxfd = lfd;
    maxi = -1;

    int i;
    for (i = 0; i < FD_SETSIZE; ++i) {
        client[i] = -1;
    }

    FD_ZERO(&allset);
    FD_SET(lfd, &allset);

    while (1) {
        rset = allset;
        nready = select(maxfd + 1, &rset, NULL, NULL, NULL);

        if(FD_ISSET(lfd, &rset)){
            clen = sizeof(cliaddr);
            cfd = accept(lfd, (struct sockaddr*)&cliaddr, &clen);
            /* 对应于client数组要设置 */
            for(i=0; i<FD_SETSIZE; ++i) {
                if(client[i] < 0){
                    client[i] = cfd;
                    break;
                }
            }

            if(i == FD_SETSIZE)
                err_quit("too many clents");

            FD_SET(cfd, &allset);
            /* 更新最大描述符 */
            if(cfd > maxfd)
                maxfd = cfd;
            if(i > maxi)
                maxi = i;
            if(--nready <= 0)
                continue;
        }

        for(i=0; i<=maxi; ++i) {
            if((sockfd = client[i]) < 0)
                continue;
            if(FD_ISSET(sockfd, &rset)) {
                if((n == Read(sockfd, buf, MAXLINE))){
                    close(sockfd);
                    FD_CLR(sockfd, &allset);
                    client[i] = -1;
                }else{
                    writen(sockfd, buf, n);
                }
                if(--nready <= 0)
                    break;
            }
        }
    }

    return 0;
}
【创建监听套接字并为调用select进行初始化】:socket、bind、listen;
【阻塞于select】:select等待某个时间发生:或是新客户连接的建立,或是数据、FIN或RST的到达;
【accept新的连接】:若监听套接字变为可读,则已建立了新连接;我们调用accept并相应地更新数据结构,使用client中的第一个未用项记录这个已连接描述
	符;就绪描述符数目减1,若其值变为0,即避免进入下一个for循环;让我们可使用select的返回值来避免检查未就绪的描述符;
【检查现有连接】:对于每个现有连接,要测试其描述符是否在select返回的描述符集中;若是,就从该客户读入一行文本并回射给它;
	如果该客户关闭了连接,则返回0,更新数据结构;
	我们从不减少maxi的值,当有客户关闭其连接时,我们可以检查是否存在这样的可能性;
3.6 拒绝服务型攻击

当有一个恶意的客户连接到该服务器,发送一个字节的数据后进入睡眠,将会发生什么呢?

- 服务器将调用read,它从客户读入这个单字节的数据,然后阻塞于下一个read调用,以等待来自该客户的其余数据;因此服务器被该客户阻塞(挂起),
	不能为其他任何客户提供服务,直到那个恶意客户发出一个换行符或者终止为止;

【概念】:当一个服务器在处理多个客户时,不能阻塞于只与单个客户相关的某个函数,否则可能导致服务器被挂起;
【解决方法】:
	- 使用非阻塞式I/O;
	- 让每个客户由单独的控制线程提供服务;
	- 对IO操作设置一个超时;

4、pselect函数

#include <sys/select.h>
#include <signal.h>
#include <time.h>

int pselect(int maxfdp1, fd_Set *readset, fd_set *writeset, fd_set *exceptset, 
			const struct timespec *timeout, const sigset_t *sigmask);
/**
return: 若有就绪描述符则为其数目,若超过则为0,若出错则为-1;
*/
struct timespec {
	time_t tv_sec;
	long tv_nsec;		// 纳秒
};
【相比于select】:
- pselect使用timespec结构;
- pselect增加了第六个参数,指向信号掩码的指针,该参数允许程序先禁止递交某些信号,在测试当前被禁止信号的信号处理函数设置的全局变量,
	在调用pselect,告诉它重新设置信号掩码;
4.1 第六参数

select中

if(intr_flag) handle_init();
if((nready = select(..)) < 0) {
	if(errno == EINTR){
		if(intr_flag) handle_intr();
	}
}
上述函数中,该信号SIGINT处理函数仅仅设置全局变量intr_flag并返回;
若进程阻塞与select调用,则从信号处理函数的返回将导致select返回EINTR错误;

pselect

sigset_t newmask, oldmask, zeromask;
sigemptyset(&zeromask);
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);

sigprocmask(SIG_BLOCK, &newmask, &oldmask);
if(intr_flag) handle_intr();
if((nready = pselect(...,&zeromask)) < 0){
	if(errno == EINTR) {
		if(intr_flag) handle_intr();
	}
}
在测试intr_flag变量前,我们阻塞SIGINT,当pselect被调用时,它先以空集替代进程的信号掩码,在检查描述符,并可能进入睡眠;
当pselect函数返回时,进程的信号掩码又被重置为调用pselect之前的值;

5、poll函数

poll相比于select,支持自定义fd数目;
#include <poll.h>

int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
/**
@func: 该函数与select功能类似,处理流设备,能够提供额外的信息; 
@param fdarray: 用于测试某个给定描述符fd的条件;
@param timeout: 等待多长时间;
return: 错误返回-1,超时,返回0,成功返回revents成员值非0的描述符个数;
若不关心某个描述符,则将pollfd中fd设置为负;则revents将被置0;
*/

struct pollfd {
	int fd;		
	short events;	// 测试的条件
	short revents;	// 返回该描述符的状态
};

【poll识别三类数据】:普通、优先级带、高优先级;

在这里插入图片描述
在这里插入图片描述

对于TCP和UDP套接字,以下条件引起poll返回特定的revent

- 所有正规TCP、UDP数据都被认为是普通数据;
- TCP的带外数据被认为是优先级带数据;
- 当TCP连接的读半部关闭时;也被认为是普通数据,随后的读操作将返回0;
- TCP连接存在错误可认为是普通数据或错误;随后的读操作将返回-1,并把errno设置成合适的值;
	这可用于处理诸如接收到RST或发生超时等条件;
- 在监听套接字上有新的连接可用既可认为是普通数据(大多)或优先级数据;
- 非阻塞式connect的完成被认为是使相应套接字可写;
结构数组中元素的个数是由nfds参数指定;
5.1 TCP回射服务器程序
#include "../Jxiepc/unp.h"
#include <limits.h>

#define OPEN_MAX 128


int main(int argv, char **argc) {

    int i, maxi, lfd, cfd, sfd;
    int nready;
    ssize_t n;
    char bur[MAXLINE];
    socklen_t clen;
    struct pollfd client[OPEN_MAX];
    struct sockaddr_in c_addr, s_addr;

    lfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&s_addr, sizeof(s_addr));
    s_addr.sin_family = AF_INET;
    s_addr.sin_addr.s_addr = htonl(INADDR_ANT);
    s_addr.sin_port = htons(SERV_PORT);

    bind(lfd, (struct sockaddr*)s_addr, sizeof(s_addr));

    listen(lfd, LISTENQ);

    client[0].fd = lfd;
    client[0].events = POLLRDNORM;
    for (i = 1; i < OPEN_MAX; ++i) {
        client[i].fd == -1;
    }
    maxi = 0;
    while (1) {
        nready = poll(client, maxi + 1, INFTIM);
        if(client[0].revents & POLLRDNORM) {
            clen = sizeof(c_addr);
            cfd = accept(lfd, (struct sockaddr*)&c_addr, &clen);

            for (i = 1; i < OPEN_MAX; ++i) {
                if(client[i].fd < 0) {
                    client[i].fd = cfd;
                    break;
                }
            }
            if(i==OPEN_MAX)
                err_quit("too many clients");
            if(i>maxi) maxi = i;
            if(--nready <= 0)
                continue;
        }
        for (i = 1; i < maxi; ++i) {
            if((sockfd = client[i].fd) < 0)
                continue;
            if(client[i].revents & (POLLRDNORM | POLLERR)) {
                if((n = Read(sockfd, buf, MAXLINE)) < 0) {
                    if(errno == ECONNRESET) {
                        close(sockfd);
                        client[i].fd = -1;
                    }else {
                        err_sys("read error");
                    }
                }else if(n == 0){
                    close(sockfd);
                    client[i].fd = -1;
                }else {
                    writen(sockfd, buf, n);
                }
                if(--nready <= 0) break;
            }
        }
    }

    return 0;
}
【调用poll,检查新连接】:
	调用poll以等待新或现有连接上有数据可读;当新的连接被接受后,在client中查找第一个成员为负的可用项;
	注意:从1开始,因为client[0]固定用于监听套接字;找到可用项后,把新连接的描述符保存到其中,并设置POLLRDNORM事件;

【检查某个现有连接上的数据】:
	检查的两个返回事件是POLLRDNORM、POLLERR;
	其中我们并没有在event成员中设置第二个事件,由于它在条件成立时总是返回;
	检查POLLERR的原因在于:有些实现在一个连接上接收到RST时返回的是POLLERR事件,其他实现只是POLLRDNORM事件;
	不论哪种情形,都调用read,当有错误时,read将返回这个错误;
	当一个现有连接由它的客户终止时,我们就把它的fd成员置为-1;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jxiepc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值