@Author : Lewis Tian (taseikyo@gmail.com)
@Link : github.com/taseikyo
@Range : 2024-05-05 - 2024-05-11
Weekly #61
readme | previous | next
本文总字数 12009 个,阅读时长约:18 分 28 秒~36 分 56 秒,统计数据来自:算筹字数统计 。
*Photo by Sebastian Knoll on Unsplash
Table of Contents
review
tinyTorrent: 从头写一个 Deno 的 BitTorrent 下载器
1、内存安全
Rust 和 Go 以不同的方式处理这个问题,但两者都旨在比其他语言更智能、更安全地管理内存。
2、性能
Go 和 Rust 都非常快。然而,虽然 Go 的设计偏向于快速编译,但 Rust 针对快速执行进行了优化。
Rust 的运行时性能也更加一致,因为它不使用垃圾回收。另一方面,Go 的垃圾回收器减轻了程序员的一些负担,使其更容易专注于解决主要问题,而不是内存管理的细节。
对于执行速度胜过所有其他考虑因素的领域,例如游戏编程、操作系统内核、Web 浏览器组件和实时控制系统,Rust 是更好的选择。
3、并发
与大多数语言不同,Go 设计有用于并发编程的内置功能,例如 goroutines(线程的轻量级版本)和 channels(在并发任务之间传输数据的安全有效方式)。
这些使 Go 成为大规模并发应用程序(如 Web 服务器和微服务)的完美选择。
4、安全
Rust 经过精心设计,以确保程序员不能做一些他们不想做的事情,例如覆盖共享变量。编译器要求您明确说明在程序的不同部分之间共享数据的方式,并且可以检测许多常见的错误和错误。
因此,所谓的 “与借用检查器(borrow checker)打架” 是新 Rust 程序员的常见抱怨。在安全的 Rust 代码中实现你的程序通常意味着从根本上重新思考它的设计,这可能会令人沮丧,但当可靠性是你的首要任务时,好处是值得的。
Jesse Li 的博客 图文并茂,讲述了如何用 Go 开发一个 BT 的下载器。内容涉及到 BT 协议以及下载器的代码设计,思路清晰,值得一读。
对于喜欢动手的朋友,可以先关掉这篇博客,参考 Jesse 的代码尝试自己写一个 BT 下载器。写完以后再回来,对比我用 Deno 开发的下载器,相信会有不一样的收获。
BT 是一个协议,和 HTTP, FTP 一样,是一个应用层的协议,这个协议被设计用来实现 P2P (Peer to Peer) 下载。
传统的下载是客户端请求服务器获取资源,下载方和资源提供方的角色很清楚。这样做的优点是简单,易于理解,我要下载东西,我就去请求服务器,缺点也很明显:
而 P2P 则不一样,每一个客户端同时也是服务器,从别人那里下载资源的同时,也提供资源给到别人。这样一来,就规避了服务器模型的缺点:
每个人都是服务器,除非所有机器都故障了,否则网络依旧可以运转
1、Torrent File
种子文件使用了一种名为 Bencode 的编码,这个编码非常简单,只支持如下四种数据类型。因为存在 List 和 Dictionary,所以也有能力表达复杂的数据结构。
deno-bencode 是我给 Deno 写的一个 Bencode 编解码库,我们现在使用这个库来看看种子文件中到底有什么。
复制 // decode.ts
import { decode } from "https://deno.land/x/bencode@v0.1.2/mod.ts"
const file = Deno .args[ 0 ]
console .log ( decode ( Deno .readFileSync (file)))
复制 $ wget https://cdimage.debian.org/debian-cd/current/amd64/bt-cd/debian-10.6.0-amd64-netinst.iso.torrent
$ deno run --allow-read decode.ts debian-10.6.0-amd64-netinst.iso.torrent
{
announce: "http://bttracker.debian.org:6969/announce",
comment: '"Debian CD from cdimage.debian.org"',
"creation date": 1601120878,
httpseeds: [
"https://cdimage.debian.org/cdimage/release/10.6.0//srv/cdbuilder.debian.org/dst/deb-cd/weekly-builds...",
"https://cdimage.debian.org/cdimage/archive/10.6.0//srv/cdbuilder.debian.org/dst/deb-cd/weekly-builds..."
],
info: {
length: 365953024,
name: "debian-10.6.0-amd64-netinst.iso",
"piece length": 262144,
pieces: Uint8Array(27920) [
144, 55, 173, 67, 115, 234, 169, 248, 222, 41, 139, 142, 125,
100, 183, 130, 43, 148, 137, 130, 2, 194, 83, 109, 140, 147,
123, 174, 234, 135, 58, 207, 217, 141, 107, 86, 245, 137, 79,
150, 23, 33, 151, 157, 125, 159, 97, 10, 200, 137, 36, 158,
74, 19, 97, 194, 171, 164, 32, 145, 175, 213, 91, 193, 120,
26, 89, 109, 114, 61, 90, 166, 168, 137, 218, 154, 219, 119,
107, 46, 240, 50, 134, 161, 162, 18, 224, 51, 210, 61, 41,
6, 207, 124, 62, 199, 227, 134, 146, 206,
... 27820 more items
]
}
}
首先,种子文件是一个 Bencode 编码的 Dictionary,里面含有一些字段,比较重要的是这些:
announce: 这是一个 URL,作用后面再说
info: 这个又是一个 Dictionary,里面含有文件相关的信息
piece length: Piece(分段) 的长度,单位是字节
pieces: 一个数组,里面对应了每个 Piece 的 SHA1 哈希值,用于校验(SHA1 哈希值长度固定为 20 个字节)
从文件里面的信息来看,我们可以得知,种子是分为 Piece 的,每个 Piece 的长度在文件中已经确定,同时,种子文件也会提供每个 Piece 的 SHA1 哈希值用于校验 Piece 的有效性。
我们来核对一下数据。
文件长度是 365953024 个字节,也就是 349MB,
每个 Piece 的长度为 262144 个字节,也就是 256KB。
那么一共是 365953024 / 262144 = 1396 个 Piece(注意,这里不一定整除,也就是说,最后一个 Piece 它的长度可能不等于 piece length)
每个 Piece 的 SHA1 哈希是 20 个字节,所以总的是 1396 * 20 = 27920 个字节
大家可以发现,上面的种子只包含一个文件,很多时候,我们打开种子时,里面会有多个文件,下载器会让我们选择哪些文件需要被下载。
这里就涉及到另外一个问题,单文件种子 和 多文件种子 ,它们存储的信息略有不同,我们用一个例子来看。
我随便找了一个 Taylor Swift 的专辑 Red 的种子,打开看看。
复制 $ deno run --allow-read decode.ts red.torrent
{
announce: "http://tracker.nwps.ws:6969/announce",
"announce-list": [
[ "http://tracker.nwps.ws:6969/announce" ],
[ "http://tracker.winglai.com/announce" ],
[ "http://fr33dom.h33t.com:3310/announce" ],
[ "http://exodus.desync.com:6969/announce" ],
[ "http://torrent.gresille.org/announce" ],
[ "http://tracker.trackerfix.com/announce" ],
[ "udp://tracker.btzoo.eu:80/announce" ],
[ "http://tracker.windsormetalbattery.com/announce" ],
[ "udp://10.rarbg.me:80/announce" ],
[ "udp://ipv4.tracker.harry.lu:80/announce" ],
[ "udp://tracker.ilibr.org:6969/announce" ],
[ "udp://tracker.zond.org:80/announce" ],
[ "http://torrent-tracker.ru/announce.php" ],
[ "http://bigfoot1942.sektori.org:6969/announce" ],
[ "http://tracker.best-torrents.net:6969/announce" ],
[ "http://announce.torrentsmd.com:6969/announce" ],
[ "udp://tracker.token.ro:80/announce" ],
[ "udp://open.demonii.com:80" ],
[ "udp://tracker.coppersurfer.tk:80" ],
[ "http://tracker.thepiratebay.org/announce" ],
[ "udp://9.rarbg.com:2710/announce" ],
[ "udp://open.demonii.com:1337/announce" ],
[ "udp://tracker.ccc.de:80/announce" ],
[ "udp://tracker.istole.it:80/announce" ],
[ "udp://tracker.publicbt.com:80/announce" ],
[ "udp://tracker.openbittorrent.com:80/announce" ],
[ "udp://tracker.istole.it:80/announce" ],
[ "http://tracker.istole.it/announce" ],
[ "udp://tracker.publicbt.com:80/announce" ],
[ "http://tracker.publicbt.com/announce" ],
[ "udp://open.demonii.com:1337/announce" ],
[ "udp://11.rarbg.me:80/announce" ],
[ "udp://10.rarbg.me:80/announce" ],
[ "udp://9.rarbg.com:2710/announce" ],
[ "udp://tracker.token.ro:80/announce" ],
[ "udp://12.rarbg.me:80/announce" ],
[ "http://tracker.trackerfix.com/announce" ]
],
comment: "Torrent downloaded from torrent cache at http://itorrents.org",
"created by": "uTorrent/3210",
"creation date": 1351095350,
encoding: "UTF-8",
info: {
files: [
{ length: 13236894, path: [Array] },
{ length: 12992666, path: [Array] },
{ length: 12031154, path: [Array] },
{ length: 11899411, path: [Array] },
{ length: 11535936, path: [Array] },
{ length: 11465792, path: [Array] },
{ length: 9888023, path: [Array] },
{ length: 9853495, path: [Array] },
{ length: 9781419, path: [Array] },
{ length: 9684472, path: [Array] },
{ length: 9681093, path: [Array] },
{ length: 9574507, path: [Array] },
{ length: 9355103, path: [Array] },
{ length: 9154619, path: [Array] },
{ length: 9028224, path: [Array] },
{ length: 8994573, path: [Array] },
{ length: 8903823, path: [Array] },
{ length: 8895321, path: [Array] },
{ length: 8859865, path: [Array] },
{ length: 8304962, path: [Array] },
{ length: 8188974, path: [Array] },
{ length: 7797281, path: [Array] },
{ length: 7357902, path: [Array] }
],
name: "Taylor Swift - Red (Deluxe Version)",
"piece length": 16384,
pieces: Uint8Array(276460) [
107, 33, 238, 211, 243, 14, 230, 146, 23, 98, 147, 188, 251, 168,
170, 253, 105, 99, 55, 208, 230, 60, 87, 198, 22, 246, 245, 186,
141, 162, 52, 196, 196, 128, 98, 236, 121, 55, 150, 208, 40, 194,
18, 57, 112, 165, 245, 17, 18, 51, 4, 44, 243, 254, 34, 207,
12, 106, 201, 132, 96, 207, 61, 144, 118, 130, 211, 91, 7, 141,
71, 36, 129, 132, 70, 115, 27, 133, 80, 240, 140, 121, 239, 28,
240, 58, 212, 35, 20, 208, 94, 203, 176, 178, 126, 90, 37, 255,
245, 17,
... 276360 more items
]
}
}
可以发现,最大的区别在于 info 里面多了一个字段叫做 files。默认的 console.log 没有打印出 path 内容,我们改一下代码,单独打印 files。
复制 // decode2.ts
import { decode } from "https://deno.land/x/bencode@v0.1.2/mod.ts"
const file = Deno.args[0]
const result = decode(Deno.readFileSync(file)) as any
console.log(result.info.files)
复制 $ deno run --allow-read decode2.ts red.torrent
[
{ length: 13236894, path: [ "Taylor Swift - All Too Well.mp3" ] },
{
length: 12992666,
path: [ "Taylor Swift - State of Grace (Acoustic Version).mp3" ]
},
{
length: 12031154,
path: [ "Taylor Swift Feat Gary Lightbody - The Last Time.mp3" ]
},
{ length: 11899411, path: [ "Taylor Swift - State of Grace.mp3" ] },
{ length: 11535936, path: [ "Taylor Swift - The Moment I Knew.mp3" ] },
{ length: 11465792, path: [ "Taylor Swift - Sad Beautiful Tragic.mp3" ] },
{
length: 9888023,
path: [ "Taylor Swift Feat Ed Sheeran - Everything Has Changed.mp3" ]
},
{ length: 9853495, path: [ "Taylor Swift - I Almost Do.mp3" ] },
{ length: 9781419, path: [ "Taylor Swift - Treacherous.mp3" ] },
{ length: 9684472, path: [ "Taylor Swift - Treacherous (Demo).mp3" ] },
{ length: 9681093, path: [ "Taylor Swift - The Lucky One.mp3" ] },
{ length: 9574507, path: [ "Taylor Swift - Begin Again.mp3" ] },
{ length: 9355103, path: [ "Taylor Swift - 22.mp3" ] },
{ length: 9154619, path: [ "Taylor Swift - Red (Demo).mp3" ] },
{ length: 9028224, path: [ "Taylor Swift - Come Back... Be Here.mp3" ] },
{ length: 8994573, path: [ "Taylor Swift - Red.mp3" ] },
{ length: 8903823, path: [ "Taylor Swift - Girl At Home.mp3" ] },
{ length: 8895321, path: [ "Taylor Swift - Starlight.mp3" ] },
{ length: 8859865, path: [ "Taylor Swift - I Knew You Were Trouble..mp3" ] },
{ length: 8304962, path: [ "Taylor Swift - Stay Stay Stay.mp3" ] },
{ length: 8188974, path: [ "Taylor Swift - Holy Ground.mp3" ] },
{
length: 7797281,
path: [ "Taylor Swift - We Are Never Ever Getting Back Together.mp3" ]
},
{ length: 7357902, path: [ "Digital Booklet - Red (Deluxe).pdf" ] }
]
现在就很清楚了,对多文件种子来说,files 里面存储了每个文件的长度,以及每个文件的路径。
2、Tracker
现在我们来看第二个问题,如何找到 Peer 以及如何让 Peer 找到我们?这里的关键就是种子文件中存储的 announce 字段,这个字段是一个 URL,这个 URL 指向了一个 Tracker 服务器。
Tracker 服务器顾名思义,是一个追踪者,或者说是中介。它本身不提供任何下载服务,它的作用是用来沟通 Peers。
每个 Peer 通过 PeerID 来标识自己,这是一个 20 字节的数据,格式没有要求。
我们可以通过请求 Tracker 获取到当前资源有哪些 Peer,同时,我们可以向 Tracker 注册自己成为一个 Peer。
Tracker 使用 HTTP 协议,请求时通过 Query 携带参数,下面是三个关键参数:
info_hash: 这个用来表明我们请求的资源是什么,在 BT 下载中,对资源的唯一标识使用的是 InfoHash,也就是种子文件中的 info 字段的内容进行 SHA1 哈希以后得到的结果,20 个字节
peer_id: 我们自己生成的标识身份的一个 ID,20 个字节
port: 我们客户端的监听端口,用于接收其他 Peer 发来的消息
Tracker 返回的信息使用 Bencode 编码,里面含有两个数据,interval 和 peers。
复制 {
interval: 900,
peers: Uint8Array ( 300 ) [
171, 33, 254, 92, 200, 213, 75, 85, 105, 120, 26, 225, 87, 122,
122, 178, 217, 4, 89, 160, 104, 18, 200, 213, 105, 233, 64, 91,
200, 213, 112, 3, 198, 231, 200, 213, 177, 136, 104, 4, 200, 213,
84, 3, 130, 32, 234, 96, 206, 144, 63, 149, 200, 213, 51, 15,
200, 26, 194, 246, 95, 78, 126, 134, 200, 213, 100, 38, 32, 104,
200, 213, 123, 113, 10, 254, 200, 213, 148, 251, 183, 98, 26, 225,
186, 179, 163, 68, 26, 225, 38, 88, 192, 43, 26, 225, 90, 189,
212, 240,
... 200 more items
]
}
peers 是一个 Byte Array,每 6 个字节代表一个 Peer,前 4 个字节为 IP 地址,后 2 个字节为 BigEndian 的端口号。
以上面的输出为例,我们可以得知,第一个 Peer 的地址是 171.33.254.92:51413。
3、Download Process
最后我们来梳理一下使用 BT 下载的完整流程:
请求 Peers,下载 Piece,根据 pieces 字段校验 Piece 的有效性
从种子文件中,我们可以知道,资源被划分为 Piece,每个 Piece 的长度在种子文件中已经确定。
这里我们说的资源可以是一个文件(单文件种子),也可以是多个文件(多文件种子),在 BT 下载的时候,其实不区分这两种情况。不管是单文件还是多文件,都是下载一定数量的 Piece。在多文件的情况下,得到总数据以后,再根据 files 字段中标明的长度和路径来进行切割。
怎样请求 Peers 下载 Piece?这里就是 BT 协议的重点部分。
当我们 TCP 连接 Peer 的时候,第一步是握手。
发送如下数据给到对方进行握手:
1 字节的协议长度 ProtocolLength,填写固定值 0x13
19 个字节的协议名 ProtocolName,填写固定值 BitTorrent protocol
8 字节的保留字段 Reserved,都填写为 0
20 个字节的 InfoHash,从种子文件中计算得到
如果对方是一个正常的 BT Peer 的话,我们会收到同样结构的响应,从中提取出 InfoHash,如果和我们发送的 InfoHash 一样,那么就握手成功了
握手成功以后,接下来便是互发消息。BT 是基于 TCP 的一个上层协议,和任何一个自定义协议一样,BT 定义了自己的消息格式 BTMessage,Peer 之间通过 BTMessage 来交换信息。
一个 BTMessage 由三部分构成:
X 字节的消息体 Payload,含有具体的数据,X 为 Length - 1
重要的消息类型有如下几种:
Bitfield: 将我有的所有 Piece 编码成 Bitfield 发送给对方
当我们连接 Peer 时,默认处于 Choked 状态,也就是不允许向 Peer 请求任何数据,必须先等待 Peer 发送 Unchoke 消息。
这里还有一个细节,当我们使用 Request 下载时,并不是一次请求一个完整的 Piece,而是分为 Block 下载,Block 的大小可以在消息体中指定,一般为 16K。
所以,从 Peer 下载数据的流程是
接收 Peer 发送的 Bitfield 信息,获知 Peer 有哪些 Piece
发送 Request 消息给 Peer,请求 Piece1 的 Block1
Piece1 的所有 Block 下载完毕,校验 SHA1 哈希值
每一个消息类型的具体消息体这样就不再展开了,这些细节对于理解 BT 不重要,在编码时对照 Spec 来做就好。
1. 如何坚持写文档
坚持写文档输出是一个非常好的习惯,不仅可以把自己所思、所学、所想沉淀下来后续回过头来再看(温故而知新),也能给别人分享从而扩大自己的影响力。
突然想写这一点是因为前两天看到群里跟我差不多时间入职的同学分享他近三年来沉淀的文档集合(他甚至打印出来上下册),自愧不如,非常佩服他的毅力以及耐心,回想自己入职近三年来可能也就前一年有所文档产出,后面基本没咋写了,正如本项目一般,突然中断掉。
那么如何坚持呢?我觉得还是要逼自己一把,挑感兴趣的东西写,无论是否技术向,当然首先还是需要输入,然后才能跟自己的思想进行碰撞产生火花,有所内容可以记录。
readme | previous | next