C++ WebServer的细节理解

本文介绍了非阻塞I/O操作(如fcntl和accept的非阻塞模式)及其应用场景,以及内存映射(mmap和munmap)在高效文件访问和进程间通信中的应用。此外,还详细讲解了如何使用iovec结构和系统调用readv和writev进行多缓冲区I/O,以及服务器如何根据请求生成并发送响应给客户端。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 文件描述符-非阻塞模式

fcntl(fd, F_SETFL, fcntl(fd, F_GETFD, 0) | O_NONBLOCK);

代码解释:
上面这句代码,先查询文件描述符 fd 当前的标志,然后将 O_NONBLOCK 标志加入,并通过 F_SETFL 更新文件描述符,最终实现将该文件描述符切换为非阻塞模式。

O_NONBLOCK 设置在文件描述符上时,后续对该文件描述符的 I/O 操作(读写等)会变为非阻塞模式。

在非阻塞模式下,如果 I/O 操作不能立即完成(例如,因为没有数据可读 或 写缓冲区满),系统不会让调用进程阻塞等待,而是立即返回一个错误(通常为 EAGAIN 或 EWOULDBLOCK)。
这样,进程可以避免因等待 I/O 完成而被长时间阻塞,实现更高的响应性和并发性。

非阻塞模式—应用场景举例:

int accept(int sockfd, struct sockaddr* addr, socklen_t* len)

accept()函数,用于接受客户端的连接请求。默认情况下,accept() 是阻塞的。这意味着,如果没有待处理的连接请求(即没有客户端尝试连接到服务器),accept() 会一直阻塞,直到有新的连接请求到达或发生其他特定条件(如超时)为止。

当 accept() 处于阻塞状态时,它所在的线程是不能去做其他事情的。
若要改变这种行为,可以采用以下方法:

  1. 非阻塞模式:如前所述,可以使用 fcntl() 函数将套接字设置为非阻塞模式(使用 O_NONBLOCK 标志)。在这种模式下,accept() 调用将不会阻塞,而是立即返回。如果此时没有待处理的连接请求,accept() 将返回一个错误(通常为 EAGAINEWOULDBLOCK)。这样,线程可以在没有可用连接时执行其他任务,然后在适当的时候再次尝试 accept()

  2. 异步 I/O:如 Linux 中的 io_uring 或 Windows 中的 overlapped I/O,可以异步地执行 accept(),使得线程在发起 accept() 请求后可以继续执行其他任务,然后通过回调、事件通知等方式获知 accept() 的结果。

2. mmap() 和 munmap()

mmap() 和 munmap() 是用于内存映射操作的系统调用函数。
mmap() 允许将文件或其他对象直接映射到进程的虚拟地址空间中,从而实现高效的数据访问。

void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);
  • void* addr

    • 指定映射开始的期望虚拟地址。如果设为 NULL,内核将自动选择一个合适的地址。
  • size_t length

    • 指定映射区域的长度。必须是页面大小的整数倍。
  • int prot

    • 控制映射区域的访问权限:
      • PROT_READ:允许读取。
      • PROT_WRITE:允许写入。
      • PROT_NONE:禁止访问。
  • int flags

    • 控制映射行为和映射内容的处理方式:
      • MAP_SHARED:创建一个可共享的映射,对映射区域的修改会影响到所有映射此区域的进程,并且可能会同步回文件。
      • MAP_PRIVATE:创建一个私有映射,对映射区域的修改仅影响当前进程,不会改变底层文件。
  • int fd

    • 用于指定要映射的文件。如果使用匿名映射,可以传入 -1 或者一个未打开的文件描述符。
  • off_t offset

    • 文件中的偏移量,映射从该位置开始。通常应是文件系统块大小的整数倍。

返回值:

  • 成功时返回映射区域的起始地址。
  • 出现错误时返回 MAP_FAILED(通常是一个负值,如 (void*) -1)。

int munmap(void* addr, size_t length);
  • void* addr
    • 指定要解除映射的内存区域的起始地址,该地址应是之前 mmap() 调用返回的地址。
  • size_t length
    • 指定解除映射的区域长度,应与对应的 mmap() 调用中的 length 参数一致。

返回值: 成功时返回 0。出现错误时返回 -1


mmap() 常用于以下场景:

  • 高效文件访问:将文件直接映射到内存,避免了常规的文件I/O操作,对于大数据文件或频繁访问的文件尤其高效。
  • 进程间通信(IPC):通过映射同一份共享内存,不同进程可以直接通过内存地址来交换数据,无需通过管道、套接字等传统IPC机制。

例如,映射一个已打开文件的部分内容到内存中:

#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <cerrno>

int main() {
    const char* filename = "/path/to/file";
    int fd = open(filename, O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    struct stat sb;
    if (fstat(fd, &sb) == -1) {
        perror("fstat");
        close(fd);
        return 1;
    }

    // 映射整个文件
    void* mapped_region = mmap(nullptr, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (mapped_region == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    // 使用映射区域...
    // ...

    // 解除映射
    if (munmap(mapped_region, sb.st_size) == -1) {
        perror("munmap");
        close(fd);
        return 1;
    }

    close(fd);
    return 0;
}

在WebServer中,使用方法也类似;


int stat(const char *path, struct stat *buf);

stat((srcDir_ + path_).data(), &mmFileStat_);

stat() 是一个标准C库函数,通常在 <sys/stat.h> 头文件中声明,用于获取文件或目录的状态信息。

  • const char *path: 一个指向包含文件路径的指针。可以是绝对路径,也可以是相对于当前工作目录的相对路径。
  • struct stat *buf: 一个指向 struct stat 结构体的指针,用于接收由 stat() 函数填充的关于指定路径对象的信息,例如:
    – 文件大小(st_size
    – 最近访问、修改和状态更改时间(st_atime, st_mtime, st_ctime
    – 权限位(st_mode

函数返回值:

  • 如果成功,返回0;否则,返回一个非零值(通常是错误代码),表示发生了某种错误(如权限不足、路径不存在等)。

3. struct iovec

struct iovec 是用于系统调用 readvwritev 的一个数据结构,这两个系统调用允许你从多个缓冲区读取数据或者向多个缓冲区写入数据。
使用 iovec 结构,可以指定多个缓冲区,每个缓冲区都有自己的地址和长度。这对于高效地处理I/O操作非常有用。

struct iovec 结构在 <sys/uio.h> 头文件中定义:

struct iovec {
    void  *iov_base;  /* 指向缓冲区的指针 */
    size_t iov_len;   /* 缓冲区的大小 */
};

在服务器中,声明 struct iovec iov_[2]; 时,实际上创建了一个包含两个 iovec 结构的数组。
这使得可以同时指定两个不同的缓冲区进行读写操作。例如,如果你想从文件中读取一些数据到两个不同的缓冲区,你可以这样做:

#include <iostream>
#include <sys/uio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    const char* filename = "example.txt";
    int fd = open(filename, O_RDONLY);
    if (fd == -1) {
        std::cerr << "Failed to open file\n";
        return 1;
    }

    char buffer1[100], buffer2[100];
    struct iovec iov[2] = {
        { .iov_base = buffer1, .iov_len = sizeof(buffer1) },
        { .iov_base = buffer2, .iov_len = sizeof(buffer2) }
    };

    ssize_t bytesRead = readv(fd, iov, 2);
    if (bytesRead == -1) {
        std::cerr << "Error reading from file\n";
        close(fd);
        return 1;
    }
    close(fd);
    return 0;
}

其中,readv(fd, iov, 2); 函数用于从文件描述符 fd 指向的文件中,按照 iov 数组中指定的顺序和大小,一次性读取数据到由 iov 描述的多个缓冲区中。这里的 2 表示数组 iov 中有两个 iovec 结构体,即两个缓冲区。

  1. readv 函数: 与常规的 read 函数不同,readv 允许将数据读、写到多个缓冲区中,而不是单一的一个。有时也将readv和writev函数称为散布读(scatter read)和聚集写(gather write)。

  2. 使用 readv 可以在单个系统调用中完成对多个缓冲区的数据读取,可以提高效率,减少上下文切换和系统调用的开销。

  3. readv 函数的返回值是实际读取的字节数,它在任何错误发生时返回 -1。

4. 服务器如何发送响应

  • 在建立连接后,如果收到了某个fd有可读事件,
  • 服务器先调用readv读取数据(http请求)
  • 然后对原数据进行解析
  • 根据解析内容,服务器端进行可能的操作(如在数据库内进行增删改查)
  • 在缓冲区中生成响应行和响应头(包括文件的映射作响应体)
  • 设置fd为EPOLLOUT
  • 由EPOLLOUT触发writev(fd_, iov_, iovCnt_); 向socket上写入服务器处理客户请求的结果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值