您好!作为您的专属计算机科学与编程教授,我们将采用一种更严谨、更符合工程实践的增量式、测试驱动的方法来完成HTTP_PRD.md中的任务。本指南将作为我们的开发蓝图,确保每一步都坚实可靠。
在编写第一行代码前,我们首先明确整个项目的技术选型与顶层设计。
- 编程语言: C (遵循 C11 标准)
- 编译器: GCC (通过 MSYS2 UCRT64 环境提供)
- 构建系统:
make(使用Makefile管理编译流程) - 核心系统API:
- 网络: Winsock2 API (在Windows环境下,通过包含
<winsock2.h>并链接ws2_32.lib使用) - 并发: POSIX Threads (pthreads)。MinGW-w64环境原生提供了
<pthread.h>,这是实现跨平台线程代码的关键。 - 进程通信: POSIX Process API (
fork,pipe,exec系列,waitpid)。同样由MSYS2环境提供,是实现CGI的核心。
- 网络: Winsock2 API (在Windows环境下,通过包含
我们将构建一个基于 生产者-消费者模型 的并发服务器:
- 主线程 (生产者): 唯一的职责是监听端口,接收新的客户端连接 (
accept)。一旦接收到新连接(一个client_socket),它便将这个“任务”放入一个全局的、线程安全的任务队列中。 - 工作线程池 (消费者): 我们将预先创建一组(
MAX_THREADS=4)固定的工作线程。这些线程循环地从任务队列中取出“任务”(client_socket),然后负责处理该连接的所有后续工作(解析请求、执行CGI、发送响应、关闭连接)。 - 任务队列 (Task Queue): 一个有界缓冲区(Bounded Buffer),用于在主线程和工作线程之间解耦。它必须是线程安全的,我们将使用互斥锁 (Mutex) 和 条件变量 (Condition Variables) 来保护对队列的并发访问。
我们将项目分解为以下几个可独立验证的阶段。每个阶段都会修改我们的 httpd.c 和 Makefile。
- 目标: 搭建项目骨架,确保编译环境和
Makefile工作正常。 - 实现思路:
- 创建
E:/Project/HTTP/目录结构 (cgi-bin,httpd.c,Makefile)。 - 在
httpd.c中编写一个最简单的main函数,仅打印一句 "Hello, Server!"。 - 编写一个基础的
Makefile,能够将httpd.c编译成httpd.exe。
- 创建
- 测试验证:
- 在MSYS2 UCRT64终端中,进入项目目录,运行
make。 - 预期结果: 成功生成
httpd.exe,无编译错误。 - 运行
./httpd.exe。 - 预期结果: 终端打印出 "Hello, Server!"。
- 在MSYS2 UCRT64终端中,进入项目目录,运行
- 目标: 实现一个能接受TCP连接,然后立即关闭它的服务器。这是网络编程的“Hello, World”。
- 实现思路:
- 在
main函数中,初始化Winsock。 - 依次调用
socket(),bind(),listen()来设置服务器监听。 - 进入一个无限循环,调用
accept()阻塞等待客户端连接。 - 一旦
accept()返回一个新的client_socket,立即打印一条消息,然后调用closesocket()关闭它。
- 在
- 测试验证:
- 编译并运行
./httpd.exe。 - 打开另一个MSYS2终端,使用
telnet或nc(netcat) 连接服务器:telnet localhost 8080。 - 预期结果:
telnet命令会立刻返回或显示连接已关闭。服务器终端会打印出接收到新连接的日志。
- 编译并运行
- 目标: 对收到的任何请求,都回复一个固定的HTTP响应。
- 实现思路:
- 在
accept()之后,closesocket()之前,增加recv()来读取客户端发来的数据(暂不解析)。 - 使用
send()发送一个硬编码的、完整的HTTP响应字符串,例如:"HTTP/1.1 200 OK\r\nContent-Length: 18\r\n\r\nUnder construction"。
- 在
- 测试验证:
- 编译并运行
./httpd.exe。 - 打开浏览器,访问
http://localhost:8080。 - 预期结果: 浏览器页面显示 "Under construction"。
- 使用
curl -v http://localhost:8080。 - 预期结果:
curl的输出中能看到> GET / HTTP/1.1和< HTTP/1.1 200 OK等信息。
- 编译并运行
- 目标: 解析HTTP请求行,提取方法和路径,并实现符合规范的日志记录。
- 实现思路:
- 在
recv()之后,对接收到的缓冲区数据进行解析,使用sscanf或strtok提取出请求方法和路径。 - 实现
log_request(method, path, status_code)函数。该函数必须使用互斥锁 (mutex) 保证多线程环境下的打印是原子操作,并调用fflush(stdout)。 - 在发送响应后,调用
log_request。
- 在
- 测试验证:
- 编译并运行
./httpd.exe。 - 浏览器访问
http://localhost:8080/test/path。 - 预期结果: 服务器终端打印出格式正确的日志:
[YYYY-MM-DD HH:MM:SS] [GET] [/test/path] [200]。
- 编译并运行
- 目标: 将服务器改造为生产者-消费者模型,实现并发处理请求。
- 实现思路:
- 定义任务队列及其同步原语(互斥锁、条件变量)。
- 实现
push_task和pop_task线程安全函数。 - 创建
handle_request函数,将阶段2、3中的请求处理逻辑(recv, 解析,send,log,close)移入其中。 - 创建
worker_thread函数,其内部是一个无限循环,调用pop_task获取任务,然后调用handle_request。 - 修改
main函数:在listen后,创建并启动4个worker_thread。main的主循环简化为只调用accept并将得到的client_socket推入任务队列。
- 测试验证:
- 编译并运行
./httpd.exe。 - 打开多个(例如5个)终端,几乎同时运行
curl http://localhost:8080/path/N(N从1到5)。 - 预期结果: 所有
curl命令都能很快得到响应。服务器终端的日志可能是交错打印的,但每一行日志本身是完整的。
- 编译并运行
- 目标: 实现对
/cgi-bin/路径的请求,能够正确执行CGI脚本并返回其输出。 - 实现思路:
- 创建
cgi-bin/echo.sh测试脚本。 - 在
handle_request中,增加路径判断逻辑:如果路径以/cgi-bin/开头,则调用execute_cgi函数,否则走原有逻辑。 - 实现
execute_cgi(client_socket, path, method, query_string)函数。 - 在
execute_cgi中,使用pipe()创建管道,fork()创建子进程。 - 子进程: 使用
setenv设置环境变量,dup2重定向stdout到管道写端,execl执行脚本。 - 父进程: 关闭管道写端,循环
read管道读端,将读到的数据send给client_socket,最后waitpid回收子进程。
- 创建
- 测试验证:
- 编译并运行
./httpd.exe。 - 浏览器访问
http://localhost:8080/cgi-bin/echo.sh?user=test。 - 预期结果: 浏览器显示
echo.sh脚本的输出,其中应包含Request Method: GET和Query String: user=test。
- 编译并运行
- 目标: 为CGI流程增加健壮的错误处理,能正确返回404和500状态码。
- 实现思路:
- 在
execute_cgi中,fork之前,使用access(script_path, X_OK)检查脚本是否存在且可执行。如果失败,直接发送404响应并返回。 - 在父进程中,
waitpid之后,检查子进程的退出状态。如果WEXITSTATUS非0,说明脚本执行出错,记录500状态码到日志。 - (高级) 如果CGI脚本在输出任何内容前就失败了,我们可以尝试发送一个500错误页面。如果已经发送了部分内容,就只能中断连接了。在日志中正确记录状态是本阶段的核心要求。
- 在
- 测试验证:
- 编译并运行
./httpd.exe。 - 访问
http://localhost:8080/cgi-bin/non_existent_script.sh。 - 预期结果: 浏览器收到404 Not Found响应。
- 创建一个会失败的脚本
fail.sh(内容为exit 1),访问http://localhost:8080/cgi-bin/fail.sh。 - 预期结果: 服务器日志记录状态码500。
- 编译并运行
我们现在有了一份清晰的、循序渐进的作战地图。我将严格按照这个流程,在您的指导下,逐一完成每个阶段的代码编写与验证.