一条 HTTP 请求只有 73 字节,比 RTSP 还小。 它天生「一问一答、答完就完」,所以不需要会话管理。
8.1、8.2 那两套是「专门的直播协议」,从 8.3 起换成最常见的 HTTP。 它本来不是为直播设计的,结果现在直播大半走它——原因在这个协议的性格里。
一、一条请求有多小
最简的一条 HTTP 请求,三行就够:
部分 字节
请求行 22 Host 头 22 User-Agent 头 27 整条请求 73
比 RTSP 那 93 字节还少 20 字节。
少在哪:URI 短(没有 rtsp://主机:端口),也没有 CSeq 这种会话头。
HTTP 1.1 里 Host 是必需的,因为一台机器上可能放好几个网站。
得靠 Host 区分你要找的是哪一个,这也是「虚拟主机」能存在的原因:一个 IP,几百个域名。
User-Agent: Lavf62.12.102 跟 RTSP 一样,libav 系的工具都会带。
服务器日志里看到 Lavf,就知道来的是 ffmpeg 或 ffplay。
二、三个状态码就够用
拿同一份文件试四种请求,返回三种状态码:
请求 状态 长度
GET /s7.mp4 200 OK 48741 HEAD /s7.mp4 200 OK 48741 GET /nope.mp4 404 Not Found 0 GET 加 Range 0-99 206 Partial 100
HEAD 的返回码与长度跟 GET 一模一样,但一个体的字节都不发。
这就是「先问大小再决定下不下」的用法——播放器常用它探文件。
404 很简单:文件不存在。 但直播里更常见的是 403 与 401:前者是「有权限但不许」,后者是「没登录」。
206 是本节的主角:它表示「你要的那一段,我给你了」。 这个状态码决定了「能不能拖进度条」。
三、Range 是 HTTP 的杀手锏
HTTP 能拖进度条,全靠一个请求头:
Range: bytes=0-99
它的意思是「我只要第 0 到第 99 字节」。服务器要回 206,并在头里带上
Content-Range: bytes 0-99/48741,告诉客户端「这段是哪一段」。
服务器行为 客户端看到什么
回 206 加 Content-Range 能跳读,可以拖进度条 忽略 Range 回 200 整份发过来,不支持拖 回 416 越界了,这段没有
很多静态服务器其实支持 Range,但不发 Accept-Ranges: bytes。
客户端看不到这个头,就不敢发 Range——所以「支持」和「敢用」是两件事。
8.3.2 会实测一件事:libav 一上来就发 Range: bytes=0-,
不是裸 GET。它默认就想要一个能随机访问的连接。
四、为什么直播也用它
HTTP 本来完全不适合直播:它是「要一个文件」的协议。 但现在的直播大半走 HTTP,理由只有两个字:能穿。
项 HTTP RTSP/RTP
端口 80/443 554/5004 等 过防火墙 基本都放行 经常被拦 能用 CDN 能,就是静态文件 很难 延迟 偏大(要缓冲) 偏小
第三行是关键:CDN 天生只会缓存 HTTP 文件。 HLS 之所以流行,就是因为它把直播切成了「一堆静态文件」,CDN 直接能用。
代价是延迟变大:切片要攒够一段才能发,所以 HLS 的延迟通常五秒起。 要更低延迟就得用 HTTP-FLV 那类「永不结束的响应」,牺牲的是 CDN 友好度。
五、动手 Demo:手写四条请求看状态码
下面这段代码量一条请求的分段字节,再手写四条请求打到本地服务器上, 分别看 200、404、206 三种返回。
import http.server, os, socket, threading
class H(http.server.BaseHTTPRequestHandler):
u"""最小 HTTP 服务:支持 HEAD 与 Range。"""
def log_message(self, *a):
pass
def do_HEAD(self):
self._send(False)
def do_GET(self):
self._send(True)
def _send(self, body):
name = self.path.strip("/").split("/")[-1]
p = os.path.join(os.getcwd(), name)
if not os.path.exists(p):
self.send_response(404)
self.send_header("Content-Length", "0")
self.end_headers()
return
n = os.path.getsize(p)
rng, code, start, cnt = self.headers.get("Range"), 200, 0, n
if rng:
a, _b, b = rng.split("=")[1].partition("-")
start = int(a or 0)
cnt = (min(int(b or n - 1), n - 1) - start + 1)
code = 206
self.send_response(code)
self.send_header("Content-Length", str(cnt))
if code == 206:
self.send_header("Content-Range",
"bytes %d-%d/%d" % (start, start + cnt - 1, n))
self.end_headers()
if body:
with open(p, "rb") as f:
f.seek(start)
self.wfile.write(f.read(cnt))
srv = http.server.ThreadingHTTPServer(("127.0.0.1", 8000), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()
req = (u"GET /s7.mp4 HTTP/1.1\r\n"
u"Host: 127.0.0.1:8000\r\n"
u"User-Agent: Lavf62.12.102\r\n"
u"\r\n")
print(u"请求行 %d 字节" % (len(u"GET /s7.mp4 HTTP/1.1") + 2))
print(u"整条 %d 字节" % len(req))
CASES = (u"GET /s7.mp4 HTTP/1.1\r\nHost: x\r\n\r\n",
u"HEAD /s7.mp4 HTTP/1.1\r\nHost: x\r\n\r\n",
u"GET /nope.mp4 HTTP/1.1\r\nHost: x\r\n\r\n",
u"GET /s7.mp4 HTTP/1.1\r\nHost: x\r\nRange: bytes=0-99\r\n\r\n")
for line in CASES:
s = socket.create_connection(("127.0.0.1", 8000), 3.0)
s.sendall(line.encode())
buf = b""
while True:
d = s.recv(65535)
if not d:
break
buf += d
if len(buf) > 4000:
break
s.close()
head = buf.split(b"\r\n\r\n")[0].decode("utf-8", "replace")
rows = head.split("\r\n")
clen = [x.split(":")[1].strip() for x in rows
if x.lower().startswith("content-length")]
print(u"%-28s %-22s %s" % (line.split(u"\r\n")[0],
rows[0].split(" ", 1)[1],
clen[0] if clen else "—"))
srv.shutdown()
srv.server_close()
实跑输出:
--------------------------------------------------------------------
8.3.1 HTTP 简介
--------------------------------------------------------------------
**① 一条 HTTP 请求有多小**
部分 字节
--------------------------
请求行 22
Host 头 22
User-Agent 头 27
整条请求 73
**比 RTSP 还小:HTTP 只要一个 Host 头就能开工,**
**因为它天生就是「一问一答、答完就完」,不需要会话管理。**
**② 手写三条请求,看三种状态码**
请求 状态行 长度
------------------------------------------------------------
GET /s7.mp4 HTTP/1.1 200 OK 48741
HEAD /s7.mp4 HTTP/1.1 200 OK 48741
GET /nope.mp4 HTTP/1.1 404 Not Found 0
GET /s7.mp4 HTTP/1.1 206 Partial Content 100
**`HEAD` 的返回码跟 `GET` 一样、长度也报出来了,但一个体字节都不发。**
**这就是「先问大小再决定要不要下」的用法。**
**③ Range 请求要服务器配合**
**最后一条请求带了 `Range: bytes=0-99`,服务器回了 206 与 `Content-Range`。**
**换成不会 Range 的服务器,它会忽略这个头、直接回 200 加整个文件。**
**客户端要能分辨:回了 200 就说明「你要的那段我不管,整份给你」。**
**④ 直播为什么不用普通 HTTP 文件服务**
**普通 HTTP 是「我要一个文件」,服务端算好长度、发完就断开。**
**直播是「我要一条永不结束的响应」——长度算不出来,只能边生成边发。**
**这就是 8.3.4 要试的 HTTP-FLV/HTTP-TS 那套。**
第一条打印 22 字节、整条 73 字节,跟节里的表对得上。 七十三字节就能开始下一部电影,这就是 HTTP 的分量。
四条请求的返回分别是 200、200、404、206。 注意第二条(HEAD)也是 200——它跟 GET 的返回码一样,只是不发体。
第四条带了 Range,服务器回 206,长度只有 100 字节。 这就是「只要一段」的效果,进度条能拖,全靠它。
30 秒实验
- 把 Range 改成 100-199:看 Content-Range 与长度是不是跟着变。
- 发一条 Range 超过文件长度:看服务器回 416 还是回 200。
- 去掉 Host 头:HTTP 1.1 服务器会回 400,因为不知道你要哪个站。
常见误区
- 以为 HEAD 不返回长度。它照样报 Content-Length,只是不发体。
- 以为支持 Range 就一定能拖。还要发 Accept-Ranges,客户端才敢用。
- 以为 HTTP 天生适合直播。它是靠「能穿防火墙、能上 CDN」赢的。
自测三问(点开对答案)
1. 一条最简 HTTP 请求多大? —— 73 字节:请求行 22、Host 22、User-Agent 27,还有一个空行。 2. 206 是什么意思? —— 部分内容。服务器按 Range 只回了一段。 3. 直播为什么愿意用 HTTP? —— 端口能穿防火墙,而且能走 CDN——代价是延迟偏大。- ☐ (本节完,下一节 8.3.2 HTTP 流媒体)