在搞定中年大叔的第一台DIY的NAS后,要做的就是怎么做内网穿透和进行P2P相关的资源上传和下载,这个过程接触到了NAT类型,UPnP以及P2P相关的技术,下面是基于Claude和相关技术的RFC,官方文档整理的相关知识,相互学习和知识沉淀。
NAT
什么是NAT,NAT 为什么存在?
NAT(Network Address Translation)是一种将私有网络地址(如局域网内的 IP)转换为公网网络地址的技术,在1994年的RFC1631提出。它允许多台设备共享同一个公网 IP 访问互联网,在缓解 IPv4 地址枯竭的同时提升了内部网络的安全性
NAT 的诞生源于一个现实问题:IPv4 地址不够用了。Pv4 地址是 32 位的,理论上总共约 43 亿个地址。在互联网早期(1980 年代),这看起来绰绰有余。但随着全球设备爆炸式增长,地址池急剧耗竭:
在互联网设计之初每台联网设备都需要一个全球唯一的公网 IP。一家公司有 1000 台电脑就需要 1000 个公网地址。有了 NAT 之后:
1 | 1000 台内网设备 → 共享 1 个公网 IP(通过不同端口区分) |
端口号是 16 位,理论上一个公网 IP 可以同时支持 ~65000 个并发连接。如下是NAT的相关设计思考以及相关的效果:
NAT类型
不同的NAT实现对数据流的处理方式各有不同。RFC 3489列出了具体实现中观察到的四种处理方式,也就是常说的四种常见的NAT类型为:
| NAT 类型 | 中文名称 | 映射方式 | 映射是否固定 | 入站允许范围 | 打洞难度 | P2P 成功率 | 安全性 | 典型场景 |
|---|---|---|---|---|---|---|---|---|
| Full Cone NAT | 完全锥形 NAT | 内网IP:端口 → 公网IP:端口 | ✅ 固定 | 任意外部主机都可访问 | ⭐ 很容易 | ⭐⭐⭐⭐ | 低 | 早期路由器、开放网络 |
| Restricted Cone NAT | 受限锥形 NAT | 同上 | ✅ 固定 | 仅允许“访问过的 IP”访问 | ⭐⭐ 较容易 | ⭐⭐⭐ | 中 | 部分家用路由器 |
| Port Restricted Cone NAT | 端口受限锥形 NAT | 同上 | ✅ 固定 | 仅允许“访问过的 IP+端口”访问 | ⭐⭐⭐ 一般 | ⭐⭐ | 较高 | 大多数家庭网络 |
| Symmetric NAT | 对称 NAT | 每个目标分配不同公网端口 | ❌ 不固定 | 仅允许目标服务器回连 | 🚫 几乎不可行 | ❌ | 很高 | 运营商 CGNAT、企业网络 |
四种常见的NAT具体的实现原理可以参考如下图,画的比较详细:
那我们自己的家庭宽带是什么样的NAT呢?下面是一个简单的Client测试脚本,bind本地的端口,然后分别向两台VPS的机器上发送请求:
1 | #!/usr/bin/env python3 |
然后分别在两台机器上,通过tcpdump抓取对应端口的udp流量,如下:
1 | $ sudo tcpdump -i any udp port 20100 -nn |
可以看到同一个端口,发往不同目标的请求,分配的外网出口的端口是不一样的,所以这里可以判断出我这个联通的宽带的NAT类型是:Symmetric NAT。
接下来我简单的测试一下NAT环境下,UDP的Echo的基本功能:如下是一个简单的Client测试脚本,在NAT内网运行,向外部发出请求:
1 | #!/usr/bin/env python3 |
下面是Server段的测试代码,监听指定的UDP端口,收到请求后,直接进行回包,运行在具有公网地址的主机上:
1 | #!/usr/bin/env python3 |
测试会发现作为NAT内的Client,UDP收发功能都是正常的,但是在UDP通道建立后,只是接受数据的话,这个通道只能保持1min:
1 | [00:35:49.515][*] 等待回包... |
这里NAT映射丢失的根因是:Client 的 NAT的计时器只按出站方向最后一次「出站包」刷新映射。这是很多 NAT 的默认行为,在RFC 4787 里定义的 “ MUST Outbound Refresh “。
1 | REQ-6: The NAT mapping Refresh Direction MUST have a "NAT Outbound |
重新改一下client的脚本,在Recv后再次Send给Server发包,就可以一直保持该UDP通道的顺畅:
1 | ... |
这个四分类模型后来被 RFC 4787 (NAT Behavioral Requirements for UDP) 用更精确的映射行为 × 过滤行为矩阵取代了。现实 NAT 的映射行为(Endpoint-Independent / Address-Dependent / Address+Port-Dependent)和过滤行为是独立的两个维度,不是 RFC 3489 简单的四分类能覆盖的。
STUN
STUN最新的定义是2002发布的RFC 5389 :Session Traversal Utilities for NAT (STUN),即定位为:“应用NAT穿透的工具”。它的发展经历过两个阶段,下面开始介绍:
RFC 3489:Simple Traversal of UDP Through NATs (STUN)
前面我们知道,在RFC 3489中定义了常见的4种NAT类型,针对常见的NAT类型,正如此RFC的主题:Simple Traversal of UDP Through NATs (STUN),它同样定义了完整的NAT穿透方案。
RFC 3489的STUN即简单NAT穿透方案在整体结构上包含两个部分:
- STUN Client:位于 NAT 后、想知道自己公网地址的一方,主动发送 Binding Request,解析响应中的
MAPPED-ADDRESS得到自己经 NAT 映射后的公网 IP:Port; - STUN Server:部署在公网上的服务器(必须有公网可达地址),被动接收请求,把”看到的源地址”(即 Client 经 NAT 后的公网地址)原样回填到响应里。要求服务器绑定4个地址/端口:
1 | (A1, P1) — 主地址,主端口(客户端默认连接这个) |
STUN的协议格式如下:
STUN算法核心逻辑:
- Test I — 建立基线,获取 MAPPED-ADDRESS
- Test II — 通过CHANGE-REQUEST协议属性,让服务器换 IP+Port 给客户端发响应,测试 NAT 是否允许任意来源(区分 Full Cone vs 其他)
- Test I’ — 访问不同服务器IP地址,服务器Port不变,检查映射是否变化(区分 Symmetric vs Cone)
- Test III — 仅换服务器 Port,区分 Restricted vs Port Restricted
具体流程如下:
上面的算法的流程比较简洁,不过结合之前的NAT的类型可以看明白STUN是怎么来判断一个CLIENT所在的NAT的类型,下面我让CLAUDE画了一个更细节的版本,详细阐述了四个测试流程,如下:
RFC 5389 :Session Traversal Utilities for NAT (STUN)
RFC 3489(经典 STUN)号称是”完整的 NAT 穿透方案”,但实践中暴露了一系列根本性缺陷。于是有了2002发布的RFC 5389 :Session Traversal Utilities for NAT (STUN),STUN的含义也发生了变化。它的核心使命就是纠正这个错误定位,把 STUN 从“解决方案”降级为“工具”。
| RFC 3489 的问题 | RFC 5389 的应对 |
|---|---|
| NAT 类型分类不可靠 —— 现实 NAT 行为千变万化,四分类探测算法常给出错误结果,导致按类型决定的穿透策略失败 | 彻底放弃 NAT 类型探测,移除 CHANGE-REQUEST/CHANGED-ADDRESS,不再猜 NAT 类型 |
| 把”探测”和”打洞”绑死 —— 应用根据探测结果决定怎么连,但探测本身就不准 | STUN 只负责获取反射地址,连通性判断交给上层(ICE 框架去实际试探) |
| 只支持 UDP —— 无法用于 TCP 场景 | 增加 TCP 和 TLS-over-TCP 传输支持 |
| 无法与其他协议复用端口 —— STUN 包和 RTP/媒体包混在同一端口时无法区分 | 引入 Magic Cookie + 首2位=0,可在同一端口多路复用(WebRTC 关键需求) |
| MAPPED-ADDRESS 明文 —— 被 NAT ALG 扫描篡改 | 新增 XOR-MAPPED-ADDRESS,地址异或编码躲过 ALG |
| 安全弱 —— Shared Secret 机制笨重,DDoS 反射风险(RESPONSE-ADDRESS) | 移除 RESPONSE-ADDRESS;引入 FINGERPRINT、短期/长期凭证、强制完整性 |
| 不可扩展 —— 难以新增方法 | 设计为可扩展框架,Binding 只是其中一个 method,TURN/ICE 在其上扩展 |
一句话:RFC 3489 想一个人干完所有事但干砸了;RFC 5389 把自己变成一块乐高积木,专心只做”告诉你公网地址”这一件事。
最新的STUN 只负责获取反射地址,连通性判断交给上层(例如:ICE 框架去实际试探),整体新STUN的概念包括:
- STUN Agent 概念:实现 STUN 的实体,可扮演 Client 或 Server;
- STUN Client:发 Request、收 Response;位于 NAT 后;解析 XOR-MAPPED-ADDRESS 拿到自己的公网地址;
- STUN Server:收 Request、回 Response;公网固定地址;本质是”镜子”,把看到的源地址回填。Server 只需 1 个 IP:Port(RFC 3489 要 4 个),因为不再做换 IP/Port 的 NAT 类型探测;
- 分层关系 STUN 是底层工具:ICE / SIP Outbound / TURN / WebRTC 是它的”使用者(STUN Usage)”,调用 Binding 等方法
如下是STUN的架构构定位:
下面是最新的RFC关于STUN的协议结构的定义:相对于RFC 3489简化了很多。
TURN
即内网穿透的方案STUN之后,2010年又发布了RFC 5766:Traversal Using Relays around NAT (TURN): Relay Extensions to Session Traversal Utilities for NAT (STUN),直白的翻译就是:使用中继进行NAT穿透:针对STUN的扩展。
为什么需要TRUN呢?原因正如RFC一开头提到了:
If a host is located behind a NAT, then in certain situations it can be impossible for that host to communicate directly with other hosts (peers). In these situations, it is necessary for the host to use the services of an intermediate node that acts as a communication relay.
有些NAT后的主机不能直接和其他主机进行通信,例如前面提到的Symmetric NAT,于是就有了TURN:使用中继服务器进行数据转发,以达到NAT穿透的效果。
实现方式就是:在公网部署一台中继服务器,Client 向它申请一个”中继地址”(Relayed Transport Address),然后告诉 Peer “你发到这个中继地址就行”,TURN 服务器负责双向转发。
协议格式也是复用 STUN 的消息格式(同一个 20 字节头、Magic Cookie、TLV 属性),只是注册了新的 Method。
以下是TURN协议详解,包括交互流程:
TURN实现的具体协议流程如下:
这里需要提醒一点:TURN只是一个RFC规范,具体实现还是具体应用的事情;例如上面提到的信令通道(信令服务器)和中继服务器的实现就是自己应用的事情,它们的差异在于:
| 信令服务器 | TURN 中继服务器 | |
|---|---|---|
| 传什么 | 控制消息(候选列表、呼叫/接听/挂断) | 实际业务数据(音视频流、游戏数据) |
| 数据量 | 极小(几KB的 JSON/SDP) | 极大(视频流可达 Mbps 级) |
| 何时参与 | 建连前交换信息,连通后可退出 | 建连后全程转发,直到通话结束 |
| 协议 | WebSocket / SIP / 自定义 TCP | STUN/TURN (UDP) |
| 带宽成本 | 几乎为零 | 很高(按流量付费) |
| 是否必须 | 是(没它 A/B 无法互相发现) | 不一定(P2P 通了就不需要 TURN) |
ICE
在TURN RFC 5766发布的同时,也发布了RFC 5245:Interactive Connectivity Establishment (ICE): A Protocol for Network Address Translator (NAT) Traversal for Offer/Answer Protocols,名为:交互式连接建立的方法。它不是一个新的传输协议,而是一套”如何使用 STUN 和 TURN 来建立连接”的算法和状态机。规定”如何收集候选地址、如何配对、如何按优先级逐一试探,最终选出一条能通的路”。后来ICE在RFC 8445和RFC 8839进行了更新。
类比:STUN/TURN 是锤子和钉子,ICE 是装家具的说明书。
ICE的RFC规范的简单组成如下:
参考ICE的RFC,它具体工作的流程主要分为如下几个阶段:
- Gathering(收集候选)
每端从三个来源收集自己的候选地址:
- 读本机网卡 → Host 候选
- 问 STUN Server → Server Reflexive 候选(NAT 映射后的公网地址)
- 问 TURN Server → Relay 候选(中继地址)
每个候选带一个优先级:Host > srflx > relay。收集完后通过信令发给对方(信令不归 ICE 管)。
- Pairing(配对排序)
收到对方候选后,本地候选 × 远端候选两两配对,生成 Check List。
配对按优先级排序——Host↔Host 排最前,relay↔relay 排最后。只有相同传输协议、相同 component-id 的才能配。
- Checking(连通性检查)
按 Check List 顺序,向对端的候选地址直接发 STUN Binding Request(不是发给 STUN Server)。
- 对端收到并回 Response → 这对通了
- 超时无回 → 这对失败
- 这个动作同时完成了 NAT 打洞(因为你向对端发了包,NAT 就为这个目标开了口)
双方同时互相检查(A→B 同时 B→A),加速收敛。
- Concluding(提名选定)
从所有通了的候选对里,Controlling 方(主控端)选一个作为最终路径。
选定后双方走这条路通信。原则:能直连就直连,直连不行走穿透,穿透不行走中继。
通过上面ICE的流程我们算是搞明白如何做NAT穿透了,NAT穿透有完整的RFC协议规范:ICE框架来做这个事情,借助STUN和TURN两个工具(RFC协议标准)ICE提供了一套完整的解决方案用来彻底解决NAT穿透的问题,通过:直连(Host) → P2P穿透(srflx) → 中继兜底(relay),逐级回退,保证总能连上。
针对ICE框架的实现有很多种,目前基于WebRTC(Web Real-Time Communication):浏览器内置的实时音视频通信能力,让两个浏览器之间直接 P2P 传输音视频/数据,这个规范使用ICE框架是最常见的。具体的实现如下:
| 实现 | 语言 | 说明 |
|---|---|---|
| libnice | C | GNOME 项目,Linux 上最常用的 ICE 库 |
| pion/ice | Go | Go 语言 WebRTC 栈的 ICE 组件 |
| libwebrtc | C++ | Chrome/Electron 内置的 WebRTC 引擎 |
| coturn | C | 专门的 STUN/TURN 服务器端实现 |
使用 WebRTC规范实现 的产品有很多,例如:腾讯会议、Zoom(Web版)、Google Meet、Discord(语音)、在线游戏语音等。
UPnP
UPnP( Universal Plug and Play)通用即插即用是一套基于互联网协议的分布式、开放的网络架构,允许诸如个人计算机、打印机、互联网网关、Wi-Fi 接入点和移动设备等网络设备无缝发现彼此在网络上的存在,并建立功能性的网络服务。它在 December 2008,作为ISO/IEC 29341的第73-part发布。
在计算机领域中,plug and play即插即用技术是一种用于动态将设备直接连接到计算机的技术,也就是我们平常说的硬件设备的热插拔。而UPnP 扩展了plug and play技术适用于住宅和 SOHO 无线网络,以达到零配置网络。UPnP 设备当连接到网络时,它们会自动与其他设备建立工作配置,无需用户手动通过 IP 地址 配置和添加设备。
UPnP 主要面向家庭网络,而非企业级设备。由于经济性、复杂性和一致性等原因,UPnP 通常被认为不适合在企业环境中部署:多播基础使其过于频繁通信,在设备数量庞大的网络上消耗过多网络资源;简化的访问控制与复杂环境不匹配。
UPnP 架构允许消费电子、移动设备、个人计算机和网络家电之间的设备间网络。它的实现是基于(TCP/IP)、HTTP、XML 和 SOAP 等既定标准的分布式、开放架构协议。UPnP 控制点(CPs:control points )是使用 UPnP 协议来控制 UPnP 受控设备(CDs:controlled devices)。
UPnP 由消费电子、网络、计算机厂商组成的联盟推动,它是基于现有的一系列互联网协议组建的协议规范,UPnP 架构本身构建在多个 RFC 标准之上,但本身不属于RFC标准。UPnP设备互联架构核心总结,可以描述如下:
基于 IP 网络,利用 Web 协议(HTTP/XML),实现局域网设备的自动发现、能力描述、远程控制和状态通知。
UPnP标准架构主要分为两部分:
- UDA(UPnP-arch-DeviceArchitecture):一份定义了通用的UPnP的设备架构规范,主要是设备之间如何通信的标准规范,可以类比为TCP/IP 协议规范;最新的已经发布了UPnP-arch-DeviceArchitecture-v2.0-20200417。
- DCP (Device Control Protocol)标准文档:多份定义了各个不同设备的具体协议内容格式,由各个 Working Committee 分别制定,可以类比为基于 TCP/IP 的各种应用协议。例如:IGD (Internet Gateway Device (IGD) v1.0/v2.0”)属于一个独立的 DCP 文档,由 UPnP Forum 的网关工作委员会单独制定,符合UPnP规范的网关设备类型,它的核心使命很简单:让局域网内的应用程序能够自动配置路由器的 NAT 和防火墙规则,不需要用户手动去路由器管理页面设置端口转发。
下图是我让Claude帮忙画的,很好的解释了UPnP标准的整体结构层次关系:
UDA:UPnP基础协议规范
从上面UDA官方标准规范可以知道,UPnP的设备架构规范包括五个核心:
0️⃣ Addressing 寻址
设备启动后优先通过 DHCP 获取 IP;若网络无 DHCP 服务器,则使用 Auto-IP(RFC 3927)从 169.254/16 链路本地地址段随机选取地址,并通过 ARP 探测避免冲突。确保设备在网络中具有唯一可路由的标识,为后续发现流程奠定基础。
DHCP本身和UPnP没有关系,网络设备开机后,大部分默认都是通过DHCP获取LAN的IP地址。所以这是Step0,这里只是提一点前置Step
1️⃣ Discovery 发现(SSDP 协议)
设备上线时向多播地址 239.255.255.250:1900 发送 NOTIFY ssdp:alive 宣告自身;控制点可发送 M-SEARCH 主动搜索目标设备。消息包含设备类型(NT)、唯一标识(USN/UUID)、描述文档 URL(LOCATION)及有效期(CACHE-CONTROL),实现零配置设备发现。
2️⃣Description 描述(HTTP + XML)
控制点通过 HTTP GET 请求 LOCATION URL,获取设备描述文档(DDD)和服务描述文档(SCPD)。两者均为 UTF-8 编码的 XML 文件:DDD 定义厂商信息、嵌入设备/服务列表及各服务的 controlURL/eventSubURL;SCPD 定义动作(Action)、参数(Argument)和状态变量(State Variable)及其数据类型与事件属性。
3️⃣ Control 控制(SOAP over HTTP)
控制点向服务的 controlURL 发送 HTTP POST 请求,请求体为 SOAP 1.1 格式的 XML,指定要执行的动作及输入参数。设备执行后返回同格式的 SOAP 响应,包含输出参数或错误码(如 401 Invalid Action、601 Argument Value Out of Range)。所有控制消息必须使用 Content-Type: text/xml; charset="utf-8"。
4️⃣ Eventing 事件(GENA 协议)
控制点通过 SUBSCRIBE 向服务的 eventSubURL 订阅事件,设备返回唯一订阅标识(SID)和有效期。当 evented 状态变量变化时,设备主动向 CALLBACK URL 推送 NOTIFY 事件,消息体为 XML 格式的变量名与新值。支持单播订阅 + 多播广播两种模式,确保多控制点状态同步。
5️⃣Presentation
设备的描述文件(XML)里有一个 <presentationURL> 字段,Control Point 拿到这个 URL 后让用户可以用浏览器直接控制设备或查看状态。
UDA的5大核心结构及其使用相关的协议栈如下:
根据文档原文,UPnP 协议栈的最上层其实代表的是三个不同层级的内容定义者,它们从上到下对协议消息进行”层层补充”:
| 层级 | 含义 | 举例 |
|---|---|---|
| UPnP Vendor(设备厂商) | 设备厂商自己定义的私有信息 | 比如某品牌电视的专属功能参数、厂商特定的扩展属性 |
| UPnP Forum(UPnP 论坛工作委员会) | 由标准工作组定义的设备/服务类型规范 | 比如 “MediaRenderer” 设备类型的 action 名称、参数名等标准化的东西 |
| UDA(UPnP Device Architecture) | 本文档定义的通用架构规则 | 比如 SSDP 发现消息的格式、SOAP 信封的结构、GENA 事件的通知方式 |
DCP:UPnP具体设备协议标准
前面介绍了UPnP的基础协议规范部分,那基于UDA的各种设备的标准DCP就很庞大了,UPnP Forum 官方发布的标准 DCP 大约有 30+ 种,更别说设备厂商私有的协议标准了。
如下是6 种常用设备的DCP描述,包含各自具体 Actions:
简单描述一下这6种DCP的标准设备类型的功能:
| DCP | 设备类型 | 功能描述 |
|---|---|---|
| IGD | 路由器 / 网关 | “我能帮你开端口”、”我能告诉你公网 IP”、”我能管理 NAT 规则”、”我能控制 WAN 连接的建立和断开” |
| MediaRenderer | 音箱 / 电视 / 播放器 | “我能播放你指定的音视频”、”我能暂停/快进/快退”、”我能调节音量”、”你告诉我 URL 我就能放” |
| MediaServer | NAS / 媒体服务器 | “我有很多音乐和视频”、”你可以浏览我的文件目录”、”你可以搜索我的内容库”、”我会告诉你内容有更新” |
| Lighting Controls | 智能灯 / 调光器 | “我能开灯/关灯”、”我能调亮度(0~100%)”、”我能告诉你当前亮度状态” |
| Printer | 网络打印机 | “我能接收打印任务”、”我能告诉你墨量/纸量”、”我能查询任务进度”、”我能取消排队中的任务” |
| HVAC | 空调 / 暖气 / 温控器 | “我能设定目标温度”、”我能告诉你当前室温”、”我能切换制冷/制热/自动模式”、”我能告诉你当前运行状态” |
IGD-NAT穿透的来龙去脉:让路由器给你留个门
IGD 是 UPnP 体系中专门为家庭路由器/网关定义的设备控制协议(DCP),让局域网内的程序能自动配置路由器的 NAT 和防火墙规则。
家庭路由器做 NAT 后,外网设备无法主动访问内网设备。以前你要手动登录路由器管理页面去配端口转发,IGD 让这件事变成了程序自动完成。典型场景:
- 游戏联机需要开端口(P2P)
- BT/eMule 下载需要被动连接
- 远程访问家里的 NAS/摄像头
- VoIP/SIP 电话穿越 NAT
具体IGD的协议格式和内容这里就不深入了,可以在UPnP官网查看所有官方标准的DCP,其中IGD V2.0。我们大概了解一下IGD的AddPortMapping()的action。
2-box 场景(最常见的实际情况):Client = CP(Control Point),在同一台设备上。 比如你的 PC 上跑了一个游戏,游戏内置了 UPnP 库,它自己既是需要接收入站流量的 Client,又是发送
AddPortMapping请求的 CP。关于CP前面提到过:在 UPnP 的术语体系里,发 SOAP 请求的能力被称为CP 角色。3-box 场景:Client 和 CP 不在同一台设备。 比如一个 IP 摄像头(Client)本身没有 UPnP 能力,需要一台 PC 上的管理软件(CP)代替它去路由器上创建端口映射。这种情况下 CP 调用
AddPortMapping时,InternalClient参数填的是摄像头的 IP,而不是自己的 IP。
如下展示了基于UPnP核心协议规范交互,请求IGD进行端口映射的详细过程:
SSDP协议测试
下面代码就是UPnP的基于HTTP的SSDP协议,通过M-SEARCH 方法搜索LAN中所有的UPnP设备:
1 | python3 - <<'PY' |
1 | Searching UPnP devices... |
因为UPnP 映射和 NAT 动态映射是路由器上两套独立的表:
1 | 路由器内部: |
- 你的 Python 测试走的是动态映射表(内网主动发包出去) → 看到 Symmetric 行为
- BT 的上传走的是静态映射表(UPnP 创建的) → 等价于 Full Cone
P2P技术
P2P(Peer to Peer):两个终端设备之间直接通信,不经过中间服务器转发数据。传统的数据交互模式一般都是C/S模式,他们两者的差异在于:
- 传统 C/S 模式:所有数据都经过服务器;
- P2P 模式:数据直接走,服务器只负责”介绍认识”;
尽管 P2P 系统此前已被应用于诸多领域,但真正让这一概念广为人知的是文件共享系统——尤其是音乐共享应用 Napster。P2P 使数百万互联网用户能够“直接互联,形成群组并协作,进而成为用户自建的搜索引擎、虚拟超级计算机和文件系统”。P2P 计算的基本理念早在更早期的软件系统和网络讨论中就已被构想,其原则可追溯至第一份RFC1。
为什么要 P2P:
- 省带宽/省钱:服务器不需要转发流量(视频通话、大文件传输,流量成本巨大)
- 低延迟:两点直连一定比绕路快(游戏联机 P2P 比经服务器少一跳)
- 可扩展:节点越多,总带宽越大(BT 下载就是典型)
P2P 的难点:两个 NAT 后面的设备互相找不到对方,需要打洞。这就是前面讨论的NAT穿透的相关的技术,包括STUN,TURN,ICE以及UPnP。
BitTorrent
BitTorrent 是一个工作在 TCP/IP 应用层的 P2P 文件分发协议(由 Bram Cohen 于 2001 年设计)。其核心思想是:
每个下载者在下载的同时也是上传者,所有 peer 互相交换文件分块(piece),把单点服务器的带宽压力均摊到整个 swarm(蜂群)网络中。从而大幅降低原始分发者的带宽成本。
2004 年的一项研究显示,BitTorrent 曾一度占据全球互联网总流量的三分之一 ,即使在 2019 年,其仍贡献了下行流量的 2.46% 和上行流量的 27.58%。
2017 年,BitTorrent, Inc. 发布了 BitTorrent v2 协议规范,这是协议诞生以来最重大的更新 。核心变更包括:
- 哈希算法升级:由于 SHA-1 已被证明存在碰撞漏洞,v2 全面切换至 SHA-256
- Merkle 哈希树:替代原有的级联哈希结构,加快从添加种子到开始下载的速度,并支持更细粒度的文件损坏检查
- 文件级去重:每个文件独立哈希,若多个种子包含相同文件,下载者可以跨种子下载该文件
- 混合模式兼容:v2 种子文件支持同时包含 SHA-1 和 SHA-256 哈希,确保与 v1 客户端的向后兼容
BitTorrent中的参与角色:
- Seeder(种子节点):拥有完整文件的节点。
- Leecher(下载节点):还在下载的节点,但同时也会上传已下载的部分。
- Tracker:中心化的”目录服务器”,维护 swarm 中的 peer 列表(IP/端口)。
- DHT 节点:去中心化场景下取代 Tracker 的分布式哈希表节点。
- Swarm:所有正在下载/上传同一文件的 peer 集合。
torrent文件
.torrent 文件是整个下载的”地图”,本质是一段 Bencode 编码的二进制文本,下图是torrent文件的结构和生成流程:
生成torrent文件的过程比较简单,文字简化版为:
1 | 原始文件 |
Bencode(发音为 Bee-Encode)是 BitTorrent (BT) 协议专门用于序列化和传输数据结构的编码方式。它以简洁高效著称,最常用于 .torrent 种子文件以及 DHT(分布式哈希表)网络中的数据交换。支持 4 种类型:字符串 (4:spam)、整数 (i42e)、列表 (l...e)、字典 (d...e)。
这里torrent构建过程有两层结构:
info字典:是描述资源文件本身的结构化信息,对info字典做 SHA-1 得到 info_hash(20 字节),这是该资源在整个 BT 网络中的“全局唯一 ID”,也是磁力链接 (magnet:?xt=urn:btih:<info_hash>) 的核心。它和外层的announce、comment无关,所以改 Tracker、改注释,不影响info字典,info_hash 不变,磁力链仍可用。- 根字典:包含
announce,announce-list,creation date,comment,以及info字典。 针对根字典进行整体Bencode 序列化,最终得到的就是torrent文件。
整体结构的重要字段的含义如下:
| 字段 | 含义 |
|---|---|
announce / announce-list |
Tracker 服务器地址 |
info.name |
文件名(或目录名) |
info.piece length |
每个分块大小(必须是 2 的整数次方,常见 256KB~4MB) |
info.pieces |
所有分块的 SHA-1 哈希拼接(每个 20 字节) |
info.files |
多文件模式下每个文件的长度和路径 |
上面我们知道BitToorent的文件分块(Piece / Block)机制,BT 对文件采用”虚拟分块”两层粒度:
- Piece(片):协议层用于哈希校验的单位,大小固定(如 512KB)。每个 piece 在
.torrent中都有一个 SHA-1 校验值。 - Block(子块):Peer Wire 协议实际请求传输的单位,通常是 16KB。一个 piece 由多个 block 组成。
在分Piece的基础上再进行分块的作用:
- 分块下载允许从多个 peer 并行拉取不同片段;
- 每收到一个完整 piece,立即用 SHA-1 校验,校验通过才标记为”已拥有”;校验失败则整片丢弃重下,实现端到端完整性。
Piece大小通常设置为 2 的 n 次幂字节(即 256KB, 512KB, 1MB, 2MB, 4MB, 8MB 等)。最核心的规律是:种子的总体积越大,单个piece的长度通常就被设得越大。
- 小体积种子:如几百兆大小,通常保持 256KB 或 512KB。
- 大体积种子:如几十GB的高清电影或游戏,piece大小往往会设到 2MB、4MB 甚至 8MB,以避免种子文件(.torrent)本身变得过于庞大。
那为什么要调整piece长度?
- 太小的piece:会使得种子文件的哈希表(记录所有分块校验值的列表)非常长,导致 .torrent 文件体积暴增。
- 太大的piece:如果在下载时其中一小块数据损坏,需要重新校验和下载整个大块,比较浪费带宽。
Peer 发现机制
1. Tracker (中心化)
Tracker 就是 BitTorrent 网络里的“通讯录服务器”。它自己不存任何文件数据,只做一件事:维护谁在下载/做种同一个文的名单,让 peers 能找到彼此。
- 客户端打开torrent文件后,通过 HTTP(S) GET 或 UDP 向 torrent中的Tracker 发起
announce请求,携带:info_hash、peer_id、port、uploaded、downloaded、left、event(started/completed/stopped) 等。 - Tracker 返回一份当前 swarm 中的 peer 列表(IP+端口)和统计信息(complete/incomplete 数)。
- 客户端按
interval周期性续报。
2. DHT 协议(去中心化,Trackerless)
DHT(Distributed Hash Table:分布式哈希表,没有中心 Tracker,所有节点共同组成一张”分布式通讯录”。每个节点只负责存一小部分种子的 peer 名单,但整个网络合起来能查到任何种子。
DHT,它提供了类似于哈希表的查找服务。在分布式计算系统中用来将一个关键值(key)的集合分散到所有在分布式系统中的节点,并且可以有效地将消息转送到唯一一个拥有查询者提供的关键值的节点(Peers)。DHT 的主要优势在于:可以轻松添加或移除节点,而无需重新分配键值。维护键值映射的责任由各个节点共同承担,这样当参与节点的数量发生变化时,对系统的影响会降到最低。这使得 DHT 能够扩展到非常庞大的节点数量,并且能够处理节点持续的增加、移除以及故障情况。
- 去中心化: 没有中心服务器,抗审查能力极强。
- 高容错性(健壮性): 网络中的节点可以随时加入或离开。当节点失效时,网络会自动将数据备份或路由转移到其他可用节点。
- 高可扩展性: 当网络规模扩大时,查找数据的耗时增长非常缓慢,能够轻松容纳数百万甚至数亿的节点。
- 负载均衡: 数据和路由任务被均匀分摊给所有节点,避免单点过载。
DHT 技术的研发初衷部分源于P2P系统,如 Freenet、Gnutella、BitTorrent 和 Napster 等。这些系统利用了互联网上分布的资源,以打造出实用的应用程序。具体来说,它们利用了增加的带宽和硬盘容量来提供文件共享服务。
DHT是一个技术规范,没有一个由国际标准化组织(如 ISO)制定的唯一、强制性的统一标准。它是根据不同的应用场景和算法演化出的一系列协议规范。目前应用最广泛的 DHT 规范:Kademlia-DHT算法,由Petar Maymounkov and David Mazières 在2002发表。常用于 BitTorrent 资源定位(如 Mainline DHT) 和去中心化网络(如 IPFS/libp2p)。
下面开始介绍Kademlia 算法的核心概念和结构:
基于 Kademlia 算法的分布式哈希表(BEP 5),核心要点:
- 每个节点有一个随机生成的 160 bit NodeID;资源的 InfoHash 也是 160 bit。
- 节点间”距离”用 XOR 异或 度量:
distance(A, B) = A XOR B。 - 路由表由 K-bucket 组成(K 通常=8),按距离前缀分桶维护。
- 通过 KRPC(基于 UDP+Bencode)完成 4 种 RPC:
ping、find_node、get_peers、announce_peer。 - 查找资源时:递归地向”离 InfoHash 更近的节点”查询,直到拿到持有该资源的 peer 列表。
🔑 这就让每个 BT 客户端自身都成为一个”小型 Tracker”,磁力链接正是基于 DHT 网络工作的(无须 .torrent 文件即可获取元数据,配合 BEP 9 元数据扩展协议)。
Node和距离
每个节点初始化的时候都会随机生成160 bits的ID作为唯一身份标记,例如qBitTorrent启动时(DHT启用)就会生成。该ID的生成流程如下:
初始随机数:节点首次加入网络时,会随机生成一个较长的数据流(例如UUID或字节序列)。
哈希运算:将这个随机数据通过加密哈希函数(经典网络使用 SHA-1,其它去中心化网络可能使用 SHA-256 或 SM3 等)进行处理。
截取与映射:将哈希后的结果截取为所需的长度(例如 160 bit),作为该节点在网络中的唯一标识。
网络中任意两个Node之间的距离通过异或运算符XOR来进行计算,和地理位置无关,只和XOR的结果有关,计算值越小,代表两个Node的距离越近,同样作为DHT中的Key,也就是P2P中的文件,也需要采用同样的格式和长度(即前面介绍torrent文件生成的info-hash),以便通过同样的XOR计算来进行所在Node的定位。
具体细节如下图:
路由表的结构
有了Node和相关距离的定义,那么在分布式网络中,我们最关键要解决的问题是:怎么找到其他的Node呢?
KAD-DHT采用固定长度的路由表,称之为k-bucket,来存储分布式网络中其他Node的信息,路由表的具体实现逻辑为:
每个Node有 160 个桶(Bucket 0 ~ Bucket 159),对应 160-bit Node ID 的每一位。
- Bucket i 存放与我的 XOR 距离在 [2^i, 2^(i+1) - 1] 范围内的节点
- 每个桶最多存 K=8 个节点,之所以通常设定为 8,是为了在网络稳定性、查找效率和带宽开销之间取得最佳的平衡,是一个经验值,当然可以修改的。
- 理论总容量:160 × 8 = 1280 个节点(实际低位桶大多是空的,真正存几百个)
具体路由表结构如下图所示(用4bit空间示意):
下表表示了k-bucket的距离和节点个数:
| Bucket | 距离范围 | 该范围包含多少个可能的 ID | 实际在线节点数(估算) |
|---|---|---|---|
| Bucket 0 | 距离 1 | 1 个位置 | 0~1 个 |
| Bucket 1 | 距离 2~3 | 2 个位置 | 0~2 个 |
| Bucket 10 | 距离 1024~2047 | 1024 个位置 | 可能几十个 |
| Bucket 80 | 距离 2^80 ~ 2^81-1 | 2^80 个位置 | 可能几百万个 |
| Bucket 159 | 距离 2^159 ~ 2^160-1 | 2^159 个位置 | 几百万个节点! |
通过上面k-bucket的介绍我们知道每个Node的路由表都是独立的,而且是基于自己的Node ID构建的,和分布式网络中的其他Node的k-bucket的数据完全不同。
路由表的构建
路由表有了,那更关键的问题来了:怎么找到其他的节点呢:
这里要理解一个Node如何在冷启动的状态下,通过Bootstrap返回的K8个节点,逐渐完善路由表的,需要明白几个关键的DHT的关键特性:
第一个就是:在请求一个Node的时候,find_node离我更近的节点的时候:Node中同一个Bucket内的节点的距离,肯定比此Node和我的距离更近,所以在填满我的DHT的时候,就是通过这种方式逐渐收敛的。
第二个性质:在针对空的BucketX的时候,会随机生成该Bucket范围的NodeX,然后通过已有的低位的Node进行find_node(NodeX)请求,返回的离NodeX更近的Node列表都是落在<=BucketX区间的,以便进行填充,原因就是:高位桶的节点和低位桶之间的距离,一定落在高位桶对应的Bucket,所以借助低位桶内的Node进行find_node高位桶的节点,返回的Node一定同样落在原Node的Bucket的高位桶或者更进一点。
Peers的存储和搜索
有的DHT路由表数据后,那哪些Node负责存储哪些Peer的信息,以及如何查找一个Torrent所在的Peers呢?Peer信息的同步过程是这样的,当你在下载或者上传一个torrent的时候:
- 你先通过 get_peers 迭代查找,找到离此torrent的info_hash最近的 K 个节点。
- 依次向这 K 个节点发送
announce_peer: announce_peer(info_hash=0xABCD, port=6881, token=xxx)。 - 这些节点收到后,在本地存储中记录:
peers[0xABCD].append(你的IP:6881, 过期时间=now+30min)。
具体流程细节如下:
一开始我有点疑问,通过get_peers是如何找到离此torrent的info_hash最近的 K 个节点,这最近的K个节点如果从XOR的意义上,不一定存在呀。另外通过XOR计算每个Bucket中有那么多Node,依次递归的查看不就是指数级的放大吗?
这里两个问题其实我想多了,
- 离此torrent的info_hash最近的 K 个节点,不是指XOR空间上绝对的1-8距离的Node,而是动态的,存活的和info_hash XOR距离最近的topK个节点,所以他会睡着Node的上下线而发生变化,这也是为什么需要K=8,如果太少就会导致有效的Peers太少,导致整个集群抖动太大,体验较差。
- 查找的过程不是指数级的递归放大,而是剪枝的贪心算法,具体细节如下:
其实这里理论上确实有小概率:N8 碰巧认识一个极近的节点但 N1 不认识。算法通过多轮迭代兜底:
1 | Round 1: 问 N1, N2, N3 → 拿到 A(d=12) |
因为越近的节点路由表越精确,它们知道的近邻一定 >= 远处节点知道的。 这不是100%数学保证,但在统计上极其可靠。
3. PEX(Peer Exchange)
PEX是在已建立连接的 peer 之间互相交换”我认识哪些其他 peer”,进一步降低对 Tracker/DHT 的依赖(BEP 11)。
- DHT — 全局分布式网络,可以查找任意 info_hash 对应的 Peer,像一个去中心化的”黄页”
- PEX — 连接级别的小范围交换,只在已有的 BT 连接内传播同文件的 Peer 地址
现实中的客户端(qBittorrent、Transmission 等)三者同时启用,谁先返回结果就用谁,它们之间的协作方式:
- Tracker负责冷启动 — 帮你找到第一批 Peers;
- DHT既能冷启动,也能热扩展;多跳路由,get_peers 要递归查找返回一批Peers;
- PEX 负责热扩展 — 有了第一个连接后,快速滚雪球式发现更多 Peer;
4. LSD(Local Service Discovery)
通过本地多播在局域网内发现同 swarm 的 peer(BEP 14)。在局域网内通过 UDP 组播(multicast)发现同一 Torrent 的 Peer:
- 组播地址:
239.192.152.143:6771(IPv4)/[ff15::efc0:988f]:6771(IPv6) - 每个客户端定期向该组播地址发送一条类似 HTTP 的报文:
1 | BT-SEARCH * HTTP/1.1\r\n |
- 同一局域网内如果有其他客户端也在做种/下载同一 info_hash,就能直接发现对方
qBittorrent、Transmission、Deluge 等主流客户端默认启用 LSD,实际触发条件苛刻:必须同局域网 + 同文件 + 同时在线,家庭场景几乎无用(就你一个人在下),校园网/公司内网是唯一高频场景。
有 DHT(分布式哈希表)和 PEX(Peer Exchange)后,Tracker 不再是必须的——磁力链接就是靠 DHT 工作的。但实际上大多数客户端三管齐下(Tracker + DHT + PEX),Tracker 依然是最快找到初始 peers 的方式。
这里需要注意:私有种子(private flag):info.private=1 时禁用 DHT/PEX/LSD,仅靠指定 Tracker,常用于 PT 站。
下面是DHT,PEX和Tracker的主要功能对比:
Peer Wire Protocol(对等节点间的数据交换协议)
跑在 TCP(或 uTP)之上,是 BT 真正搬运数据的协议。两个 Peer 之间通过 TCP/uTP 传输文件数据的二进制协议。是 BitTorrent 所有上层功能(PEX、DHT 端口通知、磁力链接元数据交换、加密传输)的底层承载。
核心博弈机制 — Choke/Unchoke:
- 每条连接有 4 个状态位:
am_choking、am_interested、peer_choking、peer_interested - 只有”对方没 choke 我 + 我 interested”时才能请求数据
- 客户端通常每 10 秒重新评估一次 unchoke 哪些 Peer(选上传速度最快的 4 个),实现 tit-for-tat(以牙还牙)策略
- 每 30 秒随机 unchoke 一个 Peer(optimistic unchoke),给新节点机会
uTP(Micro Transport Protocol)
核心问题:BT 用 TCP 会把家庭宽带挤爆。BT 流量大、长连接多,TCP 容易把家庭上行带宽塞满,影响其他应用。
- 传统 TCP 的拥塞控制对所有连接”平等”——BT 开几十个连接,等于抢了大量份额
- 用户一边下 BT 一边打网页,网页卡得要死
- ISP 因此大规模限速/封杀 BT 流量
uTP(BEP 29)是 BT 自研的基于 UDP 的传输层:
- 使用 LEDBAT 拥塞控制:以单向延迟为信号,一旦检测到链路出现排队延迟就主动让路;
- 既保持高吞吐,又对其他 TCP 流量”友好”;
- 顺带也更容易做 NAT 穿透(UDP 比 TCP 更容易打洞,这点你应该熟)。
Piece 选择策略(Piece Selection)—— 决定”先下哪块”
这是 BT 性能与健壮性的关键算法,依次采用以下策略:
Strict Priority(严格优先):一旦开始请求某个 piece 的某个 block,就优先把这个 piece 的其余 block 全部请求完,尽快完成校验。
Rarest First(最稀缺优先)在 swarm 中拥有该 piece 的 peer 数量最少的优先下载。这是核心策略,目的是:
- 避免某个稀有片段因唯一持有者下线而消失;
- 让更多稀有片在网络中扩散,提升整体上传/下载效率,避免”最后一块难下”。
Random First Piece(随机首片):刚开始时,节点还没东西可上传,rarest first 反而会要稀缺片,下载慢;所以前几片随机选,先快速拿到一些 piece 能去交换。
Endgame Mode(终结模式) ⭐:当所有剩余 block 都已被 request 出去(接近 100%),向所有 unchoked peer 同时发出剩余 block 的 request,谁先返回就取谁的,并对其他人发
cancel。解决”长尾收尾”慢的问题。
Choking 算法(阻塞算法)—— 决定”上传给谁”
每个客户端通常只同时给有限个(默认 4 个)peer 上传,必须挑选”对我最有利”的 peer,这就是 choking。基于一报还一报(Tit-for-Tat)的博弈思想:
- Regular Unchoke(常规解除阻塞)
- 每 10 秒重新评估一次。
- 按”对我下载速率”对所有 interested 的 peer 排序,选出 top N(通常 3 个) unchoke。
- 鼓励节点积极上传,谁给我数据多,我就给谁数据多 → 抑制 free-rider(白嫖)。
- Optimistic Unchoke(乐观解除阻塞)
- 每 30 秒额外随机 unchoke 1 个 peer(无论速度如何)。
- 作用:①探测潜在的更快 peer;②让新加入、还没东西可上传的 peer 有机会获得首个 piece,启动 tit-for-tat 循环。
- Anti-Snubbing(反冷落)
- 若某 peer 持续 60 秒没给我任何数据 → 认为被它”冷落”了,停止给它上传,并多触发一次乐观解除。
- Upload Only(做种模式)
完成下载后无法再用”下载速率”判优,改用”上传速率”或公平轮转策略选择上传对象。
前面提到的机制都是BitTorrent的重要扩展协议(BEP),如下是列表:
| 编号 | 名称 | 作用 |
|---|---|---|
| BEP 5 | DHT | 去中心化 peer 发现 |
| BEP 6 | Fast Extension | 快速恢复、Allowed Fast 等优化 |
| BEP 9 | Magnet/Metadata Exchange | 通过磁力链直接从 peer 拉取 .torrent 元信息 |
| BEP 10 | Extension Protocol | 通用扩展协议握手框架 |
| BEP 11 | PEX | 节点交换 |
| BEP 14 | LSD | 局域网发现 |
| BEP 15 | UDP Tracker | 减轻 Tracker 负载 |
| BEP 19 | WebSeed | 支持从 HTTP 服务器作为 seed |
| BEP 29 | uTP | 基于 UDP 的拥塞控制传输(LEDBAT),让 BT 流量自动避让前台流量 |
种子的一生
发布阶段(让世界知道这个种子)
种子生成完只是本地一个文件,必须让别人能拿到 .torrent 文件或对应的 info_hash:
- 加载到自己的 BT 客户端,开启做种
- 客户端读取 .torrent → 校验本地文件 → 进入 “Seeding” 状态。
- 客户端会立刻向
announceURL 发起第一次 announce 请求(event=started),告诉 Tracker:”我有这个 info_hash 的全部内容,IP 是 X,端口是 Y”。
- 发布渠道(任选其一或多种)
- PT/公开 BT 站:上传 .torrent 到网站(如 Linux 镜像站、PT 站)。
- 磁力链分享:复制
magnet:?xt=urn:btih:<hex>&dn=...&tr=...,丢到论坛/IM。 - DHT 直接 announce:客户端启用 DHT 后会向 Kademlia 网络做
announce_peer,让其他人通过 info_hash 在 DHT 里也能找到你。 - PEX / LSD:不主动发布,但已连接的 peer 会把你介绍给它的邻居 / 局域网。
- NAT 穿透(你已经熟悉的部分)
- 通过 UPnP / NAT-PMP 自动开端口;
- 或者使用 uTP(UDP-based)+ DHT,结合 hole punching;
- 否则只能被动等公网可达 peer 来连你 → 速度受限。
传播阶段(swarm 形成与扩张)
种子被发布出去后,会经历”冷启动 → 爆发 → 平稳”的过程:
- 冷启动(只有 1 个 seeder)
- 第一个 leecher A 通过 Tracker / DHT 拿到 seeder 的地址 → TCP/uTP 握手 → 交换
bitfield。 - A 由于没有任何 piece,只能向 seeder 单向请求;seeder 因为不需要从别人下载,采用轮转/上传速率策略选择 unchoke 对象。
- A 每下完一个 piece,立即广播
have消息给所有连接,并通过 announce 上报进度。
- 雪崩扩散(N 个 leecher 加入)
- 越来越多的 leecher 加入 swarm,从 Tracker / DHT / PEX 互相发现。
- 每个节点采用 Rarest First 策略,优先下载稀缺片 → 稀缺片快速扩散,避免单点故障。
- Tit-for-Tat choking:谁给我下载得快,我就给谁上传 → 鼓励所有人尽早成为”半成品上传者”。
- Optimistic Unchoke(每 30 秒随机解阻):让新 leecher 也能拿到第一个 piece,启动以物易物。
- 此时单一 seeder 即使带宽不高,整个 swarm 的下载速度也会指数级放大(这正是 BT 的本质优势)。
- 收尾(Endgame Mode)
- 当某个 leecher 已请求到了所有剩余 block,就向所有 peer 同时发出剩余 block 的请求。
- 谁先返回就用谁的,对其他人发
cancel,避免”最后一块卡半天”。
- 长期存活
- Leecher 下完 → 变 seeder(除非它立即关掉,即所谓 “hit-and-run”)。
- swarm 中 seeder 数 ≥ 1 时种子持续 “alive”,理论上可以无限传播。
维护阶段(种子的”健康度”管理)
种子在网络中长期存在,会涉及一些维护性技术点:
| 机制 | 作用 |
|---|---|
| 周期性 announce | 客户端每 interval(通常 30 min)向 Tracker 续报,更新 peer 列表 |
| DHT 续 announce | 默认每 30 min 向 DHT 重新 announce_peer,否则其他节点会忘了你 |
| PEX 周期交换 | 每 60 s 向已连 peer 发送新增/退出的 peer 列表 |
| 超级种子 (Super-seeding, BEP 16) | 单 seeder 场景下,假装自己没有完整文件,强制让 leecher 互相交换,防止首个 leecher 下完就跑路 |
| Tracker scrape | 客户端可主动查询 swarm 健康(seeders/leechers/completed 数) |
| 断点续传 | 重启客户端时重新对本地文件做 SHA-1 piece 校验,决定哪些片需要补 |
消亡阶段(种子的”死亡”)
种子并非永生,会因以下原因走向消亡:
- 所有 seeder 离线
- swarm 中再无 100% 完整副本,剩余 leecher 只能互相补缺。
- 如果丢失的某些 piece 没人有 → 种子 “坏种“(dead torrent),永远下不完。
- Tracker 死亡 + DHT 不可用
- 私有种子(
private=1)禁用 DHT,Tracker 一挂就再也找不到 peer。 - 公开种子还能通过 DHT/PEX/LSD 苟活。
- 私有种子(
- 资源生命周期结束
- 大家都下完了,然后陆续关客户端 → swarm 自然萎缩。
- PT 站靠分享率 / H&R 规则强制做种来延缓这一过程。
- 法律/政策性消亡
- .torrent 站被关停、磁力链接被屏蔽,新人无法发现 → swarm 失去入口、逐渐枯萎。
几个常被忽略的细节
- 种子文件本身可以很小:几 MB 甚至几十 GB 的资源,
.torrent通常只有几 KB ~ 几百 KB,因为它只存元数据 + piece 哈希。 - 改了源文件 = 完全不同的种子:哪怕只改 1 字节,
pieces字段全变 → info_hash 全变 → 在 BT 网络里是另一个资源。 - 磁力链 ≠ 种子文件:磁力链只有 info_hash;首次下载时客户端要先通过 BEP 9 从某个 peer 拉回完整的
info字典,相当于在网络中”重建” .torrent。这就是为什么磁力链有时启动比 .torrent 慢。 - 私有种子 (private=1) 是种子”长寿”的关键之一:PT 站靠中心 Tracker + 分享率激励,让 seeder 长期在线,种子寿命远超公开 BT。
- **Web Seed (BEP 19)**:可在 .torrent 里加 HTTP 镜像 URL,即使一个 P2P 节点都没有,也能从普通 HTTP 服务器下载,相当于给种子续命的”安全网”。
