MST

星途 面试题库

面试题:C语言Web服务器并发处理优化

现有一个基于C语言的Linux Web服务器,在处理高并发请求时性能不佳。请描述你会从哪些方面进行优化,例如多线程、多路复用(select、poll、epoll)等技术的应用。并选择一种优化方式,给出关键代码片段及优化前后性能对比的分析思路。
37.8万 热度难度
编程语言C

知识考点

AI 面试

面试题答案

一键面试

优化方面

  1. 多线程:创建多个线程来处理请求,每个线程负责处理一个或多个连接。主线程监听新连接,将新连接分配给空闲线程处理。
  2. 多路复用
    • select:通过监听多个文件描述符集合,阻塞等待其中有事件发生。但其支持的文件描述符数量有限,检查事件时需要遍历整个集合,时间复杂度为O(n)。
    • poll:与select类似,但通过链表结构解决了文件描述符数量限制问题,不过检查事件仍需遍历,时间复杂度同样为O(n)。
    • epoll:适用于高并发场景,采用事件驱动机制,通过epoll_ctl函数添加、修改、删除要监听的文件描述符,epoll_wait函数返回就绪的文件描述符,时间复杂度为O(1)。
  3. 优化算法:优化请求处理算法,减少不必要的计算和I/O操作。
  4. 内存管理:优化内存分配和释放,避免频繁的内存分配和碎片化。
  5. 缓存机制:设置请求和响应的缓存,减少重复处理。

选择epoll优化方式

// 优化前简单的单线程阻塞式处理
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main(int argc, char const *argv[]) {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置套接字选项
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    // 绑定套接字
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    while (1) {
        // 接受新连接
        if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
            perror("accept");
            continue;
        }

        // 接收数据
        read(new_socket, buffer, BUFFER_SIZE);
        printf("Received: %s\n", buffer);

        // 发送响应
        char *response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<html><body>Hello, World!</body></html>";
        send(new_socket, response, strlen(response), 0);

        // 关闭连接
        close(new_socket);
    }
    return 0;
}
// 优化后使用epoll的代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <sys/epoll.h>

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

int main(int argc, char const *argv[]) {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    int epoll_fd;
    struct epoll_event event, events[MAX_EVENTS];

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置套接字选项
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    // 绑定套接字
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    // 创建epoll实例
    epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        perror("epoll_create1");
        exit(EXIT_FAILURE);
    }

    // 将监听套接字添加到epoll实例
    event.data.fd = server_fd;
    event.events = EPOLLIN;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {
        perror("epoll_ctl: server_fd");
        exit(EXIT_FAILURE);
    }

    while (1) {
        // 等待事件发生
        int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("epoll_wait");
            exit(EXIT_FAILURE);
        }

        for (int i = 0; i < nfds; i++) {
            if (events[i].data.fd == server_fd) {
                // 处理新连接
                new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
                if (new_socket == -1) {
                    perror("accept");
                    continue;
                }

                // 将新连接添加到epoll实例
                event.data.fd = new_socket;
                event.events = EPOLLIN | EPOLLET;
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &event) == -1) {
                    perror("epoll_ctl: new_socket");
                    close(new_socket);
                }
            } else {
                // 处理已连接套接字的事件
                int client_fd = events[i].data.fd;
                int valread = read(client_fd, buffer, BUFFER_SIZE);
                if (valread == -1) {
                    if (errno != EAGAIN && errno != EWOULDBLOCK) {
                        perror("read");
                        epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);
                        close(client_fd);
                    }
                } else if (valread == 0) {
                    // 客户端关闭连接
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);
                    close(client_fd);
                } else {
                    // 处理请求并发送响应
                    buffer[valread] = '\0';
                    printf("Received: %s\n", buffer);
                    char *response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<html><body>Hello, World!</body></html>";
                    send(client_fd, response, strlen(response), 0);
                }
            }
        }
    }
    close(server_fd);
    close(epoll_fd);
    return 0;
}

性能对比分析思路

  1. 请求响应时间:使用工具如ab(Apache Bench)或wrk,向优化前后的服务器发送大量请求,记录平均响应时间、最大响应时间和最小响应时间。优化后应能显著降低平均响应时间,特别是在高并发场景下。
  2. 吞吐量:同样使用上述工具,统计单位时间内服务器处理的请求数量。优化后,由于能更高效地处理并发请求,吞吐量应有所提升。
  3. 资源利用率:使用top、htop等工具监控服务器在处理高并发请求时的CPU和内存使用率。优化后,CPU和内存利用率应更加合理,避免过度消耗导致系统性能下降。
  4. 并发连接数:逐步增加并发连接数,观察优化前后服务器能够稳定处理的最大并发连接数。优化后,最大并发连接数应有所提高,表明服务器对高并发的处理能力增强。