关于select , pselect , poll

    处理多个输入的时候,需要使得内核一旦发现进程指定的一个或多个I/O条件就绪(即输入准备好被读取,或描述符已能承接更多输出),它就通知进程
        此即为 I/O 复用


    应用场合:
        1、客户处理多个描述符时(交互式输入+网络套接字)
        2、客户同时处理多个套接字
        3、一个TCP服务器既要处理监听套接字,又要处理已连接套接字
        4、服务器既要处理TCP,又要UDP
        5、服务器处理多个服务或多个协议


int select(int maxfdpl, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
    (一)
        struct timeval
        {
            long tv_sec ;    //sec
            long tv_usec;    //micro-sec
        };
        1、为空: 永远等下去
        2、等待一段固定时间
        3、根本不等待 定时器值必须为 0
    (二)
        支持的异常条件:
            1、某个套接字的带外数据到达
            2、某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息
    (三)
        要用到的宏函数:
            1、void FD_ZERO(fd_set *fdset)            //clear all
            2、void FD_SET(int fd,fd_set *fdset)    //turn on
            3、void FD_CLR(int fd,fd_set *fdset)    //turn off
            4、int  FD_ISSET(int fd,fd_set *fdset)    //test
    (四)
        该函数返回后,用FD_ISSET来测试描述符,描述符集内任何与未就绪描述符对应的位返回时均清成0.
        为此,每次重新调用select函数,都得【再次】把所有所关心的描述符位置一
    (五)
        返回已就绪的总位数





因此我们将客户端读取函数改成这样:
void str_cli(FILE *fp, int sockfd)
{
    int maxfdpl;
    fd_set reset;
    char sendling[MAX],receive[MAX];

    FD_ZERO(reset);
    for(;;)
    {
        FD_SET(fileno(fp),&reset);
        FD_SET(sockfd, &reset);
        maxfdlp = max(fileno(fp),sockfd)+1;
        Select(maxfdlp, &reset,NULL,NULL,NULL);
    
        if(FD_ISSET(sockfd,&rerset))
        {
            if(Readline(sockfd,receive,MAX) == 0)
                err_quit();
            Fputs(buf,stdout);
        }
        if(FD_ISSET(fileno(fp),&reset))

        {
            if(Fgets() == NULL)        
                return;
            Writen(sockfd,sending,strlen(sending));
        }
    }
}




批量输入:
    采用一问一答的方式虽然很符合回射客户端/服务器的要求,但是既然套接字是全双工通信,我们可以将标准输入和标准输出替换为文件,充分利用。
        但是,最后发现输出文件总是小于输入文件(对于回射服务器,他们理应相等)
        问题在于我们对标准输入中的EOF的处理
             str_cli函数就此返回到main函数,而main函数随后终止。然而在这种批量方式下,标准输入中的EOF并不意味着我们同时也完成了从套接字的读入:可能仍有请求在去往服务器的路上,或仍有请求在返回客户端的路上
    
     我们需要的是一种关闭TCP连接其中一半的方法。也就是说,我们想给服务器TCP发送一个FIN,告诉服务器我们已经完成了发送数据,但是仍然保持该套接字描述符打开以便读取



P135 讲述了select 与缓冲区之间的微妙处理关系,但我没怎么看懂


终止网络连接常用close,不过close有两个限制,却可以使用shutdown函数来避免。
    1、close把描述符的引用计数减1,仅在该计数为0时关闭描述符。使用shutdown可以不管引用计数就激发TCP的正常连接终止序列(由FIN开始的4个字节)
    2、close终止读和写两个方向的数据传输。既然TCP是全双工的,有时候我们需要告知对端我们已经完成了数据发送,即使对端仍有数据要发送给我们。即上面批量输入遇到的问题。
int shutdown(int sockfd, int how)
    参数howto:
        A、SHUT_RD
            关闭连接的读这一半  ----  套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数,对一个TCP套接字调用该操作后,由该套接字接收的来自对端的任何数据都被确认,然后悄然丢弃。。。
        B、SHUT_WR
            关闭写这一半   -----   对于TCP套接字,这称为半关闭。当前留在套接字发送缓冲区的数据将被发送掉,后跟TCP正常终止序列。不管套接字描述符的引用计数是否为0,写半部分关闭照样执行,不能再对这个套接字调用任何写函数
        C、SHUT_RDWR
            都关闭



修改后,如下:
void str_cli(FILE *fp, int sockfd)
{
    int maxfdlp, stdineof;
    fd_set rset;
    int nbyte;

    FD_ZERO(&rset);
    stdineof = 0;
    for(;;)
    {
        maxfdlp = max(fileno(fp),sockfd)+1;
        if(stdinof == 0)
            FD_SET(fileno(fp), &rset);
        FD_SET(sockfd, &rset);
        if(select(maxfdlp,&rset,NULL,NULL,NULL) < 0)
        {
            perror("select");
            exit(1);
        }

        if(FD_ISSET(sockfd, &rset))
        {
            if((nbyte=read(sockfd,buf,MAXLEN)) == 0)
            {
                if(stdineof == 1)
                    return;                //normal end
                else
                {                    //service send FIN to this client unexpectedly
                    fprintf(stderr,"Service Terminated Unexpected\n");
                    exit(1);
                }
            }
            else if(nbyte < 0)
            {
                perror("Read Error");
                exit(1);
            }

            if(write(1,buf,nbyte) != nbyte)
            {
                perror("Write Error");
                exit(1);
            }
        }

        if(FD_SET(fileno(fp),&rset))
        {
            if((nbyte=read(fileno(fp),buf,MAXLEN)) == 0)
            {
                stdineof = 1;
                shutdown(sockfd, SHUT_WR);        //send FIN to service to close half-TCP about write
                                    //which means we would not send data anymore.
                FD_CLR(fileno(fp),&rset);
                continue;
            }
            else if(nbyte < 0)
            {
                perror("Read Error");
                exit(1);
            }

            if(written(sockfd,buf,nbyte) != nbyte)
            {
                perror("Write Error");
                exit(1);
            }
        }
    }
}




既然客户端用select函数进行了更改,服务器自然也可以:
#include "unp.c"

int main(int ac, char *av[])
{
    struct sockaddr_in serv_addr,clin_addr;
    int serv_fd, clin_fd;
    fd_set rset, allset;
    char table[FD_SETSIZE];                            //用一个队列将所有客户的套接字存储下来
    int maxi = -1;                                //对应于拥有最大描述符的数组的索引
    int maxfd = -1;                                //最大描述符(select第一个参数要用)
    int nready;                                    //select将返回的准备好的描述符个数
    int i_loop;
    socklen_t clin_len;
    char comm_buf[MAXLEN];
    int nread, nwrite;

    if((serv_fd=socket(AF_INET,SOCK_STREAM,0)) < 0)
        oops("socket error");
    
    bzero(&serv_addr, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(13000);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if(bind(serv_fd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) < 0)
        oops("bind error");
    
    if(listen(serv_fd, 10) < 0)
        oops("listen error");
    
    maxfd = serv_fd;                                    //当前最大描述符当然是用于监听的套接字
    for(i_loop=0; i_loop<FD_SETSIZE; i_loop++)
        table[i_loop] = -1;
    FD_ZERO(&allset);            
    FD_SET(serv_fd,&allset);                                //设置一个allset,且在循环中令一个变量等于它,就不需要每次都重置了

    for(;;)
    {
        rset = allset;
        nready = select(maxfd+1, &rset, NULL, NULL, NULL);
        
        if(nready == -1)
            oops("select error");
        
        if(FD_ISSET(serv_fd,&rset))
        {
            clin_len = sizeof(clin_addr);
            if((clin_fd=accept(serv_fd, (struct sockaddr *)&clin_addr, &clin_len)) < 0)
                oops("accept error");

            for(i_loop=0; i_loop<FD_SETSIZE; i_loop++)            //挑一个空位给这个新套接字
                if(table[i_loop] < 0)
                {
                    table[i_loop] = clin_fd;
                    break;
                }

            FD_SET(clin_fd,&allset);                        //并将这个新套接字设置进这个组中

            if(maxfd < clin_fd)
                maxfd = clin_fd;

            if(maxi < i_loop)                            //取到最大的下标,用于接下来的循环
                maxi = i_loop;

            if(--nready == 0)                            //若等于0了,就无需再经历下面的循环
                continue;
        }

        for(i_loop=0; i_loop<=maxi; i_loop++)
        {
            if(table[i_loop] < 0)
                continue;

            if(FD_ISSET(table[i_loop], &rset))
            {
                if((nread=read(table[i_loop], comm_buf, MAXLEN)) > 0)
                    if(written(table[i_loop], comm_buf, nread) != nread)
                        oops("write error");

                if(nread < 0)
                    oops("read error")
                else if(nread == 0)                //connection closed by client
                {
                    close(table[i_loop]);
                    FD_CLR(table[i_loop],&allset);
                    table[i_loop] = -1;
                }
                
                if(maxi == i_loop)
                {
                    maxfd = -1;
                    for(i_loop=0; i_loop<=maxi; i_loop++)
                        if(maxfd < table[i_loop])
                            maxfd = table[i_loop];
                }


                if(--nready == 0)            //a better solution
                    break;
            }
        }
    }
}




//    here I try pselect instead of select
//    Given that one global variation "intr_flag" will be changed if signal "SIGINT" occures
//    and the global variation will be used

/*    sigset_t set,nset,oset;
    sigemptyset(&nset);
    sigemptyset(&set);
    sigaddset(&nset,SIGINT);
    sigprocmask(SIG_BLOCK,&nset,&oset);
    if(int_flag)
        handle_intr();                //handle the signal
    if((nready = pslect(.....,&set)) == 0)
    {
        if(errno == EINTR)
            if(intr_flag)
                handle_intr();
    }

*/
/*关于信号处理的部分,之前在APUE中已经有了一定的概念。
在测试intr_flag变量之前,我们阻塞SIGINT,当pselect被调用时,它先以空集代替进程的信号掩码,再检查描述符,并可能进入睡眠;当pselect返回时,进程的信号掩码又被重置为调用pselect之前的值*/







关于 POLL
int poll(struct pollfd *fdarray,long nfds,int timeout);
struct pollfd{
    int fd;
    short events;
    short revents;
};
就TCP和UDP套接字而言,以下条件引起poll返回特定的revent:
    1、所有正规TCP数据和所有UDP数据都被认为是普通数据
    2、TCP带外数据被认为是优先级数据
    3、当TCP连接的读半部关闭时,也被认为是普通数据,随后读操作返回0
    4、TCP连接存在错误既可认为是普通数据,也可以是错误。无论如何,读操作返回-1,并设置errno。可用于处理RST或超时等条件
    5、在监听套接字上有新的连接可用既可以是普通数据(大多数),也可以是优先数据
    6、非阻塞式connect的完成被认为是使相应套接字可写

timeout:
    1、INFTIM    永远等待
    2、       0    立即返回
    3、      >0    等待指定数目的毫秒



使用poll改写服务器:
#include "unp.h"

#define OPEN_MAX 100
#define INFTIM -1

int main(int ac, char *av[])
{
    struct sockaddr_in serv_addr,clin_addr;
    struct pollfd client[OPEN_MAX];                    //poll要使用的参数
    int serv_fd, clin_fd;
    int nready;                                    //select将返回的准备好的描述符个数
    int maxi;
    int i_loop;
    socklen_t clin_len;
    char comm_buf[MAXLEN];
    int nread, nwrite;

    if((serv_fd=socket(AF_INET,SOCK_STREAM,0)) < 0)
        oops("socket error");
    
    bzero(&serv_addr, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(13000);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if(bind(serv_fd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) < 0)
        oops("bind error");
    
    if(listen(serv_fd, 10) < 0)
        oops("listen error");
    
    client[0].fd = serv_fd;
    client[0].events = POLLRDNORM;
    for(i_loop=0; i_loop<OPEN_MAX; i_loop++)
        client[i_loop].fd = -1;                            //poll会自动忽略fd为-1的参数
    maxi = 0;

    for(;;)
    {
        if((nready=poll(client, maxi+1, INFTIM)) == -1)
            oops("poll error");

        if(client[0].revents & POLLRDNORM)                    //检测是否有普通数据
        {
            clin_len = sizeof(clin_addr);
            if((clin_fd=accept(serv_fd, (struct sockaddr *)&clin_addr, &clin_len)) == -1)
                oops("accept error");

            for(i_loop=0;i_loop<OPEN_MAX;i_loop)
                if(client[i_loop].fd < 0)
                {
                    client[i_loop].fd = clin_fd;
                    client[i_loop].events = POLLRDNORM;
                }

            if(i_loop == OPEN_MAX)
            {
                fprintf(stderr,"too many descriptors\n");
                exit(1);
            }

            if(i_loop > maxi)    
                maxi = i_loop;

            if(--nready <= 0)
                continue;
        }

        for(i_loop=1; i_loop<=maxi; i_loop++)        //0固定为监听用的,之前select从0开始是因为数组元素全是存客户套接字的
        {
            if(client[i_loop].fd < 0)
                continue;
            if(client[i_loop].revents & (POLLRDNORM | POLLERR) )
            {
                if((nread=read(client[i_loop].fd, comm_buf, MAXLEN)) > 0)
                    if(written(client[i_loop].fd, comm_buf, nread) != nread)
                        oops("write error");

                if(nread < 0)
                {
                    if(errno == ECONNRESET)
                    {
                        close(client[i_loop].fd);
                        client[i_loop].fd = -1;
                    }
                    else
                    {
                        perror("read error");
                        exit(1);
                    }
                }
                else if(nread == 0)                //connection closed by client
                {
                    close(client[i_loop].fd);
                    client[i_loop].fd = -1;
                }
            }

            if(--nready <= 0)
                break;
        }
    }

    return 0;
}
//这样看下来,其实差别不大,但之前select使用的数组不再需要了,简化了些


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值