mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3659 字
10 分钟
HTTP/1.1:持久连接
2023-11-10

在上一篇实验中,体验了 HTTP/1.0 带来的改进:请求头与响应头、状态码、多种请求方法。这些特性让 HTTP 变得可扩展、可程序化处理。然而,HTTP/1.0 在实际使用中暴露了一个严重的性能瓶颈。

想象这样一个场景:你访问一个包含 20 张图片的网页。在 HTTP/1.0 中,每张图片都需要:

  1. 建立 TCP 连接(三次握手)
  2. 发送 HTTP 请求
  3. 接收 HTTP 响应
  4. 关闭 TCP 连接(四次挥手)

这意味着 21 次 TCP 连接建立和关闭!每次 TCP 握手都需要约 1-2 个 RTT(往返时间),如果客户端与服务器之间的延迟是 100ms,仅握手就消耗了 2-4 秒。这在现代网页动辄加载数十甚至上百个资源的场景下,简直是灾难。

1997 年,HTTP/1.1 作为 RFC 2068 正式发布(后由 RFC 2616 和 RFC 7230 系列更新),它针对 HTTP/1.0 的性能问题进行了全面优化,核心改进包括:

持久连接(Persistent Connection):默认保持 TCP 连接打开,一个连接可以发送多个请求和响应。这被称为「keep-alive」,消除了重复建立连接的开销。

分块传输编码(Chunked Transfer Encoding):允许服务器在不知道内容总长度时就开始传输,边生成边发送,降低了首字节延迟。

请求管道化(Pipelining):允许客户端在收到前一个响应之前就发送下一个请求,进一步减少延迟(虽然实际部署中存在兼容性问题)。

Host 头要求:必须包含 Host 请求头,这为虚拟主机(一个 IP 托管多个域名)奠定了基础。

缓存增强:引入 ETagIf-None-MatchIf-Modified-Since 等条件请求头,让缓存更加智能高效。

内容协商:客户端可以通过 Accept-LanguageAccept-Encoding 等头告诉服务器自己的偏好,服务器返回最适合的内容。

一、新增特性详解#

1.1 持久连接(Keep-Alive)#

HTTP/1.0 默认在每次响应后关闭连接。虽然 HTTP/1.0 支持非标准的 Connection: keep-alive 头,但这是可选扩展,兼容性参差不齐。

HTTP/1.1 反转了默认行为:连接默认保持打开。只有当客户端或服务器显式发送 Connection: close 时,连接才会在响应后关闭。

# HTTP/1.1 默认行为(连接保持)
GET /index.html HTTP/1.1\r\n
Host: localhost:8080\r\n
\r\n
# 显式关闭连接
GET /index.html HTTP/1.1\r\n
Host: localhost:8080\r\n
Connection: close\r\n
\r\n

持久连接的好处:

  • 减少 TCP 握手开销:一次握手,多次请求
  • 减少 TCP 慢启动影响:连接复用可以充分利用已打开的拥塞窗口
  • 降低服务器负载:减少连接建立/关闭的系统调用

1.2 分块传输编码#

HTTP/1.0 要求服务器在发送响应前知道 Content-Length。对于动态生成的内容(如数据库查询结果、实时日志流),服务器必须先缓冲所有数据才能计算长度,这增加了延迟。

HTTP/1.1 引入了 Transfer-Encoding: chunked,允许服务器将响应分成多个块发送,每个块前标注该块的长度(十六进制):

HTTP/1.1 200 OK\r\n
Transfer-Encoding: chunked\r\n
\r\n
7\r\n
Mozilla\r\n
9\r\n
Developer\r\n
7\r\n
Network\r\n
0\r\n
\r\n

上面的响应包含三个数据块:「Mozilla」(7 字节)、「Developer」(9 字节)、「Network」(7 字节),最后以 0\r\n\r\n 表示结束。客户端收到后自动拼接成完整内容。

1.3 请求管道化#

HTTP/1.0 中,如果客户端要发送多个请求,必须等待前一个响应返回后才能发送下一个。这是典型的串行模式:

请求1 -> 响应1 -> 请求2 -> 响应2 -> 请求3 -> 响应3

HTTP/1.1 允许管道化:客户端可以连续发送多个请求,服务器按顺序返回响应:

请求1 -> 请求2 -> 请求3
响应1 -> 响应2 -> 响应3

理论上这可以大幅减少延迟。然而,由于 HTTP/1.1 要求响应必须按请求顺序返回(队头阻塞问题),加上中间代理和服务器对管道化的支持参差不齐,这个特性在实际中很少启用。现代浏览器默认禁用管道化,转向 HTTP/2 的多路复用方案。

1.4 Host 头要求#

HTTP/1.0 中 Host 头是可选的。这导致一个 IP 地址只能托管一个网站——服务器无法区分请求发往哪个域名。

HTTP/1.1 要求所有请求必须包含 Host 头:

GET /index.html HTTP/1.1\r\n
Host: www.example.com\r\n
\r\n

这让虚拟主机成为可能:一个服务器 IP 可以同时托管 blog.example.comapi.example.comwww.example.com 等多个站点,服务器根据 Host 头路由到不同的应用。

1.5 缓存增强#

HTTP/1.1 引入了强大的缓存控制机制:

ETag(实体标签):服务器为资源生成唯一标识符,客户端下次请求时带上 If-None-Match: <etag>,如果资源未变化,服务器返回 304 Not Modified(无响应体),节省带宽。

If-Modified-Since:客户端告诉服务器「我上次获取这个资源的时间是 X」,如果资源自那之后没修改,服务器返回 304。

Cache-Control:更精细的缓存控制,如 max-age=3600(缓存 1 小时)、no-cache(每次使用前验证)、private(仅供单用户缓存)等。

# 首次请求
GET /resource HTTP/1.1
Host: example.com
# 首次响应
HTTP/1.1 200 OK
ETag: "abc123"
Last-Modified: Wed, 20 Mar 2026 10:00:00 GMT
Cache-Control: max-age=3600
Content-Length: 1024
[资源内容]
# 后续请求(使用 ETag 验证)
GET /resource HTTP/1.1
Host: example.com
If-None-Match: "abc123"
# 资源未变化时的响应
HTTP/1.1 304 Not Modified
ETag: "abc123"
Cache-Control: max-age=3600
[无响应体]

1.6 内容协商#

HTTP/1.1 允许客户端表达内容偏好:

  • Accept:可接受的 MIME 类型
  • Accept-Language:偏好的自然语言
  • Accept-Encoding:支持的压缩算法
  • Accept-Charset:可接受的字符集
GET /index HTTP/1.1
Host: example.com
Accept: text/html,application/xhtml+xml
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Accept-Encoding: gzip, deflate, br

服务器根据这些头返回最合适的内容。q 值表示优先级(0-1,默认 1)。

1.7 其他重要改进#

新增方法:PUT(更新资源)、DELETE(删除资源)、OPTIONS(查询支持的方法)、TRACE(诊断)、CONNECT(代理隧道)。

新增状态码:100 Continue(继续发送请求体)、206 Partial Content(范围请求)、409 Conflict(资源冲突)、410 Gone(资源永久删除)等。

100 Continue 机制:当客户端要发送大量请求体时,可以先发送 Expect: 100-continue,服务器如果愿意接收则返回 100 Continue,客户端再发送请求体。这避免了发送大量数据后被服务器拒绝的浪费。

二、实验一:实现支持 Keep-Alive 的 HTTP/1.1 服务器#

现在来实现一个支持 HTTP/1.1 核心特性的服务器,重点展示持久连接和分块传输。

源码(保存为 http11_server.py):

#!/usr/bin/env python3
# http11_server.py -- HTTP/1.1 server with keep-alive and chunked encoding
import socket
import threading
import os
import time
import hashlib
from datetime import datetime, timezone
HOST = '0.0.0.0'
PORT = 8080
WWW = 'www'
def parse_request(data):
"""解析 HTTP/1.1 请求,返回 (method, path, version, 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, None, {}, b''
# 解析请求行
request_line = lines[0]
parts = request_line.split()
if len(parts) < 2:
return None, None, None, {}, b''
method = parts[0].upper()
path = parts[1]
version = parts[2] if len(parts) > 2 else 'HTTP/1.0'
# 解析请求头
headers = {}
for line in lines[1:]:
if ': ' in line:
key, value = line.split(': ', 1)
headers[key.lower()] = value
return method, path, version, headers, body
except Exception as e:
print(f"Error parsing request: {e}")
return None, None, None, {}, b''
def build_response(status_code, status_text, headers, body, http_version='HTTP/1.1'):
"""构建 HTTP 响应"""
response = f"{http_version} {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 build_chunked_response(status_code, status_text, headers, chunks):
"""构建分块传输响应"""
response = f"HTTP/1.1 {status_code} {status_text}\r\n"
headers['Transfer-Encoding'] = 'chunked'
for key, value in headers.items():
response += f"{key}: {value}\r\n"
response += "\r\n"
result = response.encode('iso-8859-1')
for chunk in chunks:
chunk_data = chunk if isinstance(chunk, bytes) else chunk.encode('utf-8')
result += f"{len(chunk_data):X}\r\n".encode('iso-8859-1')
result += chunk_data
result += b"\r\n"
result += b"0\r\n\r\n"
return result
def get_mime_type(path):
"""根据文件扩展名返回 MIME 类型"""
ext = os.path.splitext(path)[1].lower()
mime_types = {
'.html': 'text/html; charset=utf-8',
'.htm': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.ico': 'image/x-icon',
'.txt': 'text/plain; charset=utf-8',
}
return mime_types.get(ext, 'application/octet-stream')
def compute_etag(content):
"""计算 ETag(基于内容哈希)"""
return f'"{hashlib.md5(content).hexdigest()[:16]}"'
def format_http_date(dt):
"""格式化为 HTTP 日期格式"""
return dt.strftime('%a, %d %b %Y %H:%M:%S GMT')
def handle_conn(conn, addr):
"""处理连接(支持持久连接)"""
request_count = 0
connection_timeout = 30 # 持久连接超时时间
try:
conn.settimeout(connection_timeout)
while True:
request_count += 1
print(f"[{addr}] Waiting for request #{request_count}...")
# 读取请求数据
data = b''
while b'\r\n\r\n' not in data:
try:
chunk = conn.recv(4096)
if not chunk:
print(f"[{addr}] Client closed connection")
return
data += chunk
except socket.timeout:
print(f"[{addr}] Connection timeout after {connection_timeout}s")
return
method, path, version, headers, body = parse_request(data)
if method is None:
response = build_response(400, "Bad Request",
{"Content-Type": "text/html", "Connection": "close"},
b"<html><body><h1>400 Bad Request</h1></body></html>")
conn.sendall(response)
return
# 检查是否需要关闭连接
connection = headers.get('connection', '').lower()
should_close = connection == 'close' or version == 'HTTP/1.0'
print(f"[{addr}] {method} {path} {version} (keep-alive: {not should_close})")
# 处理 Host 头(HTTP/1.1 必需)
if version == 'HTTP/1.1' and 'host' not in headers:
response = build_response(400, "Bad Request",
{"Content-Type": "text/html", "Connection": "close"},
b"<html><body><h1>400 Bad Request</h1><p>Host header required.</p></body></html>")
conn.sendall(response)
return
response_headers = {
"Server": "HTTP11-Demo/1.0",
"Date": format_http_date(datetime.now(timezone.utc)),
}
if should_close:
response_headers["Connection"] = "close"
else:
response_headers["Connection"] = "keep-alive"
response_headers["Keep-Alive"] = f"timeout={connection_timeout}"
# 处理路径
if path == '/':
path = '/index.html'
# 演示分块传输的端点
if path == '/chunked':
chunks = [
"<html><head><title>Chunked Demo</title></head><body>\n",
f"<h1>Chunked Transfer Encoding Demo</h1>\n",
f"<p>Generated at: {datetime.now().isoformat()}</p>\n",
"<ul>\n"
]
for i in range(1, 6):
chunks.append(f"<li>Item {i}</li>\n")
time.sleep(0.1) # 模拟生成延迟
chunks.append("</ul>\n</body></html>")
response = build_chunked_response(200, "OK",
{"Content-Type": "text/html; charset=utf-8"},
chunks)
conn.sendall(response)
if should_close:
return
continue
# 演示流式日志端点
if path == '/stream':
chunks = []
for i in range(5):
chunks.append(f"[{datetime.now().isoformat()}] Log entry {i+1}\n")
time.sleep(0.2)
response = build_chunked_response(200, "OK",
{"Content-Type": "text/plain; charset=utf-8"},
chunks)
conn.sendall(response)
if should_close:
return
continue
# 安全处理:防止路径遍历攻击
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):
with open(full_path, 'rb') as f:
content = f.read()
mime = get_mime_type(full_path)
etag = compute_etag(content)
mtime = os.path.getmtime(full_path)
last_modified = format_http_date(datetime.fromtimestamp(mtime, timezone.utc))
response_headers["Content-Type"] = mime
response_headers["Content-Length"] = str(len(content))
response_headers["ETag"] = etag
response_headers["Last-Modified"] = last_modified
response_headers["Cache-Control"] = "max-age=3600"
response = build_response(200, "OK", response_headers, b'')
else:
body = b'<html><body><h1>404 Not Found</h1></body></html>'
response_headers["Content-Type"] = "text/html"
response_headers["Content-Length"] = str(len(body))
response = build_response(404, "Not Found", response_headers, b'')
conn.sendall(response)
if should_close:
return
continue
# 处理 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)
etag = compute_etag(content)
mtime = os.path.getmtime(full_path)
last_modified = format_http_date(datetime.fromtimestamp(mtime, timezone.utc))
# 检查 If-None-Match(ETag 验证)
if_none_match = headers.get('if-none-match')
if if_none_match and if_none_match == etag:
response_headers["ETag"] = etag
response_headers["Cache-Control"] = "max-age=3600"
response = build_response(304, "Not Modified", response_headers, b'')
conn.sendall(response)
print(f"[{addr}] 304 Not Modified (ETag match)")
if should_close:
return
continue
# 检查 If-Modified-Since
if_modified_since = headers.get('if-modified-since')
if if_modified_since:
try:
client_time = datetime.strptime(if_modified_since, '%a, %d %b %Y %H:%M:%S GMT')
file_time = datetime.fromtimestamp(mtime, timezone.utc)
if file_time <= client_time.replace(tzinfo=timezone.utc):
response_headers["ETag"] = etag
response_headers["Last-Modified"] = last_modified
response_headers["Cache-Control"] = "max-age=3600"
response = build_response(304, "Not Modified", response_headers, b'')
conn.sendall(response)
print(f"[{addr}] 304 Not Modified (Last-Modified)")
if should_close:
return
continue
except:
pass
response_headers["Content-Type"] = mime
response_headers["Content-Length"] = str(len(content))
response_headers["ETag"] = etag
response_headers["Last-Modified"] = last_modified
response_headers["Cache-Control"] = "max-age=3600"
response = build_response(200, "OK", response_headers, content)
conn.sendall(response)
else:
body = b'<html><body><h1>404 Not Found</h1><p>The requested resource was not found.</p></body></html>'
response_headers["Content-Type"] = "text/html"
response_headers["Content-Length"] = str(len(body))
response = build_response(404, "Not Found", response_headers, body)
conn.sendall(response)
if should_close:
return
continue
# 处理 OPTIONS 请求
if method == 'OPTIONS':
response_headers["Allow"] = "GET, HEAD, POST, OPTIONS"
response_headers["Access-Control-Allow-Origin"] = "*"
response_headers["Access-Control-Allow-Methods"] = "GET, HEAD, POST, OPTIONS"
response = build_response(200, "OK", response_headers, b'')
conn.sendall(response)
if should_close:
return
continue
# 不支持的方法
body = b'<html><body><h1>501 Not Implemented</h1></body></html>'
response_headers["Content-Type"] = "text/html"
response_headers["Content-Length"] = str(len(body))
response = build_response(501, "Not Implemented", response_headers, body)
conn.sendall(response)
if should_close:
return
except Exception as e:
print(f"Error handling connection: {e}")
import traceback
traceback.print_exc()
finally:
conn.close()
print(f"[{addr}] Connection closed (handled {request_count} requests)")
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', encoding='utf-8') as f:
f.write('''<!DOCTYPE html>
<html>
<head><title>HTTP/1.1 Demo</title></head>
<body>
<h1>HTTP/1.1 Demo Server</h1>
<p>This server demonstrates HTTP/1.1 features:</p>
<ul>
<li><a href="/page1.html">Page 1</a> - Normal page</li>
<li><a href="/chunked">Chunked Demo</a> - Chunked transfer encoding</li>
<li><a href="/stream">Stream Demo</a> - Server-sent log stream</li>
<li><a href="/data.json">JSON Data</a> - JSON with caching headers</li>
</ul>
<h2>Test Keep-Alive</h2>
<p>Use nc or curl with --http1.1 to test persistent connections.</p>
<p>Run: <code>curl -v --http1.1 http://localhost:8080/</code></p>
</body>
</html>''')
page1 = os.path.join(WWW, 'page1.html')
if not os.path.exists(page1):
with open(page1, 'w', encoding='utf-8') as f:
f.write('''<!DOCTYPE html>
<html>
<head><title>Page 1</title></head>
<body>
<h1>Page 1</h1>
<p><a href="/">Back to index</a></p>
</body>
</html>''')
json_file = os.path.join(WWW, 'data.json')
if not os.path.exists(json_file):
with open(json_file, 'w', encoding='utf-8') as f:
f.write('{"name": "HTTP/1.1 Demo", "version": "1.1", "features": ["keep-alive", "chunked", "etag", "cache-control"]}')
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.1 server listening on {HOST}:{PORT}")
print(f"Serving files from: {os.path.abspath(WWW)}")
print(f"Features: keep-alive, chunked encoding, ETag, conditional requests")
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()

代码要点说明

handle_conn 函数实现了持久连接的核心逻辑。与 HTTP/1.0 不同,这里使用 while True 循环持续监听同一连接上的多个请求。通过 conn.settimeout() 设置超时,空闲连接会在超时后自动关闭。

build_chunked_response 函数演示了分块传输编码的构建方式。每个数据块前标注十六进制长度,最后以 0\r\n\r\n 结束。

ETag 和条件请求的实现让客户端可以验证缓存的 freshness。当客户端发送 If-None-MatchIf-Modified-Since 时,服务器检查资源是否变化,未变化则返回 304,节省带宽。

Host 头验证是 HTTP/1.1 的强制要求。如果 HTTP/1.1 请求缺少 Host 头,服务器应返回 400 Bad Request。

三、实验二:用 nc 观察持久连接和分块传输#

启动服务器后,用 nc 观察 HTTP/1.1 的特性。

实验 2.1:持久连接——同一连接发送多个请求

# 使用 nc 发送多个请求(在同一连接中)
printf 'GET /index.html HTTP/1.1\r\nHost: localhost:8080\r\n\r\nGET /data.json HTTP/1.1\r\nHost: localhost:8080\r\n\r\n' | nc localhost 8080

你会看到两个响应依次返回,而且服务器日志显示这是同一个连接处理了两个请求。注意 Connection: keep-alive 响应头。

实验 2.2:显式关闭连接

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

这次响应头会包含 Connection: close,服务器处理完这个请求后会关闭连接。

实验 2.3:观察分块传输

printf 'GET /chunked HTTP/1.1\r\nHost: localhost:8080\r\n\r\n' | nc localhost 8080

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

HTTP/1.1 200 OK
Server: HTTP11-Demo/1.0
Date: Thu, 20 Mar 2026 10:00:00 GMT
Connection: keep-alive
Keep-Alive: timeout=30
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
36
<html><head><title>Chunked Demo</title></head><body>
2b
<h1>Chunked Transfer Encoding Demo</h1>
2f
<p>Generated at: 2026-03-20T10:00:00.123456</p>
6
<ul>
15
<li>Item 1</li>
15
<li>Item 2</li>
15
<li>Item 3</li>
15
<li>Item 4</li>
15
<li>Item 5</li>
a
</ul>
</body></html>
0

注意每个数据块前的十六进制数字(如 362b)表示该块的字节数,最后 0 表示传输结束。

实验 2.4:流式数据演示

printf 'GET /stream HTTP/1.1\r\nHost: localhost:8080\r\n\r\n' | nc localhost 8080

你会看到日志条目逐条到达,展示了分块传输对实时数据的支持。

实验 2.5:ETag 缓存验证

首先请求资源获取 ETag:

curl -v --http1.1 http://localhost:8080/data.json 2>&1 | grep -E '(ETag|<)'

假设获取到的 ETag 是 "abc123...",然后用这个 ETag 发送条件请求:

curl -v --http1.1 -H 'If-None-Match: "你获取的ETag值"' http://localhost:8080/data.json

如果资源未变化,你会看到 304 Not Modified 响应,没有响应体,节省了带宽。

实验 2.6:Host 头缺失测试

# 发送没有 Host 头的 HTTP/1.1 请求
printf 'GET /index.html HTTP/1.1\r\n\r\n' | nc localhost 8080

服务器会返回 400 Bad Request,因为 HTTP/1.1 要求必须有 Host 头。

实验 2.7:用 curl 验证持久连接

# curl 默认使用 HTTP/1.1 并保持持久连接
curl -v http://localhost:8080/index.html http://localhost:8080/data.json
# 观察 "Re-using existing connection" 消息
curl -v http://localhost:8080/index.html http://localhost:8080/page1.html 2>&1 | grep -i connection

五、实验三:性能对比——Keep-Alive 的实际影响#

量化持久连接带来的性能提升。

创建一个测试脚本 benchmark.sh

#!/bin/bash
# benchmark.sh - 测试持久连接 vs 非持久连接
echo "=== HTTP/1.0 风格(每次新建连接)==="
time (
for i in {1..10}; do
printf 'GET /index.html HTTP/1.0\r\n\r\n' | nc localhost 8080 > /dev/null
done
)
echo ""
echo "=== HTTP/1.1 持久连接(复用连接)==="
time (
# 使用 curl 的持久连接特性
curl -s --http1.1 http://localhost:8080/index.html \
--next --http1.1 http://localhost:8080/page1.html \
--next --http1.1 http://localhost:8080/data.json \
--next --http1.1 http://localhost:8080/index.html \
--next --http1.1 http://localhost:8080/page1.html \
--next --http1.1 http://localhost:8080/data.json \
--next --http1.1 http://localhost:8080/index.html \
--next --http1.1 http://localhost:8080/page1.html \
--next --http1.1 http://localhost:8080/data.json \
--next --http1.1 http://localhost:8080/index.html > /dev/null
)

运行测试:

chmod +x benchmark.sh
./benchmark.sh

你会观察到持久连接版本明显更快,尤其是在网络延迟较高的环境中。每次 TCP 连接建立都需要三次握手,而持久连接只需一次握手即可复用。

六、HTTP/1.1 的局限性与 HTTP/2 的动机#

虽然 HTTP/1.1 相比 1.0 有巨大改进,但它仍有不足:

队头阻塞(Head-of-Line Blocking):HTTP/1.1 要求响应必须按请求顺序返回。即使服务器已经准备好后续响应,也必须等前面的响应发送完毕。管道化理论上有帮助,但由于兼容性问题,实际很少启用。

头部冗余:每次请求都要携带完整的头部信息,对于大量小请求(如 API 调用),头部开销占比很高。

优先级缺失:无法告诉服务器「这个资源更重要,请优先传输」。

这些问题催生了 HTTP/2:二进制分帧、多路复用、头部压缩、服务器推送等特性,彻底解决了 HTTP/1.1 的队头阻塞问题。

七、观察总结#

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

理解持久连接的工作原理:HTTP/1.1 默认保持连接打开,通过 Connection: close 显式关闭。Keep-Alive 头可以配置超时时间。

掌握分块传输编码的格式:每个数据块以十六进制长度开头,以 0\r\n\r\n 结束。适用于动态生成内容或流式数据。

理解条件请求和缓存机制:ETag 配合 If-None-Match 实现高效缓存验证,304 响应节省带宽。

认识到 Host 头的重要性:虚拟主机的基础,HTTP/1.1 强制要求。

能用 Python 实现支持 HTTP/1.1 核心特性的服务器:持久连接、分块传输、ETag 缓存验证。

理解 HTTP/1.1 的局限性和 HTTP/2 的改进动机:队头阻塞问题推动协议演进。

HTTP/1.1 是 Web 发展史上最重要的里程碑之一。它确立的持久连接、分块传输、缓存控制、内容协商等机制,至今仍是 Web 性能优化的基础。理解 HTTP/1.1,是迈向 HTTP/2 和 HTTP/3 的必经之路。

八、HTTP/1.1 特性速查表#

8.1 从 HTTP/1.0 到 HTTP/1.1 的演进#

| 特性 | HTTP/1.0 | HTTP/1.1 | 改进意义 | | 默认连接行为 | 每次请求后关闭 | 保持连接打开 | 减少握手开销,显著提升性能 | | Host 头 | 可选 | 必须包含 | 支持虚拟主机,一个 IP 托管多域名 | | 分块传输 | 不支持 | Transfer-Encoding: chunked | 动态内容无需预计算长度 | | 管道化 | 不支持 | 支持(理论) | 减少请求延迟(实际部署少) | | ETag | 不支持 | 支持 | 高效的缓存验证机制 | | Cache-Control | 仅 Expires | 丰富指令 | 更精细的缓存控制 | | 条件请求 | 仅 If-Modified-Since | 新增 If-None-Match | 基于 ETag 的精确验证 | | 100 Continue | 不支持 | 支持 | 避免发送无用请求体 | | 范围请求 | 不支持 | 206 Partial Content | 断点续传、分片下载 | | 新增方法 | GET, POST, HEAD | 新增 PUT, DELETE, OPTIONS, TRACE, CONNECT | RESTful API 基础 |

8.2 持久连接相关头字段#

| 头字段 | 方向 | 用途 | 示例 | | Connection: keep-alive | 请求/响应 | 请求/声明保持连接 | Connection: keep-alive | | Connection: close | 请求/响应 | 请求/声明关闭连接 | Connection: close | | Keep-Alive | 响应 | 配置连接超时和最大请求数 | Keep-Alive: timeout=30, max=100 |

8.3 分块传输编码格式#

┌─────────────────────────────────────────────────────────────┐
│ 分块传输响应示例 │
├─────────────────────────────────────────────────────────────┤
│ HTTP/1.1 200 OK\r\n │
│ Transfer-Encoding: chunked\r\n │
│ Content-Type: text/plain\r\n │
│ \r\n │
│ 7\r\n ← 块大小(十六进制) │
│ Mozilla\r\n ← 块数据(7 字节) │
│ 9\r\n ← 下一块大小 │
│ Developer\r\n ← 块数据(9 字节) │
│ 0\r\n ← 终止块(表示结束) │
│ \r\n ← 结束标记 │
└─────────────────────────────────────────────────────────────┘

缓存控制指令速查#

| Cache-Control 指令 | 用途 | 示例 | | max-age=<seconds> | 缓存有效期(秒) | Cache-Control: max-age=3600 | | no-cache | 使用前必须验证 | Cache-Control: no-cache | | no-store | 禁止缓存 | Cache-Control: no-store | | public | 可被任何缓存存储 | Cache-Control: public | | private | 仅终端用户缓存 | Cache-Control: private | | must-revalidate | 过期后必须验证 | Cache-Control: must-revalidate | | no-transform | 禁止转换(如压缩) | Cache-Control: no-transform |

内容协商头字段#

| 请求头 | 用途 | 示例 | | Accept | 可接受的 MIME 类型 | Accept: text/html, application/json | | Accept-Language | 偏好的自然语言 | Accept-Language: zh-CN, zh;q=0.9, en;q=0.8 | | Accept-Encoding | 支持的压缩算法 | Accept-Encoding: gzip, deflate, br | | Accept-Charset | 可接受的字符集 | Accept-Charset: utf-8 |

q 值表示优先级(0-1),默认为 1。例如 en;q=0.8 表示英文优先级 0.8。

8.4 HTTP/1.1 新增状态码#

| 状态码 | 含义 | 典型场景 | | 100 | Continue | 客户端可继续发送请求体 | | 206 | Partial Content | 范围请求成功 | | 409 | Conflict | 资源状态冲突 | | 410 | Gone | 资源已永久删除 | | 413 | Payload Too Large | 请求体超过服务器限制 | | 414 | URI Too Long | URL 过长 | | 415 | Unsupported Media Type | 不支持的 Content-Type | | 417 | Expectation Failed | Expect 头无法满足 |

8.5 HTTP/1.1 vs HTTP/2 关键差异预览#

| 特性 | HTTP/1.1 | HTTP/2 | | 传输格式 | 文本协议 | 二进制分帧 | | 多路复用 | 队头阻塞 | 独立流,无队头阻塞 | | 头部压缩 | 每次完整传输 | HPACK 压缩 | | 服务器推送 | 不支持 | Server Push | | 优先级 | 不支持 | 流优先级 | | 连接复用 | Keep-Alive(串行) | 多路复用(并行) |


参考#

  • RFC 2068 — Hypertext Transfer Protocol — HTTP/1.1
  • RFC 2616 — Hypertext Transfer Protocol — HTTP/1.1
  • RFC 7230 — HTTP/1.1 Message Syntax and Routing

支持与分享

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

HTTP/1.1:持久连接
https://blog.souloss.com/posts/web/http-1-1/
作者
Souloss
发布于
2023-11-10
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时