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

这篇具有很好参考价值的文章主要介绍了【Linux】C++项目实战-高并发服务器详析。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

橙色

多进程实现并发服务器

server_process.c文件内容如下:

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


第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宝贵的时间片,但是每次只能处理一个操作。
高并发服务器开发,# Linux网络编程,服务器,linux,c++

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


BIO模型:通过多线程/多进程解决每次只能处理一个操作的缺陷。但是线程/进程本身需要消耗系统资源,并且线程和进程的调度占用CPU.
高并发服务器开发,# Linux网络编程,服务器,linux,c++

NIO模型

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

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

高并发服务器开发,# Linux网络编程,服务器,linux,c++

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

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

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

select

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

高并发服务器开发,# Linux网络编程,服务器,linux,c++
前三个是系统固定已经占用的

函数解析
//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;
}

服务器端程序:

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

我对第一次循环进行一个分析,先是在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函数。

#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;
}
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个文件描述符发生变化
		

高并发服务器开发,# Linux网络编程,服务器,linux,c++

代码示例

客户端程序和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(最重要,请重点掌握)

高并发服务器开发,# Linux网络编程,服务器,linux,c++

函数解析

#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:检测文件描述符什么事情(如果是删除操作的话直接NULL就行)
		  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

//检测函数,检测内核中的eventpoll是否有文件描述符改变了,注意events是一个struct epoll_event数组的指针
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);  
	- 参数:
		- epfd:epo11实例对应的文件描述符
		- events:传出参数,是一个struct epoll_event数组的指针,保存了发送了变化的文件描述符的信息,
		- 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不会再通知我们,那发送方所发送的剩余的我们未读完的数据就丢失了。


(假设char buf[2],然后发送方向文件描述符fd中输入lllhh,那么在LT模式下,首先epoll_wait会检测到fd中的读事件就绪。那么开始读取,因为buf的容量为2,所以先读取了ll,接着返回到了循环开头的epoll_wait,因为没读完,该fd中的读事件仍就处于就绪状态,再读,直到读完为止。而在ET模式下,先读取了ll,接着返回到了循环开头的epoll_wait,即使因为没读完该fd中的读事件仍就处于就绪状态,但不予理睬。后面的数据也就相当于丢失了。值得注意的是会丢失的前提是该发送方此后没有再发送数据,则剩余未读的在缓冲区中的数据就会丢失。如果说发送方又一次数据,比如发送了rr,那就会读取出lh,rr仍旧在读缓冲区不会被读出)


所以说ET模式下要保证一次读完。那么如何一次读完呢?自然是需要while循环,但又有个问题,当读完数据后,read读不到数据了,但发送方又没有断开连接,这是read就会阻塞在这里,从而导致程序无法再继续往下运行。所以我们要设置read函数不阻塞,其实也就是设置套接字非阻塞,用到了fcntl函数。
而通过把套接字设置为非阻塞从而使read非阻塞,这就又会导致一个问题,当某次遍历已经把文件描述符缓冲区中的数据全部读完之后,下次来读,read不阻塞,但文件描述符中又没有数据,发送端连接未关闭,就会报一个EAGAIN的错误。也就是程序中第81行。这种情况下不应该退出while循环,所以这里用了一个if来判别。
在第74行printf没办法数据全部读完后打印出over,所以将74行的printf改为75行的write,第75行是直接将buf的内容写入到标准输出,将数据写入到终端或命令行界面进行显示。第76行是将buf的内容写入到curfd套接字中,目的是为了完成回射,使客户端发送过来的内容再被客户端读取出来文章来源地址https://www.toymoban.com/news/detail-702332.html

#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;
}

到了这里,关于【Linux】C++项目实战-高并发服务器详析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • linux并发服务器 —— 动态库和静态库实战(一)

    -E 预处理指定源文件 -S 编译指定源文件 -c 汇编指定源文件 -o 生成可执行文件 -I directory 指定Include包含文件的搜索目录 -g 编译的时候生成调试信息 -D 在程序编译时指定一个宏 -w 不生成任何的警告信息 -Wall 生成所有警告 -On n:0~3;表示编译器的优化选项级别 O0 - 不优化;O1 -

    2024年02月11日
    浏览(52)
  • 一、C++项目:仿muduo库实现高性能高并发服务器

    仿mudou库one thread oneloop式并发服务器实现 仿muduo库One Thread One Loop式主从Reactor模型实现高并发服务器: 通过实现的高并发服务器组件,可以简洁快速的完成一个高性能的服务器搭建。并且,通过组件内提供的不同应用层协议支持,也可以快速完成一个高性能应用服务器的搭建

    2024年02月07日
    浏览(51)
  • C++linux高并发服务器项目实践 day4

    int access(const char * pathname ,int mode); int chmod(const char * filename,int mode); int chown(const char* path,uid_t owner,gid_t group); int truncate(const char* path,off_t length); #include unistd.h int access(const char *pathname, int mode); 作用:判断某个文件是否有某个权限,或者判断文件是否存在 参数: pathname:判断文件路

    2023年04月16日
    浏览(46)
  • C++linux高并发服务器项目实践 day2

    库的定义和特点详情请看隔壁c++阶段学习的day10查看 Linux: libxxx.a lib:固定前缀 xxx:库的名字,自定义 .a:固定后缀 windows:libxxx.lib gcc获得.o文件 将.o文件打包,使用ar工具(archive) ar rcs libxxx.a xxx.o xxx.o r- 将文件插入备存文件中 c-建立备存文件 s-索引 sudo apt install tree 安装tree插件,用

    2023年04月20日
    浏览(39)
  • C++linux高并发服务器项目实践 day5

    程序 是包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程: 程序是文件,只占用硬盘的大小;进程会占用cpu和内存资源 进程 是正在运行的程序的实例。是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。他是操作系统动态执行的基本单元,

    2023年04月17日
    浏览(42)
  • C++项目:仿mudou库one thread one loop式并发服务器实现

    目录 1.实现目标 2.HTTP服务器 3.Reactor模型 3.1分类 4.功能模块划分: 4.1SERVER模块: 4.2HTTP协议模块: 5.简单的秒级定时任务实现 5.1Linux提供给我们的定时器 5.2时间轮思想: 6.正则库的简单使用 7.通用类型any类型的实现 8.日志宏的实现 9.缓冲区buffer类的实现 10.套接字Socket类实现 11.

    2024年02月08日
    浏览(42)
  • (一)专题介绍:移动端安卓手机改造成linux服务器&linux服务器中安装软件、部署前后端分离项目实战

    总体概述: 本篇文章隶属于“手机改造服务器 部署前后端分离项目”系列专栏,该专栏将分多个板块,每个板块独立成篇 来详细记录:手机(安卓)改造成个人服务器(Linux)、Linux中安装软件、配置开发环境、部署JAVA+VUE+MySQL5.7前后端分离项目,以及内网穿透实现外网访问等全过

    2024年02月04日
    浏览(51)
  • 实战:Prometheus+Grafana监控Linux服务器及Springboot项目

    相信大家都知道一个项目交付生产并不意味着结束,更多的是对线上服务的运维监控。运维监控主要涉及到部署服务器的资源情况,各个子服务的资源情况以及垃圾收集和吞吐量等等,还有故障告警等等功能。当然,作为一个搬砖人也是需要了解全链路的运维监控组件Promet

    2024年02月14日
    浏览(50)
  • 【linux高性能服务器编程】项目实战——仿QQ聊天程序源码剖析

    hello !大家好呀! 欢迎大家来到我的Linux高性能服务器编程系列之项目实战——仿QQ聊天程序源码剖析,在这篇文章中, 你将会学习到如何利用Linux网络编程技术来实现一个简单的聊天程序,并且我会给出源码进行剖析,以及手绘UML图来帮助大家来理解,希望能让大家更能了

    2024年04月28日
    浏览(42)
  • linux并发服务器 —— 多进程并发(四)

    程序是包含一系列信息的文件,描述了如何在运行时创建一个进程; 进程是正在运行的程序的实例,可以用一个程序来创建多个进程; 用户内存空间包含程序代码以及代码所使用的变量,内核数据结构用于维护进程状态信息; 进程控制块(PCB):维护进程相关的信息,tas

    2024年02月11日
    浏览(54)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包