Linux 应用层协议 HTTP 入门:从 URL、报文格式到手写最小服务器
摘要:HTTP 是浏览器和服务器之间最常见的应用层协议。理解 HTTP,不能只记
GET、POST、404这些名词,更要看懂请求和响应在网络中到底长什么样。本文从 URL 编码、HTTP 请求/响应格式、常见方法、状态码、Header 入手,最后用 C 写一个最小 HTTP 服务器,帮助你把协议格式和 Socket 编程串起来。
前言
前面理解了 TCP Socket 之后,我们已经知道:TCP 负责把字节可靠地传到对端,但业务数据怎么组织、怎么解释,需要应用层自己约定。
HTTP 就是一套已经被广泛使用的应用层约定。浏览器访问网站时,会向服务器发送 HTTP 请求;服务器处理请求后,再返回 HTTP 响应。一个网页、一张图片、一次表单提交、一次接口调用,背后基本都离不开这种请求-响应模型。
很多初学者第一次接触 HTTP 时,会把它理解成"浏览器地址栏里的网址"。这只说对了一部分。URL 是 HTTP 请求中的重要信息,但 HTTP 还包含方法、版本、请求头、响应状态码、响应头、正文等内容。真正写网络服务时,这些字段都会直接影响程序行为。
一、HTTP 解决了什么问题
HTTP 全称是 HyperText Transfer Protocol,即超文本传输协议。它定义了客户端和服务器之间如何交换数据。
一次典型访问可以抽象成下面的流程:
#mermaid-svg-7lTeUDgAeFm4WswL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7lTeUDgAeFm4WswL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7lTeUDgAeFm4WswL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7lTeUDgAeFm4WswL .error-icon{fill:#552222;}#mermaid-svg-7lTeUDgAeFm4WswL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7lTeUDgAeFm4WswL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7lTeUDgAeFm4WswL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7lTeUDgAeFm4WswL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7lTeUDgAeFm4WswL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7lTeUDgAeFm4WswL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7lTeUDgAeFm4WswL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7lTeUDgAeFm4WswL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7lTeUDgAeFm4WswL .marker.cross{stroke:#333333;}#mermaid-svg-7lTeUDgAeFm4WswL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7lTeUDgAeFm4WswL p{margin:0;}#mermaid-svg-7lTeUDgAeFm4WswL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7lTeUDgAeFm4WswL .cluster-label text{fill:#333;}#mermaid-svg-7lTeUDgAeFm4WswL .cluster-label span{color:#333;}#mermaid-svg-7lTeUDgAeFm4WswL .cluster-label span p{background-color:transparent;}#mermaid-svg-7lTeUDgAeFm4WswL .label text,#mermaid-svg-7lTeUDgAeFm4WswL span{fill:#333;color:#333;}#mermaid-svg-7lTeUDgAeFm4WswL .node rect,#mermaid-svg-7lTeUDgAeFm4WswL .node circle,#mermaid-svg-7lTeUDgAeFm4WswL .node ellipse,#mermaid-svg-7lTeUDgAeFm4WswL .node polygon,#mermaid-svg-7lTeUDgAeFm4WswL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7lTeUDgAeFm4WswL .rough-node .label text,#mermaid-svg-7lTeUDgAeFm4WswL .node .label text,#mermaid-svg-7lTeUDgAeFm4WswL .image-shape .label,#mermaid-svg-7lTeUDgAeFm4WswL .icon-shape .label{text-anchor:middle;}#mermaid-svg-7lTeUDgAeFm4WswL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7lTeUDgAeFm4WswL .rough-node .label,#mermaid-svg-7lTeUDgAeFm4WswL .node .label,#mermaid-svg-7lTeUDgAeFm4WswL .image-shape .label,#mermaid-svg-7lTeUDgAeFm4WswL .icon-shape .label{text-align:center;}#mermaid-svg-7lTeUDgAeFm4WswL .node.clickable{cursor:pointer;}#mermaid-svg-7lTeUDgAeFm4WswL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7lTeUDgAeFm4WswL .arrowheadPath{fill:#333333;}#mermaid-svg-7lTeUDgAeFm4WswL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7lTeUDgAeFm4WswL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7lTeUDgAeFm4WswL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7lTeUDgAeFm4WswL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7lTeUDgAeFm4WswL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7lTeUDgAeFm4WswL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7lTeUDgAeFm4WswL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7lTeUDgAeFm4WswL .cluster text{fill:#333;}#mermaid-svg-7lTeUDgAeFm4WswL .cluster span{color:#333;}#mermaid-svg-7lTeUDgAeFm4WswL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7lTeUDgAeFm4WswL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7lTeUDgAeFm4WswL rect.text{fill:none;stroke-width:0;}#mermaid-svg-7lTeUDgAeFm4WswL .icon-shape,#mermaid-svg-7lTeUDgAeFm4WswL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7lTeUDgAeFm4WswL .icon-shape p,#mermaid-svg-7lTeUDgAeFm4WswL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7lTeUDgAeFm4WswL .icon-shape .label rect,#mermaid-svg-7lTeUDgAeFm4WswL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7lTeUDgAeFm4WswL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7lTeUDgAeFm4WswL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7lTeUDgAeFm4WswL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HTTP 请求
HTTP 响应
浏览器/客户端
Web 服务器
HTTP 有两个非常重要的特点:
| 特点 | 说明 |
|---|---|
| 请求-响应模型 | 客户端主动发起请求,服务器返回响应 |
| 无状态 | 服务器不会天然记住两次请求来自同一个业务上下文 |
"无状态"不表示服务器不能保存用户状态,而是 HTTP 协议本身不自动保存状态。登录态、购物车、会话保持等能力,通常要借助 Cookie、Session、Token 等机制实现。
还要注意,HTTP 建立在传输层之上。常见的 HTTP/1.0、HTTP/1.1、HTTP/2 都运行在 TCP 之上,而 HTTP/3 使用 QUIC,QUIC 基于 UDP 构建。
二、认识 URL:浏览器地址栏不只是字符串
平时说的"网址",更准确地说是 URL。它描述了客户端要访问哪个资源。
一个常见 URL 长这样:
text
http://www.example.com:8080/index.html?name=zhangsan&age=20
可以拆成几个部分:
| 部分 | 示例 | 含义 |
|---|---|---|
| scheme | http |
使用的协议 |
| host | www.example.com |
主机名 |
| port | 8080 |
端口号,省略时 HTTP 默认习惯使用 80 |
| path | /index.html |
要访问的资源路径 |
| query string | name=zhangsan&age=20 |
查询参数 |
urlencode 和 urldecode
URL 中有些字符有特殊含义,例如 /、?、:、&、=。如果参数值里本身就包含这些字符,就需要进行转义。
典型规则是:把字符转成十六进制形式,再按 %XY 的格式表示。
例如:
text
+ -> %2B
urlencode 是把特殊字符转义成 URL 安全形式,urldecode 是反向还原。写服务端程序时,如果要解析表单参数或查询字符串,这一步经常绕不开。
三、HTTP 请求报文格式
HTTP 请求由三部分组成:请求行、Header、Body。
text
方法 URL 版本\r\n
Header-Key: Header-Value\r\n
Header-Key: Header-Value\r\n
\r\n
Body
对应结构如下:
| 部分 | 示例 | 说明 |
|---|---|---|
| 请求行 | GET /index.html HTTP/1.1 |
描述请求方法、资源路径、协议版本 |
| Header | Host: www.example.com |
描述请求属性 |
| 空行 | \r\n |
标记 Header 结束 |
| Body | 表单或 JSON 数据 | 可为空 |
一个最简单的 GET 请求可能是:
http
GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: curl/8.0
POST 请求通常带有请求体:
http
POST /submit HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 17
name=tom&age=18
这里需要特别注意 Content-Length。如果请求带 Body,接收方需要通过它判断正文长度,否则就不知道应该读取多少字节。
四、HTTP 响应报文格式
HTTP 响应也分成三部分:状态行、Header、Body。
text
版本 状态码 状态码解释\r\n
Header-Key: Header-Value\r\n
Header-Key: Header-Value\r\n
\r\n
Body
一个典型响应:
http
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 20
<h1>Hello World</h1>
如果服务器返回的是 HTML 页面,那么 HTML 内容就在 Body 中。浏览器拿到响应后,会根据 Content-Type 判断如何解释正文,根据 Content-Length 判断正文长度。
五、常见 HTTP 方法
HTTP 方法表示客户端希望服务器对资源执行什么操作。常见方法如下:
| 方法 | 常见用途 | 是否常带 Body |
|---|---|---|
GET |
获取 URL 指定资源 | 通常不带 |
POST |
提交表单、上传业务数据 | 常带 |
PUT |
上传或更新资源 | 常带 |
HEAD |
只获取响应头,不返回正文 | 不带 |
DELETE |
删除指定资源 | 通常不带 |
OPTIONS |
查询服务器支持哪些方法 | 通常不带 |
GET 和 POST 的区别
GET 更适合获取资源,比如访问 /index.html。参数通常放在 URL 的查询字符串中:
text
/search?keyword=linux
POST 更适合提交数据,比如表单登录、提交 JSON。数据通常放在 Body 中:
http
POST /login HTTP/1.1
Content-Type: application/json
Content-Length: 35
{"username":"tom","password":"123"}
从执行过程看,二者都能把数据传给服务器,区别主要体现在语义、参数位置、缓存行为和使用习惯上。不要简单理解成"GET 安全,POST 不安全";只要走明文 HTTP,网络路径上的数据都可能被观察到,安全传输需要 HTTPS。
HEAD 的典型用途
HEAD 和 GET 很像,但服务器只返回响应头,不返回 Body。它常用来检查资源是否存在、文件大小、最后修改时间等信息。
可以用 curl 观察:
bash
curl --head http://www.example.com/
六、状态码:服务器用三位数字表达处理结果
状态码位于响应状态行中,例如:
http
HTTP/1.1 404 Not Found
常见状态码可以按范围理解:
| 范围 | 含义 | 示例 |
|---|---|---|
1xx |
信息提示 | 100 Continue |
2xx |
成功 | 200 OK、201 Created、204 No Content |
3xx |
重定向或缓存相关 | 301、302、304 |
4xx |
客户端请求有问题 | 400、401、403、404 |
5xx |
服务器处理失败 | 500、502、503、504 |
几个高频状态码:
| 状态码 | 含义 | 常见场景 |
|---|---|---|
200 OK |
请求成功 | 访问页面成功 |
204 No Content |
成功但没有响应体 | 删除操作成功 |
301 Moved Permanently |
永久重定向 | 网站换域名 |
302 Found |
临时重定向 | 登录成功跳转 |
304 Not Modified |
资源未修改 | 浏览器缓存命中 |
403 Forbidden |
拒绝访问 | 权限不足 |
404 Not Found |
资源不存在 | 路径写错 |
500 Internal Server Error |
服务器内部错误 | 程序崩溃或异常 |
502 Bad Gateway |
网关拿不到有效响应 | 代理或上游服务异常 |
503 Service Unavailable |
服务暂时不可用 | 维护或过载 |
301/302 与 Location
重定向状态码通常要配合 Location 响应头使用:
http
HTTP/1.1 302 Found
Location: https://www.example.com/new-page
浏览器看到 Location 后,会继续访问新的地址。301 表示资源永久移动,302 表示临时移动。实际开发中,登录后跳转、旧链接迁移、新旧域名切换都可能用到重定向。
七、常见 Header 字段
Header 是 HTTP 报文中非常重要的元信息。很多行为不是由正文决定的,而是由 Header 决定的。
| Header | 作用 | 示例 |
|---|---|---|
Host |
请求的主机名和端口 | Host: www.example.com:8080 |
User-Agent |
客户端软件信息 | User-Agent: Mozilla/5.0 |
Content-Type |
Body 的媒体类型 | Content-Type: text/html |
Content-Length |
Body 的字节长度 | Content-Length: 150 |
Cookie |
客户端携带的少量状态信息 | Cookie: session_id=abc |
Referer |
当前请求来源页面 | Referer: http://example.com/a.html |
Location |
重定向目标地址 | Location: http://example.com/new.html |
Server |
服务器软件信息 | Server: nginx/1.18.0 |
Cache-Control |
缓存控制 | Cache-Control: no-cache |
Connection |
连接管理 | Connection: keep-alive |
Connection: keep-alive 和 close
Connection 用来管理连接状态:
http
Connection: keep-alive
表示希望复用 TCP 连接,后续请求可以继续在这个连接上发送。
http
Connection: close
表示本次请求/响应结束后关闭连接。
HTTP/1.1 默认倾向于持久连接;HTTP/1.0 默认更偏向短连接,如果要复用连接,需要显式声明 Connection: keep-alive。
八、手写一个最小 HTTP 服务器
理解 HTTP 最直接的方式,就是自己用 Socket 返回一段符合格式的响应。下面这个服务器只做一件事:浏览器访问后返回 <h1>hello world</h1>。
c
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
static void Usage(const char* proc) {
printf("usage: %s [ip] [port]\n", proc);
}
int main(int argc, char* argv[]) {
if (argc != 3) {
Usage(argv[0]);
return 1;
}
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
perror("socket");
return 1;
}
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr(argv[1]);
local.sin_port = htons(atoi(argv[2]));
if (bind(listen_fd, (struct sockaddr*)&local, sizeof(local)) < 0) {
perror("bind");
close(listen_fd);
return 1;
}
if (listen(listen_fd, 10) < 0) {
perror("listen");
close(listen_fd);
return 1;
}
for (;;) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int client_fd = accept(listen_fd, (struct sockaddr*)&peer, &len);
if (client_fd < 0) {
perror("accept");
continue;
}
char request[10240] = {0};
ssize_t n = read(client_fd, request, sizeof(request) - 1);
if (n > 0) {
printf("[Request]\n%s\n", request);
}
const char* body = "<h1>hello world</h1>";
char response[1024] = {0};
snprintf(response, sizeof(response),
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"Content-Length: %lu\r\n"
"Connection: close\r\n"
"\r\n"
"%s",
strlen(body), body);
write(client_fd, response, strlen(response));
close(client_fd);
}
close(listen_fd);
return 0;
}
编译运行:
bash
gcc mini_http_server.c -o mini_http_server
./mini_http_server 0.0.0.0 9090
浏览器访问:
text
http://127.0.0.1:9090
或者用 curl 查看完整响应:
bash
curl -i http://127.0.0.1:9090/
预期能看到类似输出:
http
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 20
Connection: close
<h1>hello world</h1>
这段代码的关键不在于 HTML,而在于它手动拼出了一个合法的 HTTP 响应:
text
状态行
响应头
空行
响应正文
只要响应格式符合约定,浏览器就能识别并渲染 Body。
注意:如果浏览器访问时,服务端打印出
GET /favicon.ico HTTP/1.1,这是浏览器自动请求网站图标,不是程序异常。
九、常见问题与易错点
1. 忘记 Header 和 Body 之间的空行
HTTP 报文中,空行用来表示 Header 结束。没有空行,浏览器可能无法正确判断正文从哪里开始。
2. Content-Length 写错
Content-Length 应该是 Body 的字节数,不包含响应行、Header 和空行。长度写错会导致浏览器读不全或等待更多数据。
3. 把 URL 当作文件系统路径直接使用
请求行里的 /index.html 是 URL path,不应该不加检查地拼到本地路径上。真实服务器需要做根目录限制和路径合法性检查,避免访问到不该暴露的文件。
4. 混淆状态码和业务错误
HTTP 状态码表达协议层面的处理结果。业务接口也可以在 JSON Body 中返回业务错误码。两者可以配合,但不要混成一套。
5. 认为端口必须是 80
HTTP 默认常用 80 端口,但服务完全可以运行在 8080、9090 等端口。浏览器访问非默认端口时,需要在 URL 中写明端口号。
6. 忽略大小写和换行规范
Header 字段名通常不区分大小写,但代码中最好保持标准写法。HTTP 报文行结束符标准形式是 \r\n,实际实验中有些客户端比较宽容,但写服务端时建议按标准格式输出。
十、HTTP 版本演进简述
HTTP 的版本演进,本质上是在解决性能、连接复用和传输效率问题。
| 版本 | 核心特点 |
|---|---|
| HTTP/0.9 | 只支持简单 GET,主要传输 HTML |
| HTTP/1.0 | 引入 Header、状态码、POST、HEAD、缓存等能力 |
| HTTP/1.1 | 默认持久连接,支持 Host、管道化、分块传输 |
| HTTP/2 | 二进制帧、多路复用、头部压缩、服务器推送 |
| HTTP/3 | 基于 QUIC,减少连接建立开销,改善传输效率 |
对于刚开始写网络程序的人来说,最值得先掌握的是 HTTP/1.1 的文本报文格式。因为它可读性强,用 telnet、nc、curl 都能直接观察,对理解浏览器和服务器通信非常有帮助。
总结
HTTP 不是神秘的浏览器内部机制,而是一套清晰的应用层文本协议。请求由请求行、Header、空行和 Body 组成;响应由状态行、Header、空行和 Body 组成。方法表达客户端想做什么,状态码表达服务器处理结果,Header 描述额外属性,Body 承载真正的数据内容。
把这些格式理解透,再结合 Socket 写一个最小服务器,就能真正看明白浏览器访问网页时网络中传输的内容。后续学习 Web 服务器、网关、反向代理、RESTful API、Cookie/Session、HTTPS,也都会更顺手。