Linux C++ Socket 套接字、select、poll、epoll 实例

文章目录

  • 1. 概述
  • 2. TCP 网络编程实例
    • 2.1 服务器端
    • 2.2 客户端
    • 2.3 运行截图
  • 3. I/O 模型
    • 3.1 阻塞式I/O模型
    • 3.2 非阻塞I/O模型
    • 3.3 I/O 复用模型
    • 3.4 信号驱动式I/O
    • 3.5 异步I/O模型
  • 4. I/O复用之 select
    • 4.1 select 函数描述
    • 4.2 服务端代码
    • 4.3 客户端代码
    • 4.4 运行截图
  • 5. I/O复用之 poll
    • 5.1 poll 函数描述
    • 5.2 服务端代码
    • 5.3 客户端代码
    • 5.4 运行截图
  • 6. I/O复用之 epoll
    • 6.1 常用函数
    • 6.2 服务端代码
  • 参考文献

1. 概述

  • 网络编程, 就是编写程序, 使两台联网的电脑可以交换数据,
  • 套接字是网络数据传输用的软件设备, 用来连接网络的工具
  • 在 linux中 socket被认为是文件中的一种, 在网络数据传输过程中, 使用文件I/O的相关函数
  • 套接字常用网络协议: TCP、UDP

套接字进行网络连接流程, 如下图:

服务器端:

  1. 创建服务器套接字 socket()
  2. 绑定端口 bind()
  3. 监听端口 listen()
  4. 接受客户端请求 accept()
  5. 读取客户端请求的数据 read()
  6. 返回客户端要响应的数据 write()
  7. 关闭与客户端的连接 close()
  8. 关闭服务器套接字 close()

客户端:

  1. 创建客户端套接字 socket()
  2. 连接服务端 connect()
  3. 请求服务端数据, 发送操作数和操作符到服务器 write()
  4. 从服务器读取操作结果 read()
  5. 关闭客户端套接字 close()

流程图如下, 具体代码示例可以看下面的 2. TCP 网络编程实例在这里插入图片描述

2. TCP 网络编程实例

2.1 服务器端

开放的端口号 6666:

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

// 定义缓冲区大小和操作数大小
#define BUF_SIZE 1024
#define OPSZ 4

// 错误处理函数
void error_handling(char *message);

// 计算函数,根据操作数和操作符进行计算
int calculate(int opnum, int opnds[], char oprator);

int main(int argc, char *argv[]) {
    int serv_sock, clnt_sock;
    char opinfo[BUF_SIZE]; // 用于接收客户端发送的操作数和操作符
    int result, opnd_cnt, i;
    int recv_cnt, recv_len;
    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t clnt_adr_sz;

    // 创建服务器套接字
    serv_sock = socket(PF_INET,SOCK_STREAM, 0);
    if (serv_sock == -1)
        error_handling(
            "socket() error"
        );

    // 初始化服务器地址结构体
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = 6666; // 设置端口号为 6666

    // 绑定端口
    if (bind(serv_sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1)
        error_handling(
            "bind() error"
        );

    // 监听端口
    if (listen(serv_sock, 5) == -1)
        error_handling(
            "listen() error"
        );

    clnt_adr_sz = sizeof(clnt_adr); // 设置客户端地址结构体的大小

    // 接受多个客户端连接并进行计算
    for (i = 0; i < 5; i++) {
        opnd_cnt = 0;
        clnt_sock = accept(serv_sock, (struct sockaddr *) &clnt_adr, &clnt_adr_sz); // 接受客户端连接
        read(clnt_sock, &opnd_cnt, 1); // 读取操作数个数
        recv_len = 0;
        // 循环接收操作数,直到接收完所有操作数
        while ((opnd_cnt * OPSZ + 1) > recv_len) {
            recv_cnt = read(clnt_sock, &opinfo[recv_len], BUF_SIZE - 1); // 从客户端接收数据
            recv_len += recv_cnt;
            // 计算结果并发送回客户端
            result = calculate(opnd_cnt, (int *) opinfo, opinfo[recv_len - 1]);
            write(clnt_sock, (char *) &result, sizeof(result));
            close(clnt_sock); // 关闭与客户端的连接
        }
    }
    close(serv_sock); // 关闭服务器套接字

    return 0;
}

// 计算函数,根据操作数和操作符返回计算结果
int calculate(int opnum, int opnds[], char op) {
    int result = opnds[0], i;
    switch (op) { // 根据操作符进行不同的计算
        case '+':
            for (i = 1; i < opnum; i++) result += opnds[i];
            break;
        case '-':
            for (i = 1; i < opnum; i++) result -= opnds[i];
            break;
        case '*':
            for (i = 1; i < opnum; i++) result *= opnds[i];
            break;
    }
    return result;
}


// 错误处理函数,在遇到错误时打印消息并退出程序
void error_handling(char *message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

2.2 客户端

#include <stdio.h>          // 标准输入输出
#include <stdlib.h>         // 系统函数库
#include <string.h>         // 字符串处理
#include <unistd.h>         // UNIX标准函数
#include <arpa/inet.h>      // Internet网络协议
#include <sys/socket.h>     // Socket编程相关

// 定义缓冲区大小、结果大小和操作数字节大小
#define BUF_SIZE 1024
#define RLT_SIZE 4
#define OPSZ 4

// 错误处理函数
void error_handling(char *message);

int main(int argc, char *argv[]) {
    int sock; // Socket描述符
    char opmsg[BUF_SIZE]; // 用于存储操作数和操作符的消息
    int result, opnd_cnt, i; // 结果、操作数个数和循环变量
    struct sockaddr_in serv_adr; // 服务端地址结构体

    // 创建Socket
    sock = socket(PF_INET,SOCK_STREAM, 0);
    if (sock == -1)
        error_handling(
            "socket() error"
        );

    // 初始化服务端地址
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = 6666;

    // 连接服务端
    if (connect(sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1)
        error_handling(
            "connect() error!"
        );
    else
        puts("Connected ...........");

    // 获取并存储操作数个数
    fputs("Operand count: ",stdout);
    scanf("%d", &opnd_cnt);
    opmsg[0] = (char) opnd_cnt;

    // 循环读取操作数
    for (i = 0; i < opnd_cnt; i++) {
        printf("Operand %d:", i + 1);
        scanf("%d", (int *) &opmsg[i * OPSZ + 1]);
    }

    // 暂停,等待用户输入操作符
    fgetc(stdin);
    fputs("Operator: ",stdout);
    scanf("%c", &opmsg[opnd_cnt * OPSZ + 1]);

    // 发送操作数和操作符到服务器
    write(sock, opmsg, opnd_cnt * OPSZ + 2);

    // 从服务器读取操作结果
    read(sock, &result, RLT_SIZE);

    // 打印操作结果
    printf("Operation result: %d \n", result);

    // 关闭Socket
    close(sock);

    return 0;
}

// 错误处理函数:输出错误信息并退出程序
void error_handling(char *message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

2.3 运行截图

在这里插入图片描述

3. I/O 模型

一个入门科普文章

3.1 阻塞式I/O模型

2. TCP 网络编程实例 就是一个阻塞式的模型, acceptread 都是阻塞的, 当 accept 到新连接, 或者 read 到数据程序才往下走

为了提高服务端处理能力, 一个客户端连接一个线程处理

不能一个线程处理多个客户端, 某个客户端会阻塞这个线程处理其他客户端
在这里插入图片描述

3.2 非阻塞I/O模型

不停的轮询, 看看有没有accept 到新连接, 没有连接不阻塞等待, 继续去看看已经建立的连接有没有read到客户端的新数据, read到新数据处理, read不到不处理

为了提高服务端处理能力, 可以一个客户端连接一个线程处理, 线程不停的轮询自己要处理的客户端

也可以一个线程处理多个客户端, 相较于上面的阻塞I/O模型, 非阻塞不至于某个客户端阻塞这个线程处理其他客户端
在这里插入图片描述

3.3 I/O 复用模型

可以调用 select/poll/epoll , 阻塞在select/poll/epoll, select/poll/epoll 监听多个客户端连接事件或写入的数据, 然后这些事件可再有多个线程分一分处理掉
在这里插入图片描述

3.4 信号驱动式I/O

让内核在描述符就绪时发送SINIO信号 通知我们

Linux 网络编程的5种IO模型:信号驱动IO模型 接受SINIO信号, 直接 recvfrom , 然后 sentTo 了, 不用 , 遍历socket
在这里插入图片描述

3.5 异步I/O模型

告诉内核启动某个操作, 并且把数据copy到用户缓冲区再通知我们, 和信号驱动的区别是, 信号驱动内核通知我们去I/O, 异步I/O, 通知我们I/O完成了, 送到了

[C++ 网络协议] 异步通知I/O模型

在这里插入图片描述

4. I/O复用之 select

4.1 select 函数描述

int      select(int maxfdpl, fd_set *readSet, fd_set *writeSet,
    fd_set *exceptSet, struct timeval *timeout)
  • 最大描述符+1
  • fd_set *readSet 读事件描述符
  • fd_set *writeSet 写事件描述符
  • fd_set *exceptSet 异常事件描述符
  • struct timeval *timeout 等待超时时间

fd_set 里数组初始化1024, 网上都说 select 最多监听 1024 个连接
在这里插入图片描述

  • FD_SET: 将套接字添加到文件描述符集合中
  • FD_ZERO: 用来清空文件描述符集合
  • FD_CLR: 从套接字描述符集合中清除指定的套接字描述符
  • FD_ISSET: 检查文件描述符集合中是否包含文件描述符

4.2 服务端代码

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

// 定义缓冲区大小
#define BUF_SIZE 100

// 错误处理函数
void error_handling(char *message);

int main(int argc, char *argv[]) {
    int serv_sock, clnt_sock; // 服务端和客户端套接字
    struct sockaddr_in serv_adr, clnt_adr; // 服务端和客户端地址结构体
    struct timeval timeout; // 超时时间结构体
    fd_set reads, cpy_reads; // 文件描述符集合,用于select函数

    socklen_t adr_sz; // 地址大小
    int fd_max, str_len, fd_num, i; // 各种变量初始化
    char buf[BUF_SIZE]; // 用于读取和写入数据的缓冲区

    // 创建套接字
    serv_sock = socket(PF_INET,SOCK_STREAM, 0);

    // 初始化服务端地址结构体
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = 6666;

    // 绑定端口
    if (bind(serv_sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1)
        error_handling(
            "bind() error"
        );

    // 监听端口
    if (listen(serv_sock, 5) == -1)
        error_handling(
            "listen() error"
        );

    // 用来清空文件描述符集合read
    FD_ZERO(&reads);
    // 将服务器套接字serv_sock添加到文件描述符集合reads中
    FD_SET(serv_sock, &reads);
    fd_max = serv_sock;

    // 无限循环,等待客户端连接和处理数据
    while (1) {
        cpy_reads = reads;
        timeout.tv_sec = 5;
        timeout.tv_usec = 5000;

        // 使用select函数等待事件发生
        if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
            break;

        // 没有客户端连接
        if (fd_num == 0) {
            continue;
        }

        // 遍历文件描述符集合,处理连接和数据传输
        for (i = 0; i < fd_max + 1; i++) {
            // 检查文件描述符集合cpy_reads中是否包含文件描述符i
            if (FD_ISSET(i, &cpy_reads)) {
                if (i == serv_sock) {
                    // 处理新连接
                    adr_sz = sizeof(clnt_adr);
                    clnt_sock = accept(serv_sock, (struct sockaddr *) &clnt_adr, &adr_sz);
                    FD_SET(clnt_sock, &reads);
                    if (fd_max < clnt_sock)
                        fd_max = clnt_sock;
                    printf("connected client: %d \n", clnt_sock);
                } else {
                    // 处理数据传输
                    str_len = read(i, buf, BUF_SIZE);
                    if (str_len == 0) {
                        // 从reads套接字描述符集合中清除指定的套接字描述符i
                        FD_CLR(i, &reads);
                        close(i);
                        printf("connected client: %d \n", i);
                    } else {
                        // 向客户端写入数据
                        write(i, buf, str_len);
                    }
                }
            }
        }
    }
    // 关闭服务端套接字
    close(serv_sock);
    return 0;
}

// 错误处理函数
void error_handling(char *message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

4.3 客户端代码

#include <stdio.h>          // 标准输入输出头文件
#include <stdlib.h>         // 通用实用程序头文件
#include <arpa/inet.h>      // IPv4 Internet地址转换头文件
#include <sys/socket.h>     // 用于创建和操作套接字的头文件
#include <string.h>         // 字符串操作头文件
#include <unistd.h>         // 提供一些与操作系统交互的函数
#include <sys/types.h>      // 定义各种数据类型的头文件
#include <sys/time.h>       // 时间处理头文件
#include <sys/select.h>     // 用于文件描述符选择的头文件

#define BUF_SIZE 1024      // 定义缓冲区大小

// 错误处理函数
// 参数: buf - 存储错误信息的字符数组
void error_handling(char *buf);

int main(int argc, char *argv[]){ // 程序入口
    int sock;                     // 套接字变量
    char message[BUF_SIZE];       // 用于存储消息的缓冲区
    int str_len;                  // 存储字符串长度的变量
    struct sockaddr_in serv_adr;  // 用于存储服务器地址信息的结构体

    // 创建TCP套接字
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock==-1)
    {
        error_handling("socket create error"); // 若创建失败,调用错误处理函数
    }

    // 初始化服务器地址结构体
    memset(&serv_adr, 0,sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = 6666;

    // 尝试连接到服务器
    if (connect(sock,(struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
    {
        error_handling("connect error"); // 若连接失败,调用错误处理函数
    }else{
        puts("connected......"); // 连接成功,输出提示信息
    }

    // 进入消息收发循环
    while (1)
    {
        fputs("input message (q to quit): ", stdout); // 提示用户输入消息
        fgets(message, BUF_SIZE, stdin); // 从标准输入读取消息
        if (!strcmp(message,"q\n")||!strcmp(message,"Q\n") )
        {
            break; // 如果用户输入的是 'q' 或 'Q',则退出循环
        }
        write(sock, message, strlen(message)); // 将消息写入套接字,发送给服务器
        str_len=read(sock, message, BUF_SIZE-1); // 从套接字读取消息,存储到message中
        message[str_len] = 0; // 在消息末尾添加null字符,以结束字符串
        printf("Message from server: %s", message); // 输出服务器返回的消息

    }
    close(sock); // 关闭套接字

    return 0; // 程序正常结束,返回0
}

// 错误处理函数
// 参数: buf - 存储错误信息的字符数组
// 说明: 该函数将错误信息输出到标准错误流,并退出程序
void error_handling(char* buf){
    fputs(buf, stderr); // 将错误信息输出到标准错误流
    fputc('\n', stderr); // 输出换行符
    exit(1); // 退出程序,返回码为1,表示异常结束
}

4.4 运行截图

在这里插入图片描述

5. I/O复用之 poll

5.1 poll 函数描述

poll 提供的功能和 selcet 差不多, poll 可以自己声明最大长度, 还不用自己去 FD_SET、FD_ISSET 维护状态

int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);

struct pollfd {
	int     fd;
	short   events;
	short   revents;
};
  • fdarray : pollfd数组
  • nfds : fd 数量
  • timeout : 超时时间

5.2 服务端代码

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/poll.h>

using namespace std;

int main() {
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);

    sockaddr_in serverAddr;
    memset(&serverAddr, 0, sizeof(serverAddr));

    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serverAddr.sin_port = 6666;

    ::bind(listenfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
    listen(listenfd, 10);

    /*
        poll只是对select函数传入的参数的一个封装,底层实现还是select,好处就是需要传入的参数少了
    */
    //pollfd数组, 一个元素为一个 描述符以及相对应的事件。元素的个数为可处理socket的最大个数,突破了select的1024的限制
    pollfd pfds[2048] = {0};
    pfds[listenfd].fd = listenfd;
    pfds[listenfd].events = POLLIN; //可读
    //pfds.revents //revents为poll返回的事件

    int maxfd = listenfd;

    while (1) {
        int nready = poll(pfds, maxfd + 1, -1);

        //监听socket有可读事件
        if (pfds[listenfd].revents & POLLIN) {
            sockaddr_in clientAddr;
            socklen_t len = sizeof(clientAddr);
            int clientfd = accept(pfds[listenfd].fd, (sockaddr*)&clientAddr, &len);
            printf("accept==>> clientfd:%d %s:%d\n", clientfd, inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
            if (-1 == clientfd) {
                continue;
            }

            //请注意这里的clientfd越界问题,这里就不做处理了
            //需要注意的是,如果有客户端关闭连接了,再次有新的客户端连接进来时,accept返回的是没有使用的最小的描述符
            //比如现在有4,5,6客户端建立了连接,4和5客户端断开了,再次有新的客户端建立连接后,accept会给分配4描述符,这点内核还是很人性化的。
            pfds[clientfd].fd = clientfd;
            pfds[clientfd].events = POLLIN;

            maxfd = clientfd;
        }

        for (int i = listenfd + 1; i <= maxfd; i++) {
            if (pfds[i].revents & POLLIN) {
                char buf[128] = {0};
                int count = recv(pfds[i].fd, buf, sizeof(buf), 0);
                if (count == 0) {
                    printf("close\n");

                    close(pfds[i].fd);

                    pfds[i].fd = -1;
                    pfds[i].events = 0;

                    continue;
                }
                printf("clientfd:%d, count:%d, buf:%s\n", pfds[i].fd, count, buf);
                send(pfds[i].fd, buf, count, 0);
            }
        }
    }

	close(listenfd);
    return 0;
}

5.3 客户端代码

同 4.3

5.4 运行截图

在这里插入图片描述

6. I/O复用之 epoll

epoll 比 select、poll 性能都好, 获得就绪的文件描述符, select、poll O(n) 复杂度, epoll 是 O(1)

select,poll是基于轮询实现的,将fd_set从用户空间复制到内核空间,然后让内核空间以poll机制来进行轮询,一旦有其中一个fd对应的设备活跃了,那么就把整个fd_set返回给客户端(复制到用户空间),再由客户端来轮询每个fd的,找出发生了IO事件的fd

epoll是基于事件驱动实现的,加入一个新的fd,会调用epoll_ctr函数为该fd注册一个回调函数,然后将该fd结点注册到内核中的epoll红黑树中,当IO事件发生时,就会调用回调函数,将该fd结点放到就绪链表中,epoll_wait函数实际上就是从这个就绪链表中获取这些fd。

epoll很详细的文章

6.1 常用函数

poll操作过程需要三个接口,分别如下:

int epoll_create(int size)//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  1. int epoll_create(int size); 创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。 当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 函数是对指定描述符fd执行op操作。
  • epfd:是epoll_create()的返回值。
  • op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
  • fd:是需要监听的fd(文件描述符)
  • epoll_event:是告诉内核需要监听什么事,struct epoll_event结构如下:
struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};

//events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

  1. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); 等待epfd上的io事件,最多返回maxevents个事件。 参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

6.2 服务端代码

我用的MAC电脑, 不支持 epoll, 下面的代码没运行过:

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

#define BUF_SIZE 100
#define EPOLL_SIZE 50

void error_handling(char *buf);
/**
 * 主程序:创建一个使用epoll模型的服务器,监听客户端连接,并处理客户端请求。
 *
 * @param argc 命令行参数个数
 * @param argv 命令行参数数组
 * @return 总是返回0,表示程序正常结束
 */
int main(int argc, char *argv[]) {
    // 定义服务器套接字和客户端套接字
    int serv_sock, clnt_sock;
    // 定义服务器地址结构体和客户端地址结构体
    struct sockaddr_in serv_adr, clnt_adr;
    // 定义地址大小变量
    socklen_t adr_sz;
    // 定义字符串长度和循环索引
    int str_len, i;
    // 定义缓冲区,用于读取和写入数据
    char buf[BUF_SIZE];

    // 定义epoll事件结构体数组和epoll文件描述符
    struct epoll_event *ep_events;
    struct epoll_event event;
    // 定义epoll等待事件个数变量
    int epfd, event_cnt;

    // 检查命令行参数是否正确
    if (argc != 2) {
        printf("usage error\n");
        exit(1);
    }
    // 创建服务器套接字
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    // 初始化服务器地址结构体
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = 6666;

    // 绑定服务器套接字到指定地址和端口
    if (bind(serv_sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1) {
        error_handling("bind error");
    }

    // 监听服务器套接字
    if (listen(serv_sock, 5) == -1) {
        error_handling("listen error");
    }

    // 创建epoll实例
    epfd = epoll_create(EPOLL_SIZE);
    // 分配存储epoll事件的内存
    ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);

    // 将服务器套接字添加到epoll监控列表中
    event.events = EPOLLIN;
    event.data.fd = serv_sock;
    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

    // 主循环,等待和处理客户端连接和请求
    while (1) {
        // 等待epoll事件
        event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
        if (event_cnt == -1) {
            puts("epoll wait error\n");
            break;
        }
        // 遍历所有触发的epoll事件
        for (i = 0; i < event_cnt; i++) {
            // 处理服务器套接字上的事件,即新的客户端连接
            if (ep_events[i].data.fd == serv_sock) {
                adr_sz = sizeof(clnt_adr);
                clnt_sock = accept(serv_sock, (struct sockaddr *) &clnt_adr, &adr_sz);
                // 将新连接的客户端套接字添加到epoll监控列表中
                event.events = EPOLLIN;
                event.data.fd = clnt_sock;
                epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
                printf("connnected client: %d\n", clnt_sock);
            } else {
                // 处理客户端套接字上的事件,即客户端发送的数据
                str_len = read(ep_events[i].data.fd, buf,BUF_SIZE);
                if (str_len == 0) {
                    // 如果客户端关闭了连接,则从epoll监控列表中移除,并关闭套接字
                    epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                    close(ep_events[i].data.fd);
                    printf("closed client: %d \n", ep_events[i].data.fd);
                } else {
                    // 如果客户端发送了数据,则将数据回传给客户端
                    write(ep_events[i].data.fd, buf, str_len);
                }
            }
        }
    }
    // 关闭服务器套接字和epoll文件描述符
    close(serv_sock);
    close(epfd);
    return 0;
}

/**
 * 错误处理函数:输出错误信息并退出程序。
 *
 * @param buf 包含错误信息的字符串
 */
void error_handling(char *buf) {
    fputs(buf, stderr);
    fputc('\n', stderr);
    exit(1);
}

参考文献

  • UNIX 网络编程 卷1: 套接字联网API
  • TCP/IP网络编程 尹圣雨 著 金国哲 译
  • Linux IO模式及 select、poll、epoll详解
  • 浅谈select,poll和epoll的区别

在这里插入图片描述

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

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

相关文章

音视频开发9 FFmpeg 解复用框架--如何将一个影音文件(mp4文件/wav文件) 最终播放起来

一&#xff0c;播放器框架 二 常用音视频术语 容器&#xff0f;文件&#xff08;Conainer/File&#xff09;&#xff1a; 即特定格式的多媒体文件&#xff0c; 比如mp4、flv、mkv等。 媒体流&#xff08;Stream&#xff09;&#xff1a; 表示时间轴上的一段连续数据&#xff0…

jmeter之测试计划

一、测试计划作用 测试计划是jmeter的默认控件所有线程组都是测试计划的下级控件测试计划可以配置用户自定义的变量测试计划可以配置线程组的串行或并行 二、查看界面 名称&#xff1a;可以修改自定义的名称注释&#xff1a;解释测试计划是用来做什么的用户自定义的变量&…

23种设计模式顺口溜

口诀&#xff1a; 原型 抽风 &#xff0c;单独 建造 工厂 &#xff08;寓意&#xff1a;&#xff08;这里代指本来很简单的东西&#xff0c;却要干工厂这里复杂的业务&#xff09; 抽风&#xff1a;抽象工厂单独&#xff1a;单例桥代理组合享元适配器&#xff0c;&#xff0…

驱动开发之新字符设备驱动开发

1.前言 register_chrdev 和 unregister_chrdev 这两个函数是老版本驱动使用的函数&#xff0c;现在新的 字符设备驱动已经不再使用这两个函数&#xff0c;而是使用 Linux 内核推荐的新字符设备驱动 API 函数。 旧版本的接口使用&#xff0c;感兴趣可以看下面这个博客&#…

关于C的\r回车在不同平台的问题

首先我们需要搞明白\r和\n是两回事 \r是回车&#xff0c;前者使光标到行首&#xff0c;&#xff08;carriage return&#xff09; \n是换行&#xff0c;后者使光标下移一格&#xff0c;&#xff08;line feed&#xff09; Linux平台下 #include <stdio.h> int main()…

TiDB学习4:Placement Driver

目录 1. PD架构 2. 路由功能 2. TSO 2.1 TSO 概念 2.2 TSO分配过程 2.3 TSO时间窗口 3. 调度 3.1 信息收集 3.2 生成调度(operator) 3.3 执行调度 4. Label 与高可用 4.1 Label 的配置 5. 小结 1. PD架构 PD是整个TiDB的总控&#xff0c;相当于集群的大脑 PD集成了…

Overleaf中出现文字越界、越下届、没有正确分页、换页的原因和解决方法

在使用overleaf中&#xff0c;我偶尔会遇到如标题所说的情况&#xff0c;也如图所示&#xff1a; 后来发现&#xff0c;是因为这一页前面是一个表格&#xff0c;所以怀疑是表格的格式导致的。所以让chatgpt帮我更换了表格的格式&#xff0c;成功解决问题。 对于问题可能的成因…

20232803 2023-2024-2 《网络攻防实践》实践十报告

目录 1. 实践内容1.1 SEED SQL注入攻击与防御实验1.2 SEED XSS跨站脚本攻击实验(Elgg) 2. 实践过程2.1 SEED SQL注入攻击与防御实验2.1.1 熟悉SQL语句2.1.2 对SELECT语句的SQL注入攻击2.1.3 对UPDATE语句的SQL注入攻击2.1.4 SQL对抗 2.2 SEED XSS跨站脚本攻击实验(Elgg)2.2.1 发…

视频拼接融合产品的产品与架构设计(四)分布式GPU运算合并单元

上一篇如下 视频拼接融合产品的产品与架构设计(三&#xff09;内存和显存单元数据迁移 视频合并单元说明 对下面这张图做些说明&#xff0c;视频接入是比较常见&#xff0c;可以说是普通&#xff0c;但是做到接入后随即进行比较重的算法运算&#xff0c;这个在视频领域并不多…

探索PyImGui:高效可交互图形界面的Python实现

简介 Pyimgui 是一个基于 Cython 的 Python 绑定层&#xff0c;它将功能强大的用户界面库 Dear ImGui 无缝集成到 Python 环境中。它使 Python 开发人员能够轻松地创建交互式图形用户界面 (GUI)&#xff0c;同时充分利用 Dear ImGui 的丰富功能集。 下图为用Dear ImGui开的GU…

Android 使用 ActivityResultLauncher 申请权限

前面介绍了 Android 运行时权限。 其中&#xff0c;申请权限的步骤有些繁琐&#xff0c;需要用到&#xff1a;ActivityCompat.requestPermissions 函数和 onRequestPermissionsResult 回调函数&#xff0c;今天就借助 ActivityResultLauncher 来简化书写。 步骤1&#xff1a;创…

2024年5月26日 (周日) 叶子游戏新闻

资深开发者&#xff1a;3A游戏当前处于一种尴尬的中间地带游戏行业整体&#xff0c;尤其是3A游戏正处于艰难时期。尽管2023年3A游戏佳作频出&#xff0c;广受好评&#xff0c;但居高不下的游戏开发成本&#xff08;传闻《漫威蜘蛛侠2》的制作成本高达3亿美元&#xff09;正严重…

提高Java编程效率:ArrayList类的使用技巧

哈喽&#xff0c;各位小伙伴们&#xff0c;你们好呀&#xff0c;我是喵手。运营社区&#xff1a;C站/掘金/腾讯云&#xff1b;欢迎大家常来逛逛 今天我要给大家分享一些自己日常学习到的一些知识点&#xff0c;并以文字的形式跟大家一起交流&#xff0c;互相学习&#xff0c;一…

虹科Pico汽车示波器 | 免拆诊断案例 | 2012 款雪佛兰科鲁兹车偶尔多个故障灯异常点亮

故障现象 一辆2012款雪佛兰科鲁兹车&#xff0c;搭载1.8 L 发动机&#xff0c;累计行驶里程约为9.6万km。该车组合仪表上的发动机故障灯、ABS故障灯及动力转向故障灯偶尔异常点亮&#xff0c;同时发动机转速表和发动机冷却液温度表的指针会突然归零&#xff0c;严重时发动机无…

【Linux 网络】网络基础(三)(网络层协议:IP 协议)

在复杂的网络环境中确定一个合适的路径。 一、TCP 与 IP 的关系 IP 层的核心作用是定位主机&#xff0c;具有将数据从主机 A 发送到主机 B 的能力&#xff0c;但是能力并不能保证一定能够做到&#xff0c;所以这时就需要 TCP 起作用了&#xff0c;TCP 可以通过超时重传、拥塞控…

[Vulnhub]Vulnix 通过NFS挂载+SSH公钥免密登录权限提升

端口扫描 Server IP AddressPorts Open192.168.8.103TCP:22/tcp, 25/tcp, 79/tcp, 110/tcp, 111/tcp, 143/tcp, 512/tcp, 513/tcp, 514/tcp, 993/tcp, 995/tcp, 2049/tcp, 37522/tcp, 42172/tcp, 43219/tcp, 47279/tcp, 54227/tcp $ nmap -p- 192.168.8.103 -sV -sC --min-ra…

Nginx实战(安装部署、常用命令、反向代理、负载均衡、动静分离)

文章目录 1. nginx安装部署1.1 windows安装包1.2 linux-源码编译1.3 linux-docker安装 2. nginx介绍2.1 简介2.2 常用命令2.3 nginx运行原理2.3.1 mater和worker2.3.3 Nginx 的工作原理 2.4 nginx的基本配置文件2.4.1 location指令说明 3. nginx案例3.1 nginx-反向代理案例013.…

python基于深度学习的聊天机器人设计

python基于深度学习的聊天机器人设计 开发语言:Python 数据库&#xff1a;MySQL所用到的知识&#xff1a;Django框架工具&#xff1a;pycharm、Navicat、Maven 系统功能实现 登录注册功能 用户在没有登录自己的用户名之前只能浏览本网站的首页&#xff0c;想要使用其他功能都…

ROCm上来自Transformers的双向编码器表示(BERT)

14.8. 来自Transformers的双向编码器表示&#xff08;BERT&#xff09; — 动手学深度学习 2.0.0 documentation (d2l.ai) 代码 import torch from torch import nn from d2l import torch as d2l#save def get_tokens_and_segments(tokens_a, tokens_bNone):""&qu…

html中被忽略的简单标签

1&#xff1a; alt的作用是在图片不能显示时的提示信息 <img src"https://img.xunfei.cn/mall/dev/ifly-mall-vip- service/business/vip/common/202404071019208761.jp" alt"提示信息" width"100px" height"100px" /> 2&#…