高并发处理专题研究 - epoll并发编程[更新中]

文章目录

  • 1 前置知识
    • 1.1 Socket编程基础
      • Socket概述
      • Socket通信模型
      • Socket API
      • 一个简单的Socket编程实例
    • 1.2 IO多路复用
    • 1.3 阻塞原理
  • 2 epoll原理
    • 2.1 epoll概述
    • 2.2 epoll系统调用
      • epoll_create()
      • epoll_ctl()
      • epoll_wait()
    • 2.3 epoll工作原理
  • 3 示例代码及演示

1 前置知识

1.1 Socket编程基础

Socket概述

  Socket是什么?
    Socket是一种进程之间通信的方法,允许同一主机或(通过网络连接起来的)不同主机上的应用程序进行数据交换。由于Socket起源于UNIX,继承自UNIX“一切皆文件”的思想,因此Socket本身就一种特殊的文件。

  操作Socket的核心——文件描述符
    既然Socket本身就是文件,那Socket函数(Socket API)对Socket的操作,本质上就是对文件的操作。内核为了高效管理打开的文件,会为每一个文件创建一个称为文件描述符(file descriptor)的索引。所有对文件进行I/O操作的系统调用,都需要经过文件描述符。因此,在编写Socket代码时,操作一个创建好的Socket,都需要经过文件描述符这一句柄。

  和Epoll什么关系?
    对于高并发的服务型程序,Socket连接需要处理大批量的文件描述符(连接数可达几十上百万),简单轮询如此大规模的文件描述符,普通的方法是行不通的。而Epoll就是为此而生的,能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。关于Epoll的详细介绍,会在之后的小节中展示!

Socket通信模型

  下图展示了Socket的通信模型(篇幅有限,以使用TCP协议为例)。每一个过程对应一个Socket API。在编写Socket程序时,需要遵循图中的顺序。
在这里插入图片描述

Socket API

  下面对Socket API进行简单介绍。与上图是对应的,可以对比学习。

socket()

  函数在一个通信域(domain)中创建一个(未绑定的)Socket,并返回一个文件描述符,之后介绍的Socket API就可以使用该文件描述符对Socket进行操作啦!

  函数原型:int socket(int domain, int type, int protocol);

domain:指明一个创建Socket的通信域类型。
  PF_INET:协议族,指定协议时用;
  AF_INET:地址族,设置地址时用。
type:指明待创建Socket的类型。
  SOCK_STREAM:流服务,TCP协议;
  SOCK_DGRAM:数据报服务,UDP协议。
protocol:指明待创建Socket使用的协议。
    若为0,则使用适合请求Socket类型的默认协议(一般取0)
返回值:创建好的Socket的文件描述符,应为非负整数;若创建失败,则返回-1。

bind()

  为一个(由socket函数创建的)未命名Socket绑定一个Socket地址
  函数原型:int bind(int socket, const struct sockaddr *address, socklen_t address_len);

socket:Socket的文件描述符。
address:指明一个要绑定的Socket地址(指向sockaddr结构的指针)。
address_len:指明Socket地址的长度(sockaddr结构体的长度)
    若为0,则使用适合请求Socket类型的默认协议(一般取0)
返回值:成功绑定,返回0,否则返回-1。

  注意Socket编程使用sockaddr结构体来管理Socket地址,而sockaddr_in结构体是其对应的Internet风格。二者关系具有相同的长度,并且可以相互强制转换指针。设计上类似基类和派生类的关系,Socket API的参数通常为更通用的“基类”sockaddr指针。在你想使用sockaddr_in指针作为参数时,需要先对其进行强制转换!
  sockaddr_in结构体字段

struct sockaddr_in {
	__uint8_t       sin_len;  // 结构体sin的长度
    sa_family_t     sin_family;  // 地址族,必须设为AF_INET(表示IPv4协议)
    in_port_t       sin_port;  // 端口(2B)
    struct  in_addr sin_addr;  // IPv4地址(4B)
	char            sin_zero[8];  // 未使用,设置为0(8B)
};

// in_addr结构体表示一个IPv4地址
struct in_addr {
	in_addr_t s_addr;
};

listen()

  监听Socket连接,并且可以限制监听队列长度(连接的数量)。

  函数原型:int listen(int socket, int backlog);

socket:Socket描述符。
backlog:指明监听队列长度(连接的数量)。
    若小于0,则设置为0;
    若大于Socket监听队列支持的最大长度,则设置为最大长度。
返回值:成功监听,返回0,否则返回-1。

accept()

  顾名思义,就是在Socket接受一个新的连接。具体来说是从监听队列中出队一个新的Socket连接,然后创建与其具有相同Socket类型协议和地址族的新Socket,并为之分配一个新的文件描述符。

  函数原型:int accept(int socket, struct sockaddr *address, socklen_t *address_len);

socket:正在监听的Socket描述符。
address:连接对方的Socket地址(指向sockaddr结构的指针)
address_len:连接对方的Socket的地址长度(sockaddr结构体的长度)
返回值:连接对方的Socket的文件描述符,应为非负整数;
    若创建失败,则返回-1。

send()

  在Socket上向连接的对方发送一条消息。

  函数原型:ssize_t send(int socket, const void *buffer, size_t length, int flags);

socket:Socket描述符。
buffer:指向包含所要发送消息的buffer数组。
length:指明消息长度(字节)。
flags:指明消息传输的类型,设置为0或者0与以下flag相或(|):
    MSG_EOR:终止一个记录;
    MSG_OOB:在支持带外通信的Socket上发送带外数据。
返回值:发送成功返回数据字节数,否则返回-1。

recv()

  从已连接的对方Socket接收信息

  函数原型:ssize_t recv(int socket, void *buffer, size_t length, int flags);

socket:Socket描述符。
buffer:指向用于接收消息的buffer。
length:指明消息长度(字节)。
flags:指明消息传输的类型,设置为0或者0与以下flag相或(|):
    MSG_EOR:终止一个记录;
    MSG_OOB:在支持带外通信的Socket上发送带外数据。
返回值:接收成功返回数据字节数,否则返回-1。

一个简单的Socket编程实例

  在读者熟悉Socket API的使用后,可以尝试理解如下源码(server.cclient.c)。该程序中,Client需要向Server发起连接,以从Server获取需要的信息(14字节长的"Hello, World!\n")。Server则不断接受Client发起的请求,并向其发送信息。

  server.c:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>

#define true 1
#define false 0

#define MYPORT 3490  // Server监听的端口
#define BACKLOG 10  // listen的请求接收队列长度

int main()
{
    int sfd;  // 存放创建好的服务器监听端口(sfd -> socket file descriptor)
    if ((sfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) {
        perror("Socket创建失败!");
        exit(1);
    }

    struct sockaddr_in sa;  // 存放Server自身的Socket地址信息(sa -> socket address)
    sa.sin_family = AF_INET;
    sa.sin_port = htons(MYPORT);  // htons将主机字节序转换为网络字节序
    sa.sin_addr.s_addr = INADDR_ANY;  // 自动填本机IP
    memset(&(sa.sin_zero), 0, 8); // 其余部分置0

    if (bind(sfd, (struct sockaddr *)&sa, sizeof(sa)) == -1) {
        perror("Bind失败!");
        exit(1);
    }

    if (listen(sfd, BACKLOG) == -1) {
        perror("Listen失败!");
        exit(1);
    }

    struct sockaddr_in gas;  // 存放连接对方(客户端)的Socket地址信息(gas -> guest addresses)
    unsigned int sin_size = sizeof(struct sockaddr_in);  // sockaddr_in结构体的大小

    // 主循环
    while (true) {
        int new_fd = accept(sfd, (struct sockaddr *)&gas, &sin_size);
        if (new_fd == -1) {
            perror("Accept失败!");
            continue;
        }
        printf("获得来自%s的连接\n", inet_ntoa(gas.sin_addr));

        if (fork() == 0) {  // 子进程,fork返回0
            if (send(new_fd, "Hello, World!\n", 14, 0) == -1)
                perror("send");

            close(new_fd);
            exit(0);
        }

        close(new_fd);
        while (waitpid(-1, NULL, WNOHANG) > 0);  // 清除所有子进程
    }
}

  client.c:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>

#define true 1
#define false 0

#define PORT 3490  // Server监听的端口
#define MAXDATASIZE 100  // 客户端可读入的最大字节数

int main(int argc, char *argv[]) {
    if (argc != 2) {  // 参数数量不对
        fprintf(stderr, "命令使用方法:client hostname\n");
        exit(1);
    }

    struct hostent *he;  // 主机(服务器)信息
    if ((he = gethostbyname(argv[1])) == NULL) {
        perror("Gethostbyname失败!");
        exit(1);
    }

    int sfd;  // 存放创建好的客户端监听端口(sfd -> socket file descriptor)
    if ((sfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) {
        perror("Socket创建失败!");
        exit(1);
    }

    struct sockaddr_in sa;  // 服务器地址信息
    sa.sin_family = AF_INET;
    sa.sin_port = htons(PORT);  // htons将主机字节序转换为网络字节序
    sa.sin_addr = *((struct in_addr *)he -> h_addr_list[0]);  // 指定服务器IP
    memset(&(sa.sin_zero), 0, 8); // 其余部分置0

    if (connect(sfd, (struct sockaddr *)&sa, sizeof(struct sockaddr)) == -1) {
        perror("Connect失败!");
        exit(1);
    }

    int numbytes;
    char buf[MAXDATASIZE];
    if ((numbytes = recv(sfd, buf, MAXDATASIZE, 0)) == -1) {
        perror("recv");
        exit(1);
    }
    buf[numbytes] = '\0';

    printf("接收到数据: %s", buf);
    close(sfd);

    return true;
}

  运行效果

    先将源码编译为可执行文件:

gcc client.c -o client
gcc server.c -o server

    开启一个终端执行server:
在这里插入图片描述
    开启另一个终端执行client:
在这里插入图片描述
    可以观察到Server端输出了成功获得Client端的连接信息,而Client端也成功从Server端获取到需要的数据。

1.2 IO多路复用

1.3 阻塞原理

  网卡是怎么接收到网络上的数据的?
    网卡从网线接收到传来的数据,再经过硬件电路的传输,最终将数据写入内存的某个地址上。
  CPU怎么知道接收了数据?
    当网卡通过上面的过程将数据写入内存后,网卡就会向CPU发送中断信号,以告知有数据到来。

  阻塞原理
    从进程调度的角度来看,若进程在等待某一事件(如等待接收网络数据),则会在事件发生之前进入阻塞状态(也叫等待状态)。Socket API中的recv函数和epoll本质上都是阻塞方法。

    下面通过一个例子来理解阻塞过程。假设一个接收客户端消息的服务器的Socket代码具有以下结构:

int sfd = socket(AF_INET, SOCK_STREAM, 0);
bind(sfd, ...);
listen(sfd, ...);
int new_sfd = accept(sfd, ...);
recv(sfd, ...);  // 会发生阻塞!

    在程序执行到recv时,就会进入阻塞状态一直等待,直到接收到数据才会往下执行。如下图所示,假设进程1会执行上面这个Socket程序。
在这里插入图片描述
    在进程1被操作系统调度后,会为其创建一个由文件系统管理的Socket对象,包含发送缓冲区、接受缓冲区和等待队列等成员。等待队列指向了所有需要等待该Socket事件的进程。当进程1执行到recv函数时,会因为等待数据而被加入该Socket对象的等待队列中,如下图所示:在这里插入图片描述
    主机从网线接收数据到网卡、写入内存以后,CPU接收到中断信号进行中断处理,接收数据。直到数据接收完成,才会唤醒进程1,重新放回工作队列中。
    从上面这个单个连接的例子来看,Socket程序线性执行过程中的阻塞过程是简单明了的。但存在多个连接、多个Socket对象时,又如何知道哪些数据到达、唤醒哪些进程呢?这就需要使用到epoll技术了。

2 epoll原理

2.1 epoll概述

epoll是什么?
  epoll是一个Linux实现IO多路复用的一种(最佳)工具,用于可扩展的I/O事件通知机制,管理具有可读可写事件的文件描述符(即fd、句柄,后文可能混用这三个术语)。在Linux内核的2.5.45版本中被首次引入。 它维护一个监视列表(=兴趣列表),监视多个文件描述符,查看是否可以在其中任何一个文件上进行I/O操作。相比旧的select和poll系统调用,能在要求更高的应用程序中实现更好的性能。

应用场景:
  Epoll经常应用于Linux下高并发服务型程序。尤其适合大量并发连接中只有少部分连接处于活跃下的情况。在这种情况下Epoll能显著的提高程序的CPU利用率。

四个特点:

多路复用
事件驱动
水平触发边缘触发
高性能

2.2 epoll系统调用

  epoll提供三种系统调用:epoll_createepoll_ctlepoll_wait。简单粗暴地理解,epoll_create负责创建一个epoll池,用于监控和管理fd。epollctl负责对这个池子里的fd进行增删改。epoll_wait负责在没有事件时阻塞epoll,以免占用CPU资源,一旦有事件则会唤醒。

epoll_create()

  创建一个epoll对象(epoll池)并返回一个文件描述符。这个epoll对象对用户而言是黑盒,无需考虑其中的细节。
  epoll_create1()是epoll_create()的新版本,epoll_create()在Linux内核版本2.6.27和glibc版本2.9中被废除。

  函数原型:int epoll_create(int size);int epoll_create1(int flags);

flags:用于改变epoll的行为,不改变取0,除此以外只有EPOLL_CLOEXEC一种特殊取值。
返回值:创建成功,返回一个非负整数的文件描述符,否则返回-1。

  使用示例:

efd = epoll_create1();
if (efd == -1) {
    perror("epoll_create error");
    exit(-1);
}

epoll_ctl()

  用于控制(配置)epoll对象监视的文件描述符和事件。创建好epoll池,就可以通过epoll_ctl系统调用来添加fd啦!

  函数原型:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epfd:epoll_create1()创建得到的文件描述符。
op:执行在目标文件描述符fd上的操作。
  EPOLL_CTL_ADD:在epoll文件描述符epfd的兴趣列表中添加一条记录。记录包括文件描述符fd、对fd相应打开文件描述的引用,以及通过event声明的设置。
  EPOLL_CTL_MOD:将兴趣列表中fd的设置改变为event中新指定的设置。
  EPOLL_CTL_DEL:从兴趣列表删除fd的记录。event中指定的设置将被忽略(可直接设置为NULL)。
fd:被操作的文件描述符fd。
event:指向fd所连接的对象(epoll_event结构体)。epoll_event结构体介绍见后文。
返回值:若成功返回0,否则返回-1。

  epoll_event结构体:
    该结构体指明了内核应该存储的数据,以及在数据准备好时应该返回的对应文件描述符。

struct epoll_event {
	uint32_t      events;  /* Epoll events */
	epoll_data_t  data;    /* User data variable */
};

union epoll_data {
	void     *ptr;
	int       fd;
    uint32_t  u32;
    uint64_t  u64;
};

typedef union epoll_data  epoll_data_t;

    其中,数据成员data指明了内核应该存储的数据,以及在数据准备好时应该返回的对应文件描述符。数据成员events则是由0或者其他event类型相或(|)所得的值,用于影响event的行为。

event类型详见

  使用示例:

// 将aFd句柄加入epoll池中
if (epoll_ctl(efd, EPOLL_CTL_ADD, aFd, &aEvent) == -1) {
    perror("epoll_ctl error");
    exit(-1);
}

epoll_wait()

  等待一个epoll文件描述符上的I/O事件。调用epoll_wait()会一直阻塞,直到以下情况发生:
  (1) 一个文件描述符触发了一个事件;
  (2) 调用被信号处理程序中断;
  (3) 超过参数timeout指定的时间。

  函数原型:int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epfd:epoll_create1()创建得到的文件描述符。
events:指向调用者可以使用的事件的内存区域。
maxevents:告知内核events的数量,必须大于0。
timeout:指明阻塞的毫秒数,时间是根据CLOCK_MONOTONIC时钟测量的。timeout == -1,函数会永远阻塞下去;timeout == 0,立刻返回。
返回值:若成功返回0,否则返回-1。

2.3 epoll工作原理

  熟悉epoll的系统调用后,相信读者已经能对epoll的工作机制有点感觉了。总结来说,当某个进程调用epoll_create方法时,内核会创建一个eventpoll对象(即文件描述符epfd所代表的对象,可以通俗地称之为epoll池)。之后就可以往池子里增、删、改需要监视(感兴趣)的fd,这需要使用epoll_ctl调用。

  epoll使用红黑树管理epoll池
    为了高效地对eventpoll中的fd进行增、删、改,自然就需要考虑高效的数据结构。为此,Linux内核使用红黑树来实现管理epoll池中的fd。红黑树是一种平衡二叉树,其增删改操作时间复杂度为 O ( log n ) O(\text{log}n) O(logn),能够保证稳定的查找性能。

   poll回调机制
    为了在数据准备后能让epoll发现,还需要依靠poll回调机制。在Linux内核中,文件的操作定义为结构体struct file_operation。其中的成员函数poll能够与底层交互,fd一旦读写就绪,底层硬件(网卡)会调时就会把fd对应的结构体放到就绪队列中,唤醒进程。epoll池在通过epoll_ctl添加fd时,就会调用poll函数,把fd就绪之后的回调路径提前设置好。通过这种事件通知的方式,实现高效运行。

3 示例代码及演示

  utility.h

#ifndef UTILITY_H_INCLUDED
#define UTILITY_H_INCLUDED

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

#define true 1
#define false 0

using namespace std;

list<int> clientsList;  // 存放所有用户的fd

#define SERVER_IP "127.0.0.1"  // 服务器IP
#define SERVER_PORT 8888  // 服务器端口号
#define EPOLL_SIZE 5000  // epoll大小
#define BUF_SIZE 0xFFFF  // 缓冲区大小
#define SERVER_WELCOME "欢迎加入聊天室!\n您的ID是: #%d"  // 聊天室欢迎用户信息格式
#define SERVER_MESSAGE "用户 #%d >> %s"  // 用户发言信息格式
#define EXIT "EXIT"
#define CAUTION "当前只有您一名用户,无法聊天!"

int setnonblocking(int sockfd) {
    fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0) | O_NONBLOCK);
    return 0;
}

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

int sendBroadcastmessage(int clientfd) {
    char buf[BUF_SIZE];  // 接收新的聊天信息
    char message[BUF_SIZE];  // 存放格式化的信息
    bzero(buf, BUF_SIZE);
    bzero(message, BUF_SIZE);

    // receive message
    printf("接收到来自用户#%d的消息\n", clientfd);
    int len = recv(clientfd, buf, BUF_SIZE, 0);

    if (len <= 0) { // 用户关闭连接
        close(clientfd);
        clientsList.remove(clientfd); // 服务器移除用户
        printf("用户#%d关闭.\n 聊天室现有%d名用户\n", clientfd, (int)clientsList.size());
    }
    else {  // 广播消息
        if (clientsList.size() == 1) { // 只有1名用户在聊天室中
            send(clientfd, CAUTION, strlen(CAUTION), 0);
            return len;
        }
        sprintf(message, SERVER_MESSAGE, clientfd, buf);
        for (list<int>::iterator it = clientsList.begin(); it != clientsList.end(); ++it) {
           if(*it != clientfd){
                if(send(*it, message, BUF_SIZE, 0) < 0 ) {
                    perror("发送失败!");
                    exit(-1);
                }
           }
        }
    }

    return len;
}
#endif // UTILITY_H_INCLUDED

  epoll_server.cpp

#include "utility.h"

int main(int argc, char *argv[]) {
    // 设置服务器Socket地址
    struct sockaddr_in sa;
    sa.sin_family = PF_INET;
    sa.sin_port = htons(SERVER_PORT);
    sa.sin_addr.s_addr = inet_addr(SERVER_IP);

    // 创建监听的Socket
    int sfd = socket(PF_INET, SOCK_STREAM, 0);
    if (sfd < 0) {
        perror("Socket创建失败!");
        exit(-1);
    }

    // 绑定Socket地址
    if(bind(sfd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
        perror("Bind失败!");
        exit(-1);
    }

    // 监听
    int ret = listen(sfd, 5);
    if (ret < 0) {
        perror("Listen失败!");
        exit(-1);
    }
    printf("开始监听: %s\n", SERVER_IP);

    // 在内核中创建事件表
    int epfd = epoll_create(EPOLL_SIZE);
    if (epfd < 0) {
        perror("epfd error");
        exit(-1);
    }
    printf("创建epoll, epfd = %d\n", epfd);
    static struct epoll_event events[EPOLL_SIZE];
    // 向内核事件表中添加事件
    addfd(epfd, sfd, true);

    // 主循环
    while (true) {
        int cnt = epoll_wait(epfd, events, EPOLL_SIZE, -1); // 记录就绪事件的数目
        if(cnt < 0) {
            perror("epoll失败!");
            break;
        }

        printf("就绪事件数目: %d\n", cnt);
        // 处理就绪事件(共cnt个)
        for (int i = 0; i < cnt; i++) {
            int new_sfd = events[i].data.fd;
            //新用户连接
            if (new_sfd == sfd) {
                struct sockaddr_in ca;
                socklen_t client_addrLength = sizeof(struct sockaddr_in);
                int clientfd = accept(sfd, (struct sockaddr*)&ca, &client_addrLength );

                printf("用户连接: %s : %d(IP : port), 用户fd = %d\n", inet_ntoa(ca.sin_addr),
                    ntohs(ca.sin_port), clientfd);

                addfd(epfd, clientfd, true); // 把这个新的客户端添加到内核事件列表

                // 服务端用list保存用户连接
                clientsList.push_back(clientfd);
                printf("加入新的用户fd = %d 至epoll中\n", clientfd);
                printf("当前有%d名用户在聊天室中\n", (int)clientsList.size());

                // 服务端发送欢迎信息
                printf("欢迎!\n");
                char message[BUF_SIZE];
                bzero(message, BUF_SIZE);
                sprintf(message, SERVER_WELCOME, clientfd);
                int ret = send(clientfd, message, BUF_SIZE, 0);
                if (ret < 0) {
                    perror("Send失败!");
                    exit(-1);
                }
            }
            //客户端唤醒//处理用户发来的消息,并广播,使其他用户收到信息
            else {
                int ret = sendBroadcastmessage(new_sfd);
                if(ret < 0) { perror("error");exit(-1); }
            }
        }
    }
    close(sfd);
    close(epfd);

    return 0;
}

  epoll_client.cpp

#include "utility.h"

int main(int argc, char *argv[]) {
    
    // 设置服务器Socket地址
    struct sockaddr_in sa;
    sa.sin_family = PF_INET;
    sa.sin_port = htons(SERVER_PORT);  // htons将主机字节序转换为网络字节序
    sa.sin_addr.s_addr = inet_addr(SERVER_IP);

    // 创建socket
    int sfd = socket(PF_INET, SOCK_STREAM, 0);
    if (sfd < 0) {
        perror("Socket创建失败!");
        exit(-1);
    }

    // 连接服务端
    if (connect(sfd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
        perror("Connect失败!");
        exit(-1);
    }

    // 创建管道,fd[0]用于父进程读,fd[1]用于子进程写
    int pipe_fd[2];
    if (pipe(pipe_fd) < 0) {
        perror("pipe error");
        exit(-1);
    }

    // 创建epoll
    int epfd = epoll_create(EPOLL_SIZE);
    if (epfd < 0) { perror("epfd error"); exit(-1); }
    static struct epoll_event events[2];
    //将sock和管道读端描述符都添加到内核事件表中
    addfd(epfd, sfd, true);
    addfd(epfd, pipe_fd[0], true);
    // 表示客户端是否正常工作
    bool clientSta = true;

    // 聊天信息缓冲区
    char message[BUF_SIZE];

    // Fork
    int pid = fork();
    if(pid < 0) {
        perror("fork出错!");
        exit(-1);
    }
    else if(pid == 0) {  // 子进程
        close(pipe_fd[0]);  // 子进程负责写,因此先关闭读端
        printf("请输入'exit'退出聊天室\n");

        while (clientSta) {
            bzero(&message, BUF_SIZE);
            fgets(message, BUF_SIZE, stdin);

            // 客户输出'exit',退出
            if(strncasecmp(message, EXIT, strlen(EXIT)) == 0)
                clientSta = 0;
            else {  // 子进程将信息写入管道
                if (write(pipe_fd[1], message, strlen(message) - 1) < 0) {
                    perror("fork出错!");
                    exit(-1);
                }
            }
        }
    }
    else {  // 父进程
        //父进程负责读,因此先关闭写端
        close(pipe_fd[1]);

        // 主循环(epoll_wait)
        while (clientSta) {
            int epoll_events_count = epoll_wait( epfd, events, 2, -1 );
            // 处理就绪事件
            for (int i = 0; i < epoll_events_count; i++) {
                bzero(&message, BUF_SIZE);

                // 服务端发来消息
                if (events[i].data.fd == sfd) {
                    //接受服务端消息
                    int ret = recv(sfd, message, BUF_SIZE, 0);

                    // ret= 0 服务端关闭
                    if(ret == 0) {
                        printf("Server closed connection: %d\n", sfd);
                        close(sfd);
                        clientSta = 0;
                    }
                    else printf("%s\n", message);

                }
                //子进程写入事件发生,父进程处理并发送服务端
                else {
                    int ret = read(events[i].data.fd, message, BUF_SIZE);  // 父进程从管道中读取数据
                    if (ret == 0)
                        clientSta = 0;
                    else  // 将信息发送给服务端
                        send(sfd, message, BUF_SIZE, 0);
                }
            }
        }
    }

    if (pid) {
       // 关闭父进程和Socket
        close(pipe_fd[0]);
        close(sfd);
    }
    else {
        // 关闭子进程
        close(pipe_fd[1]);
    }
    return 0;
}

  运行效果

    先将源码编译为可执行文件:

gcc -lstdc++ epoll_server.cpp -o server
gcc -lstdc++ epoll_client.cpp -o client

    开启一个终端运行服务端程序:
在这里插入图片描述
    开启另一个终端运行客户端程序:
在这里插入图片描述
    此时服务端也有用户加入的记录:
在这里插入图片描述
    由于只有一个用户,因此无法聊天:
在这里插入图片描述    再开启另一个终端运行客户端程序,加入聊天室,即可进行聊天:
在这里插入图片描述
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

了解 NSA 关于管理 OSS 和 SBOM 的最新指南

开源软件很容易受到恶意行为者的攻击&#xff0c;但软件材料清单可以帮助减轻威胁。美国国家安全局的指导为管理生态系统奠定了坚实的基础。 软件供应链安全仍然是网络安全和软件行业的一个关键话题&#xff0c;并且有充分的理由&#xff0c;从针对大型软件供应商的持续攻击到…

vue3使用vuex

vuex: 状态管理工具 使用场景&#xff1a;用户登录状态 购物车 地理位置 等 数据位置&#xff1a;内存 安装 项目根目录 yarn add vuex 在src目录下新建store文件夹 下面新建index.js src/store/index.js 在main.js中引入并使用 // 导入状态管理工具vuex import store…

基于价值认同的需求侧电能共享分布式交易策略(matlab完全复现)

目录 1 主要内容 2 部分程序 3 程序结果 4 下载链接 1 主要内容 该程序完全复现《基于价值认同的需求侧电能共享分布式交易策略》&#xff0c;针对电能共享市场的交易机制进行研究&#xff0c;提出了基于价值认同的需求侧电能共享分布式交易策略&#xff0c;旨在降低电力市…

23 UVM Event

even机制提供进程之间的同步。与System Verilo event相比&#xff0c;UVM event提供了额外的灵活性&#xff0c;如保持事件等待器/event waiters的数量和设置回调。 uvm_event类声明&#xff1a; virtual class uvm_event_base extends uvm_object class uvm_event#(type Tuv…

语义分割的应用及发展

语义分割(Semantic Segmentation)是一种计算机视觉领域的任务&#xff0c;旨在将一张图像中的每一个像素都分配一个语义标签&#xff0c;即将图像中的每个物体区域进行精确的分类划分。例如&#xff0c;在一张街景图中&#xff0c;语义分割可以将人、车、路、天空等每个像素分别…

Android14新特性 开启前台service服务

1. Android14新特性 1.1. 场景 在Android14&#xff08;targetSDK34&#xff09;系统手机开启前台service服务崩溃 ATAL EXCEPTION: mainProcess: com.inspur.lbrd, PID: 15634java.lang.RuntimeException: Unable to create service com.inspur.lbrd.service.KeepAliveServi…

计算机毕业设计 基于html5的图书管理系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

数据结构期末复习(1)数据结构和算法 线性表

数据结构期末总复习&#xff08;gaois课堂版&#xff09; 数据结构的概念 数据结构是计算机科学中的一个重要概念&#xff0c;它指的是组织和存储数据的方式。数据结构可以帮助我们高效地操作和管理数据&#xff0c;使得计算机程序能够更加有效地执行各种任务。 数据结构有很…

关于链表的一些问题

求链表的中间节点 可以定义两个指针&#xff0c;一个一次走两步一个一次走一步&#xff0c;当走的快的走到NULL时&#xff0c;走的慢的就是链表的中间节点。&#xff08;此法求出的偶数个节点的链表的中间节点是它中间的第二个&#xff09; 求倒数第K个节点 也可以定义两个指…

【HarmonyOS】ArkTS语言介绍与组件方式运用

从今天开始&#xff0c;博主将开设一门新的专栏用来讲解市面上比较热门的技术 “鸿蒙开发”&#xff0c;对于刚接触这项技术的小伙伴在学习鸿蒙开发之前&#xff0c;有必要先了解一下鸿蒙&#xff0c;从你的角度来讲&#xff0c;你认为什么是鸿蒙呢&#xff1f;它出现的意义又是…

【网络安全 | MD5截断比较】PHP、Python脚本利用

前言 在解题中&#xff0c;当遇到类似 substr(md5(a),-6,6) 7788这样的MD5截断比较的题目时&#xff0c;只有求出a的值才能进行接下来的操作。 一个一个去猜是不可能的&#xff0c;通常使用脚本解决&#xff0c;文末给出实战案例。 PHP循环脚本 <?phpfor($i1;$i<9…

小信跳房子的题解

原题描述&#xff1a; 时间&#xff1a;1s 空间&#xff1a;256M 题目描述&#xff1a; 小信在玩跳房子游戏&#xff0c;已知跳房子游戏的图表现为一颗完美的具有个节点的二叉树。从根节点依次编号为。节点的左子节点编号为&#xff0c;右子节点编号为。 小信从从节点出发&…

python观察图像的直流分量——冈萨雷斯数字图像处理

原理 在数字图像处理中&#xff0c;图像的直流分量&#xff08;DC分量&#xff09;是指图像中的平均亮度水平。这个概念源自于傅里叶变换&#xff0c;其中信号可以分解为多个频率成分。在这个上下文中&#xff0c;直流分量对应于频率为零的成分&#xff0c;即信号的平均值。 在…

【实用工具】vim常用命令

快速移动(上下左右箭头可替代) 左移 h 右移 l 下移 j 上移 K在本行操作 0 移动到本行行首 ^ 移动到本行的第一个不是 blank 字符 $ 移动到本行行尾 w 光标移动到下一个单词的开头 e 光标移动到下一个单词的结尾跨行移动光标 nG 光标定位到第n行的行首 gg 光标定位到第一行的…

基于JAVA的食品生产管理系统 开源项目

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 加工厂管理模块2.2 客户管理模块2.3 食品管理模块2.4 生产销售订单管理模块2.5 系统管理模块2.6 其他管理模块 三、系统展示四、核心代码4.1 查询食品4.2 查询加工厂4.3 新增生产订单4.4 新增销售订单4.5 查询客户 五、…

看懂基本的电路原理图(入门)

文章目录 前言一、二极管二、电容三、接地一般符号四、晶体振荡器五、各种符号的含义六、查看原理图的顺序总结 前言 电子入门&#xff0c;怎么看原理图&#xff0c;各个图标都代表什么含义&#xff0c;今天好好来汇总一下。 就比如这个电路原理图来说&#xff0c;各个符号都…

我的2023年度总结(一)

在本文开始之前&#xff0c;先对我2023年的所为进行一些道歉&#xff1a; 部分工作中的客户/合作伙伴&#xff0c;在2023年我可能时长怠慢了您的消息。但我真不是故意的(有时可能在忙其他事情)。2024年&#xff0c;如有任何问题请尽可能抛过来吧。部分粉丝朋友&#xff0c;甚至…

2023-12-22 LeetCode每日一题(得到山形数组的最少删除次数)

2023-12-22每日一题 一、题目编号 1671. 得到山形数组的最少删除次数二、题目链接 点击跳转到题目位置 三、题目描述 我们定义 arr 是 山形数组 当且仅当它满足&#xff1a; arr.length > 3存在某个下标 i &#xff08;从 0 开始&#xff09; 满足 0 < i < arr.…

精品Nodejs实现的在线菜谱食谱美食学习系统的设计与实现

《[含文档PPT源码等]精品Nodejs实现的在线菜谱学习系统的设计与实现[包运行成功]》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程、包运行成功&#xff01; 软件开发环境及开发工具&#xff1a; 操作系统&#xff1a;Windows 10、Windows 7、Windows…

【数据结构——图】图的遍历(头歌习题)【合集】

目录 第1关&#xff1a;邻接矩阵存储图的深度优先遍历任务描述相关知识邻接矩阵存储图图的遍历DFS伪代码——邻接矩阵存储实现 完整代码 第2关&#xff1a;邻接表存储图的广度优先遍历任务描述相关知识邻接表存储图图的遍历广度优先遍历过程&#xff1a;BFS伪代码——邻接表实现…