socket网络编程以及select、poll、epoll详解(万字长文)

socket网络编程的步骤

先给出大致流程:

服务端:

  1.  创建自己的socket连接
  2. 再打开自己的可以用于通信的端口,并把自己的ip告诉要通信的客户端
  3. 打开监听的socket,监听是否有客户端连接
  4. 接受客户端的连接
  5. 如果有客户端连接上来,则接收数据后,再回复
  6. 不用于通信则关闭socket

                

客户端:

  1. 创建自己的socket连接
  2. 向服务端发起连接请求
  3. 请求成功后,则发送信息,并接受服务端的信息
  4. 不用于通信则关闭socket

在建立连接的阶段,则会用到TCP的三次握手:

客户端:首先发送建立连接报文syn,表示想建立从客户端到服务端的连接

服务端:确认报文ack表示同意从客户端建立连接,并且向客户端发送建立连接报文syn,表示想从服务端建立到客户端的连接

客户端:回复收到确认报文ack,表示同意从服务端到客户端建立连接

自此连接建立。 

 由于客户端于服务端建立的连接是双向的,所以,连接在断开的需要四次挥手。

 客户端:

        首先客户端先断开自己到服务端的连接。

        向服务端发送一个断开连接请求FIN

服务端:

        对FIN请求进行确认,表示同意断开由客户端到服务端的连接

        然后服务端再向客户端发送断开连接请求FIN

客户端:

        对服务端FIN进行回复确认,表示同意断开从服务端到客户端的连接

🆗有了上面基础知识,我们就可以往下面写代码了。

首先是客户端:

int main()
{
    int sockefd = socket(AF_INET,SOCK_STREAM,0);
    if (sockefd==-1)
    {
        perror("socket");return -1;
    }

    struct hostent *h;
    if ((h=gethostbyname(ip地址)==0)
    {
        cout<<"gethostbyname failed\n";close(sockefd);return -1;
    }
    struct sockaddr_in servaddr;
    memset(&servaddr,0,sizeof (servaddr));
    servaddr.sin_family=AF_INET;
    memcpy(&servaddr.sin_addr.s_addr,h->h_addr,h->h_length);
    servaddr.sin_port=htons(通信端口));

    if (connect(sockefd,(struct sockaddr *)&servaddr,sizeof(servaddr))!=0)
    {
        perror("connect");close(sockefd);return -1;
    }

    char buffer[1024];
    for(int i=0;i<3;i++)
    {
        memset(&buffer,0,sizeof(buffer));
        int iret;
        sprintf(buffer,"这是%d号",i+1);

        if (iret=send(sockefd,buffer,sizeof(buffer),0)<=0)
        {
            perror("send");break;
        }
        cout<<"发送"<<buffer<<endl;
        memset(&buffer,0,sizeof(buffer));

        if (iret=recv(sockefd,buffer,sizeof(buffer),0)<=0)
        {
            perror("recv");break;
        }
        cout<<"接收"<<buffer<<endl;
    }
close(sockefd);
}

 先建立socket没什么好说的,主要是struct hostent 与 struct sockaddr_in 这两个结构体。

我们首先来看struct sockaddr_in结构体,事实上他有两个“兄弟”。

struct sockaddr {
     unsigned short sa_family;	
// 协议族(地址类型)
  
    unsigned char sa_data[14];	
// 14字节的端口和地址。
};
struct sockaddr_in {  
  unsigned short sin_family;	
// 协议族(地址类型)
  
  unsigned short sin_port;		// 16位端口号,大端序。用htons(整数的端口)转换。
  struct in_addr sin_addr;		// IP地址的结构体。
  unsigned char sin_zero[8];	// 未使用,为了保持与struct sockaddr一样的长度而添加。
};
struct in_addr {				// IP地址的结构体。
  unsigned int s_addr;		// 32位的IP地址,大端序。
};

 sin_family:表示使用的是什么协议,IPV4 / IPV6

 sin_port:表示用于通信的端口

 sin_addr:表示用于通信的ip地址

 sin_zero[8]:为了扩展而定义的参数

 我们用sockaddr_in来处理用于通信的ip与端口号,当发现其结构体长度不够用的时候,我们会转换为sockaddr结构体,事实上最好每次都转化为sockaddr。

struct hostent

struct hostent *gethostbyname(const char *name);
struct hostent { 
  char *h_name;     	// 主机名。
  char **h_aliases;    	// 主机所有别名构成的字符串数组,同一IP可绑定多个域名。 
  short h_addrtype; 	// 主机IP地址的类型,例如IPV4(AF_INET)还是IPV6。
  short h_length;     	// 主机IP地址长度,IPV4地址为4,IPV6地址则为16。
  char **h_addr_list; 	// 主机的ip地址,以网络字节序存储。 
};

 这个结构体主要用于把域名转化为用于网络通信的大端序ip。

    struct hostent *h;
    if ((h=gethostbyname(ip地址)==0)
    {
        cout<<"gethostbyname failed\n";close(sockefd);return -1;
    }
    struct sockaddr_in servaddr;
    memset(&servaddr,0,sizeof (servaddr));
    servaddr.sin_family=AF_INET;
    memcpy(&servaddr.sin_addr.s_addr,h->h_addr,h->h_length);
    servaddr.sin_port=htons(通信端口));

所以这部分代码,主要是绑定用于通信的ip与端口号。 

   if (connect(sockefd,(struct sockaddr *)&servaddr,sizeof(servaddr))!=0)
    {
        perror("connect");close(sockefd);return -1;
    }

这部分代码,用于向服务端发起连接请求。 

       if (iret=send(sockefd,buffer,sizeof(buffer),0)<=0)
        {
            perror("send");break;
        }
        cout<<"发送"<<buffer<<endl;
        memset(&buffer,0,sizeof(buffer));

        if (iret=recv(sockefd,buffer,sizeof(buffer),0)<=0)
        {
            perror("recv");break;
        }
        cout<<"接收"<<buffer<<endl;

这部分主要是接收服务端发过来的信息,以及向服务端发送信息。

再来看服务端:

int main()
{
    int listenfd;
    listenfd=socket(AF_INET,SOCK_STREAM,0);
    if (listenfd==-1) {
        perror("socket");return -1;
    }

    struct sockaddr_in servaddr;
    memset(&servaddr,0,sizeof(0));
    servaddr.sin_family=AF_INET;
    servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
    servaddr.sin_port=htons(5005);

    if (bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))!=0)
    {
        perror("bind");close(listenfd);return -1;
    }

    if (listen(listenfd,5)!=0)
    {
        perror("listen");close(listenfd);return -1;
    }

    int clientfd = accept(listenfd,0,0);
    if (clientfd==-1)
    {
        perror("accept");close(listenfd);return -1;
    }

    cout<<"客户端已连接"<<endl;
    char buffer[1024];
    while (true)
    {
        int iret;
        memset(buffer,0,sizeof(buffer));

        if (iret=recv(clientfd,buffer,sizeof(buffer),0)<=0)
        {
            perror("recv");break;
        }
    
        strcpy(buffer,"ok");

        if (iret=send(clientfd,buffer,sizeof(buffer),0)<=0)
        {
            perror("send");break;
        }
    cout<<"发送"<<buffer<<endl;
    }
close(listenfd);
close(clientfd);
}

服务端首先建立监听的socket,用于处理客户端到来的socket连接。

    int listenfd;
    listenfd=socket(AF_INET,SOCK_STREAM,0);
    if (listenfd==-1) {
        perror("socket");return -1;
    }

然后服务端开启自己通信的端口以及向所有客户端用于通信:

    struct sockaddr_in servaddr;
    memset(&servaddr,0,sizeof(0));
    servaddr.sin_family=AF_INET;
    servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
    servaddr.sin_port=htons(5005);

后面这部分的代码就是流程图的步骤了

    if (bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))!=0)
    {
        perror("bind");close(listenfd);return -1;
    }

    if (listen(listenfd,5)!=0)
    {
        perror("listen");close(listenfd);return -1;
    }

    int clientfd = accept(listenfd,0,0);
    if (clientfd==-1)
    {
        perror("accept");close(listenfd);return -1;
    }

    cout<<"客户端已连接"<<endl;
    char buffer[1024];
    while (true)
    {
        int iret;
        memset(buffer,0,sizeof(buffer));

        if (iret=recv(clientfd,buffer,sizeof(buffer),0)<=0)
        {
            perror("recv");break;
        }
    
        strcpy(buffer,"ok");

        if (iret=send(clientfd,buffer,sizeof(buffer),0)<=0)
        {
            perror("send");break;
        }
    cout<<"发送"<<buffer<<endl;
    }
close(listenfd);
close(clientfd);
}

 注:有些函数细节没有给出详解,这个自己查查就明白,程序员就得会自己查资料。

select执行过程详解

select函数参数

FD_ZERO(fd_set* fdset): 将fd_set变量的所有位初始化为0。

FD_CLR(int fd,fd_set *set):清除fd_set集合中指定的fd文件描述符。

FD_SET(int fd,fd_set *set):在fd_set集合中注册文件描述符fd的信息。

FD_ISSET(int fd, fd_set* fdset):若参数fd_set指向的变量中包含文件描述符fd的信息,则返回真。

第一个参数:监视的文件描述的数量(通常是最大的那个文件描述符+1)

第二个参数:表示关心读事件

第三个参数:表示关心写事件

第四个参数:表示关心异常事件

第五个参数:超时时间

返回值:错误返回-1,超时返回0。当关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符数。

select执行流程

        在了解select是如何执行之前,我们要明白,recv只能监视单个socket,而人们希望服务器一次性能够管理多个socket连接,所以才出现了select、poll、epoll这些技术。

        在进行select方法之前,它与服务端建立单个连接的准备工作是相似的。

    int sock = socket(AF_INET,SOCK_STREAM,0);
    if (sock < 0)
    {
        perror("socket() failed"); return -1;
    }
    
    /*
    int opt = 1; unsigned int len = sizeof(opt);
    setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,len);

    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(port);
    */    

    if (bind(sock,(struct sockaddr *)&servaddr,sizeof(servaddr)) < 0 )
    {
        perror("bind() failed"); close(sock); return -1;
    }

    if (listen(sock,5) != 0 )
    {
        perror("listen() failed"); close(sock); return -1;
    }

 先不用管注释部分的代码,总之,就是先建立一个socket,然后进行绑定,再对连上来的客户端进行监听。

    int listensock = initserver(端口号);   //然后初始化监听端口
    printf("listensock=%d\n",listensock);


    fd_set readfds;          // 需要监视读事件的socket的集合,大小为16字节(1024位)的bitmap。
    FD_ZERO(&readfds);       // 初始化readfds,把bitmap的每一位都置为0。
    FD_SET(listensock,&readfds);       // 把服务端用于监听的socket加入readfds。
    int maxfd=listensock;              // readfds中socket的最大值。

        我们来看fd_set之后的代码:建立 一个读事件的socket集合,将该集合初始化,再把用于监听的socket:listensock加入该集合。也就是下图表示的含义:我们假设需要的socket有:3,6,8,9。首先在建立socket集合的时候会初始化bitmap也就是位图。

那么位图是什么呢? 在了解这个概念之前,我们应该明白,select可以监视的最大连接数是1024个,为了表示哪些socket我们需要监听,用一个数组来表示,数组下标为socket的值(文件描述符),用1表示需要监听,0表示不需要监听。而这个数组就是位图。比如我们所需要的3,6,8,9这些socket在bitmap[3,6,8,9]=1,其他不需要监听的为0。而完成上述动作的就需要调用FD_SET函数。

这里的maxfd表示最大的那个文件描述符。在之后会有详细介绍。

 进行了初始化后我们就要对这些socket进行监视了: 在调用select函数的时候如果失败会返回一个负数,如果超时会返回一个0。

  while (true)       
  {
   
// 在select()函数中,会修改bitmap,所以,要把readfds复制一份给tmpfds,再把tmpfds传select()。
         fd_set tmpfds=readfds;
 
// 调用select() 等待事件的发生(监视哪些socket发生了事件)。
        int infds=select(maxfd+1,&tmpfds,NULL,NULL,0); 

// 如果infds<0,表示调用select()失败。
        if (infds<0)
        {
            perror("select() failed"); break;
        }

// 如果infds==0,表示select()超时。
        if (infds==0)
        {
            printf("select() timeout.\n"); continue;
        }

select函数的几个参数这里就不再讲解了,上面已经提过。(下面的代码接上面的部分)

// 如果infds>0,表示有事件发生,infds存放了已发生事件的个数。
        for (int eventfd=0;eventfd<=maxfd;eventfd++)
        {
            if (FD_ISSET(eventfd,&tmpfds)==0) continue;   
// 如果eventfd在bitmap中的标志为0,表示它没有事件,continue

// 如果发生事件的是listensock,表示已连接队列中有已经准备好的socket(有新的客户端连上来了)。
            if (eventfd==listensock)
            {
                /*
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
                if (clientsock < 0) { perror("accept() failed"); continue; }

                printf ("accept client(socket=%d) ok.\n",clientsock);
                */

                 FD_SET(clientsock,&readfds);                     
// 把bitmap中新连上来的客户端的标志位置为1。

                if (maxfd<clientsock) maxfd=clientsock;    // 更新maxfd的值。
            }

注释部分我们不看(在socket里面已经讲的很清楚了),主要关心select的流程。

这部分代码的含义是:检查是否有客户端的连接。如果有,那么就把连上来的客户端在位图里所表示的文件描述符置为1。同时更新需要处理的文件描述符的最大值。

下面代码继续接上面部分:

// 如果是客户端连接的socket有事件,表示接收缓存中有数据可以读(对端发送的报文已到达),或者有客户端已断开连接。
        else
            {
                
                // 存放从接收缓冲区中读取的数据。
                char buffer[1024];             
                memset(buffer,0,sizeof(buffer));
                if (recv(eventfd,buffer,sizeof(buffer),0)<=0)
                {
// 如果客户端的连接已断开。
                    printf("client(eventfd=%d) disconnected.\n",eventfd);

                    close(eventfd);                // 关闭客户端的socket
                   

                    FD_CLR(eventfd,&readfds);     // 把bitmap中已关闭客户端的标志位清空。
          
                    if (eventfd == maxfd)           
// 重新计算maxfd的值,注意,只有当eventfd==maxfd时才需要计算。
                    {
                        for (int ii=maxfd;ii>0;ii--)    // 从后面往前找。
                        {
                            if (FD_ISSET(ii,&readfds))
                            {
                                maxfd = ii; break;
                            }
                        }
                    }
                }

如果 eventfd!=listensock  表示有客户端的事件发生:(1)客户端断开连接;

                                                                                        (2)客户端发送数据过来了。 

这里是客户端断开连接。 

当客户端断开连接的时候(即recv返回值<=0),首先我们关闭监视的socket,然后在bitmap中把该socket所表示的位置置为0,表示不再监视这个socket。

这里重点来讲一下关于处理maxfd的代码部分,当发现我们要处理的最大socket恰好是我们要关闭的socket 的时候,我们要更新maxfd的值,如果不是我们则直接关闭。

这里我个人理解是:我们引入的maxfd是为了减少我们在bitmap中寻找的个数,bitmap处理1024是它的极限个数,而maxfd则表示实际我们真正需要处理socket的最多的那个值。 

只有当我们发现所需要处理的那个socket值正好是我们最大的socket的值的时候,我们才会更新maxfd。如果比maxfd还小,那么在遍历寻找的时候,现有的maxfd完全可以把目前所有需要监视的socket包括进去。

下面代码接上面:

                else
                {
                    // 如果客户端有报文发过来。
                    printf("recv(eventfd=%d):%s\n",eventfd,buffer);

                    // 把接收到的报文内容原封不动的发回去。
                    send(eventfd,buffer,strlen(buffer),0);
                }
            }
        }
    }

    return 0;
}

如果 不是断开连接,则发送数据过去就行。

以上所有代码:即可抽象成上述图片。

 服务端:

        (1)用户态:

           1:把需要监听的socket创建一个集合(集合可以用数组表示),FD_ZERO()初始化位图为0;

           2:FD_SET()把需要监听的socket在位图中置为1;

           3:调用select()把位图从用户态拷贝到内核态,并阻塞select();

           (2)内核态

           4: 把需要等待数据的进程加入与其对应的socket阻塞队列;

客户端: 1:向服务端发送数据;

服务端: 1:网卡接收数据   -----> 同时把数据写入内存;

                2:引发CPU中断;

                3:CPU执行中断程序;

                4:接收对应的socket数据;

                5:把处于阻塞队列的进程重新加入执行队列;

                6:所有位图从内核态拷贝到用户态。

                7:在用户态轮询查找对应进程的socket。

                

以上所有代码:

int main()
{    
    int listensock = initserver(5005);
    printf("listensock=%d\n",listensock);

    if (listensock < 0) { printf("initserver() failed.\n"); return -1; }

    // 读事件:1)已连接队列中有已经准备好的socket(有新的客户端连上来了);
    //               2)接收缓存中有数据可以读(对端发送的报文已到达);
    //               3)tcp连接已断开(对端调用close()函数关闭了连接)。
    // 写事件:发送缓冲区没有满,可以写入数据(可以向对端发送报文)。

    fd_set readfds;                         // 需要监视读事件的socket的集合,大小为16字节(1024位)的bitmap。
    FD_ZERO(&readfds);                // 初始化readfds,把bitmap的每一位都置为0。
    FD_SET(listensock,&readfds);  // 把服务端用于监听的socket加入readfds。

    int maxfd=listensock;              // readfds中socket的最大值。

    while (true)        // 事件循环。
    {
        // 用于表示超时时间的结构体。
        struct timeval timeout;     
        timeout.tv_sec=10;        // 秒
        timeout.tv_usec=0;        // 微秒。

        fd_set tmpfds=readfds;      // 在select()函数中,会修改bitmap,所以,要把readfds复制一份给tmpfds,再把tmpfds传给select()。

        // 调用select() 等待事件的发生(监视哪些socket发生了事件)。
        int infds=select(maxfd+1,&tmpfds,NULL,NULL,0); 

        // 如果infds<0,表示调用select()失败。
        if (infds<0)
        {
            perror("select() failed"); break;
        }

        // 如果infds==0,表示select()超时。
        if (infds==0)
        {
            printf("select() timeout.\n"); continue;
        }

        // 如果infds>0,表示有事件发生,infds存放了已发生事件的个数。
        for (int eventfd=0;eventfd<=maxfd;eventfd++)
        {
            if (FD_ISSET(eventfd,&tmpfds)==0) continue;   // 如果eventfd在bitmap中的标志为0,表示它没有事件,continue

            // 如果发生事件的是listensock,表示已连接队列中有已经准备好的socket(有新的客户端连上来了)。
            if (eventfd==listensock)
            {
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
                if (clientsock < 0) { perror("accept() failed"); continue; }

                printf ("accept client(socket=%d) ok.\n",clientsock);

                FD_SET(clientsock,&readfds);                      // 把bitmap中新连上来的客户端的标志位置为1。

                if (maxfd<clientsock) maxfd=clientsock;    // 更新maxfd的值。
            }
            else
            {
                // 如果是客户端连接的socke有事件,表示接收缓存中有数据可以读(对端发送的报文已到达),或者有客户端已断开连接。
                char buffer[1024];                      // 存放从接收缓冲区中读取的数据。
                memset(buffer,0,sizeof(buffer));
                if (recv(eventfd,buffer,sizeof(buffer),0)<=0)
                {
                    // 如果客户端的连接已断开。
                    printf("client(eventfd=%d) disconnected.\n",eventfd);

                    close(eventfd);                         // 关闭客户端的socket

                    FD_CLR(eventfd,&readfds);     // 把bitmap中已关闭客户端的标志位清空。
          
                    if (eventfd == maxfd)              // 重新计算maxfd的值,注意,只有当eventfd==maxfd时才需要计算。
                    {
                        for (int ii=maxfd;ii>0;ii--)    // 从后面往前找。
                        {
                            if (FD_ISSET(ii,&readfds))
                            {
                                maxfd = ii; break;
                            }
                        }
                    }
                }
                else
                {
                    // 如果客户端有报文发过来。
                    printf("recv(eventfd=%d):%s\n",eventfd,buffer);

                    // 把接收到的报文内容原封不动的发回去。
                    send(eventfd,buffer,strlen(buffer),0);
                }
            }
        }
    }

    return 0;
}

        

poll执行过程详解

poll函数参数

第一个参数:数组的元素首地址,元素类型是pollfd 结构体类型

第二个参数:需要关注的文件描述符的数目

第三个参数:阻塞等待的时间         

                      timeout = -1,代表永久阻塞等待

                      timeout = 0,代表永久非阻塞等待

                      timeout > 0,代表先阻塞等待 timeout 毫秒,超过这个时间变为非阻塞等待,poll函数返回

返回值:poll函数调用出错,返回-1;

                非阻塞模式下,返回0;

                有事件就绪时,返回 有事件就绪的文件描述符的个数

 

  • 第一个参数fd:希望内核帮你关注哪个文件描述符
  • 第二个参数events:希望内核帮你关注该文件描述符上的什么事件(读/写事件),事件类型以及设置方式在下面介绍。
  • 第三个参数revents:内核通知你该文件描述符上的某个事件就绪了。

poll执行流程

总的来说,poll与大体流程select一样,只不过相对于select来说,poll可以接受更高的并发度。主要的我们来看以下代码:

最主要的不同就是poll多了一个结构体来存储相应的文件描述符、要处理的事件以及实际发生的事件。

struct
{
    int fd;
    short events;
    short revents;
}

首先还是做好初始化的工作:

    int listensock = initserver(atoi(argv[1]));
    printf("listensock=%d\n",listensock);

    if (listensock < 0) { printf("initserver() failed.\n"); return -1; }

接下面代码:

    pollfd fds[2048];                 // fds存放需要监视的socket。
    
    // 初始化数组,把全部的socket设置为-1,如果数组中的socket的值为-1,那么,poll将忽略它。
    for (int ii=0;ii<2048;ii++)             
        fds[ii].fd=-1;   

    // 打算让poll监视listensock读事件。
    fds[listensock].fd=listensock;
    fds[listensock].events=POLLIN;        // POLLIN表示读事件,POLLOUT表示写事件。
    // fds[listensock].events=POLLIN|POLLOUT;

    int maxfd=listensock;        // fds数组中需要监视的socket的实际大小。

 首先定义一个fds数组,用于存放我们要监视的socket;这里fds大小可以自己定义,但是并不是越大越好,当fds大小越大性能方面就会下降。

再初始化fds数组,然后把监听客户端连接的socket--listensock加入fds。

同时,maxfd指定fds 文件描述符的最大值。主要目的和select相同。

接下面代码:

     while (true)        
     {
// 调用poll() 等待事件的发生(监视哪些socket发生了事件)。
        int infds=poll(fds,maxfd+1,NULL);      

// 如果infds<0,表示调用poll()失败。
        if (infds < 0)
        {
            perror("poll() failed"); break;
        }
// 如果infds==0,表示poll()超时。
        if (infds == 0)
        {
            printf("poll() timeout.\n"); continue;
        }

// 如果infds>0,表示有事件发生,infds存放了已发生事件的个数。
        for (int eventfd=0;eventfd<=maxfd;eventfd++)
        {
            if (fds[eventfd].fd<0) continue;             // 如果fd为负,忽略它。

            if ((fds[eventfd].revents&POLLIN)==0)  continue;  

// 如果发生事件的是listensock,表示已连接队列中有已经准备好的socket(有新的客户端连上来了)。
            if (eventfd==listensock)
            {
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
                if (clientsock < 0) { perror("accept() failed"); continue; }

                printf ("accept client(socket=%d) ok.\n",clientsock);

                // 修改fds数组中clientsock位置的元素。
                fds[clientsock].fd=clientsock;
                fds[clientsock].events=POLLIN;

                if (maxfd<clientsock) maxfd=clientsock;    // 更新maxfd的值。
            }
            else
            {
                // 如果是客户端连接的socke有事件,表示有报文发过来了或者连接已断开。

                char buffer[1024]; // 存放从客户端读取的数据。
                memset(buffer,0,sizeof(buffer));
                if (recv(eventfd,buffer,sizeof(buffer),0)<=0)
                {

// 如果客户端的连接已断开。
                    printf("client(eventfd=%d) disconnected.\n",eventfd);

                    close(eventfd);            // 关闭客户端的socket。
                    fds[eventfd].fd=-1;      

          
                    // 重新计算maxfd的值,注意,只有当eventfd==maxfd时才需要计算。
                    if (eventfd == maxfd)
                    {
                        for (int ii=maxfd;ii>0;ii--)  // 从后面往前找。
                        {
                            if (fds[ii].fd!=-1)
                            {
                                maxfd = ii; break;
                            }
                        }
                    }
                }
                else
                {
                    // 如果客户端有报文发过来。
                    printf("recv(eventfd=%d):%s\n",eventfd,buffer);

                    send(eventfd,buffer,strlen(buffer),0);
                }
            }
        }
    }

总体代码和select是一样的,流程也差不多,只不过把用位图该换为了结构体数组来存取监视的socket。流程可参考select。

 select与poll小结

总的来说,select与poll大体流程是一样的,但是也有细微差别:

selectpoll
是否需要拷贝到内核是;每次调用都需要拷贝;且需要全部拷贝是;每次调用都需要拷贝;且需要全部拷贝
是否有最大文件描述符数限制是 1024
内核实现和工作效率o(n),需要遍历fd集合判断是否有事件发生;以及遍历fd集合确定具体的socketo(n),需要遍历fd结构体数组集合判断是否有事件发生;以及遍历fd结构体数组确定具体的socket
工作模式水平触发水平触发

细微差别主要是poll结构体和select的位图所导致的。

epoll执行过程详解

epoll执行流程

开始先初始化:

int main()
{

     int listensock = initserver(5005);
     printf("listensock=%d\n",listensock);

     if (listensock < 0) { printf("initserver() failed.\n"); return -1; }

接下面代码:

// 创建epoll句柄。
    int epollfd=epoll_create(1);

// 为服务端的listensock准备读事件。
    
    epoll_event ev;          // 声明事件的数据结构。
    ev.data.fd=listensock;   // 指定事件的自定义数据,会随着epoll_wait()返回的事件一并返回。

    ev.events=EPOLLIN;      // 打算让epoll监视listensock的读事件。

// 把需要监视的socket和事件加入epollfd中。 
   epoll_ctl(epollfd,EPOLL_CTL_ADD,listensock,&ev);     

    epoll_event evs[10];      // 存放epoll返回的事件。

首先创建一个epollfd句柄,即创建一个eventpoll模型,这个其实本质也是一个文件描述符。 

eventpoll结构体里面的成员很多,我们主要关注其内部的:rdllist --->就绪队列;rb_root rbt-->红黑树的节点(即创建的eventpoll模型的文件描述符对应的数值);wq-->等待队列。

在进行下一步之前,我们要了解一些结构体,首先就是epitem,在调用epoll_ctl()的时候,我们会用到这个结构体。 

epitem
{
    rbn;
    rdllink;
    next;
    ffd;
    nwait;
    pwqlist;
    ep;
    fllink;
    event;
}

 

我们主要关心:rbn;rdllink;ffd;ep;event。

rbn:是我们文件描述符对应红黑树的节点

 rdllink:是已经就绪的文件描述符队列

ffd:是文件描述符

ep:是eventpoll结构体指针  (当我们拿到了某个事件,我们就知道他是属于哪个eventpoll)

在往下进行下一步的代码之前,我们得要了解epoll大致步骤是如何处理socket的。

 

 当服务端从网卡接收到数据的时候,会把数据写入内核缓冲区,然后,这个时候会调用epoll_ctl()函数,维护等待队列。我们假设A进程对应的文件描述符事件已经到达,这个时候,epoll区别于select与poll,他会有一个回调机制,这就使得可以不需要轮询去查找到底哪个socket有事件,把o(n)降到了o(1)。

 当找到这个目标socket的对应事件后,会把相应的文件描述符节点加入到就绪队列,同时会等待epoll_wait()的调用。这里注意:并不是真正加到就绪队列,而是我们为了描述方便所以才这么表述,实际上只是在红黑树改变了节点的指向

epoll_wait()调用该事件的时候,会把已经发生的事件节点拷贝回用户态,同时会唤起相对应的阻塞进程。这里只是拷贝了一次,而且区别于select、poll只拷贝已经发生了的事件节点。 

了解了epoll的工作方式,我们接下来对于代码的理解就更为清楚了。

继续接上面代码: 

while (true)       
    {
// 等待监视的socket有事件发生。
        int infds=epoll_wait(epollfd,evs,10,-1);

        // 返回失败。
        if (infds < 0)
        {
            perror("epoll() failed"); break;
        }

        // 超时。
        if (infds == 0)
        {
            printf("epoll() timeout.\n"); continue;
        }
epoll_wait()
第一个参数:eventpoll的返回值,也就是eventpoll对应的文件描述符
第二个参数:把内核已经就绪的事件拷贝到某个数组
第三个参数:evs数组的元素个数
第四个参数:超时时间

🆗咱们继续接着聊 ,接上面代码:

 // 如果infds>0,表示有事件发生的socket的数量。
        for (int ii=0;ii<infds;ii++)       // 遍历epoll返回的数组evs。
        {

// 如果发生事件的是listensock,表示有新的客户端连上来。
            if (evs[ii].data.fd==listensock)
            {
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                int clientsock = accept(listensock,(struct sockaddr*)&client,&len);

                printf ("accept client(socket=%d) ok.\n",clientsock);

// 为新客户端准备读事件,并添加到epoll中。
                ev.data.fd=clientsock;
                ev.events=EPOLLIN;
                epoll_ctl(epollfd,EPOLL_CTL_ADD,clientsock,&ev);
            }

如果是listensock接收到了事件,表示有客户端连接,那么把客户端连接的socket(文件描述符)加入到epoll中,同时也为他分配红黑树的节点。 (接下面代码)

         else
            {
// 如果是客户端连接的socke有事件,表示有报文发过来或者连接已断开。
                char buffer[1024]; // 存放从客户端读取的数据。
                memset(buffer,0,sizeof(buffer));
                
                if (recv(evs[ii].data.fd,buffer,sizeof(buffer),0)<=0)
                {
// 如果客户端的连接已断开。
                    printf("client(eventfd=%d) disconnected.\n",evs[ii].data.fd);
                    close(evs[ii].data.fd);            // 关闭客户端的socket
// 从epollfd中删除客户端的socket,如果socket被关闭了,会自动从epollfd中删除,所以,以下代码不必启用。
                    // epoll_ctl(epollfd,EPOLL_CTL_DEL,evs[ii].data.fd,0);     
                }

如果不是listensock有事件:(1)客户端发过来数据

                                             (2)客户端连接已经断开 

如果是客户端断开连接,我们需要从eventpoll中删除它。 

接下面代码:

                else
                {
                    // 如果客户端有报文发过来。
                    printf("recv(eventfd=%d):%s\n",evs[ii].data.fd,buffer);

                    
                   处理客户端的报文
                }
            }
        }
    }

  return 0;
}

 继续处理客户端的报文。

 以上就是epoll接收数据的整个流程。

所有代码:

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

    // 初始化服务端用于监听的socket。
    int listensock = initserver(5005);
    printf("listensock=%d\n",listensock);

    if (listensock < 0) { printf("initserver() failed.\n"); return -1; }

    // 创建epoll句柄。
    int epollfd=epoll_create(1);

    // 为服务端的listensock准备读事件。
    epoll_event ev;              // 声明事件的数据结构。
    ev.data.fd=listensock;   // 指定事件的自定义数据,会随着epoll_wait()返回的事件一并返回。
    // ev.data.ptr=(void*)"超女";   // 指定事件的自定义数据,会随着epoll_wait()返回的事件一并返回。
    ev.events=EPOLLIN;      // 打算让epoll监视listensock的读事件。

    epoll_ctl(epollfd,EPOLL_CTL_ADD,listensock,&ev);     // 把需要监视的socket和事件加入epollfd中。

    epoll_event evs[10];      // 存放epoll返回的事件。

    while (true)        // 事件循环。
    {
        // 等待监视的socket有事件发生。
        int infds=epoll_wait(epollfd,evs,10,-1);

        // 返回失败。
        if (infds < 0)
        {
            perror("epoll() failed"); break;
        }

        // 超时。
        if (infds == 0)
        {
            printf("epoll() timeout.\n"); continue;
        }

        // 如果infds>0,表示有事件发生的socket的数量。
        for (int ii=0;ii<infds;ii++)       // 遍历epoll返回的数组evs。
        {
            // printf("ptr=%s,events=%d\n",evs[ii].data.ptr,evs[ii].events);

            // 如果发生事件的是listensock,表示有新的客户端连上来。
            if (evs[ii].data.fd==listensock)
            {
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                int clientsock = accept(listensock,(struct sockaddr*)&client,&len);

                printf ("accept client(socket=%d) ok.\n",clientsock);

                // 为新客户端准备读事件,并添加到epoll中。
                ev.data.fd=clientsock;
                ev.events=EPOLLIN;
                epoll_ctl(epollfd,EPOLL_CTL_ADD,clientsock,&ev);
            }
            else
            {
                // 如果是客户端连接的socke有事件,表示有报文发过来或者连接已断开。
                char buffer[1024]; // 存放从客户端读取的数据。
                memset(buffer,0,sizeof(buffer));
                if (recv(evs[ii].data.fd,buffer,sizeof(buffer),0)<=0)
                {
                    // 如果客户端的连接已断开。
                    printf("client(eventfd=%d) disconnected.\n",evs[ii].data.fd);
                    close(evs[ii].data.fd);            // 关闭客户端的socket
                    // 从epollfd中删除客户端的socket,如果socket被关闭了,会自动从epollfd中删除,所以,以下代码不必启用。
                    // epoll_ctl(epollfd,EPOLL_CTL_DEL,evs[ii].data.fd,0);     
                }
                else
                {
                    // 如果客户端有报文发过来。
                    printf("recv(eventfd=%d):%s\n",evs[ii].data.fd,buffer);

                    // 把接收到的报文内容原封不动的发回去。
                   处理客户端代码
                }
            }
        }
    }

  return 0;
}

水平触发与边缘触发

所谓的水平触发,个人理解就是,当服务端发现有客户端的数据发送过来的时候,服务端会一直通知用户有数据到达,直到数据在缓冲区取走为止。类似于,一个很负责的快递员,当你有包裹到了,他会一直不停的给你发信息,叫你去拿,直到你所有的包裹被拿完位置,他才停止发信息。

边缘触发,个人理解就是,“不太可靠”的发信息的方式,类似于一个“不负责”快递员,当你有包裹到达了,他只会通知你一次,不管你拿没拿完,当下次还有包裹到达,他就再通知你一次。

总结

epollpollselect
是否需要拷贝到内核是;存在两次拷贝;在调用epoll_ctl()需要把监控的文件描述符拷贝到内核;在调用epoll_wait()需要从内核把已经发生的事件的文件描述符拷贝到用户态是;每次调用都要拷贝,在调用poll方法需要把所有文件描述符拷贝;在返回poll事件也需要全部拷贝是;每次调用都要拷贝,在调用select方法需要把所有文件描述符拷贝;在返回select事件也需要全部拷贝
是否有最大数限制是 1024
内核实现和工作效率采用回调机制检测就绪时间,时间复杂度o(1)需要遍历fd结构体数组集合判断是否有事件发生;以及遍历fd结构体数组确定具体的socket,时间复杂度o(n)需要遍历fd集合判断是否有事件发生;以及遍历fd集合确定具体的socket,时间复杂度o(n)
工作方式水平/边缘触发水平触发水平触

最后,各位观众姥爷如果还有补充的,欢迎指出,也希望姥爷们喜欢就点个赞喔。 

  • 23
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值