C++ project actual combat - detailed analysis of high concurrent server

The set of socket functions in this section uses the previously encapsulated ones for exception handling (optional)

Link:  C++ project actual combat - socket programming_干饭小白's blog-CSDN blog

Table of contents

BIO model

NIO model

multi-process concurrent server

multithreaded concurrent server 

NIO model 

I/O multiplexing (I/O multiplexing)

select 

poll

The most important epoll (taken out separately)

Advanced epoll


BIO model

Blocking waiting: does not take up precious time slices of the CPU, but only one operation can be processed at a time

        

BIO model: Solve the defect that only one operation can be processed at a time through multi-threading/multi-processing. But the thread/process itself needs to consume system resources, and the scheduling of threads and processes consumes CPU.

BIO model:
        1. Threads or processes consume resources

         2. The scheduling of threads or processes consumes CPU

NIO model

Non-blocking, busy polling: constantly reminding, or checking whether there is any operation every other time

                              Improves the running efficiency of the program, but takes up a lot of CPU resources and system resources

NIO model:

        

multi-process concurrent server

Consider the following points when using a multi-process concurrent server:

1. The maximum number of file descriptors in the parent process (the parent process needs to close the new file descriptor returned by accept)

2. The number of processes created by the system (related to memory size)

3. Will creating too many processes reduce overall server performance (process scheduling)

Parent process: used to be responsible for monitoring and assigning tasks to child processes (the child process exchanges data with the client)

Subprocess: data exchange with the client

Recycle child process: when each child thread ends, the parent process may still accept (slow system call)

                      The child process needs the parent process to recycle. The SIGCHLD signal will be sent when the child process ends. The default processing action is to ignore, but we need to capture and process the child process through this signal. 

#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include "wrap.h"

#define MAXLINE     80              //最大的连接数
#define SERV_PORT   8080

int main(void)
{

   //创建socket
   int listenfd;
   listenfd = Socket(AF_INET,SOCK_STREAM,0);
   
   //bind 绑定端口和IP  sockfd
   struct sockaddr_in serveraddr;
   bzero(&serveraddr,sizeof(serveraddr));
   serveraddr.sin_port = htons(SERV_PORT);
   serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
   serveraddr.sin_family = AF_INET; 
   Bind(listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));

   //listen 设置监听的最大数量 
   listen(listenfd,20); 
   
   //accept 阻塞等待,连接
   int pid,n,i;
   struct sockaddr_in clientaddr;
   socklen_t clientlen;
   int connfd;
   char buf[MAXLINE];
   char str[INET_ADDRSTRLEN];   // INET --> IPV4   ADDR-->sockaddr  str  len 
   while(1)
   {
        clientlen = sizeof(clientaddr);
        //这个一定要放在while里面,因为多进程,可能连接的客户端不同,connfd是不同的
        connfd = Accept(listenfd,(struct sockaddr *)&clientaddr,&clientlen);

        pid = fork();
        if(pid == 0)    
        {
            //子进程读取数据和处理数据,不用做监听工作。监听工作交给父进程
            Close(listenfd);
            //读写数据,阻塞的(网络IO的数据准备就绪)
            while(1)
            {
                n = read(connfd,buf,MAXLINE);
                if(n == 0)  //说明有客户端关闭了,socket的对端关闭
                {
                    printf("the other side has been closed\n");
                    break;
                }
                //打印连接的客户端信息
                printf("received from %s at PORT %d\n",
                    inet_ntop(AF_INET,&clientaddr.sin_addr,str,sizeof(str)),ntohs(clientaddr.sin_port));
                //业务处理
                for(i = 0;i<n;++i)
                {
                    buf[i] = toupper(buf[i]);   //小写转大写
                }   
                write(connfd,buf,n);
            }
            //客户端关闭,关闭 connfd文件描述符
            Close(connfd);
            return 0;
        }
        else if(pid > 0)        //父线程不需要读写数据,fork之后,父子线程的文件描述符表是相同的
        {
            Close(connfd);
        }
        else        //出错了
        {
            perr_exit("fork");
        } 
   }

    Close(listenfd);
    return 0;

    return 0;
}
#include <stdio.h>
#include <netinet/in.h>
#include "wrap.h"
#include <string.h>
#include <unistd.h>

#define MAXLINE 80
#define SERV_PORT 8080
#define SERV_IP   "127.0.0.1"

int main(void)
{

    //创建socket
    int socketfd;   
    socketfd = Socket(AF_INET,SOCK_STREAM,0);
    
    //连接connect
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET,SERV_IP,&serveraddr.sin_addr);
    Connect(socketfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
    
    //读写数据
    char buf[MAXLINE];
    int  n;
    while(fgets(buf,MAXLINE,stdin) != NULL) //fgets从键盘键入数据
    {
        Write(socketfd,buf,sizeof(buf));
        n = Read(socketfd,buf,MAXLINE);         //一个socketfd操作读写两个缓冲区
        if(n == 0)  //对端已经关闭
        {
            printf("the other side has been closed..\n");
            break;
        }
        else
        {
            Write(STDOUT_FILENO,buf,n);     //向标准终端中写入数据
        }
    }

    Close(socketfd);
    return 0;

    return 0;
}
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <error.h>

void perr_exit(const char *s)
{
    perror(s);
    exit(1);
}
int  Accept(int fd,struct sockaddr *sa,socklen_t *salenptr)
{
    int n; 
    //accept:阻塞,是慢系统调用。可能会被信息中断
    again:
    if((n = accept(fd,sa,salenptr)) < 0)
    {
        if((errno == ECONNABORTED) || (errno == EINTR))
        {
            goto again;   //重启
        }
        else
        {
            perr_exit("accept error");
        }
    }
    return n;
}
int  Bind(int fd,const struct sockaddr *sa,socklen_t salen)
{
    int n;
    if((n = bind(fd,sa,salen)) < 0)
    {
        perr_exit("bind error");
    }
    return n;
}
int  Connect(int fd,const struct sockaddr *sa,socklen_t salen)
{
    int n;
    if((n = connect(fd,sa,salen)) < 0)
    {
        perr_exit("connect error");
    }
    return n;
}
int  Listen(int fd,int backlog)
{
    int n;
    if((n = listen(fd,backlog)) < 0)
    {
        perr_exit("listen error");
    }
    return n;
}
int  Socket(int family,int type,int protocol)
{
    int n;
    if((n = socket(family,type,protocol)) < 0)
    {
        perr_exit("socket error");
    }
    return n;
}
ssize_t Read(int fd,void *ptr,size_t nbytes)
{
    ssize_t n;
    again:
    if((n = read(fd,ptr,nbytes)) == -1)
    {
        if(errno == EINTR)//被中断
        {
            goto again;
        }
        else
        {
            return -1;
        }
    }
    return n;
}
ssize_t Write(int fd,const void *ptr,size_t nbytes)
{
    ssize_t n;
    again:
    if((n = write(fd,ptr,nbytes)) == -1)
    {
        if(errno == EINTR)
        {
            goto again;
        }
        else
        {
            return -1;
        }
    }
    return n;
}
int Close(int fd)
{
    int n;
    if((n = close(fd)) == -1)
    {
        perr_exit("close error");
    }
    return n;
}
ssize_t Readn(int fd,void *vptr,size_t n)
{
    size_t nleft;
    ssize_t nread;
    char *ptr;
    ptr = vptr;
    nleft = n;

    while(nleft > 0)
    {
        if((nleft = read(fd,ptr,nleft)) < 0)
        {
           if(errno == EINTR)
           {
                nread = 0;
           }
           else
           {
                return -1;
           }
        }
        else if(nread == 0)
        {
            break;
        }
        nleft -= nread;
        ptr += nread;
    }
    return n-nleft;

}
ssize_t Writen(int fd,const void *vptr,size_t n)
{
    size_t nleft;
    ssize_t nwritten;
    const char *ptr;
    ptr = vptr;
    nleft = n;

    while(nleft > 0)
    {
        if((nwritten = write(fd,ptr,nleft)) <= 0)
        {
            if(nwritten < 0 && errno == EINTR)
            {
                nwritten = 0;
            }
            else
            {
                return -1;
            }
        }
        nleft -= nwritten;
        ptr += nwritten;
    }
    return n;
}
static ssize_t my_read(int fd,char *ptr)
{
    static int read_cnt;
    static char *read_ptr;
    static char read_buf[100];

    if(read_cnt <= 0)
    {
        again:
            if((read_cnt = read(fd,read_buf,sizeof(read_buf))) < 0)
            {
                if(errno == EINTR)
                {
                    goto again;
                }
                return -1;
            }
            else if(read_cnt == 0)
            {
                return 0;
            }
            read_ptr = read_buf;
    }
    read_cnt--;
    *ptr = *read_ptr++;
    return 1;
}
ssize_t Readline(int fd,void *vptr,size_t maxlen)
{
    ssize_t n,rc;
    char c,*ptr;
    ptr = vptr;

    for(n=1;n<maxlen;n++)
    {
        if((rc = my_read(fd,&c)) == 1)
        {
            *ptr++ = c;
            if(c == '\n')
            {
                break;
            }
        }
        else if(rc == 0)
        {
            *ptr = 0;
            return n-1;
        }
        else
        {
            return -1;
        }
    }
    *ptr = 0;
    return n;
} 
#ifndef _WRAP_H_
#define _WRAP_H_

// #include <arpa/inet.h>
// #include <stdlib.h>
// #include <string.h>
// #include <unistd.h>
// #include <stdio.h>

void perr_exit(const char *s);
int  Accept(int fd,struct sockaddr *sa,socklen_t *salenptr);
int  Bind(int fd,const struct sockaddr *sa,socklen_t salen);
int  Connect(int fd,const struct sockaddr *sa,socklen_t salen);
int  Listen(int fd,int backlog);
int  Socket(int family,int type,int protocol);
ssize_t Read(int fd,void *ptr,size_t nbytes);
ssize_t Write(int fd,const void *ptr,size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd,void *vptr,size_t n);
ssize_t Writen(int fd,const void *vptr,size_t n);
ssize_t my_read(int fd,char *ptr);
ssize_t Readline(int fd,void *vptr,size_t maxlen); 

#endif

multithreaded concurrent server 

The following issues need to be considered when developing a server using the threading model:

        1. Adjust the upper limit of the maximum file descriptor in the process

        2. If threads have shared data, thread synchronization needs to be considered

        3. Exit processing when the client thread exits. (exit value, detached state)

        4. The system load, as the connection client increases, causing other threads to not get the CPU in time

#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <pthread.h>
#include <arpa/inet.h>
#include "wrap.h"


#define MAXLINE     80
#define SERV_PORT   8080

//一个连接对应一个客户端的信息
struct s_info
{
    struct sockaddr_in cliaddr;
    int    connfd;
};

//子线程处理的逻辑
void *do_work(void *arg)
{
    int n,i;
    //要进行业务处理的必要的信息  文件描述符  发送方的IP和动态端口
    struct s_info *ts = (struct s_info *)arg;
    //缓存
    char buf[MAXLINE];
    char str[INET_ADDRSTRLEN]; //存储点分十进制的 IP
    //设置线程分离
    pthread_detach(pthread_self());
    //业务处理
    while(1)
    {
        n = Read(ts->connfd,buf,MAXLINE);   //阻塞,阻塞状态不会消耗CPU
        if(n == 0)
        {
            printf("the other side has been closed.\n");
            break;
        }
    
    printf("recevied from %s at PORT %d\n",
                inet_ntop(AF_INET,&(*ts).cliaddr,str,sizeof(str)),ntohs((*ts).cliaddr.sin_port));

    //小写转大写
    for(i = 0;i < n; ++i)
    {
        buf[i] = toupper(buf[i]);
    }

    //传回给客户端
    Write(ts->connfd,buf,n);
    }
    
    Close(ts->connfd);

    return NULL;
}

int main(void)
{

    int i = 0;
    //创建套接字(监听)
    int listenfd;
    listenfd = Socket(AF_INET,SOCK_STREAM,0);
    //绑定
    struct sockaddr_in  servaddr;  //服务器端套接字
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);   //监听任何合理的IP
    Bind(listenfd,(struct servaddr *)&servaddr,sizeof(servaddr));
    //设置监听
    Listen(listenfd,20);
    //连接
    struct  s_info ts[256];     //最大的连接数 256 
    int connfd;
    struct sockaddr_in cliaddr;
    socklen_t cliaddr_len;
    pthread_t tid;
    while(1)
    {
        cliaddr_len = sizeof(cliaddr);
        connfd = Accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddr_len);
        ts[i].cliaddr = cliaddr;
        ts[i].connfd = connfd;
        //创建工作线程
        pthread_create(&tid,NULL,do_work,(void *)&ts[i]);
        i++;
        //为了安全起见
        if(i == 255)
        {
            break;
        }
    }

    return 0;
}
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "wrap.h"

#define MAXLINE     80
#define SERV_IP     "127.0.0.1"
#define SERV_PORT   8080

int main(void)
{
    
    //创建socket
    int sockfd;
    sockfd = Socket(AF_INET,SOCK_STREAM,0);
    //连接
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET,SERV_IP,&servaddr.sin_addr);
    Connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));

    //通信
    int n;
    char buf[MAXLINE];
    while(fgets(buf,MAXLINE,stdin) != NULL)
    {
        Write(sockfd,buf,sizeof(buf));
        n = Read(sockfd,buf,MAXLINE);
        if(n == 0)
        {
            printf("the other side has been closed\n");
        }
        else
        {
            Write(STDOUT_FILENO,buf,n);
        }
    }

    Close(sockfd);
    return 0;
}

NIO model 

#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include "wrap.h"

#define MAXLINE     80
#define SERV_PORT   8080

int main(void)
{

    //创建socket
    int listenfd;
    listenfd = Socket(AF_INET,SOCK_STREAM,0);
    //将listenfd设置为非阻塞
    fcntl(listenfd,F_SETFD,fcntl(listenfd,F_GETFD,0) | O_NONBLOCK);
    //绑定
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);
    Bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
    //设置监听
    Listen(listenfd,20);
    //连接
    struct sockaddr_in cliaddr;
    socklen_t  cliaddr_len = sizeof(cliaddr);
    int connfd;
    char buf[MAXLINE];
    int n,i=0;
    while(1)
    {
        connfd = Accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);
        n = Read(connfd,buf,MAXLINE);
        fcntl(connfd,F_SETFD,fcntl(connfd,F_GETFD,0) | O_NONBLOCK);
        if(n == -1)
        {
            if(errno == EAGAIN || errno == EWOULDBLOCK)
            {
                continue;   //再次启动read
            }
            //出错  Read会处理
        }
        else if(n == 0)
        {
            break;
        }
        else
        {
            for(i = 0;i<n;++i)
            {
                buf[i] = toupper(buf[i]);
            }
            Write(connfd,buf,n);
        }
    }

    Close(connfd);

    return 0;
}

The client is the same as the previous ones, so I won’t write it

summary:

        Whether it is multi-thread or multi-process above, it is implemented in the BIO model, that is, the blocking method. It is easy to see that the main thread (main process) is responsible for monitoring, and the sub-thread is responsible for reading and writing data and performing logical processing, namely: Reactor model

        Creating a thread or process consumes system resources

        Scheduling (switching) between processes (threads) needs to consume system resources

        The cancellation of threads (threads) also consumes system resources

       

        Compared with the NIO model, NIO is implemented by polling and requires a lot of CPU  

        Whether it is the NIO model or the BIO model, only one connection request can be processed at a time. Is there a way to monitor multiple file descriptors at the same time? Let's see whether it is monitoring work, data reading and writing, or business logic processing. It is done by the user process. In this way, most of the time of the user process is used to handle the monitoring work, which is not cost-effective. Can we hand over the monitoring to the kernel? These and other problems can be solved by multiplexing technology. Multiplexing includes three methods: select, poll, epoll. Let's explore them together.

I/O multiplexing (I/O multiplexing)

        Multi-channel I/O transfer server is also called multi-tasking IO server. The main idea of ​​the server implementation is that instead of monitoring the client connection by the application itself, the kernel monitors the file descriptor for the application.

        I/O multiplexing enables the program to monitor multiple file descriptors at the same time, which can improve the performance of the program. The system calls for I/O multiplexing under Linux include: select, pool and epoll

        

        Give an example from real life to help understand. We might as well think of the server as ourselves, and the kernel as a courier station. There are two ways for us to receive the courier. The first is to receive the courier by ourselves, and the second is to let the courier station sign for it. If we take the courier by ourselves, we either have to wait for the courier (blocking the BIO at this time), or urge the courier every 10 minutes (we can clean the floor, cook and wait for NIO within 10 minutes of each reminder) . If we let the courier station sign on our behalf, we can do other things. When a courier arrives, the staff at the courier station will notify you that you have a courier. At this time, you can choose to let the courier station staff deliver it to your home ( Asynchronous) You can still continue to sweep the floor, or you can go directly to the courier site to pick it up (synchronous). At this time, you can’t sweep the floor when you go to pick up the courier.

select 

        Main idea:

        1. First construct a list of file descriptors, and add the file descriptors to be monitored to the list

        2. Call a system function to monitor the file descriptors in the list, and the function will not return until one or more of these descriptors perform I/O operations.

                a. This function is blocking

                b. The operation of the function to detect the file descriptor is done by the kernel

        3. When returning, it will tell the process how many descriptors to perform I/O operations

       Analysis:
        1. The number of file descriptors that select can monitor is limited by FD_SETSIZE, which is generally 1024. Simply changing the number of file descriptors opened by a process cannot change the number of files that select monitors. [The number of file descriptors that can be opened in the default process is 1024, the problem left over from history: recompile the linux kernel can be solved]

        2. It is very appropriate to use select when solving clients below 1024, but if there are too many connected clients, select uses the polling model (NIO), which will greatly reduce the response efficiency of the server [because select will not tell the application process whether it is Which file descriptor has data arriving, so it needs to be looped every time, for example, if there are 1000 client connections, each loop needs 1000 system calls, which consumes a lot of resources]

        3. There is a lot of copy work in the work process

        4. Select to understand the following, do not need to spend a lot of time learning, not cost-effective

        Graphical principle:

 Related APIs

        int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timerval *timeval *timeout);

        nfds: The maximum file descriptor in the monitored file descriptor set is increased by 1, telling the kernel how many file descriptors to detect

        readfds: Monitors the arrival of read data to the file descriptor set, incoming and outgoing parameters ==> readable events

        writefds: monitors the arrival of write data to the file descriptor set, incoming and outgoing parameters ==> writable events

        exceptfds: Monitor abnormal occurrences and reach the set of file descriptors, incoming and outgoing parameters ==> abnormal events

        timeout: timing blocking monitoring time

                      1. NULL waits forever until a change in the file descriptor is detected

                      2. Set timeval to wait for a fixed time [- tv_sec > 0 tv_usec > 0, block the corresponding time]

                      3. Set the timeval time to 0, return immediately after detecting the descriptor, and poll 

                                [- tv_sec = 0 tv_usec = 0, no blocking]     

struct timeval {
    long tv_sec; /* seconds */
    long tv_usec; /* microseconds */
};

        void FD_CLR(int fd,fd_set *set); //Clear fd in the file descriptor set to 0

        void FD_ISSET(int fd,fd_set *set); //Test whether fd is set to 1 in the file descriptor set

        void FD_SET(int fd,fd_set *set); //Set the fd position in the file descriptor set to 1

        void FD_ZERO(fd_set *set); //Set all outstanding 0s in the file descriptor set

Some notes:

        Select returns the total number of satisfied conditions in the monitored collection. Through the above four functions, it is possible to judge the events that occur in fear and which one meets the conditions. fd_set is a bitmap mechanism

        

Select Disadvantages:

        1. Every time select is called, the fd collection needs to be copied from the user state to the kernel state. This overhead will be very large when there are many fds

        2. At the same time, every call to select needs to traverse all the fds passed in in the kernel. This overhead is also very large when there are many fds

        3. The number of file descriptors supported by select is too small, the default is 1024

        4. The fds collection cannot be reused and needs to be reset every time

Code example:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include "wrap.h"


#define MAXLINE     80
#define SERV_PORT   8080

int main(void)
{

    int i,n;
    //创建socket套接字
    int listenfd;
    listenfd = Socket(AF_INET,SOCK_STREAM,0);
    //绑定
    struct sockaddr_in servaddr;
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    Bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
    //设置
    Listen(listenfd,20);
    //连接前 ==> 先交给select
    int connfd,sockfd;
    fd_set allset,rset;         //allset保留原来的位,
    int nready;                 //select返回值,有事件发生的总数位图
    int maxfd = listenfd;                  //最大的文件描述符位置,告诉内核监听范围
    FD_ZERO(&allset);           //全部置为0
    FD_SET(listenfd,&allset);   //把监听描述符的位置 置1
    struct sockaddr_in cliaddr;
    socklen_t cliaddr_len;
    char str[INET_ADDRSTRLEN];   //保存客户端的 IP
    int  client[FD_SETSIZE];     //保存监听的位置   client[i] = 4  表示第4个文件描述符有事件发生
    int  maxi = -1;              //client 的最大下标
    char buf[MAXLINE];

    for(i = 0;i<FD_SETSIZE;++i)
    {
        client[i] = -1;         
    }

    while(1)
    {
        //监听读事件,阻塞。select最好不要设置为轮询的方式
        rset = allset;
        nready = select(maxfd+1,&rset,NULL,NULL,NULL); 

        if(nready < 0)      //失败,退出
        {
            // void perr_exit(const char *s)
            // {
            //     perror(s);
            //     exit(1);
            // }
            perr_exit("select error");
        }

        if(FD_ISSET(listenfd,&rset))    //如果为真,有新的连接到达  监听事件不放入client
        {
            //有新连接到达,我们需要把连接的客户端的信息拿到,后面要给它返回信息
            cliaddr_len = sizeof(cliaddr);
            //连接 accept,此时Accept不会发生阻塞等待,因为listenfd已经有事件发生
            connfd = Accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);
            //打印连接的客户端信息
            printf("连接来自  %s 在  %d 端口\n",
                inet_ntop(AF_INET,&cliaddr.sin_addr,str,sizeof(str)),ntohs(cliaddr.sin_port));
            //把客户端连接的文件描述符加入到监听队列中,监听该客户端是否有数据传来
            for(i = 0; i<FD_SETSIZE; ++i)   //通过 for循环总是能得到最小的空闲位置
            {
                if(client[i] < 0)
                {
                    client[i] = connfd;
                    break;
                }
            }


            //select能监听的文件个数达到上限 1024
            if(i == FD_SETSIZE)
            {
                fputs("select能监听的文件个数达到上限\n",stdout);   //向标准设备打印提示信息
                exit(1);
            }
            if(i > maxi)
            {
                maxi = i;
            }

            FD_SET(connfd,&allset);         //添加到监听信号集
            if(connfd > maxfd)
            {
                maxfd = connfd;             //maxfd做个迭代
            }

            if(--nready == 0)   //如果没有更多的就绪文件描述符,继续回到select阻塞监听
            {
                continue;
            }
        }

        for(i = 0;i <= maxi;++i)        //检测哪一个 clients有数据就绪
        {
            sockfd = client[i];
            if(client[i] < 0)          
            {
                continue;
            }
            if(FD_ISSET(sockfd,&rset))
            {
                if((n = Read(sockfd,buf,MAXLINE)) == 0) //与客户端关闭连接
                {
                    Close(sockfd);
                    FD_CLR(sockfd,&allset);       //解除select监听此文件描述符
                    client[i] = -1;
                }
                
                int j;
                for(j=0;j<n;++j)
                {
                    buf[j] = toupper(buf[j]);
                }
                Write(sockfd,buf,n);
                

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

        }
        
    }

    Close(listenfd);


    return 0;
}

The client code is the same as the previous case


poll

        Poll is only valid for Linux. The poll model is proposed based on the maximum file descriptor limit of select. It is the same as select, but the three bit-based file descriptors (readfds/writefds/exceptfds) used by select are encapsulated into a structure. Then use the form of the array to break through the limit of the maximum file descriptor.

        #include <poll.h>

        int poll(struct pollfd *fds,nfds_t nfds,int timeout);

        parameter:

                fds: the first address of the array

                nfds_t: How many file descriptors in the monitoring array need to be monitored

                timeout: 

                                -1 blocking wait

                                 0 returns immediately, does not block

                                >0 wait for the specified number of milliseconds, if the current system time precision is not enough milliseconds, take the value up

                struct pollfd

                {

                        int fd; //file descriptor

                        short events; //Monitored events

                        short revents; //Events returned in monitoring events that meet the conditions

                }; 

                 - Return value: -1 : failure > 0(n)

                                 Success, n indicates that n file descriptors in the collection have been detected to have changed

If you no longer monitor a certain file descriptor, you can set pollfd, fd to -1, poll will no longer monitor this pollfd, and set revents to 0 when returning next time

Code example:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <errno.h>
#include "wrap.h"

#define MAXLINE     80
#define SERV_PORT   8080
#define OPEN_MAX    1024        //监控的最多的数量


int main(void)
{

    int i,n,j;
    //创建socket
    int listenfd = Socket(AF_INET,SOCK_STREAM,0);
    //绑定
    struct sockaddr_in servaddr;
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);
    Bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
    //设置监听
    Listen(listenfd,20);
    //poll
    struct pollfd fds[OPEN_MAX];         //记录监听的文件描述符
    nfds_t maxi = -1;                          //最大的那个监听文件描述符
    int nready;

    //将设置为监听描述符
    fds[0].fd = listenfd;
    fds[0].events = POLLRDNORM;          //监听为普通事件
    maxi = 0;
    //client[i] = -1   表示文件描述符 i 不处于监听状态
    for(i=1;i<OPEN_MAX;++i)
    {
        fds[i].fd = -1;
    }

    //客户端信息
    struct sockaddr_in cliaddr;
    socklen_t  cliaddr_len;
    int connfd,sockfd;
    char str[INET_ADDRSTRLEN];
    char buf[MAXLINE];

    while(1)
    {
       
        nready = poll(fds,maxi+1,-1);    //-1表示阻塞等待
        if(fds[0].revents & POLLRDNORM)      // &位操作  有客户端连接请求
        {
            cliaddr_len = sizeof(cliaddr);
            connfd = Accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);
            //打印客户端信息
            printf("连接来自 %s 在端口 %d\n",
                       inet_ntop(AF_INET,&cliaddr.sin_addr,str,sizeof(str)), ntohs(cliaddr.sin_port));
            //将 connfd 加入到监听数组
            for(i=1;i<OPEN_MAX;++i)
            {
                if(fds[i].fd < 0)
                {
                    fds[i].fd = connfd;
                    break;
                }
            }
            //判断监听事件是否超过最大限制
            if(i == OPEN_MAX)
            {
                perr_exit("too many clients\n");
            }

            fds[i].events = POLLRDNORM;
            if(i > maxi)
            {
                maxi = i;
            }
            if(--nready <= 0)
            {
                continue;
            }
        }   

        for(i = 1;i<= maxi;++i)
        {
            sockfd = fds[i].fd;
            if(fds[i].fd < 0)
            {
                continue;;
            }
            if(fds[i].revents & (POLLRDNORM|POLLERR))
            {
                if((n = Read(sockfd,buf,MAXLINE)) < 0)
                {
                    if(errno == ECONNRESET) //sockfd 不监听了
                    {
                        printf("fds[%d] aborted connection\n",i);
                        Close(sockfd);
                        fds[i].fd = -1;
                    }
                    else
                    {   
                        perr_exit("read error");
                    }
                }
                else if(n == 0)
                {
                    printf("fds[%d] closed connection\n",i);
                    Close(sockfd);
                    fds[i].fd = -1;
                }
                else
                {
                    for(j=0;j<n;++j)
                    {
                        buf[j] = toupper(buf[j]);
                    }
                    Writen(sockfd,buf,n);
                }
            
                if(--nready <= 0)
                {
                    break;
                }
            }
        }

    }

    return 0;
}

The most important epoll (taken out separately)

        epoll is an enhanced version of the multiplexed IO interface select/poll under Linux. It can significantly improve the CPU utilization of the system when there are only a few active programs in a large number of concurrent connections, because it will multiplex the file descriptor set to pass As a result, developers do not have to re-prepare the set of file descriptors to be monitored every time before waiting for an event. Another point is that when getting an event, it does not need to traverse the entire set of descriptors to be listened to, as long as it traverses those that are used by the kernel IO The event asynchronously wakes up and joins the descriptor set of the Ready queue.

        Currently epoll is a popular first model in Linux large-scale concurrent network programs

        In addition to providing level trigger (LT) of IO events like select/poll, epoll also provides edge trigger (ET). This makes it possible for user space programs to buffer the IO status, reduce calls to epoll_wait/epoll_pwait, and improve application efficiency

        

You can use the cat command to view the upper limit of socket descriptors that a process can open

cat  /proc/sys/fs/file-max

You can also modify the upper limit by modifying the configuration file

sudo you /etc/security/limits.conf

Write the configuration at the end of the file, soft soft limit, hard hard limit

soft nofile 65536

hard  nofile 100000

Basic API

1. Create an epoll handle, the parameter size is used to tell the kernel the number of file descriptors monitored, which is related to the memory size

   #include <sys/epoll.h>

   int epoll_create(int size); 

        - Parameters: size : currently meaningless. Just write a number, it must be greater than 0

        - Return value: -1: failure > 0: file descriptor, operating epoll instance

Create a new epoll instance. A data is created in the kernel. There are two important data in this data. One is the information of the file descriptor (red-black tree) that needs to be detected, and the other is the ready list, which stores the files that detect the change of data transmission. Descriptor information (doubly linked list)

2. Control events on a file descriptor monitored by epoll, register, modify, delete

  int   epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);

  epfd: handle for epoll_create

  op: Indicates an action, represented by 3 macros

        EPOLL_CTL_ADD: register new fd to epfd

        EPOLL_CTL_MOD: Modify the listening event of the registered fd

        EPOLL_CTL_DEL: delete a fd from epfd

  event: Tell the kernel the events that need to be monitored

  struct epoll_event{

          _uint32_t         events;                // Epoll events

          epoll_data       data;                    //user data variable

  };            

  typedef union epoll_data {

        void *ptr; //callback function

        int fd;

        uint32_t u32;

        uint64_t u64;

} epoll_data_t;

Common Epoll detection events:

- EPOLLIN

- EPOLLOUT

- EPOLLERR

3. Wait for an event to be generated on the monitored file descriptor, similar to the select() call

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);  

- parameters:

        - epfd : the file descriptor corresponding to the epoll instance

        - events : Outgoing parameter, which saves the information of the file descriptor that sent the change

        - maxevents : the size of the second parameter structure array

        - timeout : blocking time

                0 : do not block -

                1 : Block until a change in fd data is detected and unblock

             > 0 : duration of blocking (milliseconds)

- return value:

        - If successful, the number of file descriptors sent to change is returned > 0

        -fail -1

Code example:

#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include "wrap.h"

#define MAXLINE     80
#define SERV_PORT   8080
#define OPEN_MAX    1024


int main(void)
{

    int i,n,j,ret;
    //创建套接字
    int listenfd = Socket(AF_INET,SOCK_STREAM,0);
    //绑定
    struct sockaddr_in servaddr;
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    Bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
    //设置监听
    Listen(listenfd,20);
    //epoll
    int client[OPEN_MAX];
    int maxi = -1;

    for(i=0;i<OPEN_MAX;++i)
    {
        client[i] = -1;
    }
    maxi = -1;

    //创建一个 epoll 句柄
    int efd = epoll_create(OPEN_MAX);
    if(efd == -1)
    {
        perr_exit("epoll_create");
    }
    //设置连接
    int nready;
    struct epoll_event tep,ep[OPEN_MAX];
    tep.events = EPOLLIN;
    tep.data.fd = listenfd;
    ret = epoll_ctl(efd,EPOLL_CTL_ADD,listenfd,&tep);
    if(ret == -1)
    {
        perr_exit("epoll_ctl");
    }

    //客户端信息
    struct sockaddr_in cliaddr;
    socklen_t  cliaddr_len;
    char str[INET_ADDRSTRLEN];
    int connfd,sockfd;
    char buf[MAXLINE];

    while(1)
    {
        nready = epoll_wait(efd,ep,OPEN_MAX,-1);        //-1表示阻塞
        if(nready == -1)
        {
            perr_exit("epoll_wait");
        }

        for(i=0;i<nready;++i)
        {
            if(!ep[i].events & EPOLLIN)
            {
                continue;
            }
            if(ep[i].data.fd == listenfd)       //有新客户端连接
            {
                cliaddr_len = sizeof(cliaddr);
                connfd = Accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);
                printf("连接来自 %s 在端口 %d\n",
                    inet_ntop(AF_INET,&cliaddr.sin_addr,str,sizeof(str)),ntohs(cliaddr.sin_port));
                
                for(j=0;j<OPEN_MAX;++j)
                {
                    if(client[j] < 0)
                    {
                        client[j] = connfd;
                        break;
                    }
                }
                if(j == OPEN_MAX)
                {
                    perr_exit("too many clients");
                }
                if(j > maxi)
                {
                    maxi = j;
                }

                tep.events = EPOLLIN;
                tep.data.fd = connfd;
                ret = epoll_ctl(efd,EPOLL_CTL_ADD,connfd,&tep);
                if(ret == -1)
                {
                    perr_exit("epoll_ctl");
                }
                // if(--nready <= 0)
                // {
                //     continue;
                // }
            }
            else
            {
                sockfd = ep[i].data.fd;
                n = Read(sockfd,buf,MAXLINE);
                if(n == 0)
                {
                    for(j =0;j <= maxi;++j)
                    {
                        if(client[j] == sockfd)
                        {
                            client[j] = -1;
                            break;
                        }
                    }
                    ret = epoll_ctl(efd,EPOLL_CTL_DEL,sockfd,NULL);
                    if(ret == -1)
                    {
                        perr_exit("epoll_ctl");
                    }
                    Close(sockfd);
                    printf("cliend[%d] closed connect\n",j);
                }
                else
                {
                    for(j =0 ;j<n;++j)
                    {
                        buf[j] = toupper(buf[j]);
                    }
                    Writen(sockfd,buf,n);
                }
            }
        }
    }

    Close(listenfd);
    Close(efd);

    return 0;
}

Advanced epoll

event mode:

There are two models of EPOLL events ==>

        Edge Triggered (ET) edge trigger: trigger only when data arrives, regardless of whether there is data in the buffer

        Level Triggered (LT) level trigger: level trigger will trigger as long as there is data

case:

 1. Assume that we have added a file descriptor (RFD) used to read data from the pipeline to the epoll descriptor

 2. The other end of the pipeline has written 2KB of data

 3. Call epoll_wait, and it will return RFD, indicating that it is ready to read

 4. Read 1KB of data

 5. Call epoll_wait...

In this process, there are two working modes

client ----> 1000B

epoll_wait(cfd);

read(500B)            has read 500B

Level trigger: trigger epoll until read

Edge trigger: do not tell, do not trigger epoll, unless new data arrives

ET mode

ET mode is Edge Triggered working mode

        If we used the EPOLLET flag when adding RFD to the epoll descriptor in step 1, then there will be a hang after calling epoll_wait in step 5, because the remaining data still exists in the input buffer of the file, and The data sender is still waiting for a feedback message for the sent data. The ET working mode will report an event only when an event occurs on the monitored file handle. Therefore, at step 5, the caller may give up waiting for the remaining data that still exists in the file input buffer. When epoll works in ET mode, non-blocking sockets must be used to avoid starving the task of processing multiple file descriptors due to blocked read and write operations of a file handle.

        1). Based on non-blocking file handle

        2). Suspend and wait only when read or write returns EAGAIN (non-blocking read, no data temporarily). But this does not mean that you need to read in a loop every time you read, and the event processing is not considered complete until an EAGAIN is generated. When the length of the read data returned by read is less than the requested data length, the buffer can be determined. There is no data in the zone, so it can be considered that the event has been processed

LT mode

        Different from the LT mode, when the epoll interface is called in the LT mode, it is equivalent to a relatively fast poll, regardless of whether the subsequent data is used or not.

        LT: LT is the default working mode and supports both block and no-block sockets. In this approach, the kernel tells you whether a file descriptor is ready, and then you can heap the ready fd for IO operations. If you don't do anything, the kernel will continue to notify you, so programming errors in this mode are less likely. Traditional select/poll are representatives of this model.

        ET: ET is a high-speed working mode and only supports no-block socket. In this mode, the kernel tells you via epoll when a descriptor becomes ready from never ready. It then assumes you know the file descriptor is ready, and doesn't send any more ready notifications for that file descriptor. Please note: if this fd has not been operated on IO (so that it no longer becomes unready), the kernel will not send more notifications.

 

Code example:

        epoll ET trigger mode based on network C/S non-blocking model

#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>

#define MAXLINE     10
#define SERV_PORT   8080

int main(void)
{
    
    int res;
    //创建socket套接字
    int listenfd = socket(AF_INET,SOCK_STREAM,0);
    //绑定
    struct  sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);
    bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
    //设置监听
    listen(listenfd,20);
    //epoll + ET设置
    struct epoll_event event;
    struct epoll_event resevent[10];
    int efd = epoll_create(10);
    event.events = EPOLLIN | EPOLLET;   //ET边缘触发   默认是水平触发

    //保存客户端信息
    int connfd,len;
    char str[INET_ADDRSTRLEN];
    char buf[MAXLINE];
    struct sockaddr_in cliaddr;
    socklen_t cliaddr_len = sizeof(cliaddr);
    connfd = accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);
    printf("连接的来自 %s 在端口 %d\n",
            inet_ntop(AF_INET,&servaddr.sin_addr,str,sizeof(str)),ntohs(cliaddr.sin_port));
    
    //设置connfd 非阻塞
    int flag = fcntl(connfd,F_GETFL);
    flag |= O_NONBLOCK;
    fcntl(connfd,F_SETFL,flag);
    event.data.fd = connfd;
    epoll_ctl(efd,EPOLL_CTL_ADD,connfd,&event);

    while(1)
    {
        printf("epoll_wait begin\n");
        res = epoll_wait(efd,resevent,10,-1);
        printf("epoll_wait end res %d\n",res);

        if(resevent[0].data.fd == connfd)
        {
            while((len = read(connfd,buf,MAXLINE/2)) > 0)
            {
                write(STDOUT_FILENO,buf,len);
            }
        }

    }

    return 0;
}
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"

#define MAXLINE     10
#define SERV_PORT   8080
#define SERV_IP     "127.0.0.1"

int main(void)
{

    int     i;
    char    ch = 'a';
    char    buf[MAXLINE];
    //创建套接字
    int sockfd;
    sockfd = socket(AF_INET,SOCK_STREAM,0);
    //连接
    struct sockaddr_in servaddr;
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET,SERV_IP,&servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);   
    connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));

    while(1)
    {
        for(i=0;i<MAXLINE/2;++i)
        {
            buf[i] = ch;
        }
        buf[i-1] = '\n';
        ch++;

        for(;i<MAXLINE;++i)
        {
            buf[i] = ch;
        }
        buf[i-1] = '\n';
        ch++;

        write(sockfd,buf,sizeof(buf));
        sleep(10);
    }

    close(sockfd);

    return 0;
}

Guess you like

Origin blog.csdn.net/weixin_46120107/article/details/126559989