Day40 Web服务器原理与C语言实现:从HTTP协议到静态资源服务
day40 Web服务器原理与C语言实现:从HTTP协议到静态资源服务
一、万维网(WWW)工作原理精要
万维网采用 客户端-服务器架构,核心通信协议为 HTTP(超文本传输协议),底层依赖 TCP协议 保证数据可靠传输。
🔄 工作流程四步曲(完整闭环)
-
建立TCP连接
客户端(如浏览器)与服务器(如www.tsinghua.edu.cn
)建立TCP连接,为HTTP通信提供稳定通道。 -
发送HTTP请求报文
客户端构造HTTP请求报文,指定所需资源(如/index.html
、/logo.png
)。 -
服务器处理并返回HTTP响应报文
服务器解析请求,查找资源,构造含状态码、头部和内容的响应报文,返回客户端。 -
释放TCP连接
数据传输完成后关闭连接(除非使用Connection: keep-alive
保持长连接)。
🌐 示例:用户访问
http://localhost/
→ 浏览器发送GET / HTTP/1.1
→ 服务器返回首页HTML文档。
二、HTTP响应报文结构详解
HTTP响应报文由三部分组成,缺一不可(除无实体主体的特殊情况):
-
状态行(Status Line)
格式:HTTP版本 状态码 状态短语
作用:告知客户端请求处理结果。
示例:HTTP/1.1 200 OK
-
首部行(Header Fields)
键值对形式,提供元数据:Content-Type
: 响应体类型(如text/html
,image/png
)Content-Length
: 响应体字节长度Date
: 响应生成时间(GMT格式)Server
: 服务器标识Connection
: 连接控制(如close
表示短连接)
-
实体主体(Entity Body)
实际内容,如HTML代码、图片二进制数据。部分响应(如204 No Content
)无实体主体。
✅ 每个HTTP请求必有对应响应报文,状态行是第一行。
三、HTTP状态码分类与常见示例
状态码为三位数字,共分五类:
类别 | 含义 | 示例状态码 | 说明 |
---|---|---|---|
1xx | 通知信息 | 100 | 请求已接收,继续处理 |
2xx | 请求成功 | 200, 202 | 请求被成功处理 |
3xx | 重定向 | 301, 302 | 需进一步操作完成请求 |
4xx | 客户端错误 | 400, 404 | 请求语法错误或资源不存在 |
5xx | 服务器错误 | 500, 503 | 服务器内部错误 |
📌 常见状态行示例:
HTTP/1.1 202 Accepted
→ 请求已被接受,但尚未处理完成。HTTP/1.1 400 Bad Request
→ 客户端请求语法错误。HTTP/1.1 404 Not Found
→ 请求资源不存在。
四、简易Web服务器完整实现(C语言 + 逐行注释)
以下代码实现了一个支持静态文件服务和简单登录验证的单线程HTTP服务器。
#include <asm-generic/socket.h> // 提供特定架构的套接字相关定义(补充标准socket.h功能)
#include <errno.h> // 错误码定义,用于错误信息获取与处理
#include <fcntl.h> // 文件控制函数定义,如open()、文件状态标志等
#include <netinet/in.h> // 定义IPv4网络地址结构(sockaddr_in)及相关常量
#include <netinet/ip.h> // IP协议相关定义(如IP头结构等)
#include <stdio.h> // 标准输入输出函数,如printf()、perror()
#include <stdlib.h> // 标准库函数,如内存分配、程序退出等
#include <string.h> // 字符串处理函数,如strtok()、strcmp()、strchr()
#include <sys/socket.h> // 核心套接字函数,如socket()、bind()、listen()、accept()
#include <sys/stat.h> // 文件状态函数,如stat()(获取文件大小、类型等信息)
#include <sys/types.h> // 系统基本数据类型定义(如pid_t、size_t等)
#include <time.h> // 时间相关函数与结构(本代码中未动态使用,仅固定日期)
#include <unistd.h> // POSIX系统调用,如read()、write()、close()、sleep()// 定义套接字地址结构体指针的别名SA,简化代码中(struct sockaddr*)的重复书写
typedef struct sockaddr*(SA);// 枚举类型:定义支持的文件类型,用于区分不同资源类型以设置正确的HTTP Content-Type
typedef enum
{FILE_HTML, // HTML文本文件类型FILE_JPG, // JPG图片文件类型FILE_PNG, // PNG图片文件类型FILE_GIF // GIF图片文件类型
} FILE_TYPE;// 函数功能:发送HTTP响应头(不包含响应体)
// 参数说明:
// conn:与客户端通信的套接字描述符
// filename:要发送的目标文件名(用于通过stat()获取文件大小)
// type:文件类型(枚举值),用于设置Content-Type头
// 返回值:0表示响应头发送成功,1表示获取文件状态失败
int send_head(int conn, char* filename, FILE_TYPE type)
{struct stat st; // stat结构体:存储文件的状态信息(如大小st_size、修改时间等)// 调用stat()获取文件状态,若失败返回-1int ret = stat(filename, &st);if (-1 == ret){// 打印错误信息:包含错误原因(strerror(errno))和目标文件名fprintf(stderr, " send_head stat [%s] , err:%s\n", filename, strerror(errno));return 1;}char* http_cmd[6] = {0}; // 指针数组:存储HTTP响应头的6个部分(初始化全为NULL)char buf[512] = {0}; // 缓冲区:用于动态构建Content-Length头(文件大小可变)// 1. HTTP响应行:HTTP/1.1版本,200 OK表示请求成功http_cmd[0] = "HTTP/1.1 200 OK\r\n";// 2. Server头:标识当前服务器名称(自定义为zhagnsanServ)http_cmd[1] = "server: zhagnsanServ\r\n";// 根据文件类型设置3. Content-Type头(告知客户端响应体的MIME类型)switch (type){case FILE_HTML:http_cmd[2] = "content-type: text/html; charset=UTF-8\r\n"; // HTML文本,UTF-8编码break;case FILE_PNG:http_cmd[2] = "Content-Type: image/png\r\n"; // PNG图片break;case FILE_JPG:http_cmd[2] = "Content-Type: image/jpeg\r\n"; // JPG图片(MIME类型为image/jpeg)break;case FILE_GIF:http_cmd[2] = "Content-Type: image/gif\r\n"; // GIF图片break;}// 4. Content-Length头:动态构建,告知客户端响应体的字节大小(从stat结构体获取st_size)http_cmd[3] = buf;sprintf(http_cmd[3], "content-length: %lu\r\n", st.st_size);// 5. Date头:固定日期(实际服务器应动态生成当前GMT时间)http_cmd[4] = "date: Wed, 10 Sep 2025 03:12:50 GMT\r\n";// 6. Connection头:closed表示本次请求后关闭TCP连接(短连接),末尾\r\n\r\n标识响应头结束http_cmd[5] = "Connection: closed\r\n\r\n";// 循环发送HTTP响应头的6个部分int i = 0;for (i = 0; i < 6; i++){send(conn, http_cmd[i], strlen(http_cmd[i]), 0); // 发送数据,长度为字符串实际长度}return 0;
}// 函数功能:发送完整的HTTP响应(先调用send_head发送响应头,再发送文件内容作为响应体)
// 参数说明:同send_head
// 返回值:0表示成功,1表示文件打开失败
int send_file(int conn, char* filename, FILE_TYPE type)
{// 第一步:发送HTTP响应头send_head(conn, filename, type);// 第二步:以只读方式打开文件(O_RDONLY为只读标志)int fd = open(filename, O_RDONLY);if (-1 == fd) // 打开失败返回-1{perror("send_file open"); // 打印文件打开失败的错误信息return 1;}// 第三步:循环读取文件内容并发送(1024字节为一次读取单位,避免内存浪费)while (1){char buf[1024] = {0}; // 缓冲区:存储每次读取的文件数据// 从文件描述符fd读取数据到buf,最多读取sizeof(buf)=1024字节int ret = read(fd, buf, sizeof(buf));if (ret <= 0) // ret=0表示文件读取完毕,ret=-1表示读取错误,均退出循环{break;}// 将读取到的ret字节数据发送给客户端(注意:发送长度为实际读取的ret,而非1024)send(conn, buf, ret, 0);}// 第四步:关闭文件描述符(避免资源泄漏)close(fd);return 0;
}// 主函数:HTTP服务器的核心逻辑(创建监听套接字、接受连接、处理请求、发送响应)
int main(int argc, char** argv)
{// 第一步:创建监听套接字(用于监听客户端的连接请求)// 参数说明:AF_INET=IPv4协议族,SOCK_STREAM=TCP流式套接字,0=默认协议(TCP)int listfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == listfd) // 创建失败返回-1{perror("socket"); // 打印套接字创建失败的错误信息return 1;}// 第二步:设置套接字选项(解决"地址已在使用"问题,便于服务器重启测试)int on = 1; // 选项值:1表示启用该选项// SO_REUSEADDR:允许重用处于TIME_WAIT状态的地址(避免端口占用问题)setsockopt(listfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));// SO_REUSEPORT:允许多个进程/线程绑定到同一端口(测试场景用,正式环境需谨慎关闭)setsockopt(listfd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on));// 第三步:初始化服务器地址结构(sockaddr_in)struct sockaddr_in ser, cli; // ser=服务器地址,cli=客户端地址bzero(&ser, sizeof(ser)); // 将服务器地址结构清零(避免随机垃圾数据)bzero(&cli, sizeof(cli)); // 将客户端地址结构清零ser.sin_family = AF_INET; // 协议族:IPv4ser.sin_port = htons(80); // 服务器端口:80(HTTP默认端口,需root权限运行)// htons():将主机字节序转换为网络字节序(大端序)ser.sin_addr.s_addr = INADDR_ANY; // 绑定所有可用的网络接口(允许外部客户端连接)// 第四步:将监听套接字绑定到服务器地址(listfd <-> ser)int ret = bind(listfd, (SA)&ser, sizeof(ser));if (-1 == ret) // 绑定失败返回-1(如端口被占用、无root权限){perror("bind"); // 打印绑定失败的错误信息return 1;}// 第五步:将监听套接字设置为监听状态(开始接受客户端连接请求)// 参数说明:第二个参数3=等待连接队列的最大长度(超过则拒绝新连接)listen(listfd, 3);socklen_t len = sizeof(cli); // 存储客户端地址结构的长度(用于accept())// 第六步:服务器主循环(无限循环,持续接受并处理客户端请求)while (1){// 子步骤1:接受客户端的连接请求(阻塞等待,直到有新连接)// 参数说明:cli存储客户端地址,len传入地址长度(值-结果参数)// 返回值conn:与该客户端通信的套接字描述符(每个客户端对应一个conn)int conn = accept(listfd, (SA)&cli, &len);if (-1 == conn) // 接受连接失败(如系统资源不足){perror("accept"); // 打印接受连接失败的错误信息close(conn); // 关闭无效的通信套接字continue; // 继续等待下一个连接}// 子步骤2:接收客户端发送的HTTP请求数据(存储到buf缓冲区)char buf[1024] = {0}; // 缓冲区:存储HTTP请求(默认1024字节足够处理简单请求)// 从通信套接字conn接收数据到buf,最多接收sizeof(buf)=1024字节int rec_ret = recv(conn, buf, sizeof(buf), 0);if (rec_ret <= 0) // rec_ret=0表示客户端关闭连接,rec_ret=-1表示接收错误{close(conn); // 关闭通信套接字continue; // 处理下一个连接}printf("%s\n", buf); // 打印接收到的HTTP请求(调试用,查看请求内容)// 子步骤3:解析HTTP请求行(格式:Method URL Version\r\n,如GET / HTTP/1.1)char* method = NULL; // 存储HTTP请求方法(如GET、POST)char* url = NULL; // 存储请求的URL(如/、/login、/1.png)char* ver = NULL; // 存储HTTP版本(如HTTP/1.1)// strtok():按空格分割字符串,第一次分割取method,后续NULL表示继续分割原字符串method = strtok(buf, " ");url = strtok(NULL, " ");ver = strtok(NULL, "\r"); // 按\r分割,去掉请求行末尾的\r\n// 子步骤4:根据解析出的URL,处理不同的请求并发送响应(路由逻辑)// 场景1:请求根路径(URL为/),返回首页03.htmlif (0 == strcmp(url, "/")){send_file(conn, "./03.html", FILE_HTML);}// 场景2:请求登录路径(URL以/login开头,如/login?name=zhangsan&pw=123)else if (0 == strncmp(url, "/login", 6)) // 比较前6个字符是否为"/login"{char* name = NULL; // 存储解析出的用户名char* pw = NULL; // 存储解析出的密码char* end = NULL; // 临时指针:用于分割字符串// 解析用户名:URL格式为/login?name=xxx&pw=xxx,先找到第一个'='name = strchr(url, '='); // strchr():查找'='在URL中的第一次出现位置name += 1; // 指针后移1位,指向用户名的起始字符(跳过'=')end = strchr(name, '&'); // 查找'&'(用户名的结束位置)*end = '\0'; // 将'&'替换为字符串结束符,截断得到用户名// 解析密码:从'&'后继续查找'='pw = strchr(end + 1, '='); // 跳过'&',查找密码字段的'='pw += 1; // 指针后移1位,指向密码的起始字符// 验证用户名和密码(硬编码为zhangsan/123)if (0 == strcmp(name, "zhangsan") && 0 == strcmp(pw, "123")){send_file(conn, "./01.html", FILE_HTML); // 验证成功,返回01.html}else{send_file(conn, "./04.html", FILE_HTML); // 验证失败,返回04.html}}// 场景3:请求PNG图片(URL以.png结尾,如/1.png)else if (strlen(url) > 4 && // 确保URL长度大于4(".png"占4个字符)0 == strcmp(&url[strlen(url) - 4], ".png")) // 比较末尾4个字符是否为.png{// url+1:去掉URL中的'/',得到实际文件名(如URL为/1.png,则url+1为1.png)send_file(conn, url + 1, FILE_PNG);}// 场景4:请求GIF图片(URL以.gif结尾,如/2.gif)else if (strlen(url) > 4 && 0 == strcmp(&url[strlen(url) - 4], ".gif")){send_file(conn, url + 1, FILE_GIF);}// 场景5:请求JPG图片(URL以.jpg结尾,如/3.jpg)else if (strlen(url) > 4 && 0 == strcmp(&url[strlen(url) - 4], ".jpg")){send_file(conn, url + 1, FILE_JPG);}// 子步骤5:关闭与当前客户端的通信套接字(短连接模式)close(conn);}// 理论上不会执行到此处(主循环为无限循环),关闭监听套接字释放资源close(listfd);return 0;
}
🧪 代码运行理想结果示例:
假设当前目录下存在以下文件:
03.html
→ 首页01.html
→ 登录成功页04.html
→ 登录失败页logo.png
→ 图片资源
1. 访问首页 http://localhost/
浏览器发送:
GET / HTTP/1.1
Host: localhost
...
服务器响应:
HTTP/1.1 200 OK
server: zhagnsanServ
content-type: text/html; charset=UTF-8
content-length: 1234
date: Wed, 10 Sep 2025 03:12:50 GMT
Connection: closed<!DOCTYPE html>...(03.html内容)
→ 浏览器渲染首页。
2. 登录成功 http://localhost/login?name=zhangsan&pw=123
服务器返回 01.html
内容。
3. 登录失败 http://localhost/login?name=wrong&pw=wrong
服务器返回 04.html
内容。
4. 请求图片 http://localhost/logo.png
服务器发送PNG文件数据,浏览器显示图片。
5. 请求不存在资源(如favicon.ico)
服务器无对应处理逻辑 → 无响应或连接关闭 → 浏览器显示资源加载失败。
⚠️ 注意:当前代码未处理 favicon.ico 请求,需补充默认404响应或忽略。
五、信息查询系统项目规划
📌 功能模块设计
-
用户管理模块
- 登录功能(含表单验证)
- 通过超链接跳转至注册页面
- 用户身份验证与会话管理
-
信息检索模块
- 支持关键词搜索
- 支持按分类筛选信息
-
信息展示模块
- 类别列表展示(如新闻、公告、产品等)
- 详情页展示(点击后显示完整内容)
📋 项目其他要求
- ✅ 功能完整:各模块需可运行、交互正常。
- 📄 文档齐全:编写设计文档、使用说明、API文档等。
- 👥 多客户端支持:服务器需支持并发访问(当前代码为单线程,后续可升级)。
- 🔧 Makefile支持:提供编译脚本,一键构建项目。
- ⏱️ 开发周期:预计1.5天内完成。
✅ 总结知识点
- 万维网工作原理(客户端-服务器、TCP、HTTP)
- HTTP请求/响应报文结构
- HTTP状态码分类与含义
- C语言Socket网络编程
- HTTP服务器实现(解析请求、发送响应、文件传输)
- 项目规划与模块划分
本日学习内容完整涵盖网络基础协议、服务器实现与项目架构设计,为后续Web开发打下坚实基础。