mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3905 字
11 分钟
HTTP/1.0:扩展协议
2023-01-29

在上一篇实验中,我们亲手实现了一个 HTTP/0.9 服务器,体验了它「极简到极致」的设计哲学:客户端发一行 GET /path,服务器返回原始文档内容,然后关闭连接。这种设计在 1991 年的 Web 诞生之初是合理的——毕竟当时 Tim Berners-Lee 只是想在 CERN 内部共享文档。

然而,随着 Web 的迅速普及,HTTP/0.9 的局限性开始暴露无遗。想象一下:你是一个早期的浏览器开发者,用户访问了一个图片网站。服务器返回了一串二进制字节,但浏览器完全不知道这是什么类型的文件——是 JPEG?PNG?还是 GIF?浏览器只能靠「猜」,或者让用户自己选择。这显然不是一个好的用户体验。

更棘手的问题是错误处理。当请求的资源不存在时,HTTP/0.9 的服务器只能返回一个自定义的 HTML 页面(比如写着「404 Not Found」),但客户端无法从协议层面区分「这是正常的 HTML 页面」还是「这是一个错误页面」。机器无法自动判断,只能靠人眼阅读。

1996 年,HTTP/1.0 作为 RFC 1945 正式发布,它对 HTTP/0.9 进行了全面升级,解决了以下核心问题:

元信息的缺失:HTTP/1.0 引入了请求头和响应头,客户端可以告诉服务器自己能接受什么类型的内容,服务器也可以告诉客户端返回内容的 MIME 类型、长度、编码方式等。这就像在寄快递时,不仅寄出物品本身,还附上一份清单说明「这是照片,A4 大小,共 10 页」。

状态表达的缺失:HTTP/1.0 引入了状态码体系,服务器用三位数字明确告诉客户端请求的处理结果——成功了?重定向了?还是出错了?这让客户端程序能够自动化地处理各种情况。

方法的单一:HTTP/1.0 定义了 GET、POST、HEAD 三种方法,不再局限于「获取文档」,还支持「提交数据」「只获取头信息」等操作。这为 Web 应用的发展奠定了基础。

一、新增特性详解#

1.1 请求行与请求头#

HTTP/1.0 的请求格式发生了重大变化。首先是请求行:现在包含了 HTTP 版本号。

GET /index.html HTTP/1.0\r\n

紧接着请求行之后,可以附加多行请求头,每个头占一行,格式为 Header-Name: Header-Value。请求头与请求行之间、请求头与请求体之间都用空行分隔。

GET /index.html HTTP/1.0\r\n
Host: localhost:8080\r\n
User-Agent: MyBrowser/1.0\r\n
Accept: text/html\r\n
\r\n

注意最后的 \r\n —— 这是空行,表示请求头结束。如果请求有 body(比如 POST 请求),body 就跟在空行后面。

这种设计让协议具备了「协商能力」。客户端通过 Accept 头告诉服务器「我能接受 HTML 格式」,服务器通过检查这个头来决定返回什么内容。如果客户端发送 Accept: application/json,服务器就知道应该返回 JSON 而不是 HTML。

1.2 响应头与状态行#

HTTP/1.0 的响应也变得结构化了。首先是状态行:

HTTP/1.0 200 OK\r\n

状态行由三部分组成:HTTP 版本号、状态码(三位数字)、状态描述(人类可读的短语)。状态码是机器处理的关键,状态描述则是给人看的。

状态行之后是响应头,格式与请求头相同:

HTTP/1.0 200 OK\r\n
Content-Type: text/html\r\n
Content-Length: 1234\r\n
Server: MyServer/1.0\r\n
\r\n
<html>...

响应头结束后是一个空行,然后才是响应体。

这里最关键的两个头是 Content-TypeContent-LengthContent-Type 告诉客户端返回内容的 MIME 类型,浏览器据此决定是渲染 HTML、显示图片、还是下载文件。Content-Length 则告诉客户端响应体有多少字节,客户端可以据此判断是否接收完整,而不再依赖 TCP 连接关闭来判断结束。

1.3 状态码体系#

HTTP/1.0 定义了状态码的分类规则,第一位数字表示类别:

| 类别 | 范围 | 含义 | 常见例子 | | 1xx | 100-199 | 信息性响应 | 100 Continue(HTTP/1.1 才有) | | 2xx | 200-299 | 成功 | 200 OK, 201 Created, 204 No Content | | 3xx | 300-399 | 重定向 | 301 Moved Permanently, 302 Found | | 4xx | 400-499 | 客户端错误 | 400 Bad Request, 404 Not Found, 403 Forbidden | | 5xx | 500-599 | 服务器错误 | 500 Internal Server Error, 503 Service Unavailable |

这套状态码体系是 HTTP 协议最具前瞻性的设计之一。浏览器看到 301 就知道要自动跳转,看到 404 就显示「页面未找到」,看到 500 就提示「服务器错误」。程序可以自动处理,无需人工干预。

1.4 三种请求方法#

HTTP/1.0 定义了三种请求方法:

GET:获取资源。这是最常用的方法,请求体通常为空。GET 请求应该是「幂等」的,即多次请求同一资源应该得到相同结果,且不会改变服务器状态。

POST:提交数据。客户端可以在请求体中发送数据给服务器处理。比如提交表单、上传文件。POST 请求可能会改变服务器状态(比如创建新记录)。

HEAD:只获取响应头。与 GET 类似,但服务器只返回状态行和响应头,不返回响应体。这在检查资源是否存在、获取文件大小、验证缓存是否有效等场景非常有用。

二、实验一:用 Python 实现 HTTP/1.0 服务器#

现在我们来实现一个支持 HTTP/1.0 的服务器。相比上一篇的 HTTP/0.9 服务器,这个版本需要解析请求头、生成响应头、正确处理状态码。

源码(保存为 http10_server.py):

#!/usr/bin/env python3
# http10_server.py -- HTTP/1.0 server for learning
import socket
import threading
import os
from datetime import datetime
HOST = '0.0.0.0'
PORT = 8080
WWW = 'www'
def parse_request(data):
"""解析 HTTP/1.0 请求,返回 (method, path, headers, body)"""
try:
# 分离请求头和请求体
if b'\r\n\r\n' in data:
header_part, body = data.split(b'\r\n\r\n', 1)
else:
header_part = data
body = b''
lines = header_part.decode('iso-8859-1').split('\r\n')
if not lines:
return None, None, {}, b''
# 解析请求行:GET /path HTTP/1.0
request_line = lines[0]
parts = request_line.split()
if len(parts) < 2:
return None, None, {}, b''
method = parts[0].upper()
path = parts[1]
# HTTP 版本号(可能是 HTTP/1.0 或 HTTP/0.9 无版本)
version = parts[2] if len(parts) > 2 else 'HTTP/0.9'
# 解析请求头
headers = {}
for line in lines[1:]:
if ': ' in line:
key, value = line.split(': ', 1)
headers[key.lower()] = value
return method, path, headers, body
except Exception as e:
print(f"Error parsing request: {e}")
return None, None, {}, b''
def build_response(status_code, status_text, headers, body):
"""构建 HTTP/1.0 响应"""
response = f"HTTP/1.0 {status_code} {status_text}\r\n"
for key, value in headers.items():
response += f"{key}: {value}\r\n"
response += "\r\n"
return response.encode('iso-8859-1') + body
def get_mime_type(path):
"""根据文件扩展名返回 MIME 类型"""
ext = os.path.splitext(path)[1].lower()
mime_types = {
'.html': 'text/html',
'.htm': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.ico': 'image/x-icon',
'.txt': 'text/plain',
}
return mime_types.get(ext, 'application/octet-stream')
def handle_conn(conn, addr):
try:
# 读取请求数据(简单实现:读取直到遇到空行)
data = b''
while b'\r\n\r\n' not in data:
chunk = conn.recv(1024)
if not chunk:
break
data += chunk
if not data:
return
method, path, headers, body = parse_request(data)
if method is None:
response = build_response(400, "Bad Request",
{"Content-Type": "text/html", "Content-Length": "11"},
b"Bad Request")
conn.sendall(response)
return
print(f"[{addr}] {method} {path} HTTP/1.0")
# 处理路径
if path == '/':
path = '/index.html'
# 安全处理:防止路径遍历攻击
safe_path = os.path.normpath(path).lstrip(os.sep)
full_path = os.path.join(WWW, safe_path)
# 处理 HEAD 请求
if method == 'HEAD':
if os.path.isfile(full_path):
mime = get_mime_type(full_path)
size = os.path.getsize(full_path)
response = build_response(200, "OK",
{"Content-Type": mime, "Content-Length": str(size)},
b'')
else:
response = build_response(404, "Not Found",
{"Content-Type": "text/html", "Content-Length": "9"},
b'')
conn.sendall(response)
return
# 处理 GET 请求
if method == 'GET':
if os.path.isfile(full_path):
with open(full_path, 'rb') as f:
content = f.read()
mime = get_mime_type(full_path)
response = build_response(200, "OK",
{"Content-Type": mime, "Content-Length": str(len(content))},
content)
else:
body = b'<html><body><h1>404 Not Found</h1><p>The requested resource was not found.</p></body></html>'
response = build_response(404, "Not Found",
{"Content-Type": "text/html", "Content-Length": str(len(body))},
body)
conn.sendall(response)
return
# 处理 POST 请求(简单示例)
if method == 'POST':
# 这里只是演示,返回收到的数据
response_body = f"<html><body><h1>POST Received</h1><p>Path: {path}</p><p>Body length: {len(body)}</p></body></html>".encode()
response = build_response(200, "OK",
{"Content-Type": "text/html", "Content-Length": str(len(response_body))},
response_body)
conn.sendall(response)
return
# 不支持的方法
body = b'<html><body><h1>501 Not Implemented</h1></body></html>'
response = build_response(501, "Not Implemented",
{"Content-Type": "text/html", "Content-Length": str(len(body))},
body)
conn.sendall(response)
except Exception as e:
print(f"Error handling connection: {e}")
finally:
conn.close()
def main():
os.makedirs(WWW, exist_ok=True)
# 创建测试文件
idx = os.path.join(WWW, 'index.html')
if not os.path.exists(idx):
with open(idx, 'w') as f:
f.write('<!DOCTYPE html>\n<html>\n<head><title>HTTP/1.0 Demo</title></head>\n'
'<body>\n<h1>HTTP/1.0 Demo Server</h1>\n'
'<p>This is a simple HTTP/1.0 server for learning.</p>\n'
'<ul>\n<li><a href="/page1.html">Page 1</a></li>\n'
'<li><a href="/data.json">JSON Data</a></li>\n</ul>\n</body>\n</html>')
page1 = os.path.join(WWW, 'page1.html')
if not os.path.exists(page1):
with open(page1, 'w') as f:
f.write('<!DOCTYPE html>\n<html>\n<head><title>Page 1</title></head>\n'
'<body>\n<h1>Page 1</h1>\n<p><a href="/">Back to index</a></p>\n</body>\n</html>')
json_file = os.path.join(WWW, 'data.json')
if not os.path.exists(json_file):
with open(json_file, 'w') as f:
f.write('{"name": "HTTP/1.0 Demo", "version": "1.0", "items": ["a", "b", "c"]}')
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))
s.listen(5)
print(f"HTTP/1.0 server listening on {HOST}:{PORT}")
print(f"Serving files from: {os.path.abspath(WWW)}")
try:
while True:
conn, addr = s.accept()
threading.Thread(target=handle_conn, args=(conn, addr), daemon=True).start()
except KeyboardInterrupt:
print("\nShutting down.")
finally:
s.close()
if __name__ == '__main__':
main()

代码要点说明

parse_request 函数负责解析 HTTP 请求。它首先根据 \r\n\r\n 分离请求头和请求体,然后逐行解析请求行和各个头字段。注意使用 iso-8859-1 编码,这是 HTTP 协议的默认编码,可以安全处理任意字节。

build_response 函数负责构建标准格式的 HTTP 响应。它将状态行、响应头和响应体组合成符合 HTTP/1.0 规范的字节序列。特别注意最后的空行 \r\n 必不可少,它分隔响应头和响应体。

get_mime_type 函数是一个简单的 MIME 类型映射表。实际生产环境中应该使用 mimetypes 标准库模块,但这里手动实现有助于理解原理。

handle_conn 函数是请求处理的核心。它解析请求后根据方法类型分发处理:HEAD 请求只返回头部,GET 请求返回完整内容,POST 请求演示了如何接收请求体。每个响应都包含 Content-TypeContent-Length,这正是 HTTP/1.0 相比 0.9 的关键进步。

三、实验二:用 nc 观察请求/响应的完整格式#

启动服务器后,用 nc(netcat)来手动发送 HTTP/1.0 请求,观察完整的协议格式。

实验 2.1:GET 请求获取 HTML 页面

printf 'GET /index.html HTTP/1.0\r\nHost: localhost:8080\r\n\r\n' | nc localhost 8080

你会看到类似这样的输出:

HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 212
<!DOCTYPE html>
<html>
<head><title>HTTP/1.0 Demo</title></head>
<body>
<h1>HTTP/1.0 Demo Server</h1>
<p>This is a simple HTTP/1.0 server for learning.</p>
<ul>
<li><a href="/page1.html">Page 1</a></li>
<li><a href="/data.json">JSON Data</a></li>
</ul>
</body>
</html>

注意观察:响应的第一行是状态行 HTTP/1.0 200 OK,接着是两个响应头 Content-TypeContent-Length,然后是一个空行,最后是 HTML 内容。

实验 2.2:GET 请求获取 JSON 文件

printf 'GET /data.json HTTP/1.0\r\nHost: localhost:8080\r\nAccept: application/json\r\n\r\n' | nc localhost 8080

输出:

HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 63
{"name": "HTTP/1.0 Demo", "version": "1.0", "items": ["a", "b", "c"]}

这里的 Content-Typeapplication/json,浏览器看到这个头就会按照 JSON 格式处理内容。

实验 2.3:HEAD 请求(只获取响应头)

printf 'HEAD /index.html HTTP/1.0\r\nHost: localhost:8080\r\n\r\n' | nc localhost 8080

输出:

HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 212

HEAD 请求的响应只有状态行和响应头,没有响应体。这在检查文件是否存在、获取文件大小、验证缓存是否有效时非常有用。

实验 2.4:POST 请求(发送数据)

printf 'POST /submit HTTP/1.0\r\nHost: localhost:8080\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 19\r\n\r\nname=test&value=123' | nc localhost 8080

输出:

HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 86
<html><body><h1>POST Received</h1><p>Path: /submit</p><p>Body length: 19</p></body></html>

POST 请求需要在请求头中指定 Content-TypeContent-Length,请求体跟在空行后面。我们的演示服务器会返回收到的数据信息。

实验 2.5:404 错误

printf 'GET /nonexistent.html HTTP/1.0\r\nHost: localhost:8080\r\n\r\n' | nc localhost 8080

输出:

HTTP/1.0 404 Not Found
Content-Type: text/html
Content-Length: 89
<html><body><h1>404 Not Found</h1><p>The requested resource was not found.</p></body></html>

状态码 404 明确告诉客户端资源不存在,客户端程序可以根据这个状态码自动处理错误情况,而不用依赖解析 HTML 内容来判断。

实验 2.6:用 curl 验证

现代的 curl 默认使用 HTTP/1.1,但可以通过参数指定 HTTP/1.0:

# GET 请求
curl --http1.0 http://localhost:8080/index.html
# HEAD 请求
curl --http1.0 -I http://localhost:8080/index.html
# POST 请求
curl --http1.0 -X POST -d "name=test" http://localhost:8080/submit

curl -I(或 curl --head)会发送 HEAD 请求并只显示响应头。

实验 2.7:用 tcpdump 观察网络包

在另一个终端运行 tcpdump,观察完整的 HTTP 请求和响应:

sudo tcpdump -A -s 0 'tcp port 8080'

然后在第一个终端发送请求。你会清晰地看到请求和响应的完整格式:请求行/状态行、请求头/响应头、空行、请求体/响应体。这对于理解 HTTP 协议的工作原理非常有帮助。

四、局限性:每次请求重新建立 TCP 连接#

HTTP/1.0 虽然相比 0.9 有了巨大进步,但它仍然有一个严重的性能问题:每个请求都需要建立新的 TCP 连接

在 HTTP/1.0 中,默认的行为是 Connection: close。这意味着:

  1. 客户端建立 TCP 连接(三次握手)
  2. 发送 HTTP 请求
  3. 接收 HTTP 响应
  4. 服务器关闭连接
  5. 如果需要请求另一个资源,重复步骤 1-4

想象一下,一个网页有 10 张图片、3 个 CSS 文件、2 个 JavaScript 文件。加载这个页面需要建立 16 次 TCP 连接!每次 TCP 连接都需要三次握手,这增加了显著的延迟。

你可以用 time 命令来观察这个行为:

# 连续请求两个资源
time (printf 'GET /index.html HTTP/1.0\r\n\r\n' | nc localhost 8080 > /dev/null && \
printf 'GET /data.json HTTP/1.0\r\n\r\n' | nc localhost 8080 > /dev/null)

你会发现每次请求都是独立的 TCP 连接。

HTTP/1.0 有一个非标准的扩展 Connection: keep-alive,允许复用 TCP 连接。但这是可选的,不同实现之间可能不兼容。这个问题直到 HTTP/1.1 才真正解决——HTTP/1.1 默认使用持久连接(persistent connection),一个 TCP 连接可以发送多个请求。

五、观察总结#

通过这次实验,你应该能够:

理解 HTTP/1.0 的协议格式:请求由请求行、请求头、空行、请求体组成;响应由状态行、响应头、空行、响应体组成。每一部分都有其特定作用。

掌握状态码的意义:状态码让客户端程序能够自动判断请求的处理结果。2xx 表示成功,3xx 表示重定向,4xx 表示客户端错误,5xx 表示服务器错误。

理解请求头和响应头的作用Content-Type 告诉客户端内容的类型,Content-Length 告诉客户端内容的长度,Host 指定目标主机……这些头字段让 HTTP 成为一种真正可扩展的协议。

能用 Python 实现一个支持 HTTP/1.0 的服务器:解析请求、生成响应、处理不同方法、返回正确的状态码和头信息。

能用 nc 手动发送 HTTP 请求并观察响应格式:通过手工构造请求来深入理解协议的每一个细节。

认识到 HTTP/1.0 的局限性:每个请求都需要新建 TCP 连接,这在现代网页(包含大量资源)的场景下效率低下。这也解释了为什么后来 HTTP/1.1 要引入持久连接。

HTTP/1.0 虽然有局限,但它奠定了现代 Web 的基础。请求头/响应头、状态码、多种方法——这些设计至今仍在使用。理解 HTTP/1.0,是理解后续 HTTP/1.1、HTTP/2、HTTP/3 的。

六、HTTP/1.0 特性速查表#

6.1 从 HTTP/0.9 到 HTTP/1.0 的演进#

| 特性 | HTTP/0.9 | HTTP/1.0 | 改进意义 | | 请求行 | GET /path | GET /path HTTP/1.0 | 明确协议版本,便于协商 | | 请求头 | 不支持 | 支持 | 客户端可表达偏好、传递元信息 | | 响应头 | 不支持 | 支持 | 服务器可声明内容类型、长度等 | | 状态码 | 不支持 | 支持 | 程序可自动判断处理结果 | | Content-Type | 需猜测 | 明确指定 | 浏览器正确处理各类文件 | | Content-Length | 依赖 EOF | 支持 | 无需等待连接关闭即可判断结束 | | 方法支持 | GET | GET, POST, HEAD | 支持表单提交、元数据查询 | | 连接复用 | 每次新建 | 默认关闭(可选 keep-alive) | 性能瓶颈,HTTP/1.1 解决 |

6.2 HTTP/1.0 新增的核心请求头#

| 请求头 | 用途 | 示例 | | Host | 指定目标主机(可选) | Host: www.example.com | | User-Agent | 标识客户端类型 | User-Agent: Mozilla/5.0 | | Accept | 声明可接受的 MIME 类型 | Accept: text/html, application/json | | Accept-Language | 声明偏好的自然语言 | Accept-Language: zh-CN, en | | Accept-Encoding | 声明支持的压缩算法 | Accept-Encoding: gzip | | Content-Type | 请求体的 MIME 类型 | Content-Type: application/x-www-form-urlencoded | | Content-Length | 请求体的字节长度 | Content-Length: 42 | | Authorization | 身份验证凭证 | Authorization: Basic xxx | | If-Modified-Since | 条件请求(缓存验证) | If-Modified-Since: Wed, 20 Mar 2026 10:00:00 GMT |

6.3 HTTP/1.0 新增的核心响应头#

| 响应头 | 用途 | 示例 | | Content-Type | 响应体的 MIME 类型 | Content-Type: text/html; charset=utf-8 | | Content-Length | 响应体的字节长度 | Content-Length: 1234 | | Server | 服务器软件标识 | Server: Apache/2.4 | | Location | 重定向目标 URL | Location: https://example.com/new | | WWW-Authenticate | 身份验证要求 | WWW-Authenticate: Basic realm="Admin" | | Expires | 缓存过期时间 | Expires: Thu, 01 Dec 2026 16:00:00 GMT | | Last-Modified | 资源最后修改时间 | Last-Modified: Wed, 20 Mar 2026 10:00:00 GMT |

6.4 HTTP/1.0 状态码速查#

| 类别 | 状态码 | 含义 | 典型场景 | | 2xx 成功 | 200 | OK | 请求成功,返回请求的资源 | | | 201 | Created | POST 请求成功创建资源 | | | 204 | No Content | 成功但无返回内容(如 DELETE) | | 3xx 重定向 | 301 | Moved Permanently | 资源永久移动到新 URL | | | 302 | Found | 资源临时移动到新 URL | | | 304 | Not Modified | 缓存有效,无需重新传输 | | 4xx 客户端错误 | 400 | Bad Request | 请求格式错误 | | | 401 | Unauthorized | 需要身份验证 | | | 403 | Forbidden | 拒绝访问 | | | 404 | Not Found | 资源不存在 | | 5xx 服务器错误 | 500 | Internal Server Error | 服务器内部错误 | | | 501 | Not Implemented | 不支持该请求方法 | | | 503 | Service Unavailable | 服务暂时不可用 |

6.5 HTTP/1.0 vs HTTP/1.1 关键差异预览#

| 特性 | HTTP/1.0 | HTTP/1.1 | | 连接行为 | 默认 Connection: close | 默认 Connection: keep-alive | | Host 头 | 可选 | 必须 | | 分块传输 | 不支持 | Transfer-Encoding: chunked | | 管道化 | 不支持 | 支持(但实际部署较少) | | 缓存控制 | Expires 等简单机制 | ETagCache-Control 等增强机制 | | 方法支持 | GET, POST, HEAD | 新增 PUT, DELETE, OPTIONS, TRACE, CONNECT |


参考#

  • RFC 1945 — Hypertext Transfer Protocol — HTTP/1.0

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

HTTP/1.0:扩展协议
https://blog.souloss.com/posts/web/http-1-0/
作者
Souloss
发布于
2023-01-29
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时