【网络编程小结(三)】IO多路复用(1)

在我们之前的例子中,客户端把id发送过去,服务器端接收并处理这个id,总的来说客户端十分的简单,仅仅是,提前设定好一些消息,并且发送过去,然后等待响应。

但是当我们有一种这样的客户端——等待用户输入,然后把输入内容传递给服务端。这样的话,传递给服务端的内容是由客户自己决定的,而不是一条固定的消息。

有了这种场景以后,可以想象客户端程序要迭代地处理客户输入、服务器响应。我们把这看作两个读事件,就可以利用IO多路复用来解决。

 

一、select函数


void cli_input(FILE* fp,int sockfd){
    int maxfdp1,stdineof=0;
    fd_set rset;
    const int buffersize=1024;
    int n;
    char buf[buffersize];
    
    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, nullptr, nullptr, nullptr);
        if(FD_ISSET(sockfd,&rset)){
            if((n=read(sockfd,buf,buffersize))!=0){
                buf[n]='\0';
                cout<<"get server message:"<<buf<<endl;
            }
            else{
                if(stdineof==1)
                    return;
                else
                    exit(1);
            }
            
        }
        if(FD_ISSET(fileno(fp),&rset)){
            if((n=read(fileno(fp),buf,buffersize))==0){
                stdineof=1;
                shutdown(sockfd,SHUT_WR);
                FD_CLR(fileno(fp),&rset);
                continue;
            }
            write(sockfd,buf,n);
        }
            
    }
}

首先我们要有一个事件的集合,这里面是rset,就是读事件的集合,可以“读”套接字的描述符,也可以读标准IO的描述符号(在这里面就是fileno(fp),可以返回FILE类的描述符)。使用之前先对rset清空(FD_ZERO),每次select返回后,都必须把要监听的描述符重新置位,也就是FD_SET方法,maxfdp1意味着select以循环扫描各个事件时候,范围有多大,一般来说,select最多监听1024个。

下面两个if是为了确认是哪个事件准备就绪了。

第一个if表明的是一个套接字可以读了,说白了就是服务器的数据已经接收到了,与前面不同的关键一点就是read是立即完成的,但read是个阻塞函数,如果数据不准备好会一直阻塞,在select已经通知了我们数据准备完毕后,会立即读取到数据。

第二个if表面是用户的输入准备就绪了,一个细节就是这里面的shutdown函数,当用户输入0个字节,说明已经要结束输入了,shutdown(sockfd,SHUT_WR)表明对此套接字执行关闭写的一端(也就是用户端)这样只会接收服务器数据但不会再发送数据了。最后,清理对此描述符的监听。

 

二、服务器中select的运用

前面我们提到了多线程多进程的使用,顺便说一句,多进程很可能导致系统崩溃,所以一般情况下用的都是多线程。多线程的好处很明显,那就是并发的处理多个客户端请求,但是实际上,我们看到15秒后面多出来的零点几秒就是处理的代价,因为要不断切换上下文,当线程非常多的时候,依然会造成大延迟。

考虑我们刚才提到的情况,我们利用多线程构造了大量对用户输入处理的线程,但用户输入可是随机的,这些线程,必须时时刻刻都存在着。但是实际上跟计算机相比,人的输入速度可就慢多了,所以可以说这种形式很浪费时间。

如果我们用一个线程循环地监听呢?这样就可以当有输入时候处理,没输入的时候循环等待。

void server4(){
    const uint16_t listened_port=9000;
    const char* localhost="127.0.0.1";
    const int listening_queue_length=1024;
    const int buffersize=1024;

    int listen_fd=socket(AF_INET,SOCK_STREAM,0);
    sockaddr_in server_addr;

    bzero(&server_addr,sizeof(server_addr));
    server_addr.sin_family=AF_INET;
    in_addr temp;
    inet_pton(AF_INET,localhost,&server_addr.sin_addr);
    //server_addr.sin_addr.s_addr=htonl(temp.s_addr);
    server_addr.sin_port=htons(listened_port);

    bind(listen_fd,(const sockaddr*)&server_addr,sizeof(server_addr));
    listen(listen_fd,listening_queue_length);

    int maxfd=listen_fd,maxi=-1;
    int nready;
    vector<int> clients(FD_SETSIZE,-1);
    socklen_t len;

    fd_set rset,allset;
    FD_ZERO(&allset);
    FD_SET(listen_fd,&allset);
    char buf[buffersize];

    while(1){
        rset=allset;
        nready=select(maxfd+1,&rset, nullptr, nullptr, nullptr);
        if(FD_ISSET(listen_fd,&rset)){
            int i;
            int connect_fd=accept(listen_fd,(sockaddr*)&server_addr,&len);
            for(i=0;i<FD_SETSIZE;++i){
                if(clients[i]<0){
                  clients[i]=connect_fd;
                    break;
                }
            }
            if(i==FD_SETSIZE){
                cerr<<"to much clients"<<endl;
                exit(1);
            }
            FD_SET(connect_fd,&allset);
            maxfd=max(maxfd,connect_fd);
            maxi=max(maxi,i);
            if(--nready<=0)
                continue;
        }
        int sockfd;
        for(int i=0;i<=maxi;++i){
            if((sockfd=clients[i])<0)
                continue;
            if(FD_ISSET(sockfd,&rset)){
                int n;
                if((n=read(sockfd,buf,buffersize))==0){
                    close(sockfd);
                    FD_CLR(sockfd,&allset);
                    clients[i]=-1;
                }
                else{
                    write(sockfd,buf,n);
                }
                if(--nready<=0)
                    break;
            }
                
        }
        
    }
}

在这里可以看到有两种事件,一个是listen_fd“可读”了,也就是说一个新的连接到达,这时候应立即调用accept,因为肯定会立即返回的。

另一种就是connect_fd“可读”了,也就是客户端数据到达,这里面做了个很简单的处理,就是把原数据回写到客户端。用长度为1024的数组来保存这些fd,nready的意思是,有的时候不一定一瞬间只有一个事件就绪,也有可能多个事件就绪,这个时候要做的是既要处理新连接,又要处理数据。

现在考虑一个常见问题,就是如果服务端的处理是一个耗时操作(比如我们之前写的3秒钟处理),那在下面的for循环中如果迭代处理那将严重损耗性能。正确的做法是用一个线程来处理任务,与上一节的处理方法一样。

 

二、poll

其实select的限制很大,每次只能监听至多1024个描述符,于是有了可以监听更多的函数——poll。

int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout)
struct pollfd
  {
    int fd;			/* File descriptor to poll.  */
    short int events;		/* Types of events poller cares about.  */
    short int revents;		/* Types of events that actually occurred.  */
  };

 

其中pollfd就是它要监听的一个结构,我们把感兴趣(等待)的事件写入events,然后返回后,会有事件出现,查看revents即可。

第二个参数可以自行设定监听的数量。下面用这个改写一下服务器端:

void server5(){
    const uint16_t listened_port=9000;
    const char* localhost="127.0.0.1";
    const int listening_queue_length=1024;
    const int buffersize=1024;
    const int MAX_LISTENED=10240;

    int listen_fd=socket(AF_INET,SOCK_STREAM,0);
    sockaddr_in server_addr;

    bzero(&server_addr,sizeof(server_addr));
    server_addr.sin_family=AF_INET;
    in_addr temp;
    inet_pton(AF_INET,localhost,&server_addr.sin_addr);
    //server_addr.sin_addr.s_addr=htonl(temp.s_addr);
    server_addr.sin_port=htons(listened_port);

    bind(listen_fd,(const sockaddr*)&server_addr,sizeof(server_addr));
    listen(listen_fd,listening_queue_length);
    socklen_t len;

    pollfd clients[MAX_LISTENED];//10240,10 multiple .
    clients[0].fd=listen_fd;
    clients[0].events=POLLRDNORM;
    for(int i=1;i<MAX_LISTENED;++i){
        clients[i].fd=-1;
    }
    int maxi=0,nready;
    char buf[buffersize];
    while(1){
        int i;
        nready=poll(clients,maxi+1,1e12);
        if(clients[0].revents&POLLRDNORM){
            int connect_fd=accept(listen_fd,(sockaddr*)&server_addr,&len);
            for(i=1;i<MAX_LISTENED;++i){
                if(clients[i].fd<0){
                    clients[i].fd=connect_fd;
                    break;
                }
            }
            if(i==MAX_LISTENED){
                cerr<<"too much clients"<<endl;
                exit(1);
            }
            clients[i].events=POLLRDNORM;
            maxi=max(i,maxi);
            if(--nready<=0)
                continue;
        }
        int sockfd,n;
        for(i=1;i<=maxi;++i){
            if((sockfd=clients[i].fd)<0){
                continue;
            }
            if(clients[i].revents&(POLLRDNORM|POLLERR)) {
                if((n=read(sockfd,buf,buffersize))<0){
                    if(errno==ECONNRESET){
                        close(sockfd);
                        clients[i].fd=01;
                    }
                    else{
                        cerr<<"read error"<<endl;
                        exit(1);
                    }
                }
                else if(n==0){
                    close(sockfd);
                    clients[i].fd=-1;
                }
                else{
                    write(sockfd,buf,n);
                }
                if(--nready<=0){
                    break;
                }
            }
        }
    }



}

可以看到,逻辑基本差不多。POLLRDNORM表示事件是标准数据可读(tcp/udp正规数据都被认为是普通数据),由此可以看出和select几乎差不多的方法。但是可以处理的数量远大于select。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值