首页 > 编程语言 >C++ 网络编程 IO多路复用、select、poll、epoll知识点总结

C++ 网络编程 IO多路复用、select、poll、epoll知识点总结

时间:2024-10-29 15:19:08浏览次数:6  
标签:知识点 socket epoll int C++ 描述符 fd 连接

1.什么是 I/O 多路复用?

I/O 多路复用(I/O Multiplexing)是一种编程技术,允许一个线程或进程同时管理多个 I/O 通道(如文件描述符、套接字等)。它使得单个进程能够在不使用多个线程或进程的情况下,同时处理多个 I/O 操作。这在网络编程和高性能服务器中尤为重要,因为它可以有效地利用系统资源,减少上下文切换的开销。

关键概念

  1. 文件描述符:在 Unix/Linux 系统中,每个打开的文件、套接字或设备都有一个唯一的整数标识符,称为文件描述符。使用 I/O 多路复用时,程序通过这些文件描述符来进行 I/O 操作。

  2. 阻塞与非阻塞:默认情况下,I/O 操作是阻塞的,即当操作未完成时,调用进程会被挂起。非阻塞 I/O 则允许程序在调用 I/O 操作时立即返回,这样可以进行其他处理。

  3. 事件通知:I/O 多路复用允许程序注册关心的事件(如可读、可写、异常等)。当这些事件发生时,程序会被通知,以便它可以进行相应的处理。

常见的 I/O 多路复用机制

在 Unix/Linux 系统中,常见的 I/O 多路复用机制包括:

1. select
  • 基本概念select 是最早的 I/O 多路复用机制,允许程序监视多个文件描述符,以检测它们是否可以进行 I/O 操作。

  • 函数原型

    int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    
    • nfds:监视的文件描述符的数量,即最大文件描述符加 1。
    • readfds:监视可读事件的文件描述符集合。
    • writefds:监视可写事件的文件描述符集合。
    • exceptfds:监视异常事件的文件描述符集合。
    • timeout:超时时间结构,指示 select 等待事件的时间。
  • 使用步骤

    1. 定义文件描述符集合
      fd_set readfds;
      FD_ZERO(&readfds); // 初始化集合
      FD_SET(sockfd, &readfds); // 添加文件描述符
      
    2. 设置超时时间
      struct timeval timeout;
      timeout.tv_sec = 5;  // 5 秒
      timeout.tv_usec = 0; // 0 微秒
      
    3. 调用 select
      int activity = select(nfds, &readfds, NULL, NULL, &timeout);
      
    4. 处理就绪事件
      if (FD_ISSET(sockfd, &readfds)) {
          // 处理可读事件
      }
      
#include <sys/select.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>

int main() {
    // 创建 TCP 套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        return 1;
    }

    // 设置服务器地址
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);  // 监听 8080 端口
    server_addr.sin_addr.s_addr = INADDR_ANY; // 接受任意地址

    // 绑定套接字
    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(sockfd);
        return 1;
    }

    // 开始监听
    if (listen(sockfd, 3) < 0) {
        perror("listen failed");
        close(sockfd);
        return 1;
    }

    while (1) {
        // 初始化文件描述符集合
        fd_set readfds;
        FD_ZERO(&readfds);
        FD_SET(sockfd, &readfds); // 添加监听套接字到集合

        // 设置超时时间
        struct timeval timeout;
        timeout.tv_sec = 5;  // 5 秒
        timeout.tv_usec = 0; // 0 微秒

        // 调用 select 监听文件描述符
        int activity = select(sockfd + 1, &readfds, NULL, NULL, &timeout);
        if (activity < 0) {
            perror("select error");
            break; // 错误处理
        } else if (activity == 0) {
            printf("Timeout occurred!\n");
        } else {
            // 检查是否有新连接到达
            if (FD_ISSET(sockfd, &readfds)) {
                int new_socket = accept(sockfd, NULL, NULL); // 接受新连接
                if (new_socket < 0) {
                    perror("accept failed");
                } else {
                    printf("New connection accepted\n");
                    // 这里可以处理新连接(如读取数据)
                    close(new_socket); // 示例中直接关闭新连接
                }
            }
        }
    }

    // 关闭监听套接字
    close(sockfd);
    return 0;
}
2. poll
  • 基本概念pollselect 的增强版,支持更大的文件描述符数量,没有描述符数量的限制。

  • 函数原型

    int poll(struct pollfd *fds, nfds_t nfds, int timeout);
    
  • pollfd 结构体

    struct pollfd {
        int fd;         // 文件描述符
        short events;   // 监视的事件
        short revents;  // 实际发生的事件
    };
    
  • 使用步骤

    1. 定义 pollfd 数组
      struct pollfd fds[1]; // 假设监视一个文件描述符
      fds[0].fd = sockfd;    // 设置文件描述符
      fds[0].events = POLLIN; // 监听可读事件
      
    2. 调用 poll
      int ret = poll(fds, 1, 5000); // 5 秒超时
      
    3. 检查事件
      if (fds[0].revents & POLLIN) {
          // 处理可读事件
      }
      
      #include <poll.h>
      #include <unistd.h>
      #include <arpa/inet.h>
      #include <string.h>
      #include <stdio.h>
      
      int main() {
          // 创建 TCP 套接字
          int sockfd = socket(AF_INET, SOCK_STREAM, 0);
          if (sockfd < 0) {
              perror("socket creation failed");
              return 1;
          }
      
          // 设置服务器地址
          struct sockaddr_in server_addr;
          server_addr.sin_family = AF_INET;
          server_addr.sin_port = htons(8080); // 监听 8080 端口
          server_addr.sin_addr.s_addr = INADDR_ANY; // 接受任意地址
      
          // 绑定套接字
          if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
              perror("bind failed");
              close(sockfd);
              return 1;
          }
      
          // 开始监听
          if (listen(sockfd, 3) < 0) {
              perror("listen failed");
              close(sockfd);
              return 1;
          }
      
          struct pollfd fds[1]; // 只监视一个文件描述符
          fds[0].fd = sockfd;   // 设置文件描述符
          fds[0].events = POLLIN; // 监听可读事件
      
          while (1) {
              // 调用 poll
              int ret = poll(fds, 1, 5000); // 5 秒超时
              if (ret < 0) {
                  perror("poll error");
                  break; // 错误处理
              } else if (ret == 0) {
                  printf("Timeout occurred!\n");
              } else {
                  // 检查是否有新连接到达
                  if (fds[0].revents & POLLIN) {
                      int new_socket = accept(sockfd, NULL, NULL); // 接受新连接
                      if (new_socket < 0) {
                          perror("accept failed");
                      } else {
                          printf("New connection accepted\n");
                          // 这里可以处理新连接(如读取数据)
                          close(new_socket); // 示例中直接关闭新连接
                      }
                  }
              }
          }
      
          // 关闭监听套接字
          close(sockfd);
          return 0;
      }
      

3. epoll
  • 基本概念epoll 是 Linux 特有的 I/O 多路复用机制,特别设计用于处理大量并发连接。它支持边缘触发和水平触发模式,性能高效。

  • 函数原型

    int epoll_create1(int flags);
    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);
    
  • epoll_event 结构体

    struct epoll_event {
        uint32_t events;    // 监听的事件
        void *data;        // 用户自定义数据
    };
    
  • 使用步骤

    1. 创建 epoll 实例
      int epfd = epoll_create1(0);
      
    2. 注册文件描述符
      struct epoll_event ev;
      ev.events = EPOLLIN; // 监听可读事件
      ev.data.fd = sockfd; // 存储文件描述符
      epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
      
    3. 等待事件
      struct epoll_event events[MAX_EVENTS];
      int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
      for (int i = 0; i < nfds; ++i) {
          if (events[i].events & EPOLLIN) {
              // 处理可读事件
          }
      }
      
1. epoll_create
函数原型

int epoll_create(int flags);

参数
  • flags:可以是以下标志之一:
  • 0:默认行为。
  • EPOLL_CLOEXEC:在 exec 调用时关闭该文件描述符。
  • EPOLL_NONBLOCK:使得在读取 epoll 事件时不阻塞。
返回值
  • 成功时,返回一个新的 epoll 文件描述符,通常用于后续的 epoll_ctlepoll_wait 调用。
  • 失败时,返回 -1,并设置 errno 来指示错误类型(例如 ENFILEENOMEM)。
2. epoll_ctl
函数原型

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

参数
  • epfd:通过 epoll_create1 返回的 epoll 文件描述符。
  • op:要执行的操作,可以是以下之一:
    • EPOLL_CTL_ADD:将一个新的文件描述符 fd 添加到 epoll 实例中。
    • EPOLL_CTL_MOD:修改已存在的文件描述符 fd 的事件。
    • EPOLL_CTL_DEL:从 epoll 实例中删除文件描述符 fd
  • fd:要添加、修改或删除的文件描述符。
  • event:指向 epoll_event 结构的指针,描述要监视的事件。
返回值
  • 成功时,返回 0
  • 失败时,返回 -1,并设置 errno(如 ENOENT 表示找不到该文件描述符)。
3. epoll_wait
函数原型

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

参数
  • epfd:通过 epoll_create1 返回的 epoll 文件描述符。
  • events:指向 epoll_event 结构数组的指针,用于存放就绪的事件。
  • maxevents:数组 events 的大小,指定最大可以返回的事件数。
  • timeout:等待事件的超时时间(以毫秒为单位)。可以设置为:
    • -1:无限期等待。
    • 0:立即返回,不等待。
    • 大于 0:等待指定时间后返回。
返回值
  • 成功时,返回就绪事件的数量(可能小于 maxevents)。
  • 失败时,返回 -1,并设置 errno(例如 EINTR 表示调用被信号中断)。
#include <sys/epoll.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>

#define MAX_EVENTS 10 // 最大事件数

int main() {
    // 创建 TCP 套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        return 1;
    }

    // 设置服务器地址
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    // 绑定套接字
    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(sockfd);
        return 1;
    }

    // 开始监听
    if (listen(sockfd, 3) < 0) {
        perror("listen failed");
        close(sockfd);
        return 1;
    }

    // 创建 epoll 实例
    int epfd = epoll_create(0);
    if (epfd < 0) {
        perror("epoll_create1 failed");
        close(sockfd);
        return 1;
    }

    // 注册监听套接字
    struct epoll_event ev;
    ev.events = EPOLLIN; // 监听可读事件
    ev.data.fd = sockfd; // 存储文件描述符
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) < 0) {
        perror("epoll_ctl failed");
        close(epfd);
        close(sockfd);
        return 1;
    }

    while (1) {
        struct epoll_event events[MAX_EVENTS]; // 事件数组
        int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1); // 等待事件
        for (int i = 0; i < nfds; ++i) {
            // 检查是否有新连接到达
            if (events[i].events & EPOLLIN) {
                int new_socket = accept(sockfd, NULL, NULL); // 接受新连接
                if (new_socket < 0) {
                    perror("accept failed");
                } else {
                    printf("New connection accepted\n");
                    // 处理新连接(例如可以将其添加到 epoll 中以监视更多事件)
                    close(new_socket); // 示例中直接关闭新连接
                }
            }
        }
    }

    // 关闭 epoll 实例和监听套接字
    close(epfd);
    close(sockfd);
    return 0;
}

优势与劣势

  • select

    • 优点:简单易用,广泛支持。
    • 缺点:最大文件描述符数量有限,性能在处理大量连接时下降。
  • poll

    • 优点:没有文件描述符数量限制。
    • 缺点:每次调用时都需要复制文件描述符集合,性能仍然有限。
  • epoll

    • 优点:适合处理大量连接,支持边缘触发,性能优越。
    • 缺点:仅适用于 Linux,不同平台间的移植性差。

应用场景

I/O 多路复用广泛应用于以下场景:

  • 网络服务器:如 HTTP、FTP 服务器,需要同时处理多个客户端的请求。
  • 高并发应用:如实时聊天系统、游戏服务器等,需要处理大量的并发连接。
  • 事件驱动编程:在 GUI 应用或网络框架中,使用 I/O 多路复用来处理用户输入和网络事件。

2.I/O 多路复用的优缺点

I/O 多路复用的优点

  1. 高效利用单线程

    • 传统的多线程模型为每个连接分配一个线程,可能导致系统内存和 CPU 资源的高开销。而 I/O 多路复用允许在一个线程中同时处理多个连接,这显著降低了资源占用。
  2. 减少线程切换

    • 线程的创建、销毁和上下文切换都会引入额外的开销。通过将多个 I/O 操作集中在一个线程中,I/O 多路复用避免了频繁的线程切换,从而提高了性能。
  3. 适用性广

    • selectpollepoll 等多路复用技术是操作系统内核提供的通用机制,能够处理多种文件类型,包括 sockets、管道和 FIFO 等,使用范围广泛。
  4. 实现难度适中

    • 与纯粹的异步 I/O 相比,多路复用在控制流方面更易于理解,代码逻辑通常更容易编写和调试。对于中小规模应用,I/O 多路复用的性能已足够满足需求。

I/O 多路复用的缺点

  1. 只适合 I/O 密集型应用

    • 由于多路复用本质上仍是同步阻塞模型,它不能充分利用多核 CPU。如果应用的非 I/O 操作占用大量 CPU 时间,多路复用可能并不合适。
  2. 可扩展性有限

    • 尽管 epoll 在高并发场景下性能优于 selectpoll,但在监听的文件描述符数量达到百万级时,可能会出现内存占用和 CPU 效率的瓶颈,此时可能需要结合多线程或多进程等技术进行优化。
  3. 编程复杂度高

    • 相比传统的阻塞 I/O,多路复用要求开发者手动管理事件和缓冲区,控制流变得相对复杂。这会增加调试和维护的难度。
  4. 系统依赖性强

    • 不同操作系统对多路复用的支持不尽相同,例如 Windows 不支持 epoll 机制。而异步 I/O 在各操作系统中的支持通常较好,实现更加透明。

Echo 服务端示例

以下是一个简单的 Echo 服务端示例,分别展示了阻塞 I/O、多线程和 I/O 多路复用的实现。

1. 阻塞 I/O 实现
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>

void blockingEcho(int connFd) {
    char buf[256];
    while (true) {
        memset(buf, 0, sizeof(buf));
        int n = read(connFd, buf, sizeof(buf) - 1); // 读取数据
        if (n <= 0) {
            break; // 处理错误或连接关闭
        }
        write(connFd, buf, n); // 回写数据
    }
}
2. 多线程实现
#include <pthread.h>

void* threadFunc(void* arg) {
    int connFd = *(int*)arg;
    blockingEcho(connFd);
    close(connFd); // 关闭连接
    delete (int*)arg; // 释放内存
    return nullptr;
}

void threadEcho(int listenFd) {
    while (true) {
        int* connFd = new int; // 动态分配内存
        *connFd = accept(listenFd, nullptr, nullptr); // 接受连接
        pthread_t tid;
        pthread_create(&tid, nullptr, threadFunc, connFd); // 创建新线程处理连接
        pthread_detach(tid); // 分离线程
    }
}
3. I/O 多路复用实现
#include <sys/epoll.h>

void multiplexEcho(int listenFd) {
    int epfd = epoll_create1(0); // 创建 epoll 实例
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = listenFd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listenFd, &ev); // 注册监听文件描述符

    while (true) {
        struct epoll_event events[10]; // 事件数组
        int nfds = epoll_wait(epfd, events, 10, -1); // 等待事件
        for (int i = 0; i < nfds; ++i) {
            if (events[i].data.fd == listenFd) { // 处理新连接
                int connFd = accept(listenFd, nullptr, nullptr);
                ev.events = EPOLLIN; // 监听可读事件
                ev.data.fd = connFd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, connFd, &ev); // 注册新连接
            } else { // 处理已连接的 socket
                blockingEcho(events[i].data.fd); // 使用阻塞 I/O 回显数据
                close(events[i].data.fd); // 关闭连接
            }
        }
    }
}

总结

  • 阻塞 I/O:简单易用,但对于大量连接会导致性能问题。
  • 多线程:适合高并发场景,但资源消耗大。
  • I/O 多路复用:在处理高并发连接时资源利用高效,代码结构较清晰,但复杂性有所增加。

3.epoll中et和lt的区别与实现原理

Epoll支持两种事件触发模式:边缘触发(Edge Triggered, ET)和水平触发(Level Triggered, LT)。它们的区别在于事件通知的时机和次数。

特性水平触发(LT)边缘触发(ET)
触发时机只要文件描述符处于就绪状态,都会通知应用程序仅在文件描述符状态发生变化时通知一次
事件通知次数多次通知,只要状态保持就绪只通知一次,后续状态不变则不再通知
处理方式每次调用 epoll_wait 都能处理已就绪事件需要在一次调用中处理所有可读数据,直到 EAGAIN
编程复杂度较低,易于实现较高,需处理所有数据以避免漏掉事件
适用场景一般应用、短连接高性能服务器、大量并发连接
性能相对较低,但实现简单性能更高,减少重复通知和 CPU 开销
  1. 水平触发(LT):
    • 当一个文件描述符注册到epoll实例中时,如果该描述符处于就绪状态,epoll_wait调用会立即返回该事件。
    • 如果该文件描述符在调用epoll_wait之前一直处于就绪状态,epoll_wait会在每次调用时都返回该事件,直到该事件被处理。
    • 也就是说,在LT模式下,只要文件描述符处于就绪状态,epoll就会不断通知应用程序。
  2. 边缘触发(ET):
    • 当一个文件描述符注册到epoll实例中时,如果该描述符从非就绪状态转为就绪状态,epoll_wait调用会立即返回该事件。
    • 但是,如果该文件描述符在调用epoll_wait之前已经处于就绪状态,epoll_wait不会返回该事件,直到该描述符状态发生新的变化(比如新的数据到达)。
    • 也就是说,在ET模式下,epoll只在文件描述符状态发生变化时通知一次应用程序,不会重复通知。
// 水平触发(LT)
while (true) {
    int nReady = epoll_wait(epfd, events, 1024, -1);
    for (int i = 0; i < nReady; ++i) {
        int fd = events[i].data.fd;
        if (fd == listenFd) {
            // 接受新连接 
        } else {
            char buf[256];
            int n = read(fd, buf, sizeof(buf));
            if (n == -1) {
                // 处理错误
            } else if (n == 0) {
                // 客户端关闭连接
            } else {
                // 处理读到的数据
            }
        }
    }
}

// 边缘触发(ET)  
while (true) {
    int nReady = epoll_wait(epfd, events, 1024, -1);
    for (int i = 0; i < nReady; ++i) {
        int fd = events[i].data.fd;
        if (fd == listenFd) {
            // 接受新连接
        } else {
            char buf[256];
            while (true) {
                int n = read(fd, buf, sizeof(buf));
                if (n == -1) {
                    if (errno == EAGAIN) {
                        // 数据读完,退出读循环
                        break;  
                    } else {
                        // 处理错误 
                    }
                } else if (n == 0) {
                    // 客户端关闭连接
                    break;
                } else {
                    // 处理读到的数据
                }
            }
        }
    }
}

在LT模式下,每次epoll_wait返回就绪事件后,我们直接处理该事件对应的文件描述符。如果该描述符在下次epoll_wait调用前一直处于就绪状态,epoll_wait会再次返回该事件。

而在ET模式下,当epoll_wait返回一个就绪事件后,我们必须将该文件描述符的数据全部处理完毕。因为在ET模式下,epoll不会再次通知同一个就绪事件,除非该文件描述符状态发生新的变化。因此,我们通常会在一个while循环中不断读取数据,直到read返回EAGAIN错误,表示当前没有更多数据可读。

从实现原理上看,LT和ET的区别在于内核维护就绪队列的方式不同:

  • 在LT模式下,一个文件描述符对应的就绪事件在被处理前不会从就绪队列中移除。因此,epoll_wait每次都会返回处于就绪状态的文件描述符,不论它是否被处理过。

  • 在ET模式下,一旦一个就绪事件被处理完毕,内核就会把它从就绪队列中移除。因此,epoll_wait只会在文件描述符状态发生变化时返回一次就绪事件。这种机制可以避免应用程序重复处理同一个事件,提高了系统效率。

需要注意的是,ET模式对应用程序的编程要求更高。因为一旦epoll_wait返回就绪事件,应用程序必须将相应的I/O操作全部处理完毕。否则,如果只处理了部分数据就退出,剩余的数据可能会丢失。而LT模式对此则更为宽松,不会有事件丢失的风险。

总之,epoll的LT和ET模式各有优缺点。LT模式编程简单,不易出错;而ET模式效率更高,但对编程者的要求也更高。在实际开发中,需要根据具体的应用场景和性能要求,灵活选择合适的触发模式。

4.select机制的缺点

Select机制虽然实现了I/O多路复用,但它也存在一些明显的缺点和局限性:

  1. 监听文件描述符数量受限。Select通过位图表示文件描述符集合,而位图的大小通常由内核中FD_SETSIZE常量决定,在32位系统中默认是1024。这意味着,使用select最多只能同时监听1024个socket连接,对于需要高并发的服务端程序来说,这个上限是远远不够的。

  2. 线性扫描效率低下。无论内核还是用户态,都需要遍历整个文件描述符集合才能找到就绪的socket。当监听的socket较少时,这种线性扫描的开销可以接受。但socket数量越多,每次扫描耗时也会线性增长,造成cpu资源的浪费。

  3. 内存拷贝开销大。在调用select时,需要把进程的文件描述符集拷贝到内核里,在返回时,又要把内核更新过的描述符集拷贝回进程中。当监听的socket较多时,这种频繁的内存拷贝会带来可观的性能损耗。

  4. 不能直接定位就绪的socket。Select只返回已就绪的文件描述符总数,具体是哪些socket就绪还需要用户自己去遍历。这种轮询方式效率低下,尤其是在大量连接中只有少量活跃时更加明显。

#include <iostream>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
using namespace std;

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

    sockaddr_in srvAddr;
    srvAddr.sin_family = AF_INET;
    srvAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    srvAddr.sin_port = htons(9999);
    bind(listenFd, (sockaddr*)&srvAddr, sizeof(srvAddr));

    listen(listenFd, 5);

    int maxFd = listenFd;
    fd_set readfds, allFds;
    FD_ZERO(&readfds);
    FD_ZERO(&allFds);
    FD_SET(listenFd, &allFds);

    while (true) {
        readfds = allFds;
        int nReady = select(maxFd + 1, &readfds, NULL, NULL, NULL);
        if (nReady == -1) {
            break;
        }

        for (int i = 0; i <= maxFd; ++i) {
            if (FD_ISSET(i, &readfds)) {
                if (i == listenFd) {
                    int connFd = accept(listenFd, NULL, NULL);
                    FD_SET(connFd, &allFds);
                    maxFd = max(maxFd, connFd);
                } else {
                    char buf[256];
                    memset(buf, 0, sizeof(buf));
                    int n = read(i, buf, 255);
                    if (n == -1 || n == 0) {
                        close(i);
                        FD_CLR(i, &allFds);
                    } else {
                        cout << "Received: " << buf;
                        write(i, buf, strlen(buf));
                    }
                }
            }
        }
    }

    close(listenFd);
    return 0;
}

这段代码使用select实现了一个简单的回显服务器。可以看到,无论是在调用select之前,还是处理就绪socket时,都需要遍历整个文件描述符集合。假设监听的socket数量达到了1000个,其中只有10个socket活跃,select机制下90%的遍历都是徒劳的。

此外,代码中维护了readfdsallFds两个描述符集,每次调用select都要将allFds拷贝给readfds。这种不断重复的内存拷贝也会影响系统的性能。

正是由于select存在这些缺陷,后来才催生了poll和epoll等更高效的I/O多路复用机制。它们或是取消了文件描述符数量的限制,或是引入了事件驱动机制避免了无谓的遍历,从而更好地支撑了高并发服务端程序的开发需求。

5.使用epoll的好处

Epoll是Linux下高效的I/O多路复用机制,它在设计上克服了select的多个缺陷,具有以下优点:

  1. 突破文件描述符数量限制。Epoll使用一个文件描述符管理多个socket连接,将用户关心的socket事件通过epoll_ctl维护在内核中,因此不存在描述符数量的限制,一般只与系统资源有关。

  2. O(1)时间复杂度。Epoll使用事件驱动机制,当某个socket有事件发生时,内核会使用回调函数将其加入就绪队列。Epoll_wait只需要从就绪队列中取出事件,无须遍历整个描述符集,因此时间复杂度是O(1)。

  3. 内存拷贝次数少。Epoll使用mmap在内核和用户空间之间建立映射,通过这个映射区域传递事件,减少了内存拷贝的次数。此外,内核还可以通过共享内存直接访问用户态的数据,再一次减少了数据拷贝。

  4. 支持多种事件触发模式。Epoll支持边缘触发(edge-triggered)和水平触发(level-triggered)两种事件模式。边缘触发只在socket状态发生变化时才触发事件,避免了重复触发。而水平触发与select和poll的行为类似,只要socket处于就绪状态就一直触发。

#include <iostream>
#include <cstring>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
using namespace std;

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

    sockaddr_in srvAddr;
    memset(&srvAddr, 0, sizeof(srvAddr));
    srvAddr.sin_family = AF_INET;
    srvAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    srvAddr.sin_port = htons(9999);
    bind(listenFd, (sockaddr*)&srvAddr, sizeof(srvAddr));

    listen(listenFd, 5);

    int epfd = epoll_create(1);
    epoll_event ev, events[1024];
    ev.data.fd = listenFd;
    ev.events = EPOLLIN;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listenFd, &ev);

    while (true) {
        int nReady = epoll_wait(epfd, events, 1024, -1);
        for (int i = 0; i < nReady; ++i) {
            if (events[i].data.fd == listenFd) {
                sockaddr_in cliAddr;
                socklen_t len = sizeof(cliAddr);
                int connFd = accept(listenFd, (sockaddr*)&cliAddr, &len);
                ev.data.fd = connFd;
                ev.events = EPOLLIN | EPOLLET;
                epoll_ctl(epfd, EPOLL_CTL_ADD, connFd, &ev);
            } else {
                int fd = events[i].data.fd;
                char buf[256];
                while (true) {
                    memset(buf, 0, sizeof(buf));
                    int n = read(fd, buf, sizeof(buf));
                    if (n == -1) {
                        if (errno == EAGAIN) {
                            break;
                        }
                        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                        close(fd);
                        break;
                    } else if (n == 0) {
                        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); 
                        close(fd);
                        break;
                    } else {
                        write(fd, buf, strlen(buf));
                    }
                }
            }
        }
    }

    close(listenFd);
    return 0;
}

该例子中使用了epoll的以下特性:

  1. 使用epoll_create创建一个epoll实例,返回一个表示epoll的文件描述符。

  2. 使用epoll_ctl将需要监听的socket添加到epoll实例中,并设置关心的事件类型。例子中,我们对监听socket关心”可读”事件,对已连接socket关心”可读”和”边缘触发”事件。

  3. 使用epoll_wait等待事件发生。一旦有事件发生,epoll_wait就会返回,并将发生的事件填充到传入的数组中。

  4. 遍历事件数组,根据事件类型进行不同处理。例子中,如果发生事件的是监听socket,则接受新连接;如果是已连接socket,则进行读写。

  5. 由于使用了边缘触发,一次事件到来时需要将socket的数据全部处理完毕。因此,例子中使用了while循环不断读取数据,直到read返回EAGAIN错误,表示数据已被读完。

总的来说,epoll是Linux下高性能网络编程的利器。它在高并发场景下表现优异,已被Nginx、Redis等知名项目广泛使用。对epoll的深入理解和应用,是C++服务端开发者的必备技能。

6.epoll需要在用户态和内核态拷贝数据么?

Epoll在数据传输过程中,仍然需要在用户态和内核态之间进行一定的数据拷贝,但与select和poll相比,epoll通过以下机制减少了数据拷贝的次数和量:

  1. 内核和用户空间共享epoll的就绪列表。
    • 当应用程序调用epoll_create创建epoll实例时,内核会分配一块内存用于存储已就绪的文件描述符列表。
    • 这块内存被mmap映射到用户空间,因此内核和用户空间可以共享访问。
    • 当文件描述符状态发生变化时,内核将就绪的文件描述符直接写入这块共享内存,而不需要再拷贝到用户空间。
  2. 使用事件驱动机制避免无谓的数据拷贝。
    • 在select/poll中,即使只有少量文件描述符就绪,内核也要将整个文件描述符集拷贝到用户空间。
    • 而epoll使用事件驱动机制,只将就绪的文件描述符通知给用户空间,大大减少了数据拷贝的量。
  3. 内核可以直接访问用户空间的数据缓冲区。
    • 传统的read/write操作需要先将数据从内核空间拷贝到用户空间,再由应用程序进行处理。
    • 而epoll支持使用mmap将用户空间的一块内存映射到内核空间,应用程序可以直接在这块内存上进行读写。
    • 内核也可以直接访问这块内存,从而避免了数据在内核态和用户态之间的拷贝。

下面是一个使用mmap优化数据读取的例子:

int fd = open("file.txt", O_RDONLY);
size_t size = lseek(fd, 0, SEEK_END);
char* buf = (char*)mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);

// 使用buf进行数据处理 
// ...

munmap(buf, size);
close(fd);

在这个例子中,我们首先打开一个文件,并获取文件的大小。然后,使用mmap将文件内容直接映射到用户空间的一块内存buf中。这样,我们就可以直接在buf上进行数据读取和处理,而无需再进行read系统调用。内核也可以直接访问buf,将文件数据写入其中,避免了内核态到用户态的数据拷贝。

当然,使用mmap进行文件读写时也需要权衡利弊。mmap适合用于对大文件的随机访问和频繁读写,而对于小文件或者顺序读写,使用传统的read/write反而更高效。此外,mmap在映射大文件时可能会占用过多的虚拟内存空间,也需要谨慎使用。

总的来说,尽管epoll没有完全消除用户态和内核态的数据拷贝,但通过共享内存、事件驱动等机制,它在很大程度上减少了数据拷贝的次数和量。这也是epoll能够支持高并发、高性能网络I/O的重要原因。在实际开发中,我们可以根据具体的应用场景,灵活利用epoll的这些特性,来优化数据读写的效率。

7.网络编程的一般步骤

网络编程是指编写运行在多个设备上的程序,通过网络进行数据交换。在Linux环境下,网络编程通常使用Socket API。无论是编写客户端程序还是服务器程序,网络编程都遵循一定的基本步骤。下面以TCP通信为例,说明网络编程的一般步骤:

服务器端:

  1. 创建socket
    • 使用socket()函数创建一个socket,指定通信域(AF_INET表示IPv4)、通信类型(SOCK_STREAM表示TCP)和协议(IPPROTO_TCP)。
    • socket()函数返回一个socket文件描述符,用于后续的通信操作。
  2. 绑定socket到本地地址和端口
    • 使用bind()函数将socket绑定到一个本地的IP地址和端口号。
    • 服务器通常会绑定到一个固定的、众所周知的端口,以便客户端能够找到它。
  3. 开始监听连接请求
    • 使用listen()函数让socket进入监听状态,准备接受客户端的连接请求。
    • listen()函数的参数指定了socket的等待队列大小,表示可以同时处理多少个客户端连接请求。
  4. 接受客户端连接
    • 使用accept()函数接受客户端的连接请求,建立一个新的socket与客户端进行通信。
    • accept()函数会阻塞等待,直到有客户端连接到达。它返回一个新的socket文件描述符,专门用于与该客户端通信。
  5. 与客户端进行数据交换
    • 使用read()/write()或send()/recv()等函数在新的socket上与客户端进行数据读写。
    • 服务器可以根据业务需求,对接收到的数据进行处理,并将结果发送给客户端。
  6. 关闭连接
    • 数据交换完毕后,使用close()函数关闭与客户端通信的socket,释放资源。
    • 服务器通常会继续监听其他客户端的连接,而不会立即退出。

客户端:

  1. 创建socket
    • 与服务器类似,客户端也要使用socket()函数创建一个socket。
  2. 连接服务器
    • 使用connect()函数向服务器发起连接请求。
    • connect()函数需要指定服务器的IP地址和端口号。如果连接成功,socket就建立了与服务器的连接。
  3. 与服务器进行数据交换
    • 连接建立后,客户端可以使用read()/write()或send()/recv()等函数在socket上与服务器进行数据读写。
    • 客户端将请求数据发送给服务器,并接收服务器返回的响应数据。
  4. 关闭连接
    • 数据交换完毕后,使用close()函数关闭socket,结束与服务器的通信。
    • 客户端通常会在关闭连接后退出,而不像服务器那样继续监听。

下面是一个简单的TCP服务器和客户端的例子:

// 服务器
int main() {
    int listenFd = socket(AF_INET, SOCK_STREAM, 0);

    sockaddr_in srvAddr;
    srvAddr.sin_family = AF_INET;
    srvAddr.sin_addr.s_addr = htonl(INADDR_ANY); 
    srvAddr.sin_port = htons(9999);
    bind(listenFd, (sockaddr*)&srvAddr, sizeof(srvAddr));

    listen(listenFd, 5);

    while (true) {
        sockaddr_in cliAddr;
        socklen_t len = sizeof(cliAddr);
        int connFd = accept(listenFd, (sockaddr*)&cliAddr, &len);

        char buf[256];  
        int n = read(connFd, buf, 255);
        write(connFd, buf, n);

        close(connFd);
    }

    close(listenFd);
    return 0;
}

// 客户端
int main() {
    int sockFd = socket(AF_INET, SOCK_STREAM, 0);

    sockaddr_in srvAddr;  
    srvAddr.sin_family = AF_INET;
    srvAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    srvAddr.sin_port = htons(9999);
    connect(sockFd, (sockaddr*)&srvAddr, sizeof(srvAddr));

    char buf[256] = "Hello, server!";
    write(sockFd, buf, strlen(buf)); 

    memset(buf, 0, sizeof(buf));
    int n = read(sockFd, buf, 255);
    cout << "Server replied: " << buf << endl;

    close(sockFd);
    return 0;
}

这个例子中,服务器绑定到本地的9999端口,等待客户端连接。每当有客户端连接到达,服务器就接受连接,读取客户端发送的数据,并原样发送回去,然后关闭连接。而客户端则连接到服务器的9999端口,发送一条消息,并接收服务器的响应,最后关闭连接。

当然,实际的网络程序要比这个例子复杂得多。我们还需要考虑字节序转换、错误处理、并发控制、超时机制等诸多细节。此外,对于不同的应用场景,网络模型也可能有所不同。比如,我们可以使用多进程、多线程、I/O多路复用等技术来处理并发连接,提高服务器的性能。

8.socket编程,如果client断电了,服务器如何快速知道?

在Socket编程中,如果客户端异常断开连接(如断电、崩溃等),服务器并不会立即知道这一情况。因为TCP连接是一种”有状态”的连接,服务器只有在尝试向客户端发送数据时,才能发现连接已经断开。这可能会导致服务器资源的浪费和响应的延迟。

为了让服务器能够快速知道客户端的异常断开,我们可以采取以下几种策略:

  1. 心跳机制
    • 客户端定期向服务器发送心跳包,告知服务器自己还在线。
    • 如果服务器在一定时间内没有收到客户端的心跳包,就认为客户端已经断开,主动关闭连接。
    • 心跳机制可以及时清理无效连接,但会增加一些网络开销。
  2. TCP保活机制(TCP Keep-Alive)
    • TCP协议提供了保活机制,可以检测连接的有效性。
    • 服务器可以通过设置socket的SO_KEEPALIVE选项启用保活机制。
    • 启用后,如果一个连接在一定时间内没有数据交互,TCP会自动发送保活探测包。
    • 如果多次探测都没有响应,TCP会认为连接已经断开,并通知应用程序。
    • TCP保活可以自动检测连接有效性,但检测的时间间隔较长(默认2小时)。
  3. 应用层协议
    • 在应用层协议中加入连接状态的控制和反馈机制。
    • 例如,客户端在发送业务数据时,同时报告自己的连接状态。
    • 服务器如果长时间没有收到客户端的状态报告,就可以认为连接已经断开。
    • 应用层协议可以根据业务需求定制连接管理策略,但实现起来比较复杂。
  4. 使用TCP的SO_OOBINLINE选项
    • 将socket的SO_OOBINLINE选项设置为1,允许接收TCP的带外数据。
    • 当客户端异常断开时,服务器可以立即收到一个带外数据,从而得知连接已经断开。
    • 带外数据可以最快地通知连接断开,但并非所有的异常断开都会触发带外数据。

下面是一个使用TCP保活机制的简单例子:

int fd = socket(AF_INET, SOCK_STREAM, 0);

int keepAlive = 1;  // 启用保活机制
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &keepAlive, sizeof(keepAlive));

int keepIdle = 60;  // 如果60秒内没有数据交互,开始发送保活探测包
setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &keepIdle, sizeof(keepIdle));

int keepInterval = 5;  // 每5秒发送一次保活探测包
setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &keepInterval, sizeof(keepInterval));

int keepCount = 3;  // 尝试3次保活探测,全部超时则认为连接已断开
setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &keepCount, sizeof(keepCount));

在这个例子中,我们首先创建一个TCP socket,然后通过设置socket选项启用TCP保活机制。其中,TCP_KEEPIDLE设置空闲时间,TCP_KEEPINTVL设置探测包的发送间隔,TCP_KEEPCNT设置探测次数。这样,如果一个连接在60秒内没有任何数据交互,TCP将开始每5秒发送一次保活探测包,最多尝试3次。如果3次探测都超时,TCP将认为连接已经断开,并返回一个错误,应用程序就可以及时关闭socket,释放资源。

需要注意的是,TCP保活机制只能检测连接是否存在,但并不能检测连接的可用性。也就是说,即使连接没有断开,但由于网络拥塞等原因,连接可能已经无法正常通信。因此,在实际应用中,我们通常需要结合多种机制(如心跳、应用层协议等),来全面监控和管理网络连接的状态。

此外,TCP保活机制的参数设置也需要根据具体的应用场景进行调整。空闲时间过短会增加无谓的探测开销,过长则会延迟连接断开的检测。探测间隔过短会增加网络负担,过长则会延长检测时间。因此,我们需要在实时性和开销之间找到一个合适的平衡点。

总之,检测客户端异常断开连接是网络编程中的一个常见问题。通过心跳、TCP保活、应用层协议等机制,我们可以让服务器更快地感知连接的状态变化,从而采取相应的处理措施,提高系统的可靠性和稳定性。在实际开发中,我们需要根据具体的业务需求和网络环境,选择和优化合适的连接管理策略。

9.socket在什么情况下可读?

一个socket在以下几种情况下会变为可读:

  1. 有数据到达
    • 当对端发送数据时,socket的接收缓冲区会有数据到达。
    • 此时,对该socket调用read()/recv()等读操作,可以无阻塞地读取数据。
    • 如果缓冲区已满,继续到达的数据会被丢弃,直到应用程序读取一些数据,释放缓冲区空间。
  2. 连接被关闭
    • 当对端正常关闭连接(调用close()/shutdown())时,socket也会变为可读。
    • 此时,对该socket调用read()/recv()等读操作,会立即返回0,表示连接已关闭。
    • 这种情况下,socket的可读事件可以用于检测连接的关闭。
  3. 发生错误
    • 当连接发生错误(如对端崩溃、网络中断等)时,socket也会变为可读。
    • 此时,对该socket调用read()/recv()等读操作,会返回-1,并设置errno为相应的错误码。
    • 这种情况下,socket的可读事件可以用于检测连接的异常。
  4. 带外数据到达
    • 当对端发送带外数据(Out-of-Band Data)时,socket会变为可读,并产生一个特殊的可读事件。
    • 带外数据通常用于传递一些紧急或特殊的信息,如中断、取消等。
    • 对带外数据的处理需要使用特殊的socket选项和读操作(如MSG_OOB标志)。

下面是一个简单的例子,演示了如何使用select检测socket的可读事件:

int fd = socket(AF_INET, SOCK_STREAM, 0);
// 连接到服务器...

fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd, &readfds);

timeval timeout = {10, 0};  // 等待10秒

int ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
if (ret == -1) {
    // select出错
} else if (ret == 0) {
    // 等待超时,没有可读事件
} else {
    if (FD_ISSET(fd, &readfds)) {
        // socket可读
        char buf[1024];
        int n = recv(fd, buf, sizeof(buf), 0);
        if (n == -1) {
            // 读取出错
        } else if (n == 0) {
            // 连接已关闭  
        } else {
            // 成功读取到数据
        }
    }
}

在这个例子中,我们首先创建一个socket,并连接到服务器。然后,使用select函数等待socket的可读事件,超时时间设置为10秒。当select返回时,我们检查返回值和可读集合,判断是否有可读事件发生。如果socket在可读集合中,我们就调用recv函数读取数据。根据recv的返回值,我们可以判断是否读取成功,以及连接是否已关闭。

需要注意的是,一个socket变为可读并不意味着总能读到数据。在某些情况下,即使socket可读,读操作也可能被阻塞。例如,当对端发送数据后立即关闭连接,而本端还没来得及读取时,读操作就会被阻塞。因此,在实际应用中,我们通常需要结合可读事件和读操作的返回值,来综合判断socket的状态和数据的可用性。

此外,对于监听socket,它的可读事件有特殊的含义。当新的连接请求到达时,监听socket会变为可读。此时,我们可以调用accept函数接受新连接,并将新连接的socket加入到select的监听集合中。这样,我们就可以同时监听多个连接的可读事件,实现并发的数据处理。

总之,理解socket的可读条件,是进行高效、可靠的网络编程的基础。通过合理地监听和处理可读事件,我们可以及时地读取数据、检测连接状态,并采取相应的措施,保证网络应用的稳定性和性能。在实际开发中,我们需要根据具体的业务场景和网络环境,选择合适的I/O模型和读写策略,来优化socket的可读事件处理。

10.connect方法会阻塞,什么方法可以避免其长时间阻塞?

connect() 方法确实可能会导致长时间阻塞,这在网络编程中是一个常见的问题。让我们来看看几种避免长时间阻塞的方法:

  1. 非阻塞模式:
    我们可以将socket设置为非阻塞模式。这样,connect() 会立即返回,即使连接还没有建立。
    int flags = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
    int ret = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    
  2. select() 或 poll() 函数:
    我们可以使用 select() 或 poll() 来等待连接完成,同时设置一个超时时间。
    fd_set wset;
    struct timeval tv;
    
    FD_ZERO(&wset);
    FD_SET(sockfd, &wset);
    tv.tv_sec = 5;  // 5秒超时
    tv.tv_usec = 0;
    
    if (select(sockfd + 1, NULL, &wset, NULL, &tv) > 0) {
       // 连接成功或失败
    } else {
       // 超时
    }
    
  3. 使用异步连接:
    在某些系统上,我们可以使用异步I/O操作来进行非阻塞连接。

  4. 多线程方法:
    我们可以在单独的线程中进行连接操作,主线程可以继续执行其他任务。

    std::thread connect_thread([&]() {
       connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    });
    connect_thread.detach();  // 或者之后使用 join()
    
  5. 设置 socket 超时:
    虽然这不能避免阻塞,但可以限制阻塞的时间。
    struct timeval tv;
    tv.tv_sec = 5;  // 5秒超时
    tv.tv_usec = 0;
    setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv);
    

这些方法各有优缺点,选择哪种方法取决于具体的应用场景和需求。非阻塞模式和 select()/poll() 方法在多数情况下是比较好的选择,因为它们提供了更好的控制和灵活性。

标签:知识点,socket,epoll,int,C++,描述符,fd,连接
From: https://blog.csdn.net/qq_50373827/article/details/143326957

相关文章

  • 点云学习笔记2——使用VoxelGrid滤波器进行点云降采样(c++)
    #include<iostream>#include<pcl/point_cloud.h>#include<pcl/io/pcd_io.h>#include<pcl/point_types.h>#include<pcl/filters/voxel_grid.h>#include<pcl/common/common_headers.h>#include<pcl/io/pcd_io.h>#inclu......
  • 三维变换矩阵知识点
    一、行矩阵与列矩阵、行主序与列主序行主序和列主序影响如何在内存中访问和存储矩阵数据,与行矩阵和列矩阵的概念没有直接的关系行矩阵与列矩阵:行矩阵:        $$\begin{pmatrix}a_{11}&a_{12}\\a_{21}&a_{22}\end{pmatrix}$$列矩阵:指的是矩阵的元......
  • C++连接SqlServer
    C++连接SqlServer连接外加查询sqlConnection.h#pragmaonce#ifndefSQLCONNECTION_H#defineSQLCONNECTION_H#include<iostream>#include<windows.h>#include<sql.h>#include<sqlext.h>#include<fstream>#include<string>//......
  • python知识点100篇系列(23)- 使用stylecloud生成词云
    使用stylecloud生成词云stylecloud是wordcloud优化版,相对来说操作更简单;一个很方便的功能是,可以使用FontAwesome提供的免费图标更改词云的形状;FontAwesome提供的免费图标库参考这里安装所需库主要是安装一下分词用到的jieba和生成词云使用的stylecloud安装方式......
  • C++之OpenCV入门到提高002:加载、修改、保存图像
    一、介绍今天是这个系列《C++之Opencv入门到提高》得第二篇文章。今天这个篇文章很简单,只是简单介绍如何使用Opencv加载图像、显示图像、修改图像和保存图像,先给大家一个最直观的感受。但是,不能认为很简单,只是让学习的过程没那么平滑一点,以后的路就好走了。OpenCV具......
  • c++11智能指针
    普通指针的不足new和new[]的内存需要用delete和deletel]释放。程序员的主观失误,忘了或漏了释放。程序员也不确定何时释放。普通指针的释放类内的指针,在析构函数中释放。C++内置数据类型,如何释放?new出来的类,本身如何释放?C++11新增三个智能指针类型unique_pt......
  • C/C++ 中有哪些基本数据类型?它们的存储大小和取值范围是多少?
      1.整型类型int存储大小:4字节(32位)取值范围:有符号(signedint):-2^(31)到2^(31)-1(即-2,147,483,648到2,147,483,647)short存储大小:2字节(16位)取值范围:有符号(signedshort):-2^(15)到2^(15)-1(即-32,768到32,767)long存储大小:通常为4字......
  • C/C++ 中有哪些常见的运算符?
    1.算术运算符用于执行数学运算。+(加法)-(减法)*(乘法)/(除法)%(取模,返回余数)2.关系运算符用于比较两个值的关系,结果为布尔值(true或false)。==(等于)!=(不等于)>(大于)<(小于)>=(大于等于)<=(小于等于)3.逻辑运算符用于在逻辑表达式中执行布尔逻辑运算。&&(逻辑与,AND)||(逻辑或,OR)!(逻辑非......
  • 【C++】—— priority_queue :平衡效率与秩序的算法利器
    去感受一棵草、一缕风、一场日落,去重新触摸真正的生活。——高盛元目录1、优先级队列1.1什么是优先级队列1.2 priority_queue的使用1.3仿函数2、priority_queue的模拟实现2.1整体框架接口2.2插入&&向上调整2.2删除&&向下调整2.3其他接口2.4优先级队列的应用......
  • 【c++篇】:探索c++中的std::string类--掌握字符串处理的精髓
    ✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨✨个人主页:余辉zmh–CSDN博客✨文章所属专栏:c++篇–CSDN博客文章目录前言一.`std::string`对象的创建二.`std::string`对象的访问三.`std::string`对象的容量四.`std::string`对......