C语言如何编写Web服务器?

99ANYc3cd6
预计阅读时长 34 分钟
位置: 首页 C语言 正文
  1. 监听指定端口。
  2. 接受客户端连接。
  3. 解析客户端发来的HTTP请求。
  4. 根据请求的路径,从本地文件系统读取文件内容。
  5. 作为HTTP响应发送回客户端。
  6. 支持处理404(文件未找到)错误。
  7. 支持并发处理多个客户端连接(使用select实现)。

第一步:理解核心概念

在开始编码前,我们需要了解几个关键的C语言和网络库概念:

c语言编写web服务器
(图片来源网络,侵删)
  1. Socket (套接字):是网络编程的API,它允许程序发送和接收数据,我们可以把它想象成一个通信的端点。
  2. sys/socket.h:创建、绑定、监听和接受socket连接的头文件。
  3. netinet/in.h:定义了IP地址和端口号的数据结构(如struct sockaddr_in)。
  4. arpa/inet.h:提供了IP地址转换函数(如inet_addr)。
  5. unistd.h:提供了文件描述符(socket也是一个文件描述符)操作函数,如read, write, close
  6. select 函数:这是一个I/O多路复用函数,它允许我们监视多个文件描述符(多个socket),当其中任何一个描述符“就绪”(有数据可读)时,select函数会返回,这是实现简单并发服务器的好方法。

第二步:项目结构

我们将创建一个名为simple_webserver.c的文件。


第三步:完整代码与详细解释

下面是完整的代码,并附有详细的注释来解释每一部分的功能。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <fcntl.h>
// 定义服务器监听的端口
#define PORT 8080
// 定义缓冲区大小
#define BUFFER_SIZE 4096
// 定义最大同时连接的客户端数
#define MAX_CLIENTS 100
// 错误处理函数
void handle_error(const char *msg) {
    perror(msg);
    exit(EXIT_FAILURE);
}
// 发送HTTP响应
void send_http_response(int client_socket, int status_code, const char *status_msg, const char *content_type, const char *body) {
    char response[BUFFER_SIZE];
    int response_len;
    // 构建HTTP响应头
    response_len = snprintf(response, BUFFER_SIZE,
        "HTTP/1.1 %d %s\r\n"
        "Content-Type: %s\r\n"
        "Content-Length: %ld\r\n"
        "Connection: close\r\n" // 告诉客户端连接将在响应后关闭
        "\r\n", // 空行,表示头部结束
        status_code, status_msg, content_type, strlen(body));
    // 发送响应头
    if (send(client_socket, response, response_len, 0) < 0) {
        handle_error("send() failed for header");
    }
    // 发送响应体
    if (send(client_socket, body, strlen(body), 0) < 0) {
        handle_error("send() failed for body");
    }
}
// 处理客户端请求
void handle_client(int client_socket) {
    char request_buffer[BUFFER_SIZE];
    int bytes_received;
    // 1. 接收客户端的HTTP请求
    bytes_received = recv(client_socket, request_buffer, BUFFER_SIZE - 1, 0);
    if (bytes_received <= 0) {
        // 客户端关闭连接或出错
        close(client_socket);
        return;
    }
    request_buffer[bytes_received] = '\0'; // 确保字符串以null结尾
    printf("Received request:\n%s\n", request_buffer);
    // 2. 解析请求行,获取请求的文件路径
    char method[16], path[256], version[16];
    sscanf(request_buffer, "%15s %255s %15s", method, path, version);
    // 3. 处理路径,默认为根目录下的index.html
    if (strcmp(path, "/") == 0) {
        strcpy(path, "/index.html");
    }
    // 移除路径开头的'/',以便与本地文件系统路径对应
    char file_path[512];
    snprintf(file_path, sizeof(file_path), "./www%s", path);
    // 4. 尝试打开请求的文件
    int file_fd = open(file_path, O_RDONLY);
    if (file_fd < 0) {
        // 文件不存在,发送404 Not Found
        const char *not_found_body = "<html><body><h1>404 Not Found</h1><p>The requested resource was not found on this server.</p></body></html>";
        send_http_response(client_socket, 404, "Not Found", "text/html", not_found_body);
    } else {
        // 文件存在,读取文件内容
        struct stat file_stat;
        if (fstat(file_fd, &file_stat) < 0) {
            handle_error("fstat failed");
        }
        char *file_content = (char *)malloc(file_stat.st_size + 1);
        if (read(file_fd, file_content, file_stat.st_size) < 0) {
            handle_error("read failed");
        }
        file_content[file_stat.st_size] = '\0';
        // 5. 根据文件扩展名确定Content-Type
        const char *content_type = "text/plain"; // 默认类型
        if (strstr(path, ".html")) {
            content_type = "text/html";
        } else if (strstr(path, ".css")) {
            content_type = "text/css";
        } else if (strstr(path, ".js")) {
            content_type = "application/javascript";
        } else if (strstr(path, ".jpg") || strstr(path, ".jpeg")) {
            content_type = "image/jpeg";
        } else if (strstr(path, ".png")) {
            content_type = "image/png";
        }
        // 6. 发送200 OK响应和文件内容
        send_http_response(client_socket, 200, "OK", content_type, file_content);
        free(file_content);
        close(file_fd);
    }
    // 7. 关闭客户端连接
    close(client_socket);
}
int main() {
    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    fd_set read_fds; // 用于select的文件描述符集合
    int max_fd; // 集合中最大的文件描述符
    // 1. 创建TCP socket (AF_INET for IPv4, SOCK_STREAM for TCP)
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket < 0) {
        handle_error("socket() failed");
    }
    // 设置socket选项,允许地址重用,避免"Address already in use"错误
    int opt = 1;
    if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        handle_error("setsockopt() failed");
    }
    // 2. 绑定socket到指定端口和IP地址 (INADDR_ANY表示接受任何网络接口的连接)
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT); // htons将端口号从主机字节序转为网络字节序
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        handle_error("bind() failed");
    }
    // 3. 开始监听连接
    if (listen(server_socket, 10) < 0) { // 10是等待连接的队列长度
        handle_error("listen() failed");
    }
    printf("Server listening on port %d...\n", PORT);
    // 4. 主循环,使用select实现并发
    while (1) {
        // 清空文件描述符集合
        FD_ZERO(&read_fds);
        // 将服务器socket加入集合
        FD_SET(server_socket, &read_fds);
        max_fd = server_socket;
        // 检查所有已连接的客户端socket
        // 注意:为了简化,这里只处理了新连接和客户端数据。
        // 一个更完整的实现会维护一个活跃客户端列表。
        // 在这个简单版本中,我们只处理服务器socket上的新连接事件。
        // 客户端数据读取在handle_client中同步处理。
        // 使用select等待事件
        if (select(max_fd + 1, &read_fds, NULL, NULL, NULL) < 0) {
            handle_error("select() failed");
        }
        // 检查是否有新的连接请求
        if (FD_ISSET(server_socket, &read_fds)) {
            client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len);
            if (client_socket < 0) {
                perror("accept() failed");
                continue; // 继续等待下一个连接
            }
            printf("Accepted connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
            // 处理客户端请求(这里同步处理,会阻塞直到请求完成)
            // 在更高级的版本中,这里会创建一个线程或将socket加入一个队列,
            // 然后立即accept下一个连接。
            handle_client(client_socket);
        }
    }
    // 理论上,上面的while(1)永远不会退出,所以这里不会执行
    close(server_socket);
    return 0;
}

第四步:如何编译和运行

  1. 创建一个www目录:为了存放要提供的网页文件,我们需要在运行服务器的同一目录下创建一个www文件夹。

    mkdir www
  2. www目录中创建一些测试文件

    c语言编写web服务器
    (图片来源网络,侵删)
    • www/index.html:
      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>Welcome to My Simple Server!</title>
          <style>
              body { font-family: sans-serif; text-align: center; margin-top: 50px; }
              h1 { color: #333; }
          </style>
      </head>
      <body>
          <h1>Hello from a C Web Server!</h1>
          <p>This page is served by our custom C program.</p>
          <a href="/style.css">Check out the CSS</a>
      </body>
      </html>
    • www/style.css:
      body {
          background-color: #f0f0f0;
      }
      h1 {
          color: #0056b3;
      }
      a {
          color: #d9534f;
          font-weight: bold;
      }
  3. 编译C代码:打开终端,使用gcc编译simple_webserver.c

    gcc simple_webserver.c -o my_webserver
  4. 运行服务器

    ./my_webserver

    你应该会看到输出:

    Server listening on port 8080...
  5. 测试服务器

    c语言编写web服务器
    (图片来源网络,侵删)
    • 打开你的网页浏览器。
    • 在地址栏输入 http://localhost:8080http://127.0.0.1:8080
    • 你应该能看到 index.html 的内容。
    • 点击 "Check out the CSS" 链接,你应该能看到应用了CSS样式的页面。
    • 尝试访问一个不存在的文件,http://localhost:8080/notfound.html,你应该会看到一个 "404 Not Found" 错误页面。

第五步:进阶与改进

这个简单的服务器有很多可以改进的地方:

  1. 真正的并发:当前版本在handle_client中是同步处理请求的,如果一个请求需要很长时间(比如一个大文件下载),服务器将无法处理其他客户端,改进方法:

    • 多线程/多进程:在accept到一个新连接后,创建一个新的线程或进程来处理handle_client
    • I/O多路复用 (epoll/kqueue):在Linux上,epoll是比select更高效的I/O多路复用机制,特别适合处理大量并发连接。
  2. 更健壮的HTTP解析:当前的解析使用sscanf,非常脆弱,如果请求格式稍有不同,就会出错,应该编写一个更健壮的解析器,能够正确处理头部、Content-Length等。

  3. 支持HTTP/1.1持久连接:当前服务器在发送完响应后立即关闭连接,HTTP/1.1支持Keep-Alive,可以在一个连接上处理多个请求,减少握手开销。

  4. 支持动态内容:目前只能提供静态文件,可以通过CGI(Common Gateway Interface)或集成一个简单的解释器(如Lua)来支持动态生成内容。

  5. 安全性:没有进行任何输入验证,可能存在路径遍历等安全风险,需要对客户端请求的路径进行清理。

  6. 性能优化:使用sendfile系统调用在内核中直接将文件从一个文件描述符发送到另一个(socket),避免在用户空间和内核空间之间拷贝数据,能大大提高文件传输效率。

这个项目是学习C语言网络编程的绝佳起点,从它开始,你可以逐步添加新功能,最终构建一个功能强大的Web服务器。

-- 展开阅读全文 --
头像
dede如何显示图片?
« 上一篇 今天
dede站点描述标签怎么用?
下一篇 » 今天

相关文章

取消
微信二维码
支付宝二维码

目录[+]