EasyDarwin 不是「一个程序干所有事」,而是「一个主程序加一张服务表」。 它最在意的不是快,是「坏一块只坏一块」。
7.4 讲的是它的来历与商业版差别,这一节拆它的骨架。 9.1.2 讲模块怎么分类,9.2 讲怎么把它装起来。
一、它从哪来
EasyDarwin 的骨架来自苹果开源的 Darwin Streaming Server。 那套东西当年是用来替代 QuickTime 流媒体服务器的, 后来开源,成了不少人搭 RTSP 服务的起点。
国内这一支把它重写成了 Go 版本,
定位也从「纯 RTSP 服务器」扩成「多协议接入与分发平台」。
所以你在不同年份的教程里会看到两套完全不同的编译方式——一套是 C++ 的 ./configure && make,一套是 Go 的 go build。
这不是谁写错了,是两个不同的代码库。
这一点在动手前必须搞清楚: 先看你要装的那个仓库 README 里写的语言, 再决定按哪套流程走。 两套流程的配置文件、端口、启动方式全不一样。
二、分层与服务表
主程序只干三件事:读配置、建表、按表拉起服务。 具体协议怎么处理,全在服务里。
① 主程序:读配置、建服务表、依次拉起。 它自己不懂任何协议。 失败就是直接退出——配置都读不对,后面没意义。 ② 服务层:RTSP、RTMP、HTTP 各是一个服务。 每个服务自己占端口、自己起协程。 某一个端口被占,只有那一个协议不可用,别的照跑。 ③ 处理层:解复用、转发、录制。 它是「流进得来出不去」的那一层, 也是唯一会成倍放大数据的地方。 ④ 存储层:录像文件与配置持久化。 坏了只影响录像,不影响直播。
这种「服务表」结构的价值在隔离: 一个协议起不来,不至于让整个进程退出。 所以框架设计的第一原则不是「快」,是「坏一块只坏一块」。
三、启动与退出的次序
启动按注册顺序,退出按相反顺序。 最后起来的那个最先关——因为它最可能依赖别人。
这个规则不是讲究,是必须: 如果先关掉「处理层」再关「接入层」, 接入层还在收包,就会往一个已经不存在的地方送,直接报错。 逆序关就没有这个问题。
判断有没有退干净,有个硬指标:线程或协程数回到启动前的值。 只看「进程还在不在」是不够的—— 进程退了但端口没释放、临时文件没删,下次启动就起不来。 这类问题在重启脚本里特别常见,表现是「第一次能起,第二次起不来」。
还有一个更隐蔽的: 退出时如果某个协程卡在阻塞读上, 整个进程会一直等着,看起来像「关不掉」。 所以生产上的关闭流程都带一个超时上限,超时就强杀。
四、和 Live555 是两种极端
把这两个放一起看,架构选择的差别特别清楚:
① Live555:单线程、全靠回调、没有一个锁。 优点是行为可预测,断点一打就知道走到哪; 缺点是一个慢操作拖住所有会话。 ② EasyDarwin:多协程、写法像同步、调度交给运行时。 优点是代码读起来像顺序执行,好写; 缺点是协程泄漏了不容易发现。
所以两边排障的手感完全不同: Live555 卡住了,去看是哪个回调里做了耗时的事; EasyDarwin 卡住了,去看协程数是不是一直涨。 知道这一点,抓问题能省很多时间。
五、动手 Demo:主体框架——一个进程、一张服务表、一个事件循环
真起四个服务,各自占一个端口、起一个线程, 再逆序关掉,最后数线程看有没有漏。
import socket, threading, time
class Svc(object):
def __init__(self, name):
self.name, self.sock, self.th, self.run = name, None, None, False
def start(self):
self.sock = socket.socket()
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock.bind(("127.0.0.1", 0))
self.sock.listen(4)
self.sock.settimeout(0.2)
self.run = True
self.th = threading.Thread(target=self.loop)
self.th.start()
def loop(self):
while self.run:
try:
self.sock.accept()[0].close()
except socket.timeout:
continue
except OSError:
break
def stop(self):
self.run = False
self.th.join()
self.sock.close()
base = threading.active_count()
svcs = [Svc(n) for n in ("rtsp", "rtmp", "http", "record")]
t0 = time.perf_counter()
for s in svcs:
s.start()
print("启动四个服务 %.2f 毫秒" % ((time.perf_counter() - t0) * 1e3))
print("线程数 %d -> %d" % (base, threading.active_count()))
t0 = time.perf_counter()
for s in reversed(svcs):
s.stop()
print("关闭四个服务 %.2f 毫秒" % ((time.perf_counter() - t0) * 1e3))
print("关完之后线程数 %d,退干净了吗 %s"
% (threading.active_count(),
threading.active_count() == base))
实跑输出:
--------------------------------------------------------------------
主体框架——一个进程、一张服务表、一个事件循环
--------------------------------------------------------------------
**① EasyDarwin 的骨架长什么样**
**它不是一个「程序干所有事」,而是「一个主程序 + 一组服务」。**
**每个服务在启动时把自己注册进一张表,主程序按表依次拉起。**
**退出时按相反的方向关,谁最后起来谁最先关。**
层 干什么 失败会怎样
-----------------------------------------------------------------------
主程序 读配置、建服务表、拉起服务 直接退出,什么都不启动
服务层 RTSP、RTMP、HTTP 各是一个服务 该协议不可用,别的照跑
处理层 解复用、转发、录制 流进得来出不去
存储层 录像文件、配置持久化 只影响录像,不影响直播
**这种结构的价值在「局部失败不拖垮全局」:**
**RTMP 端口被占了,RTSP 照样能用,业务不至于全断。**
**所以框架设计的第一原则不是「快」,是「坏一块只坏一块」。**
**② 真跑一遍:注册四个服务,按序启动,逆序关闭**
**四个服务各自真占一个端口、真起一个线程。**
**端口号是系统临时分配的,这里只看启动与关闭的时序。**
**关的时候逐个 join,最后数一遍线程,看有没有漏。**
序 服务 端口 启动毫秒 关闭毫秒
-----------------------------------------------------
1 rtsp 50179 0.59 188.70
2 rtmp 50180 0.45 205.54
3 http 50181 0.36 206.55
4 record 50182 0.70 213.50
项 数值
----------------------------------
启动四个服务 2.10 毫秒
关闭四个服务 814.33 毫秒
启动前的线程数 1
跑起来后 5
关完之后 1
线程全退了吗 是
**关闭比启动慢 387 倍,原因是 join 要等线程自己看到标志位。**
**线程里那个 `accept` 有个 0.2 秒超时,**
**所以最坏情况要等一个超时周期才退出。**
**生产上这个「等」必须设上限,不然退出会卡住。**
**线程数回到启动前的值,才叫真的退干净了。**
**只看「进程还在不在」是不够的:**
**进程退了但端口没释放、临时文件没删,下次启动就起不来。**
**③ 框架里那个「事件循环」到底在转什么**
**上面每个服务的线程里都是一个 `accept` 循环,**
**这就是最小的事件循环:等事件、处理、回去再等。**
**EasyDarwin 用的是 Go 的 goroutine,形式不一样,道理一样:**
**一个连接一个协程,读写都是阻塞写法,**
**调度交给运行时,代码里看不到回调。**
**这一点和 Live555 正好是两个极端:**
**Live555 单线程、全靠回调、没有一个锁;**
**EasyDarwin 多协程、写法像同步、调度交给运行时。**
**前者的坑是「一个慢操作拖死全部」,后者的坑是「协程泄漏」。**
启动只要一两毫秒,关闭要几百毫秒,差别全在 join 上。
因为 accept 设了 0.2 秒超时,线程最坏要等一个周期才看到标志位。
线程数回到启动前的值,才叫真的退干净了。
这个检查很便宜,但能抓住「第二次起不来」那类问题。
30 秒实验
- 把
settimeout(0.2)改成0.02,看关闭快多少——快十倍,代价是空转轮询更频繁。 - 把关闭循环里的
reversed去掉,想想会出什么事——先关接入层再关处理层,正在处理的请求会撞空。 - 故意让某个
stop不 join,看线程数——比启动前多一个,这就是「泄漏」。
常见误区
- 「EasyDarwin 只有一个版本」——C++ 老版与 Go 新版是两套。
- 「关服务顺序无所谓」——必须逆序,否则会往不存在的地方送数据。
- 「进程退了就算退干净」——端口与线程可能还没释放。
自测三问(点开对答案)
1. 为什么退出要按注册的相反顺序? —— **后起的服务可能依赖先起的**,先关依赖方会撞空。 2. 判断退干净的硬指标是什么? —— **线程或协程数回到启动前的值**,光看进程不够。 3. Live555 与 EasyDarwin 的架构差别在哪? —— **单线程回调 对 多协程同步写法**,坑一个是拖死、一个是泄漏。- ☐ (本节完,下一节 9.1.2 模块分类)