实战案例8:网络协议解析
网络协议解析,说白了就是让程序能读懂网络上的“对话”。
我刚开始接触网络编程时,总觉得协议解析很神秘。后来用C语言写了一个简单的HTTP解析器,才发现核心逻辑其实不复杂——正则表达式就是那把钥匙。
今天咱们就一起动手,用正则表达式解析HTTP请求行、头部、URL参数和Cookie。这些技能在实际项目中非常实用,比如写一个轻量级Web服务器、抓包工具,或者嵌入式设备的网络模块。
8.1 解析HTTP请求行
HTTP请求行长这样:
GET /index.html?name=alice&age=25 HTTP/1.1
它包含三个部分:方法、URI、协议版本。中间用空格分隔。
对应的正则表达式:
^([A-Z]+)\s+([^\s]+)\s+HTTP/(\d+\.\d+)$
我来拆解一下:
^([A-Z]+)— 匹配请求方法(GET、POST等)\s+— 一个或多个空格([^\s]+)— 匹配URI,不含空格\s+HTTP/(\d+\.\d+)$— 匹配协议版本
核心思路:用分组捕获三个关键字段,然后分别处理。
代码示例:
#include <stdio.h>
#include <regex.h>
int main() {
char *request_line = "GET /index.html?name=alice&age=25 HTTP/1.1";
regex_t regex;
regmatch_t pmatch[4];
char *pattern = "^([A-Z]+)\\s+([^\\s]+)\\s+HTTP/(\\d+\\.\\d+)$";
if (regcomp(®ex, pattern, REG_EXTENDED) != 0) {
printf("正则编译失败\n");
return 1;
}
if (regexec(®ex, request_line, 4, pmatch, 0) == 0) {
printf("方法: %.*s\n", pmatch[1].rm_eo - pmatch[1].rm_so,
request_line + pmatch[1].rm_so);
printf("URI: %.*s\n", pmatch[2].rm_eo - pmatch[2].rm_so,
request_line + pmatch[2].rm_so);
printf("版本: %.*s\n", pmatch[3].rm_eo - pmatch[3].rm_so,
request_line + pmatch[3].rm_so);
} else {
printf("匹配失败\n");
}
regfree(®ex);
return 0;
}
小技巧:我习惯用 regmatch_t 的 rm_so 和 rm_eo 来截取子串,比用 strncpy 更安全。
8.2 解析HTTP头部
HTTP头部是键值对,每行一个:
Host: www.example.com
Content-Type: application/json
User-Agent: Mozilla/5.0
正则表达式:
^([^:]+):\s*(.*)$
这个模式匹配“键: 值”结构。注意冒号后面可能有空格,用 \s* 处理。
实战中,我经常需要解析多行头部。可以用循环配合 regexec 逐行匹配:
char *headers[] = {
"Host: www.example.com",
"Content-Type: application/json",
"User-Agent: Mozilla/5.0",
NULL
};
regex_t regex;
regcomp(®ex, "^([^:]+):\\s*(.*)$", REG_EXTENDED);
regmatch_t pmatch[3];
for (int i = 0; headers[i] != NULL; i++) {
if (regexec(®ex, headers[i], 3, pmatch, 0) == 0) {
printf("键: %.*s\n", pmatch[1].rm_eo - pmatch[1].rm_so,
headers[i] + pmatch[1].rm_so);
printf("值: %.*s\n", pmatch[2].rm_eo - pmatch[2].rm_so,
headers[i] + pmatch[2].rm_so);
}
}
regfree(®ex);
注意:头部键是大小写不敏感的。我建议匹配后统一转小写再比较,避免“Host”和“host”不匹配的问题。
8.3 解析URL参数
URL参数在问号后面,格式是 key=value&key=value:
/search?q=hello&lang=en&page=2
解析思路分两步:
- 先用
\?分割出参数字符串 - 再用
([^&=]+)=([^&]*)匹配每个参数
完整代码:
char *uri = "/search?q=hello&lang=en&page=2";
char *params = strchr(uri, '?');
if (params == NULL) {
printf("没有参数\n");
return 1;
}
params++; // 跳过 '?'
regex_t regex;
regcomp(®ex, "([^&=]+)=([^&]*)", REG_EXTENDED);
regmatch_t pmatch[3];
char *cursor = params;
while (regexec(®ex, cursor, 3, pmatch, 0) == 0) {
printf("参数: %.*s = %.*s\n",
pmatch[1].rm_eo - pmatch[1].rm_so, cursor + pmatch[1].rm_so,
pmatch[2].rm_eo - pmatch[2].rm_so, cursor + pmatch[2].rm_so);
cursor += pmatch[0].rm_eo;
}
regfree(®ex);
避坑指南:我曾经遇到URL参数中包含 %20 这样的编码字符。正则只负责提取,解码需要单独处理。建议用 strstr 查找 % 然后转换。
8.4 解析Cookie
Cookie头部格式:
Cookie: session_id=abc123; user=alice; theme=dark
每个Cookie用分号分隔,键值对用等号连接。
正则表达式:
([^=;\s]+)=([^;]*)
这个模式匹配“键=值”,键不含等号、分号和空格,值不含分号。
代码示例:
char *cookie = "session_id=abc123; user=alice; theme=dark";
regex_t regex;
regcomp(®ex, "([^=;\\s]+)=([^;]*)", REG_EXTENDED);
regmatch_t pmatch[3];
char *cursor = cookie;
while (regexec(®ex, cursor, 3, pmatch, 0) == 0) {
printf("Cookie: %.*s = %.*s\n",
pmatch[1].rm_eo - pmatch[1].rm_so, cursor + pmatch[1].rm_so,
pmatch[2].rm_eo - pmatch[2].rm_so, cursor + pmatch[2].rm_so);
cursor += pmatch[0].rm_eo;
// 跳过分号和空格
while (*cursor == ';' || *cursor == ' ') cursor++;
}
regfree(®ex);
个人经验:Cookie值里可能包含特殊字符,比如 user=alice smith。我建议用 [^;] 匹配值,而不是 [^;=],这样能保留空格。
8.5 知识体系总览
下面这张图总结了本章的核心逻辑:
从原始报文出发,经过四个分支解析,最终得到结构化数据。每个分支都用正则表达式做核心匹配。
8.6 综合实战建议
在实际项目中,我通常会把解析逻辑封装成函数:
parse_request_line()— 解析请求行parse_headers()— 解析所有头部parse_url_params()— 解析URL参数parse_cookies()— 解析Cookie
每个函数内部用正则表达式提取关键信息,返回结构体或填充回调函数。
性能提醒:正则表达式编译(regcomp)比较耗时。我建议在程序初始化时编译好所有正则,运行时只做匹配(regexec),不要每次解析都重新编译。
嗯,到这里HTTP协议解析的四个核心场景就讲完了。你想想看,其实每个场景的正则都不复杂,关键是把模式写对,然后处理好边界情况。
我曾经在一个嵌入式项目里,用这套方法解析HTTP请求,整个解析器只用了200行C代码,跑在只有64KB RAM的芯片上,稳得很。
公众号:蓝海资料掘金营,微信deep3321