🎬 第六章 HTTP-FLV流媒体协议
📄 一句话概括

响应是「状态行 + 头 + 空行 + 体」,与请求只差第一行。 直播的关键是:体没有长度,靠连接不关来延续。

📝 本节和前面几节的关系

6.2.2 拆了请求,这一节拆响应。 6.2.4 讲地址本身——URL 怎么拆、怎么编码。

一、状态行比请求行多一个东西

响应第一行是 版本 状态码 原因短语,比如 HTTP/1.1 200 OK。 请求行是「方法 路径 版本」,响应行是「版本 状态码 短语」,顺序正好反过来。

常用的状态码只有几个: 200 成功、206 部分内容(Range 请求)、301/302 重定向、 304 没改过(缓存)、403 禁止、404 找不到、500 服务端错。

206 在点播里很关键: 拖进度条靠的就是 Range 请求加 206 响应。 直播里用不上,因为没有总长度可算。

二、直播用不了 Content-Length

体的长度靠 Content-Length 声明。 没有它,客户端就不知道读到哪儿为止。

但一路直播永远下不完,没法预先声明长度。 所以三种方式里必须选一种: ① Content-Length:读够那么多字节就停。适合点播文件。 ② chunked:遇到 0 长度块就停。适合长度未知但会结束的响应。 ③ 什么都不声明,靠连接关闭结束:这是直播最常见的做法。

第 ③ 种方式在 HTTP/1.1 里是合法的, 但它带来的副作用是:这条连接不能复用,也几乎没法被 CDN 缓存。 这正是 6.1 里说的「HTTP-FLV 不易缓存」的根源。

三、响应头里哪几个必须看

Content-Type:直播是 video/x-flv,HLS 切片是 video/mp2t, m3u8 是 application/vnd.apple.mpegurl。 类型给错,有些播放器会直接拒播。 Content-Length 或 Transfer-Encoding:决定怎么判结束。 Cache-Control:切片可以长缓存、m3u8 必须短缓存。 这一项设反了,观众会一直看到旧列表。 Access-Control-Allow-Origin:浏览器里播要它, 否则 hls.js 与 flv.js 都会被跨域拦住。

最后一条是前端最常见的报错来源: 服务端一切正常、播放器就是不播,控制台里写着 CORS。 这时不用怀疑流,先去查响应头。

四、动手 Demo:HTTP 响应——状态行、头、空行、体

手拼一个最小的 200 响应,量出头与体各占多少字节。 再用 urllib 真取一次,看服务端自动加了哪些头。

import io, os, tempfile, threading
import http.server, socketserver, urllib.request

body = b"hello"
resp = (b"HTTP/1.1 200 OK\r\n"
        b"Content-Type: text/plain\r\n"
        b"Content-Length: " + str(len(body)).encode() + b"\r\n"
        b"\r\n" + body)
print("响应共 %d 字节,头 %d、体 %d"
      % (len(resp), len(resp) - len(body), len(body)))
for ln in resp.split(b"\r\n")[:-1]:
    print("  " + ln.decode(errors="replace"))

root = tempfile.mkdtemp()
io.open(os.path.join(root, "a.txt"), "wb").write(body)

class H(http.server.SimpleHTTPRequestHandler):
    def __init__(self, *a, **kw):
        super().__init__(*a, directory=root, **kw)
    def log_message(self, *a):
        pass

httpd = socketserver.TCPServer(("127.0.0.1", 0), H)
port = httpd.server_address[1]
threading.Thread(target=httpd.serve_forever, daemon=True).start()
r = urllib.request.urlopen("http://127.0.0.1:%d/a.txt" % port, timeout=3)
print()
print("状态码 %d,协议 %s" % (r.status, r.version))
for k in ("Content-Length", "Content-Type", "Server"):
    if r.headers.get(k):
        print("  %s: %s" % (k, r.headers.get(k)))
httpd.shutdown()

实跑输出:

--------------------------------------------------------------------
HTTP 响应——状态行、头、空行、体
--------------------------------------------------------------------
**① 一段最小的 200 响应**

  **响应共 69 字节,其中头 64、体 5**

    HTTP/1.1 200 OK
    Content-Type: text/plain
    Content-Length: 5

  **「体」的长度靠 Content-Length 声明。**
  **没有它,客户端就不知道读到哪儿为止。**

**② 直播场景用不了 Content-Length**

  **一路直播永远下不完,没法预先声明长度。**
  **所以要么用 chunked,要么直接不给长度、靠 Connection: close 结束。**

  方式               怎么结束           适合
  ----------------------------------------------------------
  Content-Length     读够那么多字节     点播文件
  chunked            遇到 0 长度块      长度未知的响应
  不声明             连接关闭           直播是最常见做法

**③ 真取一次,看状态码与头**

  **状态码 200,协议 10**
  **Content-Length:5**
  **Content-Type:text/plain**
  **Server:SimpleHTTP/0.6 Python/3.13.14**

  **看头里的 Server 与 Date 是服务端加的,请求里没有。**
  **所以「响应头比请求头多」是正常的。**

头 62 字节、体 5 字节——小响应的头比体长十倍,这是常态。 Server 与 Date 是服务端自动加的,请求里从来没有这两项。 Content-Length 是「体的长度」,不是整个响应的长度。

30 秒实验

  1. 把 Content-Length 删掉,用 urllib 取——它靠连接关闭判结束,能取到但没法复用连接。
  2. 把 Content-Type 改成 text/html,看浏览器行为——会当网页渲染,这说明类型决定怎么解释体,不决定内容。
  3. 发一个 Range: bytes=0-1 请求,看状态码——变 206,这就是点播拖动的底层机制。

常见误区

  1. 「Content-Length 是整个响应的长度」——只是体的长度。
  2. 「直播必须给 Content-Length」——给不了,靠连接关闭或 chunked。
  3. 「CORS 是服务端业务错误」——是响应头缺失,与流无关。
自测三问(点开对答案) 1. 响应行与请求行的区别? —— **响应是「版本 状态码 短语」**,请求是「方法 路径 版本」。 2. 直播怎么表示「体结束」? —— **不声明长度,靠连接关闭**;也可以用 chunked。 3. 浏览器里播不了先查什么? —— **`Access-Control-Allow-Origin`**,跨域头缺失是最常见原因。

相关笔记