第九章 I/O复用/高级应用

I/O复用使得程序能同时监听多个文件描述符,以下情况需要使用I/O复用技术:
1、客户端程序要同时处理多个socket
2、客户端程序要同时处理用户输入和网络连接
3、TCP服务器要同时处理监听socket和连接socket。这是I/O复用使用最多的场合
4、服务器要同时处理TCP请求和UDP请求
5、服务器要同时监听多个端口,或者处理多种服务
I/O复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依次处理其中的每一个文件描述符,这使得服务器程序看起来像是串行工作的。如果要实现并发,只能使用多进程或多线程等编程手段。

select系统调用

用途:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件

select API

在这里插入图片描述
1)ndfs参数指定被监听的文件描述符的总数,通常被设置为select监听的所有文件描述符中的最大值加1。
2)readfds、writefds和exceptfds参数分别指向可读、可写和异常事件对应的文件描述符集合。应用程序调用select函数时,通过这三个参数传入自己感兴趣的文件描述符。select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。这三个参数是fd_set结构指针类型。仅包含一个整形数组,该数组的每个元素的每一位标记一个文件描述符。fd_set能容纳的文件描述符数量由FD_SETSIZE指定,这就限制了select能同时处理的文件描述符的总量。
使用下列宏访问fd_set结构体中的位:
在这里插入图片描述
3)timeout参数用来设置select函数的超时时间,它是一个timeval结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序select等待了多久。调用失败时timeout值不确定。定义如下:
在这里插入图片描述
select给我们提供了一个微秒级的定时方式,如果给timeout变量的tv_sec成员和tv_usec成员都传递0,则select将立即返回。如果给timeout传递NULL,则select将一直阻塞,直到某个文件描述符就绪。
select成功返回就绪(可读、可写、异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select将返回0.如果在select等待期间,程序接收到信号,立即返回-1.

文件描述符就绪条件

哪些情况下文件描述符可以被认为是可读、可写或者出现异常,对于select的使用非常关键,下列情况下socket可读:
在这里插入图片描述
socket可写:
在这里插入图片描述
select能够处理的异常情况只有一种:socket上接收到的带外数据。

处理带外数据

socket上接收到普通数据和带外数据都将使select返回,但socket处于不同的就绪状态:前者可读,后者异常,同时处理二者:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>

int main(int argc,char* argv[])
{
    if(argc<=2)
    {
        printf("usage: %s ip_address port_number\n",basename(argv[0]));
        return 1;
    }
    const char* ip=argv[1];
    int port=atoi(argv[2]);

    int ret=0;
    struct sockaddr_in address;
    bzero(&address,sizeof(address));
    address.sin_family=AF_INET;
    inet_pton(AF_INET,ip,&address.sin_addr);
    address.sin_port=htons(port);

    int listenfd=socket(PF_INET,SOCK_STREAM,0);
    assert(listenfd>=0);
    ret=bind(listenfd,(struct sockaddr*)&address,sizeof(address));
    assert(ret!=-1);
    ret=listen(listenfd,5);
    assert(ret!=-1);
    struct sockaddr_in client_address;
    socklen_t client_addrlength=sizeof(client_address);
    int connfd=accept(listenfd,(struct sockaddr*)&client_address,
                  &client_addrlength);

    char buf[1024];
    fd_set read_fds;
    fd_set exception_fds;
    FD_ZERO(&read_fds);
    FD_ZERO(&exception_fds);

    while(1)
    {
        memset(buf,'\0',sizeof(buf));
        /*每次调用select前都要重新在read_fds和exception_fds中设置文件描述符connfd
          ,因为事件发生之后,文件描述符集合将被内核修改*/
        FD_SET(connfd,&read_fds);
        FD_SET(connfd,&exception_fds);
        ret=select(connfd+1,&read_fds,NULL,&exception_fds,NULL);
        if(ret<0)
        {
            break;
        }
        /*对于可读事件,采用普通的recv函数读取数据*/
        if(FD_ISSET(connfd,&read_fds))
        {
            ret=recv(connfd,buf,sizeof(buf)-1,0);
            if(ret<=0)
                break;
            printf("get %d bytes of normal data: %s\n",ret,buf);
        }
        /*对于异常事件,采用带MSG_OOB的recv函数读取带外数据*/
        else if(FD_ISSET(connfd,&exception_fds))
        {
            ret=recv(connfd,buf,sizeof(buf)-1,MSG_OOB);
            if(ret<=0)
                break;
            printf("get %d bytes of normal data: %s\n",ret,buf);
        }
    }
    close(connfd);
    close(listenfd);
    return 0;
}

poll系统调用

poll系统调用和select类似,都是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪
在这里插入图片描述
1)fds参数时一个pollfd结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件。pollfd结构体定义如下:
在这里插入图片描述
其中,fd成员指定文件描述符:events成员告诉poll监听fd上的哪些事件,它是一系列事件的按位或:revent成员则由内核修改,以通知应用程序fd上发生了哪些事件。
在这里插入图片描述
在这里插入图片描述
应用程序需要根据recv调用的返回值来区分socket上接收到的是有效数据还是对方关闭连接的请求,并做相应的处理。poll系统增加了一个POLLRDHUP事件,它在socket上接收到对方关闭连接的请求之后触发,可以区分上述情况。使用该事件时,在代码最开始处定义_GNU_SOURCE
2)nfds参数指定被监听事件集合fds大小
3)timeout参数指定poll的超时值,单位是毫秒。当timeout为-1时,poll调用将永远阻塞,直到某个事件发生;当timeout为0时,poll调用将立即返回。
返回值的含义与select相同。

epoll系列系统调用

epoll使用一组函数来完成任务,把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无需每次调用都要重复传入文件描述符集或事件集,但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表:
在这里插入图片描述
size参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大。该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。
在这里插入图片描述
fd参数是要操作的文件描述符,op参数则指定操作类型:
EPOLL_CTL_ADD:往事件表中注册fd上的事件
EPOLL_CTL_MOD:修改fd上的注册事件
EPOLL_CTL_DEL:删除fd上的注册事件
event参数指定事件,它是epoll_event结构指针类型:
在这里插入图片描述
其中events成员描述事件类型。epoll支持的事件类型和poll基本相同,表示epoll事件类型的宏是在poll对应的宏前加上“E”,比如epoll的数据可读事件是EPOLLIN,但epoll有两个额外的事件类型——EPOLLET和EPOLLONESHOT。它们对于epoll的高效运作非常关键。data成员用于存储用户数据,其类型epoll_data_t的定义:
在这里插入图片描述
epoll_data_t是一个联合体,其四个成员中使用最多的是fd,它指定事件所从属的目标文件描述符。ptr成员可用来指定与fd相关的用户数据。但由于epoll_data_t是一个联合体,我们不能同时使用其ptr成员和fd成员,如果要将文件描述符和用户数据关联起来实现快速访问,只能使用其他手段。比如放弃使用epoll_data_t的fd成员,而在ptr指向的用户数据中包含fd。成功返回0,失败返回-1

epoll_wait函数

epoll系列系统调用的主要接口是epoll_wait函数,它在一段超时时间内等待一组文件描述符上的事件
在这里插入图片描述
该函数成功时返回就绪的文件描述符的个数。
epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表(由epfd参数指定)中复制到它的第二个参数events指向的数组中。这个数组只用于输出epoll_wait检测到的就绪事件,而不像select和poll的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。这就极大的提高了应用程序索引就绪文件描述符的效率。

LT和ET模式

epoll对文件描述符的操作有两种模式:LT(电平触发)模式和ET(边缘触发)模式。LT模式是默认的工作模式,这种情况下epoll相当于一个效率较高的poll。当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。ET模式是epoll的高效工作模式。
对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件,直到该事件被处理。而对于采用ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续将不再通知这一事件。可见ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率更高。

EPOLLONESHOT事件

即使使用ET模式,一个socket上的某个事件还是可能被触发多次。这在并发程序中会引起一个问题。比如一个线程或进程在读取完某个socket上的数据后开始处理这些数据,而在数据处理过程中该socket上又有新数据可读(EPOLLIN再次触发),此时另外一个线程被唤醒来读取这些新的数据。于是就出现了两个线程同时操作一个socket的局面。我们想要的是一个socket连接在任意时刻都只被一个线程处理,可以使用epoll的EPOLLONESHOT事件实现。
对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。该socket被处理完毕之后,应立即重置事件,确保该socket下一次可读时,EPOLLIN事件能够被触发。

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <pthread.h>

#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 1024

struct fds
{
    int epollfd;
    int sockfd;
};

int setnonblocking(int fd)
{
    int old_option=fcntl(fd,F_GETFL);
    int new_option=old_option | O_NONBLOCK;
    fcntl(fd,F_SETFL,new_option);
    return old_option;
}

/*重置fd上的事件,尽管fd上的EPOLLONESHOT事件被注册,但三操作系统仍然会触发fd上的
  EPOLLIN事件,且只触发一次*/
void reset_oneshot(int epollfd,int fd)
{
    epoll_event event;
    event.data.fd=fd;
    event.events=EPOLLIN | EPOLLET |EPOLLONESHOT;
    epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&event);
}

/*将fd上的EPOLLIN和EPOLLET事件注册到epollfd指示的epoll内核事件表中,参数oneshot
  指定是否注册fd上的EPOLLONESHOT事件*/
void* worker(void* arg)
{
    int sockfd=((fds*)arg)->sockfd;
    int epollfd=((fds*)arg)->epollfd;
    printf("start new thread to receive data on fd: %d\n",sockfd);
    char buf[BUFFER_SIZE];
    memset(buf,'\0',BUFFER_SIZE);
    /*循环读取sockfd上的数据,直到遇到EAGAIN错误*/
    while(1)
    {
        int ret=recv(sockfd,buf,BUFFER_SIZE-1,0);
        if(ret==0)
        {
            close(sockfd);
            printf("foreiner closed the connection\n");
            break;
        }
        else if(ret<0)
        {
            if(errno==EAGAIN)
            {
                reset_oneshot(epollfd,sockfd);
                printf("read later\n");
                break;
            }
        }
        else
        {
            printf("get content: %s\n",buf);
            /*休眠五秒,模拟数据处理过程*/
            sleep(5);
        }
    }
}

void addfd(int epollfd,int fd,bool oneshot)
{
    epoll_event event;
    event.data.fd=fd;
    event.events=EPOLLIN | EPOLLET;
    if(oneshot)
        event.events |= EPOLLONESHOT;
    epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&event);
    setnonblocking(fd);
}

int main(int argc,char* argv[])
{
    if(argc<=2)
    {
        printf("usage: %s ip_address port_number\n",basename(argv[0]));
        return 1;
    }
    const char* ip=argv[1];
    int port=atoi(argv[2]);

    int ret=0;
    struct sockaddr_in address;
    bzero(&address,sizeof(address));
    address.sin_family=AF_INET;
    inet_pton(AF_INET,ip,&address.sin_addr);
    address.sin_port=htons(port);

    int listenfd=socket(PF_INET,SOCK_STREAM,0);
    assert(listenfd>=0);

    ret=bind(listenfd,(struct sockaddr*)&address,sizeof(address));
    assert(ret!=-1);

    ret=listen(listenfd,5);
    assert(ret!=-1);

    epoll_event events[MAX_EVENT_NUMBER];
    int epollfd=epoll_create(5);
    assert(epollfd!=-1);
    /*监听socket listenfd不能注册EPOLLONESHOT事件,否则应用程序只能处理一个客户连接!
     * 因为后续的客户连接请求将不再触发listenfd上的EPOLLIN事件*/
    addfd(epollfd,listenfd,false);

    while(1)
    {
        int ret=epoll_wait(epollfd,events,MAX_EVENT_NUMBER,-1);
        if(ret<0)
        {
            printf("epoll failure\n");
            break;
        }

        for(int i=0;i<ret;i++)
        {
            int sockfd=events[i].data.fd;
            if(sockfd==listenfd)
            {
                struct sockaddr_in client_address;
                socklen_t client_addrlength=sizeof(client_address);
                int connfd=accept(listenfd,(struct sockaddr*)&client_address,
                                  &client_addrlength);
                /*对每个非监听文件描述符都注册EPOLLONESHOT事件*/
                addfd(epollfd,connfd,true);
            }
            else if(events[i].events & EPOLLIN)
            {
                pthread_t thread;
                fds fds_for_new_worker;
                fds_for_new_worker.epollfd=epollfd;
                fds_for_new_worker.sockfd;
                /*新启动一个工作线程为sockfd服务*/
                pthread_create(&thread,NULL,worker,(void*)&fds_for_new_worker);
            }
            else
                printf("something else happened \n");
        }
    }
    close(listenfd);
    return 0;
}

从工作线程函数worker来看,如果一个工作线程处理完某个socket上的一次请求(休眠五秒来模拟)之后,又接收到该socket上新的客户请求,则该线程将继续为这个socket服务。并且因为该socket上注册了EPOLLONESHOT事件,其他线程没有机会接触这个socket,如果工作线程等待5s后仍然没收到该socket上的下一批客户数据,则它将放弃为该socket服务。同时,它调用reset_oneshot函数来重置该socket上的注册事件,这将使epoll有机会再次检测到该socket上的EPOLLIN事件,进而使得其他线程有机会为该socket服务。
由此看来,尽管一个socket在不同时间可能被不同的线程处理,但同一时刻肯定只有一个线程在为它服务,这就保证了连接的完整性,从而避免了很多可能的竞态条件。

三组I/O复用函数的比较

这三组系统调用都能同时监听多个文件描述符。它们将等待由timeout参数指定的超时时间,直到一个或者多个文件描述符上有事件发生时返回,返回值是就绪的文件描述符的数量,返回0表示没有事件发生,现在我们从事件集、最大支持文件描述符数、工作模式和具体实现等四个方面比较它们。
这三组函数都通过某种结构体变量来告诉内核监听哪些文件描述符上的哪些事件,并使用该结构体类型的参数来获取内核处理的结果。select的参数类型fd_set没有将文件描述符和事件绑定,它仅仅是一个文件描述符集合,因此select需要提供3个这种类型的参数来分别传入和输出可读、可写及异常等事件。这一方面使得select不能处理更多类型的事件,另一方面由于内核对fd_set集合的在线修改,应用程序下次调用select前不得不重置这3个fd_set集合。poll的参数类型pollfd把文件描述符和事件都定义其中,任何事件都被统一处理,使得编程接口简洁很多。并且内核每次修改的是pollfd类型的事件集参数。由于每次select和poll调用都返回整个用户注册的事件集合(包括就绪和未就绪的),所以应用程序索引就绪文件描述符的时间复杂度为O(n)。epoll在内核中维护一个事件表,并提供了一个独立的系统调用epoll_ctl来控制往其中添加、删除、修改事件。这样,每次epoll_wait调用都直接从该内核事件表中取得用户注册的事件,无需反复从用户空间读入这些事件。epoll_wait系统调用的events参数仅用来返回就绪的文件,这使得应用程序索引就绪文件描述符的时间复杂度达到O(1)。
poll和epoll_wait分别用ndfs和maxevents参数指定最多监听多少个文件描述符和事件,这两个数值都能达到系统允许打开的最大文件描述符数目,即65535(cat/proc/sys/file-max)。而select允许监听的最大文件描述符数量通常有限制。
select和poll都只能工作在相对低效的LT模式,而epoll可以工作在ET高效模式,并且epoll还支持EPOLLONESHOT事件,该事件进一步减少可读、可写和异常事件被触发的次数。从实现原理上来说,select和poll采用的都是轮询的方式,epoll_wait采用的是回调的方式,内核检测到就绪的文件描述符时,将触发回调函数,回调函数就将该文件描述符上对应的事件插入内核就绪事件队列。内核最后在适当的时机就将该就绪事件队列中的内容拷贝到用户空间。当活动连接比较多的时候,回调函数被触发得过于频繁,效率降低。所以epoll_wait适用于连接数量多,但活动连接较少的情况。
在这里插入图片描述

高级应用

一:非阻塞connect

非阻塞的socket导致connect始终失败,其次select对处于EINPROGRESS状态下的socket可能不起作用

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <pthread.h>

#define BUFFER_SIZE 1024

int setnonblocking(int fd)
{
    int old_option=fcntl(fd,F_GETFL);
    int new_option=old_option | O_NONBLOCK;
    fcntl(fd,F_SETFL,new_option);
    return old_option;
}

/*超时连接函数,参数分别是服务器ip,端口号和超时时间,函数成功返回已经处于连接状态的socket*/
int unlock_connect(const char* ip,int port,int time)
{
    int ret=0;
    struct sockaddr_in address;
    bzero(&address,sizeof(address));
    address.sin_family=AF_INET;
    inet_pton(AF_INET,ip,&address.sin_addr);
    address.sin_port=htons(port);

    int sockfd=socket(PF_INET,SOCK_STREAM,0);
    int fdopt=setnonblocking(sockfd);
    ret=connect(sockfd,(struct sockaddr*)&address,sizeof(address));
    if(ret==0)
    {
        /*如果连接成功,则恢复sockfd的属性,并立即返回之*/
        printf("connect with server immediately\n");
        fcntl(sockfd,F_SETFL,fdopt);
        return sockfd;
    }
    else if(errno!=EINPROGRESS)
    {
        /*如果连接没有立即建立,那么只有当errno是EINPROGRESS时才表示连接还在进行,
          否则出错返回*/
        printf("unblock connect not support\n");
        return -1;
    }
    fd_set readfds;
    fd_set writefds;
    struct timeval timeout;

    FD_ZERO(&readfds);
    FD_SET(sockfd,&writefds);

    timeout.tv_sec=time;
    timeout.tv_usec=0;

    ret=select(sockfd+1,NULL,&writefds,NULL,&timeout);
    if(ret<=0)
    {
        /*select超时或者出错,立即返回*/
        printf("connection time out");
        close(sockfd);
        return -1;
    }

    if(!FD_ISSET(sockfd,&writefds))
    {
        printf("no events on sockfd found\n");
        close(sockfd);
        return -1;
    }

    int error=0;
    socklen_t length=sizeof(error);
    /*调用getsockopt来获取并清除sockfd上的错误*/
    if(getsockopt(sockfd,SOL_SOCKET,SO_ERROR,&error,&length)<0)
    {
        printf("get socket option failed\n");
        close(sockfd);
        return -1;
    }
    /*错误号不为0表示连接出错*/
    if(error!=0)
    {
        printf("connection failed after select with the error: %d\n",error);
        close(sockfd);
        return -1;
    }
    /*连接成功*/
    printf("connection ready after se;ect with the socket: %d \n",sockfd);
    fcntl(sockfd,F_SETFL,fdopt);
    return sockfd;
}

int main(int argc,char* argv[])
{
    if(argc<=2)
    {
        printf("usage: %s ip_address port_number\n",basename(argv[0]));
        return 1;
    }
    const char* ip=argv[1];
    int port=atoi(argv[2]);

    int sockfd=unlock_connect(ip,port,10);
    if(sockfd<0)
        return 1;
    close(sockfd);
    return 0;
}

在这里插入图片描述

二、聊天室程序

以poll为例实现一个简单的聊天室程序,以阐述如何使用I/O复用技术来同时处理网络连接和用户输入。该聊天室程序能让所有用户同时在线群聊,分为客户端和服务器端:
客户端从标准输入终端读入用户数据,并将用户数据发送至服务器;往标准输出终端打印服务器发送给它的数据。
服务器接收客户数据。并把客户数据发送给每一个登录到该服务器上的客户端(数据发送者除外)。

客户端

客户端程序使用poll同时监听用户输入和网络连接,并利用splice函数将用户输入内容直接定向到网络连接上以发送之,实现数据零拷贝,提高程序执行效率

#define _GNU_SOURCE 1
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <poll.h>

#define BUFFER_SIZE 64


int main(int argc,char* argv[])
{
    if(argc<=2)
    {
        printf("usage: %s ip_address port_number\n",basename(argv[0]));
        return 1;
    }
    const char* ip=argv[1];
    int port=atoi(argv[2]);

    struct sockaddr_in server_address;
    bzero(&server_address,sizeof(server_address));
    server_address.sin_family=AF_INET;
    inet_pton(AF_INET,ip,&server_address.sin_addr);
    server_address.sin_port=htons(port);

    int sockfd=socket(PF_INET,SOCK_STREAM,0);
    assert(sockfd>=0);
    if(connect(sockfd,(struct sockaddr*)&server_address,sizeof(server_address))<0)
    {
        printf("connection failed\n");
        close(sockfd);
        return 1;
    }

    pollfd fds[2];
    /*注册文件描述符0(标准输入)和文件描述符sockfd上的可读事件*/
    fds[0].fd=0;
    fds[0].events=POLLIN;
    fds[0].revents=0;
    fds[1].fd=sockfd;
    fds[1].events=POLLIN | POLLRDHUP;
    fds[1].revents=0;
    char read_buf[BUFFER_SIZE];
    int pipefd[2];
    int ret=pipe(pipefd);
    assert(ret!=-1);

    while(1)
    {
        ret=poll(fds,2,-1);
        if(ret<0)
        {
            printf("poll failure\n");
            break;
        }

        if(fds[1].revents & POLLRDHUP)
        {
            printf("server close the connection\n");
            break;
        }
        else if(fds[1].revents & POLLIN)
        {
            memset(read_buf,'\0',BUFFER_SIZE);
            recv(fds[1].fd,read_buf,BUFFER_SIZE-1,0);
            printf("%s\n",read_buf);
        }

        if(fds[0].revents & POLLIN)
        {
            /*使用splice将用户输入的数据直接写到sockfd上(零拷贝)*/
            ret=splice(0,NULL,pipefd[1],NULL,32768,
                        SPLICE_F_MORE | SPLICE_F_MOVE);
            ret=splice(pipefd[0],NULL,sockfd,NULL,32768,
                        SPLICE_F_MORE | SPLICE_F_MOVE);
        }
    }
    close(sockfd);
    return 0;
}
服务器

服务器程序使用poll同时管理监听socket和连接socket,并且使用牺牲空间换取时间的策略来提高服务器性能

#define _GNU_SOURCE 1
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <poll.h>
#include <errno.h>

#define USER_LIMIT 5 /*最大用户数量*/
#define BUFFER_SIZE 64 /*读缓冲区的大小*/
#define FD_LIMIT 65535 /*文件描述符数量限制*/
/*客户数量:客户端socket地质,待写到客户端数据的位置、从客户端读入的数据*/
struct client_data
{
    sockaddr_in address;
    char* write_buf;
    char buf[BUFFER_SIZE];
};

int setnonblocking(int fd)
{
    int old_option=fcntl(fd,F_GETFL);
    int new_option=old_option | O_NONBLOCK;
    fcntl(fd,F_SETFL,new_option);
    return old_option;
}


int main(int argc, char *argv[])
{
    if(argc<=2)
    {
        printf("usage: %s ip_address port_number\n",basename(argv[0]));
        return 1;
    }
    const char* ip=argv[1];
    int port=atoi(argv[2]);

    int ret=0;
    struct sockaddr_in address;
    bzero(&address,sizeof(address));
    address.sin_family=AF_INET;
    inet_pton(AF_INET,ip,&address.sin_addr);
    address.sin_port=htons(port);

    int listenfd=socket(PF_INET,SOCK_STREAM,0);
    assert(listenfd>=0);

    ret=bind(listenfd,(struct sockaddr*)&address,sizeof(address));
    assert(ret!=-1);

    ret=listen(listenfd,5);
    assert(ret!=-1);

    /*创建users数组,分配FD_LIMIT个client_data对象,可以预期:每个可能的socket连接
 都可以获得一个这样的对象,并且socket的值可以直接用来索引socket连接对应的client_data对象
,这是将socket和客户数据关联的简单而高效的方式*/
    client_data* users=new client_data[FD_LIMIT];
    /*尽管分配了足够多的client_data对象,但是为了提高poll的性能,仍然限制用户数量*/
    pollfd fds[USER_LIMIT+1];
    int user_counter=0;
    for(int i=1;i<=USER_LIMIT;++i)
    {
        fds[i].fd=-1;
        fds[i].events=0;
    }
    /*对监听socket注册可读事件和错误事件*/
    fds[0].fd=listenfd;
    fds[0].events=POLLIN | POLLERR;
    fds[0].revents=0;

    while(1)
    {
        ret=poll(fds,user_counter+1,-1);
        if(ret<0)
        {
            printf("poll failure\n");
            break;
        }

        for(int i=0;i<user_counter+1;++i)
        {
            if(fds[i].fd==listenfd && fds[i].revents & POLLIN)
            {
                struct sockaddr_in client_address;
                socklen_t client_addrlength=sizeof(client_address);
                int connfd=accept(listenfd,(struct sockaddr*)
                                  &client_address,&client_addrlength);
                if(connfd<0)
                {
                    printf("errno is: %d\n",errno);
                    continue;
                }
                /*如果请求太多,则关闭新到的连接*/
                if(user_counter>=USER_LIMIT)
                {
                    const char* info="too many users\n";
                    printf("%s",info);
                    send(connfd,info,strlen(info),0);
                    close(connfd);
                    continue;
                }
                /*对于新的连接,同时修改fds和users数组,users[connfd]对应于
                新连接文件描述符connfd的客户数据*/
                user_counter++;
                users[connfd].address=client_address;
                setnonblocking(connfd);
                fds[user_counter].fd=connfd;
                fds[user_counter].events=POLLIN|POLLRDHUP|POLLERR;
                fds[user_counter].revents=0;
                printf("comes a new user,now have %d users\n",user_counter);
            }
            else if(fds[i].revents & POLLERR)
            {
                printf("get an error from %d\n",fds[i].fd);
                char errors[100];
                memset(errors,'\0',100);
                socklen_t length=sizeof(errors);
                if(getsockopt(fds[i].fd,SOL_SOCKET,SO_ERROR,&errors,
                              &length)<0)
                    printf("get socket option failed\n");
                continue;
            }
            else if(fds[i].revents & POLLRDHUP)
            {
                /*如果客户端关闭连接,则服务器也关闭对应的连接,并将用户总数-1*/
                users[fds[i].fd]=users[fds[user_counter].fd];
                close(fds[i].fd);
                fds[i]=fds[user_counter];
                i--;
                user_counter--;
                printf("a client left\n");
            }
            else if(fds[i].revents & POLLIN)
            {
                int connfd=fds[i].fd;
                memset(users[connfd].buf,'\0',BUFFER_SIZE);
                ret=recv(connfd,users[connfd].buf,BUFFER_SIZE-1,0);
                printf("get %d bytes of client data %s from %d\n",ret,
                       users[connfd].buf,connfd);
                if(ret<0)
                {
                    /*操作出错关闭连接*/
                    if(errno !=EAGAIN)
                    {
                        close(connfd);
                        users[fds[i].fd]=users[fds[user_counter].fd];
                        fds[i]=fds[user_counter];
                        i--;
                        user_counter--;
                    }
                }
                else if(ret==0)
                {

                }
                else
                {
                    /*如果接收到客户数据,则通知其他socket连接准备写数据*/
                    for (int j = 1; j <= user_counter; ++j) {
                        if(fds[j].fd==connfd)
                            continue;
                        fds[j].events|=~POLLIN;
                        fds[j].events|=POLLOUT;
                        users[fds[j].fd].write_buf=users[connfd].buf;
                    }
                }
            }
            else if(fds[i].revents & POLLOUT)
            {
                int connfd=fds[i].fd;
                if(!users[connfd].write_buf)
                    continue;
                ret=send(connfd,users[connfd].write_buf,
                         strlen(users[connfd].write_buf),0);
                users[connfd].write_buf=NULL;
                /*写完数据需要重新注册fds[i]上的可读事件*/
                fds[i].events |=~POLLOUT;
                fds[i].events |=POLLIN;
            }
        }
    }
    delete [] users;
    close(listenfd);
    return 0;
}

三、同时处理TCP和UDP服务

实际应用中,有不少服务器程序能同时监听多个端口,比如超级富翁inetd和android的调试服务adbd。
从bind系统调用的参数来看,一个socket只能与一个socket地址绑定,服务器创建多个socket监听多个端口。即使是同一个端口,如果服务器要同时处理该端口的TCP和UDP请求,需要创建流socket和数据报socket,并将它们都绑定到端口上

#define _GNU_SOURCE 1
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <errno.h>

#define MAX_EVENT_NUMBER 1024
#define TCP_BUFFER_SIZE 512
#define UDP_BUFFER_SIZE 1024

int setnonblocking(int fd)
{
    int old_option=fcntl(fd,F_GETFL);
    int new_option=old_option | O_NONBLOCK;
    fcntl(fd,F_SETFL,new_option);
    return old_option;
}

void addfd(int epollfd,int fd)
{
    epoll_event event;
    event.data.fd=fd;
    event.events=EPOLLIN|EPOLLET;
    epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&event);
    setnonblocking(fd);
}


int main(int argc, char *argv[])
{
    if(argc<=2)
    {
        printf("usage: %s ip_address port_number\n",basename(argv[0]));
        return 1;
    }
    const char* ip=argv[1];
    int port=atoi(argv[2]);

    int ret=0;
    struct sockaddr_in address;
    bzero(&address,sizeof(address));
    address.sin_family=AF_INET;
    inet_pton(AF_INET,ip,&address.sin_addr);
    address.sin_port=htons(port);

    /*创建TCP socket,并将其绑定到端口port上*/
    int listenfd=socket(PF_INET,SOCK_STREAM,0);
    assert(listenfd>=0);

    ret=bind(listenfd,(struct sockaddr*)&address,sizeof(address));
    assert(ret!=-1);

    ret=listen(listenfd,5);
    assert(ret!=-1);

    /*创建UDPsocket*/
    bzero(&address,sizeof(address));
    address.sin_family=AF_INET;
    inet_pton(AF_INET,ip,&address.sin_addr);
    address.sin_port=htons(port);
    int udpfd=socket(PF_INET,SOCK_DGRAM,0);
    assert(udpfd>=0);

    ret=bind(udpfd,(struct sockaddr*)&address,sizeof(address));
    assert(ret!=-1);

    epoll_event events[MAX_EVENT_NUMBER];
    int epollfd=epoll_create(5);
    assert(epollfd!=-1);
    /*注册TCP和UDPsocket上的可读事件*/
    addfd(epollfd,listenfd);
    addfd(epollfd,udpfd);

    while(1)
    {
        int number=epoll_wait(epollfd,events,MAX_EVENT_NUMBER,-1);
        if(number<0)
        {
            printf("epoll failure\n");
            break;
        }

        for(int i=0;i<number;i++)
        {
            int sockfd=events[i].data.fd;
            if(sockfd==listenfd)
            {
                struct sockaddr_in client_address;
                socklen_t client_addrlength=sizeof(client_address);
                int connfd=accept(listenfd,(struct sockaddr*)
                                  &client_address,&client_addrlength);
                addfd(epollfd,connfd);
            }
            else if(sockfd==udpfd)
            {
                char buf[UDP_BUFFER_SIZE];
                memset(buf,'\0',UDP_BUFFER_SIZE);
                struct sockaddr_in client_address;
                socklen_t client_addrlength=sizeof(client_address);

                ret=recvfrom(udpfd,buf,UDP_BUFFER_SIZE-1,0,
                             (struct sockaddr*)&client_address,&client_addrlength);
                if(ret>0)
                {
                    sendto(udpfd,buf,UDP_BUFFER_SIZE-1,0,
                           (struct sockaddr*)&client_address,client_addrlength);

                }
            }
            else if(events[i].events & EPOLLIN)
            {
                char buf[TCP_BUFFER_SIZE];
                while(1)
                {
                    memset(buf,'\0',TCP_BUFFER_SIZE);
                    ret=recv(sockfd,buf,TCP_BUFFER_SIZE-1,0);
                    if(ret<0)
                    {
                        if(errno==EAGAIN || errno==EWOULDBLOCK)
                            break;
                        close(sockfd);
                        break;
                    }
                    else if(ret==0)
                    {
                        close(sockfd);
                    }
                    else
                    {
                        send(sockfd,buf,ret,0);
                    }
                }
            }
            else
                printf("something else happened \n");
        }
    }
    close(listenfd);
    return 0;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值