一、核心知识点解析
1. TCP socket 编程基础
【是什么】
就像现实中打电话的流程:先买个电话机(创建 socket),装上电话线(绑定 bind),打开铃声等待来电(监听 listen),有人打电话就接起来(accept),然后开始聊天(recv/send),聊完挂电话(close)。
【为什么】
网络通信的基础中的基础!没有 socket,程序之间就像隔了一堵墙,啥话也说不了。咱们的 HTTP 服务器本质上就是个懂 HTTP 语言的 socket 聊天高手。
【怎么用】
cpp
运行
// 1. 买电话机(创建套接字)
int sock = socket(AF_INET, SOCK_STREAM, 0);
// 2. 允许电话号码重复使用(端口复用)
int reuse = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
// 3. 绑定电话号码(绑定IP和端口)
struct sockaddr_in local;
local.sin_family = AF_INET; // 用IPv4协议
local.sin_port = htons(_port); // 端口号(注意转网络字节序)
local.sin_addr.s_addr = INADDR_ANY; // 任何IP都能连
bind(sock, (struct sockaddr*)&local, sizeof(local));
// 4. 打开铃声(开始监听)
listen(sock, 128); // 最多同时128人排队
// 5. 接电话(接受连接)
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int client_sock = accept(sock, (struct sockaddr*)&peer, &len);
【坑在哪】
- 端口号别用 0-1023,这些是 VIP 端口,普通用户用不了(除非 sudo),咱们用 8080 就挺好
- 绑定之前一定要设置端口复用,不然程序崩了再启动会提示 "地址已在使用"
- accept 函数会阻塞等待客户端连接,所以要用多线程处理,不然一次只能服务一个客户
2. 多线程并发处理
【是什么】
就像餐厅里一个服务员(主线程)负责迎接客人,来了客人就喊一个厨师(子线程)去服务,自己继续迎接下一个,这样效率才高嘛!
【为什么】
如果单线程处理,一个客人点完菜服务员才能去招呼下一个,后面的客人早等急了。多线程能让服务器同时服务多个客户端。
【怎么用】
cpp
运行
// 线程处理函数
void* msg_request(void* arg){
int client_sock = *(int*)arg; // 拿到客户端socket
delete (int*)arg; // 记得释放内存,不然会内存泄漏!
handler_msg(client_sock); // 处理请求
close(client_sock); // 服务完关闭连接
return NULL;
}
// 主线程中接受连接后创建线程
pthread_t tid;
// 这里用new传参,因为直接传局部变量地址可能会被覆盖
if(pthread_create(&tid, NULL, msg_request, new int(client_sock)) != 0){
perror("创建线程失败");
}
pthread_detach(tid); // 线程分离,自动回收资源
【坑在哪】
- 线程函数的参数传递是个坑!不能直接传局部变量地址,因为线程启动时可能变量已经变了
- 一定要记得释放动态分配的内存,不然运行久了内存会被吃完
- 线程分离 (pthread_detach) 很重要,不然线程结束后资源不释放,会变成 "僵尸线程"
3. HTTP 请求解析
【是什么】
客户端(浏览器)发过来的 "暗号",服务器得看懂这些暗号才能正确回应。就像顾客点菜,服务器得知道点的是宫保鸡丁还是鱼香肉丝。
【为什么】
HTTP 协议规定了请求的格式,服务器只有按规矩解析,才能知道客户端想要什么(是要个网页?还是提交数据?)。
【怎么用】
cpp
运行
// 获取请求行(第一行最重要)
char buf[SIZE] = "";
int count = get_line(sock, buf); // 读取一行数据
// 解析请求方法(GET/POST)
char method[30] = "";
int i = 0, k = 0;
while(i < count && !isspace(buf[i])){
method[k++] = buf[i++]; // 把方法名抠出来
}
method[k] = '\0'; // 字符串结束符不能少
// 跳过空格
while(isspace(buf[i]) && i < SIZE) i++;
// 解析请求路径(类似/index.html这种)
char path[256] = "";
k = 0;
while(i < count && !isspace(buf[i]) && buf[i] != '?'){
path[k++] = buf[i++]; // 把路径抠出来
}
path[k] = '\0';
【坑在哪】
- 解析字符串时一定要注意数组越界!别把缓冲区撑爆了
- HTTP 请求里的换行是 \r\n,不是单纯的 \n,处理不好会读错行
- GET 和 POST 的参数位置不一样:GET 在 URL 里,POST 在请求体里,别搞混了
4. HTTP 响应构建
【是什么】
服务器看完客户端的请求后,给客户端的 "回复"。就像餐厅做好菜,得装在盘子里(响应头)再端给顾客。
【为什么】
客户端也只认 HTTP 协议格式的回复,不然浏览器也不知道怎么显示内容。比如得告诉浏览器 "这是个 HTML 文件"、"文件大小是多少"。
【怎么用】
cpp
运行
// 构建响应头
const char* msg = "HTTP/1.1 200 OK\r\n"; // 状态行:协议版本 状态码 状态描述
send(sock, msg, strlen(msg), 0);
// 发送响应头和响应体的分隔符(空行)
send(sock, "\r\n", 2, 0);
// 发送响应体(比如HTML文件内容)
int fd = open(path, O_RDONLY); // 打开文件
struct stat st;
stat(path, &st); // 获取文件信息(大小等)
sendfile(sock, fd, NULL, st.st_size); // 高效发送文件
close(fd);
【坑在哪】
- 响应头后面必须跟一个空行(\r\n),不然浏览器会解析错误
- 发送大文件时别用 recv/send 循环读写,用 sendfile 效率高多了
- 别忘了处理各种错误情况(比如文件不存在),这时候要返回 404 等错误码
5. GET 和 POST 请求处理
【是什么】
HTTP 协议里两种最常用的 "说话方式":GET 比较直接,啥话都放 URL 里;POST 比较含蓄,重要的话藏在请求体里。
【为什么】
- GET 适合获取资源(比如看网页),参数少且不敏感
- POST 适合提交数据(比如登录、上传),参数多或敏感(密码等)
【怎么用】
cpp
运行
// 处理GET请求
if (strcasecmp(method, "GET") == 0) {
clear_header(sock); // 跳过请求头
// GET参数在query_string里,直接处理即可
}
// 处理POST请求
else if (strcasecmp(method, "POST") == 0) {
// 先从请求头里找Content-Length,知道有多少数据
int content_len = -1;
do {
ret = get_line(sock, line);
if (strncasecmp(line, "content-length", 14) == 0) {
content_len = atoi(line + 16); // 提取长度
}
} while (ret != 1 && strcmp(line, "\n") != 0);
// 再读取content_len长度的数据(POST参数)
char req_buf[SIZE] = "";
recv(sock, req_buf, content_len, 0);
}
【坑在哪】
- GET 参数有长度限制(因为 URL 长度有限),别用 GET 传大量数据
- POST 参数需要先读 Content-Length,不然不知道要读多少数据
- 处理完请求后记得清理资源,特别是文件描述符和动态内存
6. 静态资源处理
【是什么】
服务器上那些现成的文件,比如 HTML、CSS、图片等,不需要加工就能直接发给客户端的 "现成菜"。
【为什么】
网站里大部分内容都是静态的(比如首页、图片),直接读取文件发送是最基本的功能。
【怎么用】
cpp
运行
// 处理静态文件请求
char full_path[256] = "../wwwroot"; // 网站根目录
strcat(full_path, path); // 拼接成完整路径
// 如果访问的是根目录,默认返回index.html
if(strcmp(path, "/") == 0){
strcat(full_path, "index.html");
}
// 检查文件是否存在
struct stat st;
if(stat(full_path, &st) == -1){
echo_error(sock, 404); // 文件不存在,返回404
return;
}
// 发送文件内容
echo_www(sock, full_path, st.st_size);
【坑在哪】
- 路径拼接要小心!别让用户通过输入
../../etc/passwd这种路径访问到系统文件(路径遍历攻击) - 别忘了处理默认页面(访问 / 时返回 index.html)
- 不同类型的文件(html、css、图片)应该返回不同的 Content-Type,现在的代码简化了这部分
7. 业务逻辑处理
【是什么】
服务器除了送现成的文件,还能做些 "加工",比如计算求和、验证登录等动态处理。就像餐厅不光能上现成的凉菜,还能根据客人要求做热菜。
【为什么】
光有静态页面不够啊!用户登录、表单提交这些都需要服务器处理数据,这才是动态网站的核心。
【怎么用】
cpp
运行
// 业务逻辑处理入口
int parse_and_process(int sock, const char *querry_string, char *req_buf)
{
// 判断是求和请求
if (strstr(req_buf, "data1=") && strstr(req_buf, "data2="))
{
return handle_add(sock, req_buf);
}
// 判断是登录请求
else if(strstr(req_buf, "username=") && strstr(req_buf, "password="))
{
return handle_login(sock, req_buf);
}
return 0;
}
// 求和处理
static int handle_add(int sock, char *req_buf)
{
int num1, num2;
sscanf(req_buf, "data1=%d&data2=%d", &num1, &num2); // 解析参数
char reply[1024] = "";
sprintf(reply, "%d", num1 + num2); // 计算结果
send(sock, reply, strlen(reply), 0); // 返回结果
return 0;
}
【坑在哪】
- 参数解析要注意格式!sscanf 的格式字符串一定要和实际参数格式匹配
- 登录逻辑别这么简单!真实项目中密码要加密存储,不能明文比较
- 业务逻辑和网络处理要分开,就像现在的 custom_handle.cpp,这样代码更清晰
二、代码完善与注释
1. 修复内存泄漏问题(main.cpp)
cpp
运行
// 线程处理函数
void* msg_request(void* arg){
// 解析传进来的客户端套接字
int sock = *(int*)arg;
delete (int*)arg; // 重要:释放动态分配的内存,防止泄漏
// 处理请求
handler_msg(sock);
close(sock); // 关闭客户端连接
return NULL;
}
2. 完善 HTTP 请求解析(http.cpp 补充完整 handler_msg 函数)
cpp
运行
int handler_msg(int sock)
{
// ... 前面代码省略 ...
// 判断是否为POST请求
int need_handle = 0;
if (strcasecmp(method, "POST") == 0) {
need_handle = 1;
}
// 解析请求路径和查询字符串
char path[256] = ""; // 存储请求路径
char query_string[256] = ""; // 存储查询字符串(GET参数)
k = 0;
// 提取路径部分
while (i < count && !isspace(buf[i]) && buf[i] != '?') {
path[k++] = buf[i++];
}
path[k] = '\0';
// 如果是GET请求且有查询参数,提取查询字符串
if (strcasecmp(method, "GET") == 0 && buf[i] == '?') {
i++; // 跳过问号
k = 0;
while (i < count && !isspace(buf[i])) {
query_string[k++] = buf[i++];
}
query_string[k] = '\0';
need_handle = 1; // GET带参数也需要处理
}
// 处理静态文件请求
char full_path[256] = "../wwwroot";
if (strcmp(path, "/") == 0) {
// 访问根目录,默认返回index.html
strcat(full_path, "/index.html");
} else {
// 拼接完整路径
strcat(full_path, path);
}
// 检查文件是否存在
struct stat st;
if (stat(full_path, &st) == 0 && S_ISREG(st.st_mode)) {
// 文件存在,发送文件
echo_www(sock, full_path, st.st_size);
} else {
// 文件不存在,检查是否需要业务处理
if (need_handle) {
handle_request(sock, method, path, query_string);
} else {
// 既不是静态文件也不需要处理,返回404
echo_error(sock, 404);
}
}
return 0;
}
3. 完善登录逻辑(custom_handle.cpp)
cpp
运行
int handle_login(int sock, char* req_buf){
char reply_buf[HTML_SIZE] = "";
// 解析账号密码(更健壮的解析方式)
char* uname = strstr(req_buf, "username=");
char* passwd = strstr(req_buf, "password=");
if(!uname || !passwd){
// 参数不完整
sprintf(reply_buf, "<script>alert('参数错误'); history.back();</script>");
send(sock, reply_buf, strlen(reply_buf), 0);
return -1;
}
uname += strlen("username=");
passwd += strlen("password=");
// 截断多余部分(处理可能的其他参数)
char* end = strchr(uname, '&');
if(end) *end = '\0';
// 简单验证(实际项目中应查询数据库并验证加密后的密码)
if(strcmp(uname, passwd) == 0){
// 登录成功,跳转首页
sprintf(reply_buf,
"<script>localStorage.setItem('usr_user_name','%s'); window.location.href='/index.html';</script>",
uname);
} else {
// 登录失败,返回登录页
sprintf(reply_buf,
"<script>alert('账号或密码错误'); window.location.href='/login.html';</script>");
}
send(sock, reply_buf, strlen(reply_buf), 0);
return 0;
}
三、快速复习查找表
| 知识点 | 代码位置 | 关键函数 / 概念 | 常见问题 |
|---|---|---|---|
| TCP 服务器初始化 | http.cpp | init_server() | 端口复用、绑定失败 |
| 多线程处理 | main.cpp | pthread_create() | 内存泄漏、线程同步 |
| HTTP 请求读取 | http.cpp | get_line() | 换行处理 (\r\n)、缓冲区溢出 |
| 请求方法解析 | http.cpp | handler_msg() | GET/POST 区分、参数位置 |
| 静态文件处理 | http.cpp | echo_www() | 路径拼接、404 处理 |
| 404 错误处理 | http.cpp | show_404() | 错误页面路径、响应格式 |
| GET 参数处理 | http.cpp | handle_request() | URL 解析、特殊字符 |
| POST 参数处理 | http.cpp | handle_request() | Content-Length、请求体读取 |
| 业务逻辑处理 | custom_handle.cpp | parse_and_process() | 参数解析、返回格式 |
| 登录功能 | custom_handle.cpp | handle_login() | 数据提取、页面跳转 |
| 求和功能 | custom_handle.cpp | handle_add() | 数据类型、计算逻辑 |
四、总结
这个 HTTP 服务器虽然简单,但包含了网络编程的核心知识点:从 socket 创建到多线程并发,从 HTTP 协议解析到动态业务处理。就像搭积木一样,每个部分都有其作用,组合起来就形成了一个能干活的服务器。
复习时可以对照快速查找表,哪里忘了就去看对应的代码和说明。重点关注网络编程中的资源释放(内存、文件描述符)和边界情况处理(比如文件不存在、参数错误),这些都是实际开发中最容易出 bug 的地方。
记住:好的代码不是一次写成的,而是不断完善出来的。下次再看这些代码时,说不定你又能发现可以优化的地方啦!