《UNIX网络编程:套接字联网API》啃书笔记(第6章select函数、第7章套接字选项)

5种I/O模型

对于一个套接字上的输入操作而言,第一步通常为等待数据从网络中到达,当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

阻塞式I/O模型
阻塞式I/O模型
如图,当进程调用recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误才返回。也就是说进程在从调用recvfrom开始到它返回的整段时间内是被阻塞的。

非阻塞I/O模型
进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把进程投入睡眠,而是返回一个错误。
非阻塞I/O模型
如图,应用进程对一个非阻塞描述符循环调用recvfrom,我们称之为轮询。应用进程持续轮询内核,以查看某个操作是否就绪。

I/O复用模型
在第5章给出的TCP回射程序中,我们遇到的主要问题在于客户阻塞与fgets调用期间,服务器会被杀死,服务器虽然正确的向客户发送了FIN,但客户看不到这个EOF直到从套接字读时为止。这样的进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪,他就通知进程,这个能力被称为I/O复用。

I/O复用通过调用select或poll阻塞在这两个系统调用中的某一个上而不是阻塞在真正的I/O系统调用上。
I/O复用模型

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

信号驱动式I/O模型
用信号让内核在描述符就绪时发送SIGIO信号通知我们。
信号驱动式I/O模型

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

此模型的优势某过于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。

异步I/O模型
罕见,不做介绍。

各种I/O模型对比
5中I/O模型比较

select函数

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);

timeout参数
timeout参数告知内核等待所指定描述符中的任何一个就绪可花多长时间。

其timeval结构用于指定这段时间的秒数和微秒数:

struct timeval{
    long tv_sec;             //秒数
    long tv_usec;            //微秒数
};

此参数有以下三种可能:

  1. 永远等待下去:仅在有一个描述符准备好I/O时才返回,把该参数设置为空指针即可。
  2. 等待一段固定时间:在有一个描述符准备好I/O时返回,但不超过参数结构指定的秒数和微秒数。
  3. 根本不等待:检查描述符后立即返回,即轮询。将参数结构中的定时器值设置为0。

由于内核支持的时间分辨率和调度延迟问题,定时器到后可能还会花点时间来调度相应进程运行

timeout参数不会被select修改,即即使定时器到之前select就返回了,timeou参数所指向的timeval结构不会被更新成该函数返回时剩余的秒数。

中间三fd_set类型参数
readset、writeset、exceptset指定我们要让内核测试读、写和异常条件的描述符。

目前支持的异常条件只有两个:
1. 某个套接字的带外数据的到达
2. 某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息

select使用描述符集,通常是一个整数数组,其中每个整数中的每一位对应一个描述符。我们分配一个fd_set数据类型的描述符集,并以下4个宏设置或测试该集合中的每一位,也可以直接赋值成另外一个描述符集:

void FD_ZERO(fd_set *fdset);                //清除fdset中的所有bit
void FD_SET(int fd,fd_set *fdset);          //打开fdset中的第fd个bit
void FD_CLR(int fd,fd_set *fdset);          //关闭fdset中的第fd个bit
int FD_ISSET(int fd,fd_set *fdset);         //测试fdset中的第fd个bit是否打开了

注意描述符是从0开始计数的。

三个参数中,如果我们对某一个的条件不感兴趣,就可以把它设为空指针。

若把这三个指针都设为空,则select退化成了一个微秒级别的定时器

参数maxfdp1
maxfdp1参数指定待测试的描述符个数,即描述符序号为0,1,2,3,…,maxfdp1-1。常值FD_SETSIZE是数据类型fd_set中的描述符总数,其值通常是1024。

返回值
select函数返回时,三个fd_set指针参数将指示哪些描述符已就绪,我们使用FD_ISSET宏来测试fd_set数据类型中的描述符。描述符集内任何与未就绪描述符对应的位返回时均清成0,因此,每次重新调用select函数时,我们都得再次把所有描述符集内所关心的位均置为1。

select函数的返回值表示跨所有描述符集的已就绪的总位数;如果在任何描述符就绪之前定时器到时,那么返回0;若出错则返回-1。

描述符就绪条件
满足下列四个条件中的任何一个时,一个套接字准备好读:

  1. 该套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的当前大小。返回一个大于0的值(也就是返回准备好读入的数据)。
  2. 该连接的读半部关闭(即接收了FIN)。返回值0(即返回EOF)。
  3. 该套接字是一个监听套接字且已完成的连接数不为0。
  4. 有一个套接字错误待错误,返回-1。

满足下列四个条件中的任何一个满足时,一个套接字准备好写:

  1. 该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小,且或者该套接字已连接,或者该套接字不需要连接。返回一个正值。
  2. 该连接的写半部关闭。若继续对套接字进行写操作将产生SIGPIPE信号。
  3. 使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终。
  4. 有一个套接字错误待处理,返回-1、

若一个套接字存在带外数据或仍处于带外标记,那么它有异常条件待处理。

注:
1. 当某个套接字上发送错误时,它将由select标记为既可读又可写。
2. 接收低水位标记和发送低水位标记的目的在于:允许应用进程控制在select返回可读或可写条件之前有多少数据可读或有多大空间可用于写。
3. 我们可以使用SO_RCVLOWAT套接字选项设置该套接字的低水位标记,对于TCP和UDP套接字而言其默认值为1。
4. 我们可以使用SO_SNDLOWAT套接字选项来设置该套接字的低水位标记,对于TCP和UDP套接字而言其默认值为2048。

任何UDP套接字只要其发送低水位标记小于等于发送缓冲区大小就总是可写的,因为UDP套接字不需要连接。

就绪条件小结:
套接字就绪条件小结

shutdown函数
注意在之前的str_cli函数中我们发现服务器并不能批量的处理客户发送过来的文本,也就是说假设客户输入的文本有9行,服务器每次只返回一行导致其余行仍在缓冲区中,然后select再次被调用以等待新的工作。另外原来的str_cli函数当遇到EOF时会就此返回到main函数,而main函数随后终止;但在批量方式下,标准输入中的EOF并不意味着我们同时也完成了从套接字的读入,可以仍有请求在去往服务器的路上,或仍有应答在返回客户的路上。

为此,我们需要一种半关闭TCP连接的方法:给服务器发送一个FIN,告诉它我们已经完成了数据发送,但是仍然保持套接字描述符打开以便读取。

终止网络连接的通常方法是调用close函数,但close有两个限制:

  1. close把描述符的引用计数减1,仅在该计数变为0时才关闭套接字。
  2. close终止读和写两个方向的数据传送。

shutdown函数可避免以上两种限制:

  1. 使用shutdown可以不管引用计数就激发TCP正常连接终止序列
  2. TCP连接是全双工的,shutdown只是通知对端我们已经完成了数据发送,对端仍然可以把数据发送给我们
#include<sys/socket.h>
int shutdown(int sockfd,int howto);

若成功则返回0,若出错则为-1。

该函数的行为依赖于howto参数的值:

  • SHUT_RD: 关闭连接的读这一半—套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数。对一个TCP套接字这样调用shutdown函数后,由该套接字接收的来自对端的任何数据都被确认,然后悄然丢弃。典型值为0。
  • SHUT_WR:关闭连接的写这一半—对于TCP套接字,这称为半关闭(half-close)。当前留在套接字发送缓冲区中的数据将被发送掉,后跟TCP的正常连接终止序列,不管套接字描述符的引用计数是否等于0。进程不能再对这样的套接字调用任何写函数。
  • SHUT_RDWR:连接的读半部和写半部都关闭—相当于调用两次shutdown,第一次调用指定SHUT_RD,第二次调用指定SHUT_WR。

str_cli函数修订

void str_cli(FILE *fp,int sockfd) {
    int maxfdp1,stdineof;
    fd_set rset;
    char buf[MAXLINE];                     //文本缓冲区
    int n;

    stdineof=0;                            //标志输入是否为EOF
    FD_ZERO(&rset);                        //初始化检查可读性的描述符集
    while(true){
        if(stdineof==0)                    //若没遇到EOF,则打开fp的描述符 
            FD_SET(fileno(fp),&rset);      //fileno函数把标准I/O文件指针转换为对应的描述符
        FD_SET(sockfd,&rset);              //打开套接字sockfd的描述符
        maxfdp1=max(fileno(fp),sockfd)+1;  //待测试的描述符个数
        select(maxfdp1,&rset,NULL,NULL,NULL);//阻塞select到某个描述符就绪为止

        if(FD_ISSET(sockfd,&rset)){        //如果是套接字是可读的
            if((n==read(sockfd,buf,MAXLINE))==0){//从缓冲区读入回射文本,当我们在套接字上读到EOF时
                if(stdineof==1) 
                    return;                //若已在标准输入上遇到EOF,则为正常的终止,函数返回
                else                       //没有遇到则表明服务器过早终止
                    err_quit("str_cli:server terminated prematurely");
            }
            write(fileno(stdout),buf,n);   //数据标准输出
        }

        if(FD_ISSET(fileno(fp),&rset)){    //若输入是可读的
            if((n==read(fileno(fp),buf,MAXLINE))==0){//从输入中读取文本到缓冲区,若遇到EOF时
                stdineof=1;                //把标志设置为1表明输入遇到了EOF
                shutdown(sockfd,SHUT_WR);  //半关闭,发送FIN表明此端再无数据发送
                FD_CLR(fileno(fp),&rset);  //关闭fp描述符表明再无数据可写
                continue;
            }
            write(sockfd,buf,n);          //将发送缓冲区的数据写入到套接字
        }
    } 
}

TCP回射服务器select版

int main(int argc,char **argv){
    int i,maxi,maxfd,listenfd,connfd,sockfd;
    int nready,client[FD_SETSIZE];
    ssize_t n;
    fd_set rset,allset;
    char buf[MAXLINE];
    socklen_t clilen;
    struct sockaddr_in cliaddr,servaddr;

    listenfd=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(9877);

    bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));

    listen(listenfd,LISTENQ);

    maxfd=listenfd;                         //最大描述符数目
    maxi=-1;                                //最大已连接描述符数,即最大客户数
    for(i=0;i<FD_SETSIZE;i++)
        client[i]=-1;                       //client数组包含每个客户的已连接套接字描述符
    FD_ZERO(&allset);
    FD_SET(listenfd,&allset);               //设置监听描述符
    //以上都是初始化工作
    while(true){
        rset=allset;                        //设置套接字描述符
        nready=select(maxfd+1,&rset,NULL,NULL,NULL); //nready表示已就绪的描述符数目

        //监听到新的客户连接
        if(FD_ISSET(listenfd,&rset)){       
            clilen=sizeof(cliaddr);
            connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&clilen);//建立连接

            for(i=0;i<FD_SETSIZE;i++)
                if(client[i]<0){            //找到第一个可用项
                    client[i]=connfd;       //记录已连接套接字的描述符
                    break;
                }
            if(i==FD_SETSIZE)              //已连接客户数已达上限,报错
                err_quit("too many clients");

            FD_SET(connfd,&allset);        //把新的已连接套接字描述符加到读描述符集中

            if(connfd>maxfd) maxfd=connfd;//更新最大描述符数
            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))==0){//读取数据到缓冲区,若为FIN
                    close(sockfd);       //关闭连接
                    FD_CLR(sockfd,&allset);//关闭该描述符
                    client[i]=-1;        //关闭已连接描述符
                }else
                    write(sockfd,buf,n); //从缓冲区写入数据到套接字发送

                if(--nready<=0)          //已处理则就绪数减一
                    break;               //没有已就绪的描述符,继续监听
            }
        }
    }
}

拒绝服务型攻击:
对于上面程序,若有一个恶意的客户连接到该服务器,发送一个字节的数据(非换行符)后进入睡眠,服务器将调用read,它从客户读入这个单字节的数据,然后阻塞于下一个read调用,以等待来自该客户的其余数据。服务器于是因为这么一个客户而被阻塞(挂起),不能再为其它任何客户提供服务(无论新连接或现客户数据),直到那个恶意客户发出一个换行符或者终止为止。

当一个服务器在处理多个客户时,它绝对不能阻塞于只与单个客户相关的某个函数调用。否则可能导致服务器被挂起,拒绝为所有其他客户提供服务。这就是拒绝服务型攻击。

可解决办法包括:(a)使用非阻塞式I/O。(b)让每个客户由单独的控制线程提供服务。(c)对I/O操作设置一个超时。

poll函数

poll提供的功能与select类似,不过在处理流设备时,它能够提供额外的信息。

#include<poll.h>
int poll(struct pollfd *fdarray,unsigned long nfds,int timeout);

参数fdarray
第一个参数是指向一个结构数组的第一个元素的指针,每个数组元素都是一个pollfd结构,用于指定测试某个给定描述符fd的条件:

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

这两个成员中的每一个都由指定某个特定条件的一位或多位构成,以下为指定events标志以及测试revents标志的一些常值:
读条件:

常值 说明
POLLIN 普通或优先级带数据可读
POLLRDNORM 普通数据可读
POLLRDBAND 优先级带数据可读
POLLPRI 高优先级数据可读

写条件:

常值 说明
POLLOUT 普通数据可写
POLLWRNORM 普通数据可写
POLLWRBAND 优先级带数据可写

revents返回的错误标志:

常值 说明
POLLERR 发生错误
POLLHUP 发生挂起
POLLNVAL 描述符不是一个打开的文件

poll识别三类数据:普通(normal)、优先级带(priority band)、高优先级(high priority)。就TCP和UDP套接字而言,以下条件引起poll返回特定的revent:

  • 所有正规TCP数据和所有UDP数据都被认为是普通数据
  • TCP的带外数据被认为是优先级带数据
  • 当TCP连接的读半部关闭时也被认为是普通数据,但随后的读操作将返回0
  • TCP连接存在错误既可认为是普通数据也可认为是错误,无论哪种随后的读操作将返回-1
  • 在监听套接字上有新的连接既可认为是普通数据也可认为是优先级数据
  • 非阻塞式connect的完成被认为是使相应套接字可写

参数nfds
结构数组中元素的个数,内核无需知道pollfd结构的大小

参数timeout
指定poll函数返回前等待多长时间,它是一个指定应等待毫秒数的正值。若为INFTIM则表示永远等待,常被定义为一个负值,若为0表示立即返回,不阻塞进程。

返回值
当发生错误时,poll函数的返回值为-1,若定时器到时之前没有任何描述符就绪,则返回0,否则返回就绪描述符的个数,即revents成员值非0的描述符个数。

若我们不再关心某个特定描述符,那么可以把与它对应的pollfd结构的fd成员设置成一个负值。poll函数将忽略这样的pollfd结构的events成员,返回时将它的revents成员的值置为0。

poll函数的使用

client[0].fd=listenfd;               //设置监听描述符
client[0].events=POLLRDNORM;         //设置监听描述符的事件为普通数据可读

if(client[0].revents & POLLRDNORM){} //当有新的连接

client[i].fd=connfd;                 //设置已连接套接字的描述符
client[i].events=POLLRDNORM;         //设置事件为普通数据可读

close(sockfd);
client[i].fd=-1;                     //关闭描述符

套接字选项(粗学)

getsockopt和setsockopt函数

#include<sys/socket.h>
int getsockopt(int sockfd,int level,int optname,void *optval,socklen_t *optlen);
int setsockopt(int sockfd,int level,int optname,const void *optval,socklen_t optlen);

若成功则返回0,出错则返回-1。

  1. sockfd必须指向一个打开的套接字描述符
  2. level指定系统中解释选项的代码,或为通用套接字代码,或为某个特定于协议的代码
  3. optval是一个指向某个变量(*optval)的指针,setsockopt从*optval中取得选项待设置的新值,getsockopt则把已获取的选项当前值存放到*optval中
  4. *optval的大小由最后一个参数optlen指定,它对于setsockopt是一个值参数,对于getsockopt是一个值-结果参数。

套接字选项粗分为两大基本类型:一种是启用或禁止某个特性的二元选项(称标志选项),二是取得并返回我们可以设置或检查的特定值的选项(称值选项)。

  1. 当给标志选项调用getsockopt函数时,*optval是一个整数,*optval中返回的值为0表示相应选项被禁止,不为0表示相应选项被启用。类似的,setsockopt函数需要一个不为0的*optval值来启用选项,一个为0的*optval值来禁止选项。
  2. 若为值选项则相应选项用于在用户进程与系统之间传递所指定数据类型的值。

套接字层和IP层的套接字选项汇总

传输层的套接字选项汇总

猜你喜欢

转载自blog.csdn.net/sinat_30477313/article/details/80314433
今日推荐