在上一篇实验中,体验了 HTTP/1.0 带来的改进:请求头与响应头、状态码、多种请求方法。这些特性让 HTTP 变得可扩展、可程序化处理。然而,HTTP/1.0 在实际使用中暴露了一个严重的性能瓶颈。
想象这样一个场景:你访问一个包含 20 张图片的网页。在 HTTP/1.0 中,每张图片都需要:
- 建立 TCP 连接(三次握手)
- 发送 HTTP 请求
- 接收 HTTP 响应
- 关闭 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 托管多个域名)奠定了基础。
缓存增强:引入 ETag、If-None-Match、If-Modified-Since 等条件请求头,让缓存更加智能高效。
内容协商:客户端可以通过 Accept-Language、Accept-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\nHost: localhost:8080\r\n\r\n
# 显式关闭连接GET /index.html HTTP/1.1\r\nHost: localhost:8080\r\nConnection: 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\nTransfer-Encoding: chunked\r\n\r\n7\r\nMozilla\r\n9\r\nDeveloper\r\n7\r\nNetwork\r\n0\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 -> 响应3HTTP/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\nHost: www.example.com\r\n\r\n这让虚拟主机成为可能:一个服务器 IP 可以同时托管 blog.example.com、api.example.com、www.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.1Host: example.com
# 首次响应HTTP/1.1 200 OKETag: "abc123"Last-Modified: Wed, 20 Mar 2026 10:00:00 GMTCache-Control: max-age=3600Content-Length: 1024[资源内容]
# 后续请求(使用 ETag 验证)GET /resource HTTP/1.1Host: example.comIf-None-Match: "abc123"
# 资源未变化时的响应HTTP/1.1 304 Not ModifiedETag: "abc123"Cache-Control: max-age=3600[无响应体]1.6 内容协商
HTTP/1.1 允许客户端表达内容偏好:
Accept:可接受的 MIME 类型Accept-Language:偏好的自然语言Accept-Encoding:支持的压缩算法Accept-Charset:可接受的字符集
GET /index HTTP/1.1Host: example.comAccept: text/html,application/xhtml+xmlAccept-Language: zh-CN,zh;q=0.9,en;q=0.8Accept-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 encodingimport socketimport threadingimport osimport timeimport hashlibfrom datetime import datetime, timezone
HOST = '0.0.0.0'PORT = 8080WWW = '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-Match 或 If-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 OKServer: HTTP11-Demo/1.0Date: Thu, 20 Mar 2026 10:00:00 GMTConnection: keep-aliveKeep-Alive: timeout=30Content-Type: text/html; charset=utf-8Transfer-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注意每个数据块前的十六进制数字(如 36、2b)表示该块的字节数,最后 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(串行) | 多路复用(并行) |
参考
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






