流媒体服务器只做一件事:收一路流,转给很多人。 搭起来至少要过四关——端口在听、握手能通、命令能认、媒体能收。
这是第八章唯一没有小节的一节,也是本章的收口。 8.1 到 8.4 讲的那些协议,最后都要落到一台服务器上。
一、三条路线
自己搭服务器,绕不开这三个选择。
方案 擅长 不擅长
SRS RTMP/HLS 一把抓,配置简单 高并发要调 ZLMediaKit 协议最全,HTTP-FLV 强 配置项多 Nginx-rtmp 跟网站混布,运维熟 版本老,功能少
三家做的是同一件事,差别只在「支持哪些协议」与「配置有多细」。 选型时先问自己一个问题:客户端是什么?
客户端是浏览器,就必须有 HTTP-FLV 或 HLS; 客户端是 VLC 或 ffplay,RTMP 与 RTSP 就够了; 客户端是手机 App,还要考虑低延迟与弱网重连。
二、端口是认协议的暗号
每种协议都有一个约定俗成的端口,看到端口就能猜到对面在跑什么。
端口 协议 干什么
1935 RTMP 推流与拉流的老家 554 RTSP 控制指令 8554 RTSP 不用 root 的替代端口 8080 HTTP 网页、HLS、HTTP-FLV 1936 RTMP over TLS 加密的 RTMP
554 是 RTSP 的标准端口,但它小于 1024, 非 root 用户绑不上,所以开发机上普遍用 8554。
「服务器起没起来」可以先看端口:端口在听,说明进程活着一半。 另一半是「能不能收流」,那得真推一路才知道。
三、最小服务器:RTMP 握手
要理解服务器在做什么,最好的办法是自己当一次服务器。 下面用一段代码顶替 SRS,只做 RTMP 握手。
阶段 方向 字节
C0 版本号 客户端→服务器 1 C1 随机数 客户端→服务器 1536 S0 加 S1 加 S2 服务器→客户端 3073 C2 回应 客户端→服务器 1536
服务器收到的 C0 是 0x03,C1 长度 1536 字节,加起来正好 1537。
回给客户端的 3073 字节里,前 1537 是 S0 与 S1,后 1536 是 S2。
S2 就是把 C1 原样抄回去——这是 RTMP 握手的核心: 双方各发一段随机数,再各自抄回来,谁也不验证内容, 只确认「来回通了」。所以握手快,但也不防中间人。
0x03 这个版本号是明文 RTMP,加密版是 0x06。
版本号不对,正经服务器会直接断开,连错误信息都不给。
四、握手之后还有 chunk
握手只证明链路通,业务一点没做。真正的力气在后面这层。
层 干什么 字节
握手 确认链路通 3073 chunk 分块 把消息切成小段 每段最多 128 命令消息 connect/createStream/publish 几十字节 媒体消息 FLV 标签 按帧变
chunk 的默认大小只有 128 字节,一条命令要切好几段才能发完。 连上之后双方可以协商一个更大的 chunk size,把开销压下去。
所以「服务器搭好了」这件事,最少要过四关: 端口在听、握手能通、命令能认、媒体能收。 前三关全过、第四关卡住,就是最常见的「推流失败」: 日志里连接成功、命令也认了,但一直没有画面。
五、动手 Demo:自己当一次服务器,跑完 RTMP 握手
下面这段起一个只做握手的最小服务器,再用一个客户端连上去。 服务器不认任何命令,握完就关——够用来验证「四关里的第二关」。
import socket, struct, threading, time
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 0))
port = srv.getsockname()[1]
srv.listen(1)
got = {}
def serve():
srv.settimeout(8.0)
c, _a = srv.accept()
c.settimeout(8.0)
buf = b""
while len(buf) < 1537: # C0 一个字节 + C1 1536 个
d = c.recv(4096)
if not d:
break
buf += d
got["c0"] = buf[0]
got["c1"] = len(buf) - 1
# S0 + S1 + S2:版本号 + 1536 个 0 + 把 C1 原样抄回去
c.sendall(bytes([3]) + struct.pack(">II", 0, 0)
+ b"\x00" * 1528 + buf[1:1537])
try:
got["c2"] = len(c.recv(1536))
except Exception:
got["c2"] = 0
c.close()
th = threading.Thread(target=serve)
th.daemon = True
th.start()
time.sleep(0.2)
cli = socket.create_connection(("127.0.0.1", port), 3.0)
c1 = struct.pack(">II", 0, 0) + b"\x11" * 1528
cli.sendall(bytes([3]) + c1)
s = b""
cli.settimeout(5.0)
while len(s) < 3073:
d = cli.recv(4096)
if not d:
break
s += d
cli.sendall(c1[:1536])
cli.close()
th.join(6.0)
srv.close()
print(u"C0=0x%02X C1=%d 字节 S0S1S2=%d 字节"
% (got["c0"], got["c1"], len(s)))
实跑输出:
--------------------------------------------------------------------
8.5 流媒体服务器的搭建
--------------------------------------------------------------------
**① 三条路线,各自适合什么**
方案 擅长 不擅长
----------------------------------------------------------------
SRS RTMP/HLS 一把抓,配置简单 高并发要调
ZLMediaKit 协议最全,HTTP-FLV 强 配置项多
Nginx-rtmp 跟网站混布,运维熟 版本老,功能少
**三家都做同一件事:收一路流,转给很多人。**
**差别在「支持哪些协议」与「配置有多细」。**
**② 端口是认协议的暗号**
端口 协议 干什么
---------------------------------------------------
1935 RTMP 推流与拉流的老家
554 RTSP 控制指令
8554 RTSP 不用 root 的替代端口
8080 HTTP 网页、HLS、HTTP-FLV
1936 RTMP over TLS 加密的 RTMP
**所以「服务器起没起来」可以用端口判断:端口在听,说明进程活着一半。**
**另一半是「能不能收流」,那得真推一路才知道。**
**③ 一个服务器最少要做什么:用 RTMP 握手演示**
阶段 方向 字节
---------------------------------------------
C0 版本号 客户端→服务器 1
C1 随机数 客户端→服务器 1536
S0 + S1 + S2 服务器→客户端 3073
C2 回应 客户端→服务器 1536
**服务器收到 C0 是 `0x03`,C1 长度 1536 字节——对得上 1537 这个总数。**
**S2 就是「把 C1 原样抄回去」,这是 RTMP 握手的核心:**
**双方各发一段随机数,再各自抄回来,谁也不验证内容,只确认「来回通了」。**
**④ 握手之后还有一层:chunk**
层 干什么 字节
------------------------------------------------------------------
握手 确认链路通 3073
chunk 分块 把消息切成小段 每段最多 128
命令消息 connect/createStream/publish 几十字节
媒体消息 FLV 标签 按帧变
**所以「服务器搭好了」这件事,最少要过四关:**
**端口在听、握手能通、命令能认、媒体能收。**
**前三关全过、第四关卡住,就是最常见的「推流失败」。**
C0 读出来是 0x03,C1 是 1536 字节,服务器回的 3073 也分毫不差。
这段代码故意不认 connect 命令——所以真正的客户端连上来会卡在第三关。
「握手能通、命令不认」正是调服务器时最典型的中间状态。
30 秒实验
- 把 C0 改成
0x06:看客户端会不会因为版本号不认识而断开。 - 把 S2 改成全 0:正经客户端会拒绝,「抄回去」这一步是有用的。
- 把
listen(1)改成listen(5):同时连三路,看第几路被拒。
常见误区
- 以为端口在听就是服务器好了。端口只说明进程活着,不说明能收流。
- 用 554 起 RTSP 却不用管理员权限。小于 1024 的端口绑不上。
- 把握手当成鉴权。S2 只是把 C1 抄回来,谁都能通过。
自测三问(点开对答案)
1. RTMP 握手一共几个字节? —— 客户端发 1537(C0 加 C1),服务器回 3073(S0 加 S1 加 S2),客户端再回 1536(C2)。 2. 为什么 RTSP 开发商普遍用 8554? —— 554 小于 1024,非 root 用户绑不上,8554 是常用的替代端口。 3. 「推流失败」最常见的样子是什么? —— 端口通、握手成、命令也认了,就是没有媒体数据——卡在第四关。- ☐ 8.5 完,下一节 8.6.1 RTMP 简介