HTTP 服务器项目学习笔记

一、核心知识点解析

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 的地方。

记住:好的代码不是一次写成的,而是不断完善出来的。下次再看这些代码时,说不定你又能发现可以优化的地方啦!

相关推荐
_不会dp不改名_3 小时前
HCIP笔记8--中间系统到中间系统协议1
网络·笔记·hcip
真正的醒悟3 小时前
图解网络10
网络·智能路由器
小李独爱秋3 小时前
计算机网络经典问题透视——简述TCP拥塞控制算法中的快重传和快恢复
服务器·网络·tcp/ip·计算机网络·安全
拾忆,想起3 小时前
Dubbo服务访问控制(ACL)完全指南:从IP黑白名单到自定义安全策略
前端·网络·网络协议·tcp/ip·微服务·php·dubbo
jennychary13 小时前
网工学习笔记:loopback 和route id
网络·笔记·学习
blackorbird3 小时前
美国紧急通信系统:保障国家紧急状态下通信畅通
网络
渡我白衣3 小时前
计算机组成原理(2):计算机硬件的基本组成
运维·服务器·网络·c++·人工智能·网络协议·dubbo
九思x4 小时前
技巧.虚拟机中固定IP地址
网络·网络协议·tcp/ip
YJlio12 小时前
Active Directory 工具学习笔记(10.0):AdExplorer / AdInsight / AdRestore 导读与场景地图
网络·笔记·学习