mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3250 字
9 分钟
HTTP/1.0:扩展协议
2024-04-26

在上一篇实验中,我们亲手实现了一个 HTTP/0.9 服务器,体验了它「极简到极致」的设计哲学。但 0.9 的局限也暴露得很清楚:浏览器收到一串二进制字节,不知道是什么文件类型;请求失败了,服务器没法告诉客户端到底出了什么错;每个资源都要新建 TCP 连接,性能很差。1996 年,HTTP/1.0 作为 RFC 1945 正式发布,逐一解决了这些问题。

元信息的缺失: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
Note

最后的 \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 定义了状态码的分类规则,第一位数字表示类别:

类别范围含义常见例子
2xx200-299成功200 OK, 201 Created, 204 No Content
3xx300-399重定向301 Moved Permanently, 302 Found
4xx400-499客户端错误400 Bad Request, 404 Not Found, 403 Forbidden
5xx500-599服务器错误500 Internal Server Error, 503 Service Unavailable

这个分类在 30 年后的今天还在用,足以说明设计的合理性。浏览器看到 301 就自动跳转,看到 404 就显示”页面未找到”,看到 500 就提示”服务器错误”。程序可以自动处理,无需人工干预。在 0.9 时代,这些情况只能靠人眼阅读响应内容来判断。

1.4 三种请求方法#

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

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

POST:提交数据。客户端可以在请求体中发送数据给服务器处理。比如提交表单、上传文件。POST 请求可能会改变服务器状态(比如创建新记录)。CGI 时代的刚需:表单提交、文件上传,没有 POST 就没有 Web 应用,只有静态文档。

HEAD:只获取响应头。与 GET 类似,但服务器只返回状态行和响应头,不返回响应体。检查链接是否还活着、获取文件大小决定是否下载、验证缓存是否有效,HEAD 让这些操作不用下载完整内容。

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

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

核心逻辑只有两个函数。parse_request 负责解析请求:

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''

先按 \r\n\r\n 把原始数据切成「头部」和「体」两部分,再逐行解析请求行和各个头字段。iso-8859-1 是 HTTP 协议的默认编码,可以安全处理任意字节。

build_response 负责构建响应:

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

状态行、响应头、空行、响应体,按顺序拼接成字节序列。最后的空行 \r\n 必不可少,它分隔响应头和响应体。

完整服务器实现(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:
not_found_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(not_found_body))},
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()

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

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

Terminal window
HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 268
<!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

输出:

Terminal window
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 69
{"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

输出:

Terminal window
HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 268

HEAD 请求的响应只有状态行和响应头,没有响应体。注意 Content-Length 仍然标注了资源的大小,方便客户端判断是否需要获取完整内容。这在检查文件是否存在、获取文件大小、验证缓存是否有效时非常有用。

实验 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

输出:

Terminal window
HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 90
<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

输出:

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

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

对比 0.9 的 nc 输出,这里你能看到三个关键变化:状态行 HTTP/1.0 200 OK 明确告诉客户端请求成功;Content-Type: text/html 让浏览器知道这是 HTML 而不是图片或 JSON;Content-Length: 268 让客户端知道响应有多少字节,不用等连接关闭才判断结束。这些在 0.9 里都不存在。

实验 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 特性速查表#

5.1 影响评估矩阵#

特性解决了什么没有它会怎样常见误区
请求头/响应头客户端和服务器可以传递元信息浏览器猜 MIME 类型,无法缓存以为头部只是”附加信息”,其实是协议协商的基础
状态码程序可以自动判断请求结果只能靠人眼阅读响应内容区分成功和失败404 是状态码不是页面内容,很多人混淆
Content-Type浏览器正确处理各类文件收到二进制字节只能猜类型以为文件扩展名就够了,但 HTTP 传输不看扩展名
Content-Length无需等连接关闭即可判断结束无法复用连接,性能差和”文件大小”是两回事,动态内容可以没有
POST 方法支持提交数据,Web 应用成为可能只有 GET,无法提交表单POST 不是”更安全的 GET”,语义完全不同
HEAD 方法只获取元信息,节省带宽检查资源必须下载完整内容HEAD 响应的 Content-Length 是 GET 时的值,不是 0
详细参考数据

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

请求头用途示例
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

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

响应头用途示例
Content-Length响应体的字节长度Content-Length: 1234
Server服务器软件标识Server: Apache/2.4
Location重定向目标 URLLocation: 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

5.4 HTTP/1.0 状态码速查#

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

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

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

这些差异正是下一篇 HTTP/1.1 的核心内容。


参考资料#

支持与分享

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

HTTP/1.0:扩展协议
https://blog.souloss.cn/posts/web/http/http-1-0/
作者
Souloss
发布于
2024-04-26
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时