当前位置: 首页 > article >正文

linux网络编程以及epoll IO多路复用

常见库

头文件定位主要功能
<sys/socket.h>网络io提供套接字相关的函数和常量,如 socket()bind()listen()accept() 等。
<sys/epoll.h>io复用提供 epoll 相关的函数和结构体,用于高效监控多个文件描述符的 I/O 事件。
<netinet/in.h>数据结构提供网络编程相关的结构体和常量,如 sockaddr_inhtons()INADDR_ANY 等。
<arpa/inet.h>转换算法提供 IP 地址转换函数,如 inet_pton()inet_ntop() 等。
<fcntl.h>文件io提供文件控制相关的函数和常量,如 fcntl()O_NONBLOCK 等。

Socket

简介

  • TCP 是面向连接的协议,适用于需要可靠数据传输的场景。
  • UDP 是无连接的协议,适用于对实时性要求高、允许丢包的场景。
  • 多进程可以用于处理多个客户端的并发连接,但需要注意资源管理和进程间通信的问题。

地址

在绑定时需要选择类型,其中AF_ 前缀通常表示地址族,而 PF_ 前缀表示协议族(Protocol Family)。两者在实际使用中通常等价。

比如常用的AF_INET 是 Socket 编程中的一个常量,表示使用 IPv4 地址和 TCP/UDP 协议进行网络通信。

TCP

基本流程

TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP Socket 的基本流程如下:

客户端

  1. socket() 使用socket()函数创建一个套接字,指定协议族、套接字类型和协议。

  2. connect() 使用connect()函数连接到服务器的指定IP地址和端口。

  3. send()/recv() 使用send()函数发送数据,使用recv()函数接收数据。

  4. close() 使用close()函数关闭套接字连接。

服务器

  1. socket() 使用socket()函数创建一个套接字。

  2. bind() 使用bind()函数将套接字绑定到本地地址和端口。

  3. listen() 使用listen()函数使套接字进入监听状态,等待客户端连接。

  4. accept() 使用accept()函数接受客户端连接,返回新的套接字用于通信。

  5. send()/recv() 使用send()函数发送数据,使用recv()函数接收数据。

  6. close() 使用close()函数关闭套接字连接。

示例

下面是一个使用多进程实现的 TCP 服务器示例代码,它允许多个客户端同时连接并处理数据。

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

#define PORT 8080
#define BUFFER_SIZE 1024

void handle_client(int client_socket) {
    char buffer[BUFFER_SIZE];
    int bytes_received;

    while ((bytes_received = recv(client_socket, buffer, BUFFER_SIZE, 0)) > 0) {
        buffer[bytes_received] = '\0';
        printf("Received: %s\n", buffer);

        // Echo back the received data
        send(client_socket, buffer, bytes_received, 0);
    }

    close(client_socket);
    printf("Client disconnected.\n");
    exit(0);
}

int main() {
    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    pid_t pid;

    // Create socket
    if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // Bind socket
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("Bind failed");
        close(server_socket);
        exit(EXIT_FAILURE);
    }

    // Listen for connections
    if (listen(server_socket, 5) == -1) {
        perror("Listen failed");
        close(server_socket);
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d...\n", PORT);

    while (1) {
        // Accept a new connection
        if ((client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len)) == -1) {
            perror("Accept failed");
            continue;
        }

        printf("New client connected: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

        // Fork a new process to handle the client
        pid = fork();
        if (pid < 0) {
            perror("Fork failed");
            close(client_socket);
        } else if (pid == 0) {
            // Child process
            close(server_socket);  // Close the server socket in the child process
            handle_client(client_socket);
        } else {
            // Parent process
            close(client_socket);  // Close the client socket in the parent process
        }
    }

    close(server_socket);
    return 0;
}

客户端测试:

可以使用 telnetnc 工具作为客户端连接到服务器:

telnet 127.0.0.1 8080

UDP

基本流程

UDP(用户数据报协议)是一种无连接的、不可靠的、基于数据报的传输层通信协议。UDP Socket 的基本流程如下:

客户端

  1. socket() 使用socket()函数创建一个UDP套接字,指定协议族、套接字类型和协议。

  2. sendto()/recvfrom() 使用sendto()函数发送数据到服务器,使用recvfrom()函数接收服务器的数据。

  3. close() 使用close()函数关闭套接字连接。

服务器

  1. socket() 使用socket()函数创建一个UDP套接字。

  2. bind() 使用bind()函数将套接字绑定到本地地址和端口。

  3. recvfrom()/sendto() 使用recvfrom()函数接收客户端的数据,使用sendto()函数发送数据到客户端。

  4. close() 使用close()函数关闭套接字连接。

示例

UDP 的实现相对简单,以下是一个 UDP 服务器示例代码:

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

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int server_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE];

    // Create socket
    if ((server_socket = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // Bind socket
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("Bind failed");
        close(server_socket);
        exit(EXIT_FAILURE);
    }

    printf("UDP Server listening on port %d...\n", PORT);

    while (1) {
        // Receive data from client
        int bytes_received = recvfrom(server_socket, buffer, BUFFER_SIZE, 0, (struct sockaddr *)&client_addr, &client_addr_len);
        if (bytes_received < 0) {
            perror("Recvfrom failed");
            continue;
        }

        buffer[bytes_received] = '\0';
        printf("Received from %s:%d: %s\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buffer);

        // Echo back the received data
        sendto(server_socket, buffer, bytes_received, 0, (struct sockaddr *)&client_addr, client_addr_len);
    }

    close(server_socket);
    return 0;
}

客户端测试:

可以使用 nc 工具作为客户端连接到服务器:

nc -u 127.0.0.1 8080

IO复用

select和poll

无论是 select 还是 poll,它们的工作原理都是:把需要监听的文件描述符(fd)集中在一起,然后轮询检查这些 fd 的状态。当某个 fd 的状态发生变化(比如变得可读、可写或发生异常)时,它们会通知程序,让程序去处理。


select

工作原理

  • select 使用一个叫 fd_set 的东西来存放需要监听的文件描述符。fd_set 实际上是一个位图(bitmap),每个 bit 代表一个文件描述符。
  • 程序把需要监听的 fd 加入 fd_set,然后调用 select 函数。select 会阻塞(等待),直到有 fd 的状态发生变化。
  • 当某个 fd 的状态变化时,select 会返回,并告诉程序哪些 fd 已经准备好了(比如可读或可写)。
  • select 能监听的文件描述符数量是有限的,受限于 FD_SETSIZE,通常是 1024。也就是说,select 最多只能同时监听 1024 个 fd。

poll

工作原理

  • poll 使用一个叫 struct pollfd 的数组来存放需要监听的文件描述符。每个 struct pollfd 包含一个 fd 和需要监听的事件(比如可读、可写)。
  • 程序把需要监听的 fd 加入 struct pollfd 数组,然后调用 poll 函数。poll 会阻塞(等待),直到有 fd 的状态发生变化。
  • 当某个 fd 的状态变化时,poll 会返回,并告诉程序哪些 fd 已经准备好了。
  • poll 没有固定的文件描述符数量限制,理论上可以监听任意数量的 fd,只要系统资源允许。

优缺点

  • 效率低 : 把整个文件描述符集合从用户空间复制到内核空间,并且在返回时需要遍历所有文件描述符来检查状态。
  • 兼容性好 : select几乎兼容所有操作系统, poll兼容unix

epoll

epoll 的核心思想是:事件驱动,即只在文件描述符状态发生变化时通知程序,而不是每次都遍历所有文件描述符。

实现原理

  • 高效epoll 是 Linux 特有的 I/O 多路复用机制,它通过事件驱动的方式工作,只有在文件描述符状态发生变化时才会通知用户程序,避免了不必要的遍历。
  • 文件描述符限制epoll 能够监控的文件描述符数量没有固定限制,受限于系统资源。
  • 内存使用epoll 使用红黑树和链表来管理文件描述符,内存使用效率较高。
  • 边缘触发(ET)和水平触发(LT)epoll 支持两种触发模式,边缘触发(Edge Triggered, ET)和水平触发(Level Triggered, LT)。边缘触发只在状态变化时通知一次,而水平触发在状态持续时会不断通知。

工作流程

  1. 创建 epoll 实例:使用 epoll_createepoll_create1 创建一个 epoll 实例,它会返回一个文件描述符,用于后续操作。
  2. 注册需要监听的文件描述符:使用 epoll_ctl 将需要监听的文件描述符添加到 epoll 实例中,并指定需要监听的事件(比如可读、可写)。
  3. 等待事件发生:使用 epoll_wait 等待文件描述符的状态变化。当某个文件描述符的状态发生变化时,epoll_wait 会返回,并告诉程序哪些文件描述符已经准备好了。

常用API

epoll 是 Linux 提供的一种高效的多路复用 I/O 机制,用于监控多个文件描述符(通常是套接字)的状态变化。以下是 epoll 相关的函数及其解释:

epoll_create1

函数原型:

int epoll_create1(int flags);

参数:
flags: 控制 epoll 实例的创建行为。常用的标志有:
0: 默认行为。
EPOLL_CLOEXEC: 在执行 exec 时关闭文件描述符。

返回值:
• 成功时返回一个新的 epoll 文件描述符。
• 失败时返回 -1,并设置 errno

功能:
创建一个新的 epoll 实例,并返回与之关联的文件描述符。

是epoll_create的增强版, 支持通过额外标志控制行为. 而原本epoll_create的size参数在现代内核中已被忽略,但仍然需要传递一个大于 0 的值。所以最好直接用新版这个

epoll_ctl

函数原型:

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

参数:
epfd: epoll 实例的文件描述符。
op: 操作类型,可以是以下之一:
EPOLL_CTL_ADD: 添加一个文件描述符到 epoll 实例。
EPOLL_CTL_MOD: 修改已注册的文件描述符的事件。
EPOLL_CTL_DEL: 从 epoll 实例中删除一个文件描述符。
fd: 要操作的文件描述符。
event: 指向 struct epoll_event 的指针,用于指定事件类型和相关数据。

返回值:
• 成功时返回 0
• 失败时返回 -1,并设置 errno

功能:
用于控制 epoll 实例中的文件描述符,可以添加、修改或删除文件描述符及其关联的事件。

epoll_wait

函数原型:

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

参数:
epfd: epoll 实例的文件描述符。
events: 指向 struct epoll_event 数组的指针,用于存储发生的事件。
maxevents: events 数组的大小,即最多可以存储多少个事件。
timeout: 等待事件的超时时间(毫秒)。-1 表示无限等待,0 表示立即返回。

返回值:
• 成功时返回就绪的文件描述符的数量。
• 失败时返回 -1,并设置 errno

功能:
等待 epoll 实例中的文件描述符上发生的事件,并将这些事件存储在 events 数组中。

常用数据结构

epoll_event

结构体定义:

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

成员:
events: 事件类型,可以是以下宏的组合:
EPOLLIN: 文件描述符可读。
EPOLLOUT: 文件描述符可写。
EPOLLRDHUP: 对端关闭连接或半关闭。
EPOLLPRI: 有紧急数据可读。
EPOLLERR: 文件描述符发生错误。
EPOLLHUP: 文件描述符被挂起。
EPOLLET: 边缘触发模式(默认是水平触发)。
EPOLLONESHOT: 一次性事件,事件发生后会自动从 epoll 实例中移除。
data: 用户数据,通常用于存储文件描述符或指向用户自定义数据的指针。

功能: 一般创建两个该类型的分别为ev和event

  • ev 用于在调用 epoll_ctl 时,指定要添加、修改或删除的文件描述符的事件和用户数据。
  • events 是一个 epoll_event 类型的数组,用于存储 epoll_wait 返回的就绪事件。
epoll_data_t

联合体定义:

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

成员:
ptr: 指向用户自定义数据的指针。
fd: 文件描述符。
u32: 32 位无符号整数。
u64: 64 位无符号整数。

示例代码

  1. set_nonblocking: 将文件描述符设置为非阻塞模式。
  2. handle_client: 处理客户端的数据读取和写入。
  3. main
    • 创建并绑定服务器套接字。
    • 创建 epoll 实例并将服务器套接字添加到 epoll 中。
    • 使用 epoll_wait 等待事件发生。
    • 当有新的连接时,接受连接并将客户端套接字添加到 epoll 中。
    • 当客户端套接字有数据可读时,调用 handle_client 处理数据。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <fcntl.h>

#define MAX_EVENTS 10
#define PORT 8080
#define BUFFER_SIZE 1024

int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

void handle_client(int client_fd) {
    char buffer[BUFFER_SIZE];
    ssize_t bytes_read = read(client_fd, buffer, BUFFER_SIZE - 1);
    if (bytes_read > 0) {
        buffer[bytes_read] = '\0';
        printf("Received: %s\n", buffer);
        write(client_fd, buffer, bytes_read);
    } else if (bytes_read == 0) {
        printf("Client disconnected\n");
        close(client_fd);
    }
}

int main() {
    int server_fd, client_fd, epoll_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    struct epoll_event ev, events[MAX_EVENTS];
    int nfds;

    // Create socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);

    // Set socket options
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // Bind socket
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);
    bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));

    // Listen for connections
    listen(server_fd, SOMAXCONN);

    // Create epoll instance
    epoll_fd = epoll_create1(0);

    // Add server socket to epoll
    ev.events = EPOLLIN;
    ev.data.fd = server_fd;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev);

    printf("Server is running on port %d\n", PORT);

    while (1) {
        nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);

        for (int i = 0; i < nfds; i++) {
            if (events[i].data.fd == server_fd) {
                // Accept new connection
                client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);

                // Set client socket to non-blocking
                set_nonblocking(client_fd);

                // Add client socket to epoll
                ev.events = EPOLLIN | EPOLLET; // Edge-triggered mode
                ev.data.fd = client_fd;
                epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);

                printf("New client connected\n");
            } else {
                // Handle client data
                handle_client(events[i].data.fd);
            }
        }
    }

    close(server_fd);
    close(epoll_fd);
    return 0;
}

可以使用telnet连接

telnet 127.0.0.1 8080

http://www.kler.cn/a/611685.html

相关文章:

  • 计算机网络基础:量子通信技术在网络中的应用前景
  • 解决Cubemx生产的 .ioc文件不能外部打开的方法
  • Vulhub靶机--FAll
  • 数据湖的崛起:从大数据到智能未来的钥匙
  • CMake入门及生成windows下的项目示例讲解
  • Postman 请求头详解:快速掌握
  • flutter 获取设备的唯一标识
  • 国产 FPGA 的崛起之路,能否打破 Xilinx 的垄断?
  • nodejs-原型污染链
  • 基于核选择融合注意力机制TCN-MTLATTENTION-MAMBA模型(Python\matlab代码)
  • 【点盾云】加密技术如何防止视频内容随意传播?
  • Windows卸载以压缩包形式安装的MySQL
  • qt+opengl 加载三维obj文件
  • 跨网段投屏(by quqi99)
  • STM32编写触摸按键
  • 安全工具膨胀的隐性成本及其解决方法
  • 使用string和string_view(二)——数值转换、std::string_view和非标准字符串
  • Flutter常用功能教程:新手入门指南
  • 【读论文】——基于高光谱的玉米籽粒黄曲霉侵染方法研究
  • 性能测试理论基础-性能指标及jmeter中的指标