【Linux】C++项目实战-高并发服务器详析

news2024/10/6 11:24:32

目录

  • 多进程实现并发服务器
  • 多线程实现并发服务器
  • BIO模型
  • NIO模型
  • I/O多路复用(I/O多路转接)
      • select
          • 主旨思想
          • 图解原理
          • 函数解析
          • 代码举例
          • select的缺点
      • poll
          • 函数解析
          • 代码示例
  • epoll(最重要,请重点掌握)
      • 函数解析
      • 代码举例
      • epoll的两种工作模式

橙色

多进程实现并发服务器

server_process.c文件内容如下:

注意第70行的if(errno == EINTR),如果没有这个if判断的话,当同时多个客户端链接进来,停掉一个客户端,然后再启动一个客户端,就会发现没法连接了,accept会报一个错误。因为一个客户端停掉,在服务器端就相当于一个子进程终止执行,会发出SIGCHLD信号,被信号捕捉函数所捕捉,而此时程序正停在accept处阻塞,等待下一个客户端的连接。当信号捕捉函数处理完再返回accpet时,就会报一个错误,该错误为EINTR。这个也可以去看accept函数的介绍,有说明(如下图)。所以这里要做一个处理,如果errno是EINTR的话,则略过该报错。
在这里插入图片描述


第101行strlen(recvBuf) + 1是很有必要的,strlen在计数的时候是结束符’\0’为止,但不包含结束符。+1之后写入文件描述符的字符串就会带上结束符。如果不带上结束符的话,在另一端通过文件描述符读出的时候,数据的最末尾很容易出现一个奇怪的符号。

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

void recyleChild(int arg) {
    while(1) {
        int ret = waitpid(-1, NULL, WNOHANG);
        if(ret == -1) {
            // 所有的子进程都回收了
            break;
        }else if(ret == 0) {
            // 还有子进程活着
            break;
        } else if(ret > 0){
            // 被回收了
            printf("子进程 %d 被回收了\n", ret);
        }
    }
}

int main() {

    struct sigaction act;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    act.sa_handler = recyleChild;
    // 注册信号捕捉
    sigaction(SIGCHLD, &act, NULL);
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    if(lfd == -1){
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 监听
    ret = listen(lfd, 128);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 不断循环等待客户端连接
    while(1) {

        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);
        // 接受连接
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
        if(cfd == -1) {
            if(errno == EINTR) {
                continue;
            }
            perror("accept");
            exit(-1);
        }

        // 每一个连接进来,创建一个子进程跟客户端通信
        pid_t pid = fork();
        if(pid == 0) {
            // 子进程
            // 获取客户端的信息
            char cliIp[16];
            inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
            unsigned short cliPort = ntohs(cliaddr.sin_port);
            printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

            // 接收客户端发来的数据
            char recvBuf[1024];
            while(1) {
                int len = read(cfd, &recvBuf, sizeof(recvBuf));

                if(len == -1) {
                    perror("read");
                    exit(-1);
                }else if(len > 0) {
                    printf("recv client : %s\n", recvBuf);
                } else if(len == 0) {
                    printf("client closed....\n");
                    break;
                }
                write(cfd, recvBuf, strlen(recvBuf) + 1);
            }
            close(cfd);
            exit(0);    // 退出当前子进程
        }

    }
    close(lfd);
    return 0;
}

client.c文件内容如下:

// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {

    // 1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }

    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "192.168.193.128", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

    if(ret == -1) {
        perror("connect");
        exit(-1);
    }
    
    // 3. 通信
    char recvBuf[1024];
    int i = 0;
    while(1) {
        
        sprintf(recvBuf, "data : %d\n", i++);
        
        // 给服务器端发送数据
        write(fd, recvBuf, strlen(recvBuf)+1);

        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1) {
            perror("read");
            exit(-1);
        } else if(len > 0) {
            printf("recv server : %s\n", recvBuf);
        } else if(len == 0) {
            // 表示服务器端断开连接
            printf("server closed...");
            break;
        }

        sleep(1);
    }

    // 关闭连接
    close(fd);

    return 0;
}

多线程实现并发服务器

客户端文件内容同上

服务器端文件内容如下:

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

struct sockInfo {
    int fd; // 通信的文件描述符
    struct sockaddr_in addr;
    pthread_t tid;  // 线程号
};

struct sockInfo sockinfos[128];

void * working(void * arg) {
    // 子线程和客户端通信   需要cfd 客户端的信息 线程号
    // 获取客户端的信息
    struct sockInfo * pinfo = (struct sockInfo *)arg;

    char cliIp[16];
    inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIp, sizeof(cliIp));
    unsigned short cliPort = ntohs(pinfo->addr.sin_port);
    printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

    // 接收客户端发来的数据
    char recvBuf[1024];
    while(1) {
        int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf));

        if(len == -1) {
            perror("read");
            exit(-1);
        }else if(len > 0) {
            printf("recv client : %s\n", recvBuf);
        } else if(len == 0) {
            printf("client closed....\n");
            break;
        }
        write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
    }
    close(pinfo->fd);
    return NULL;
}

int main() {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    if(lfd == -1){
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 监听
    ret = listen(lfd, 128);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 初始化数据
    int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
    for(int i = 0; i < max; i++) {
        bzero(&sockinfos[i], sizeof(sockinfos[i]));//将结构体里面所有的成员都初始化为0
        sockinfos[i].fd = -1;
        sockinfos[i].tid = -1;
    }

    // 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
    while(1) {

        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);
        // 接受连接
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);

        struct sockInfo * pinfo;
        for(int i = 0; i < max; i++) {
            // 从这个数组中找到一个可以用的sockInfo元素
            if(sockinfos[i].fd == -1) {
                pinfo = &sockinfos[i];
                break;
            }
            if(i == max - 1) {
                sleep(1);
                i=-1;
            }
        }

        pinfo->fd = cfd;
        memcpy(&pinfo->addr, &cliaddr, len);//拷贝数据

        // 创建子线程,因为线程号仅仅在线程创建后才有,所以直接在这里传入pinfo->tid,就很方便
        pthread_create(&pinfo->tid, NULL, working, pinfo);

        //这里不能使用pthread_join,因为它是阻塞函数,那么一个子线程没结束主线程就只能阻塞在这里,没办法创建新的线程
        pthread_detach(pinfo->tid);
    }

    close(lfd);
    return 0;
}

BIO模型

阻塞等待:不占用CPU宝贵的时间片,但是每次只能处理一个操作。
在这里插入图片描述

当对方暂时没有发送数据时,程序就会阻塞在read处


BIO模型:通过多线程/多进程解决每次只能处理一个操作的缺陷。但是线程/进程本身需要消耗系统资源,并且线程和进程的调度占用CPU.
在这里插入图片描述

NIO模型

非阻塞、忙轮询:不断的去催,或者说每隔一端时间就去查看有没有操作

提高了程序的运行效率、但占用大量CPU资源和系统资源(假设有1w个客户端链接进来,那么服务器端读取某一个客户端的内容最慢可能达到第1w次才能读到,因为它要依次对这1w个客户端进行轮询。但可能这1w次轮询中,仅有一个客户端的数据到达了,那么其余的9999次遍历就都浪费了)

在这里插入图片描述

I/O多路复用(I/O多路转接)

把文件中的数据写入到内存中就是输入,把内存中的数据写入到文件中就是输出

       I/O多路复用使得程序能够同时监听多个文件描述符,能够提高程序的性能,Linux下实现I/O多路复用的系统调用有:select、pool和epoll

select

主旨思想
  1. 首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中
  2. 调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或多个进行I/O操作时,该函数才返回。
           a. 这个函数是阻塞的
           b. 函数对文件描述符的检测的操作是由内核完成的
  3. 在返回时,它会告诉进程有多少描述符要进行I/O操作
图解原理

在这里插入图片描述
前三个是系统固定已经占用的

函数解析
//sizeof(fd_set)=128字节   也就是1024位
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/select.h>

int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timerval *timeval);
	- 参数:
		- nfds:委托内核检测的最大的文件描述符的值+1
        - readfds:要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性
        	- 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区
        	- 是一个传入传出参数(比如我想看第5个文件描述符是否可以读,那我把它置为1,传入函数,函数会把这个列表指针交给内核,内核来检查,如果该文件描述符确实可以读,那么内核会把它置为1,不可读,内核就会把它置为0- writefds:要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性
        	- 委托内核检测写缓冲区是不是还可以写数据〈不满的就可以写,也就是置为1)
        - exceptfds:检测发生异常的文件描述符的集合(一般不用)
        - timeout:设置的超时时间
        	struct timeval {
               time_t      tv_sec;         /* seconds */
               suseconds_t tv_usec;        /* microseconds */
           };

			- NULL:永远等待,直到检测到了文件描述符有变化
			- tv_sec=0 tv_usec=0, 不阻塞
			- tv_sec>0 tv_usec>0,阻塞对应的时间

		- 返回值:
			- -1:失败
			- >0(n):检测的集合中有n个文件描述符发生了变化		

//将参数文件描述符fd对应的标志位设为0
void FD_CLR(int fd, fd_set *set);
//判断fd对应的标志位是0还是1,返回值:fa对应的标志位的值是0,返回0,是1,返回1
int  FD_ISSET(int fd, fd_set *set);
//将参数文件描述符fd对应的标志位设为1
void FD_SET(int fd, fd_set *set);
//fd_set一共有1024位,全部初始化为0
void FD_ZERO(fd_set *set);   
代码举例

客户端程序:

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

int main() {

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
        perror("connect");
        return -1;
    }

    int num = 0;
    while(1) {
        char sendBuf[1024] = {0};
        sprintf(sendBuf, "send data %d", num++);
        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if(len == -1) {
            perror("read");
            return -1;
        }else if(len > 0) {
            printf("read buf = %s\n", sendBuf);
        } else {
            printf("服务器已经断开连接...\n");
            break;
        }
        sleep(1);
        
    }

    close(fd);

    return 0;
}

服务器端程序:

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

int main() {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 创建一个fd_set的集合,存放的是需要检测的文件描述符
    fd_set rdset, tmp;
    FD_ZERO(&rdset);
    FD_SET(lfd, &rdset);
    int maxfd = lfd;

    while(1) {

        tmp = rdset;

        // 调用select系统函数,让内核帮检测哪些文件描述符有数据
        int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
        if(ret == -1) {
            perror("select");
            exit(-1);
        } else if(ret == 0) {  //不可能为0,因为上面select设置的是阻塞,只有当文件描述符有变化时才会到这里
            continue;
        } else if(ret > 0) {
            // 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
            //为什么要检测lfd是否为1呢?因为第一次发生了改变肯定是lfd,但后面发生改变就可能是其他的文件描述符,而不是lfd(也就是说不是有新的文件描述符加进来)
            if(FD_ISSET(lfd, &tmp)) {
                // 表示有新的客户端连接进来了
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // 将新的文件描述符加入到集合中
                FD_SET(cfd, &rdset);

                // 更新最大的文件描述符
                maxfd = maxfd > cfd ? maxfd : cfd;
            }
            //要检测的是连接描述符的数据有没有变化,所以不需要检测监听文件描述符,循环从lfd+1开始
            for(int i = lfd + 1; i <= maxfd; i++) {
                if(FD_ISSET(i, &tmp)) {
                    // 说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {0};
                    int len = read(i, buf, sizeof(buf));
                    if(len == -1) {
                        perror("read");
                        exit(-1);
                    } else if(len == 0) {
                        printf("client closed...\n");
                        close(i);
                        FD_CLR(i, &rdset);
                    } else if(len > 0) {
                        printf("read buf = %s\n", buf);
                        write(i, buf, strlen(buf) + 1);
                    }
                }
            }

        }

    }
    close(lfd);
    return 0;
}

这个服务器端的程序里还是蕴含了很多细节需要注意的。

我对第一次循环进行一个分析,先是在rdset中把监听描述符lfd置为了1。接着进入了while(1)死循环。

为了避免循环中select函数在传入rdset时改变了rdset(因为rdset中记录的是我所需要检测的文件描述符,应该一直是1,但如果将rdset传入select,在该次循环中需要被检测的文件描述符并没有数据传入,那么就会被内核置为0,所以需要tmp),所以在循环开始将rdset拷贝给tmp。

接着,当ret>0时,说明肯定有文件描述符变了,那就先看lfd,看是否是有新的客户端连接进来,如果有的话,则加入到集合rdset中(这里可能会有疑惑,为什么不在FD_SET(cfd, &rdset);后加一行FD_SET(cfd, &tmp);呢?这样这个新端口传入的数据也就能在该次死循环中读取出来了。但考虑到可能这个新端口仅仅只是连接,并没有传入数据,那read读不到数据就会阻塞在这里,因此没有加,让它在下一次循环中再读是比较保险的)

将这两个程序运行起来,客户端无论几个,服务器都是能运行的,既没有借助多线程也没有借助多进程,而是依靠了select函数。

select的缺点
  1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

  2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

  3. select支持的文件描述符数量太小了,默认是1024

  4. fds集合不能重用,每次都需要重置(其实说的就是上面服务器端程序定义了两个fd_set,如果只用一个传入内核,该要检测的端口这时并没有数据到达,那么就会被内核置为0再传递出来。那么下次再传入就不会检测该端口了,而这显然是不行的)

poll

poll只针对Linux有效,poll模型是基于select最大文件描述符限制提出的,跟select一样,只是将select使用的三个基于位的文件描述符(readfds/writefds/exceptfds)封装成了一个结构体,然后通过数组的是形式来突破最大文件描述符的限制。

函数解析
#include <poll.h>
struct pollfd{
	int fd;                  //委托内核检测的文件描述符
	short  events;           //委托内核检测文件描述符的什么事件
	short  revents;          //文件描述符实际发生的事件
}; 

//既要检测读也要检测写该怎么写?
struct po11fd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;

int poll(struct pollfd *fds,nfds_t nfds,int timeout);
	- 参数:
		- fds:数组的首地址
		- nfds:这个是第一个参数数组中最后一个有效元素的下标+1
		- timeout:阻塞时长
			0:不阻塞
			-1:阻塞,当检测到需要检测的文件描述符有变化,解除阻塞
			>0:阻塞时长(单位是毫秒)
	- 返回值:
		-1:失败
		>0(n):成功, n表示检测到集合中有n个文件描述符发生变化
		

在这里插入图片描述

代码示例

客户端程序和select中的一样
服务器端程序如下:

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


int main() {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 初始化检测的文件描述符数组
    struct pollfd fds[1024];
    for(int i = 0; i < 1024; i++) {
        fds[i].fd = -1;
        fds[i].events = POLLIN;
    }
    fds[0].fd = lfd;
    int nfds = 0;
    int i;
    while(1) {

        // 调用poll系统函数,让内核帮检测哪些文件描述符有数据
        int ret = poll(fds, nfds + 1, -1);
        if(ret == -1) {
            perror("poll");
            exit(-1);
        } else if(ret == 0) {
            continue;
        } else if(ret > 0) {
            // 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
            if(fds[0].revents & POLLIN) {
                // 表示有新的客户端连接进来了
                //先看结构体数组中是否有空位,没空位的话就等下次再accept新的客户端,有的话就直接accept
                for(i = 1; i < 1024; i++) {
                    if(fds[i].fd == -1) {                      
                        struct sockaddr_in cliaddr;
                        int len = sizeof(cliaddr);
                        int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                        // 将新的文件描述符加入到集合中
                        fds[i].fd = cfd;
                        fds[i].events = POLLIN;
                    
                        // 更新最大的文件描述符的索引
                        nfds = nfds > i ? nfds : i;
                        break;
                    }
                }   
            }

            for(int i = 1; i <= nfds; i++) {
                if(fds[i].revents & POLLIN) {
                    // 说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {0};
                    int len = read(fds[i].fd, buf, sizeof(buf));
                    if(len == -1) {
                        perror("read");
                        exit(-1);
                    } else if(len == 0) {
                        printf("client closed...\n");
                        close(fds[i].fd);
                        fds[i].fd = -1;
                    } else if(len > 0) {
                        printf("read buf = %s\n", buf);
                        write(fds[i].fd, buf, strlen(buf) + 1);
                    }
                }
            }
        }
    }
    close(lfd);
    return 0;
}

epoll(最重要,请重点掌握)

在这里插入图片描述

函数解析

#include <sys/epoll.h>
//创建一个新的epoll示例。在内核中创建了一个数据。这个数据中有两个比较重要的数据,一个是需要检测的文件描述符的信息(红黑树〉,还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向链表〉。
int epoll_create(int size); 
 	 - 参数: size : 目前没有意义了。随便写一个数,必须大于0
 	 - 返回值: -1 : 失败, > 0 : 文件描述符,操作epoll实例的




//对epo11实例进行管理:添加文件描述符信息,删除信息,修改信息
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
	- 参数:
		- epfd:epoll实例对应的文件描述符
		- op:要进行什么操作
			EPOLL_CTL_ADD:添加
			EPOLL_CTL_MOD:修改
			EPOLL_CTL_DEL:删除
		- fd:要检测的文件描述符
		- event:检测文件描述符什么事情
		  struct epoll_event{
		  	  _uint32_t         events;                // Epoll events
	 		  epoll_data       data;                    //user data variable
		  };
		  typedef union epoll_data {
			  void *ptr;                                        //回调函数
			  int fd;
			  uint32_t u32;
			  uint64_t u64;
		  } epoll_data_t;	

常见的Epoll检测事件(events):
- EPOLLIN
- EPOLLOUT
- EPOLLERR
- EPOLLET(边沿模式)如果想要使用边沿模式并检测是否可以读,events可以这么写:EPOLLIN | EPOLLET

//检测函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);  
	- 参数:
		- epfd:epo11实例对应的文件描述符
		- events:传出参数,保存了发送了变化的文件描述符的信息
		- maxevent:第二个参数结构体数组的大小
		- timeout:阻塞时间
			- 0:不阻塞
			- -1:阻塞,直到检测到fd数据发生变化,解除阻塞
			- >0:阻塞的时长(毫秒)
	- 返回值:
		- 成功,返回发送变化的文件描述符的个数>0
		- 失败 -1 

代码举例

服务器端:

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

int main() {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        //ret代表的是发生改变的文件描述符的数量
        for(int i = 0; i < ret; i++) {

            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
                if(epevs[i].events & EPOLLOUT) {
                    continue;
                }   
                // 有数据到达,需要通信
                char buf[1024] = {0};
                int len = read(curfd, buf, sizeof(buf));
                if(len == -1) {
                    perror("read");
                    exit(-1);
                } else if(len == 0) {
                    printf("client closed...\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                } else if(len > 0) {
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }

            }

        }
    }
    close(lfd);
    close(epfd);
    return 0;
}

第59行的if(epevs[i].events & EPOLLOUT)则是为了避免一种情况:当我同时检测文件描述符的读和写时,因为下面的代码都是处理读这种情况的,所以如果该文件描述符的epevs[i].events是写的话,则continue,略过这个文件描述符的变动


问题:在最开始调用epoll_ctl把监听的文件描述符放进红黑树的时候传入了&epev,也就是epev的指针,为什么后面传入新的文件描述符的时候可以重用这个epev呢,这样重用epev的话前面传入的监听描述符不就被改动了嘛?还是说其实调用这个函数传入红黑树之后,epev里面的数据已经被拷贝了?
答:当调用epoll_ctl函数将文件描述符添加到epoll对象中时,epoll会将epoll_event结构体中的数据拷贝一份,存储在自己的内存空间中,并将这个拷贝的结构体作为一个节点插入到红黑树中。
这样做的好处是,当文件描述符上的事件发生时,epoll可以直接从自己的内存空间中获取相应的事件信息,而不需要每次都去访问用户空间中的epoll_event结构体。这样可以提高效率,减少系统调用的次数。

epoll的两种工作模式

  • Level Triggered(LT)水平触发:LT (level - triggered)是缺省(缺省也就是默认的意思)的工作方式,并且同时支持 block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
假设委托内核检测读事件>检测fd的读缓冲区
    读缓冲区有数据- > epoll检测到了会给用户通知
        a.用户不读数据,数据一直在缓冲区,epoll会一直通知
        b.用户只读了一部分数据,epoll会通知
        c.缓冲区的数据读完了
  • Edge Triggred(ET) 边缘触发:ET (edge - triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(onlyonce)。

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

假设委托内核检测读事件->检测fd的读缓冲区
    读缓冲区有数据- > epoll检测到了会给用户通知
        a.用户不读数据,数据一致在缓冲区中,epoll下次检测的时候就不通知了
		b.用户只读了一部分数据,epoll不通知
		c.缓冲区的数据读完了,不通知

代码举例:

客户端程序如下:

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

int main() {

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
        perror("connect");
        return -1;
    }

    int num = 0;
    while(1) {
        char sendBuf[1024] = {0};
        // sprintf(sendBuf, "send data %d", num++);
        fgets(sendBuf, sizeof(sendBuf), stdin);

        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if(len == -1) {
            perror("read");
            return -1;
        }else if(len > 0) {
            printf("read buf = %s\n", sendBuf);
        } else {
            printf("服务器已经断开连接...\n");
            break;
        }
    }

    close(fd);

    return 0;
}

epoll水平触发模式代码如下:

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

int main() {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for(int i = 0; i < ret; i++) {

            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
                if(epevs[i].events & EPOLLOUT) {
                    continue;
                }   
                // 有数据到达,需要通信
                char buf[5] = {0};
                int len = read(curfd, buf, sizeof(buf));
                if(len == -1) {
                    perror("read");
                    exit(-1);
                } else if(len == 0) {
                    printf("client closed...\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                } else if(len > 0) {
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }

            }

        }
    }

    close(lfd);
    close(epfd);
    return 0;
}

epoll边沿触发模式代码如下:

边沿触发模式的代码与水平触发模式的代码是有所不同的,在前面的概念中,已经了解到了边沿触发模式仅会通知一次文件描述符从未就绪变为就绪(也就是有数据到了)。所以在边沿触发模式下,我们需要一次就读完发送方所发送的所有内容。如果没有读完的话,文件描述符的状态仍会是就绪,而在下次循环中epoll不会再通知我们,那发送方所发送的剩余的我们未读完的数据就丢失了。

如何一次读完呢?自然是需要while循环,但又有个问题,当读完数据后,read读不到数据了,但发送方又没有断开连接,这是read就会阻塞在这里,从而导致程序无法再继续往下运行。所以我们要设置read函数不阻塞,其实也就是设置套接字非阻塞,用到了fcntl函数。
而通过把套接字设置为非阻塞从而使read非阻塞,这就又会导致一个问题,当某次遍历已经把文件描述符缓冲区中的数据全部读完之后,下次来读,read不阻塞,但文件描述符中又没有数据,发送端连接未关闭,就会报一个EAGAIN的错误。也就是程序中第81行
在第74行printf没办法数据全部读完后打印出over,所以将74行的printf改为75行的write,直接将结果输出到终端

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>

int main() {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for(int i = 0; i < ret; i++) {

            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // 设置cfd属性非阻塞
                int flag = fcntl(cfd, F_GETFL);
                flag |= O_NONBLOCK;
                fcntl(cfd, F_SETFL, flag);

                epev.events = EPOLLIN | EPOLLET;    // 设置边沿触发
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
                if(epevs[i].events & EPOLLOUT) {
                    continue;
                }  

                // 循环读取出所有数据
                char buf[5];
                int len = 0;
                while( (len = read(curfd, buf, sizeof(buf))) > 0) {
                    // 打印数据
                    // printf("recv data : %s\n", buf);
                    write(STDOUT_FILENO, buf, len);
                    write(curfd, buf, len);
                }
                if(len == 0) {
                    printf("client closed....");
                }else if(len == -1) {
                    if(errno == EAGAIN) {
                        write(STDOUT_FILENO, "over.\n", strlen("over.\n") + 1);
                    }else {
                        perror("read");
                        exit(-1);
                    }
                    
                }

            }

        }
    }

    close(lfd);
    close(epfd);
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/706015.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

GO语言使用最简单的UI方案govcl

接触go语言有一两年时间了。 之前用Qt和C#写过桌面程序&#xff0c;C#会被别人扒皮&#xff0c;极度不爽&#xff1b;Qt默认要带一堆dll&#xff0c;或者静态编译要自己弄或者找库&#xff0c;有的库还缺这缺那&#xff0c;很难编译成功。 如果C# winform可以编译成二进制原生…

Android 应用层 到 HAL 层

Android 应用层 到 HAL 层 1、相关知识点1.1 概要1.2 参考 2、拿SensorService举例2.1 Android Apps > Android Framework阶段2.2 Android Framework内部阶段2.2.1 frameworks/base2.2.2 frameworks/native 2.3 Android Framework > HAL 阶段2.3.1 旧版 HAL 1、相关知识点…

前段搜索框不请求接口隐藏数据

项目介绍&#xff1a;uview-ui 1.x的&#xff0c;并且使用语言切换功能&#xff08;i18n&#xff0c;hbuilder新建项目选择i18n项目&#xff09;&#xff0c;因为是h5项目&#xff0c;所以使用location.reload()进行刷新 效果图&#xff1a; 主要判断在 v-if“!keyword || i…

Git 之 reset --hard 回退/回滚到之前的版本代码后,后悔了,如何在恢复之后的版本的方法简单整理

Git 之 reset --hard 回退/回滚到之前的版本代码后&#xff0c;后悔了&#xff0c;如何在恢复之后的版本的方法简单整理 目录 Git 之 reset --hard 回退/回滚到之前的版本代码后&#xff0c;后悔了&#xff0c;如何在恢复之后的版本的方法简单整理 一、简单介绍 二、操作步骤…

Redis是什么?(详细安装步骤)

一、Redis简介&#x1f349; 背景 在Web应用发展的初期&#xff0c;那时关系型数据库受到了较为广泛的关注和应用&#xff0c;原因是因为那时候Web站点基本上访问和并发不高、交互也较少。而在后来&#xff0c;随着访问量的提升&#xff0c;使用关系型数据库的Web站点多多少少…

代码随想录二刷 day38 | 动态规划之 509. 斐波那契数 70. 爬楼梯 746. 使用最小花费爬楼梯

day38 509. 斐波那契数1 确定dp数组以及下标的含义2 确定递推公式3 dp数组如何初始化4 确定遍历顺序5 举例推导dp数组 70. 爬楼梯1 确定dp数组以及下标的含义2 确定递推公式3 dp数组如何初始化4 确定遍历顺序5 举例推导dp数组 746. 使用最小花费爬楼梯1 确定dp数组以及下标的含…

Golang每日一练(leetDay0113) 奇偶链表、链表随机节点

目录 328. 奇偶链表 Odd Even Linked-list &#x1f31f;&#x1f31f; 382. 链表随机节点 Llinked-list Random Node &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Rust每日一练 专栏 Golang每日一练 专栏 Python每日一练 专栏 C/C每日…

docker安装php GD库

故事是这样的&#xff1a; 公司采购了一套商城源码&#xff0c;使用的是 TP5&#xff0c;同事先行&#xff0c;用宝塔部署到生产环境&#xff0c;运行正常。后面我忙完手里的项目&#xff0c;也加入其中&#xff0c;我本地使用的是 docker 当我部署好开始运行时&#xff0c;发…

初学mybatis(三)ResultMap及分页

学习回顾&#xff1a;初学mybatis&#xff08;二&#xff09; 一、查询为null问题 要解决的问题&#xff1a;属性名和字段名不一致 环境&#xff1a;新建一个项目&#xff0c;将之前的项目拷贝过来 1、查看之前的数据库的字段名 2、Java中的实体类设计 public class User {pri…

Redis各数据类型操作命令

一、Redis数据类型及命令 &#xff08;一&#xff09;String 类别命令描述命令示例备注取/赋值操作赋值set key valueset lclkey lclvalue取值 get keyget lclkey取值并赋值getset key valuegetset lclkey1 lclvalue1获取原值&#xff0c;并设置新的值仅当不存在时赋值setnx k…

服务器解析漏洞与cms靶场搭建教程

文章目录 一、解析漏洞定义二、Kali安装docker并搭建DVWA靶场三、Win7 IIS7漏洞复现四、BEES靶场搭建五、CPMS靶场搭建六、SDCMS靶场搭建 一、解析漏洞定义 解析漏洞主要是一些特殊文件被Apache、IIS、Nginx等Web服务器在某种情况下解释成脚本文件格式并得以执行而产生的漏洞 …

The Company Requires Superficial StudyPHP 变量的使用 ③

作者 : SYFStrive 博客首页 : HomePage &#x1f4dc;&#xff1a; PHP MYSQL &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f517; &#x1f4cc;&#xff1a;觉得文章不错可以点点关注 &#x1f44…

基于Java电动车租赁网站设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

信号链噪声分析20

文章目录 概要整体架构流程技术名词解释技术细节小结 概要 所有模数转换器(ADC)都有一定量的“折合到输入端噪声”&#xff0c;可以将其模拟为与无噪声 ADC 输入串联的噪声源。折合到输入端噪声与量化噪声不同&#xff0c;后者仅在 ADC 处理交流 信号时出现。多数情况下&#x…

嵌入式中C++开发的基本操作方法

第一&#xff1a;面向对象 1、配置环境 虚拟机上网&#xff08;ping www.baidu.com&#xff09;sudo apt-get update //更新软件包sudo apt-get install -f //更新软件依赖sudo apt-get install g //安装c编译器 2、C发展 c98,第一版 c03,c11,c17 3、为什么学习C 4、面向对…

python包的研究

目录 json的方法timecollectionsdatetimetimestampsocket json的方法 json.load&#xff1a;表示读取文件&#xff0c;返回python对象 json.dump&#xff1a;表示写入文件&#xff0c;文件为json字符串格式&#xff0c;无返回 json.dumps&#xff1a;将python中的字典类型转换…

11-Vue常见优化手段

前言&#xff1a; 永远不要过早优化&#xff0c;见招拆招 使用key 对于通过循环生成的列表&#xff0c;应给每个列表项一个稳定且唯一的key,这有利于在列表变动时&#xff0c;尽量少的删除&#xff0c;新增&#xff0c;改动元素 index作为key值是唯一的&#xff0c;但不够稳…

STM32外设系列—sg90(舵机)

文章目录 一、sg90简介二、引脚连接三、控制方法四、程序设计4.1 配置定时器4.2 编写控制程序 五、360舵机 一、sg90简介 首先介绍说一下什么是舵机。舵机是一种位置&#xff08;角度&#xff09;伺服的驱动器。适用于一些需要角度不断变化的&#xff0c;可以保持的控制系统。…

threejs物理效果和声音

个人博客地址: https://cxx001.gitee.io 一、Threejs中如何创建物理场景 threejs中创建物理场景我们用它的扩展库&#xff1a;Physijs。它可以使场景中的对象有重力效果&#xff0c;可以相互碰撞&#xff0c;施加力之后可以移动&#xff0c;还可以通过合页和滑块在移动过程中…

LeetCode 打卡day44--完全背包问题及其应用

一个人的朝圣 — LeetCode打卡第44天 知识总结 Leetcode 518. 零钱兑换 II题目说明代码说明 Leetcode 377. 组合总和 Ⅳ题目说明代码说明 知识总结 今天结束了完全背包问题, 完全背包问题与01背包问题的区别在于可以无限次的使用物品的数量. 其和01背包的差别在于, 01背包先遍…