[博客翻译]在Go中从头开始构建BitTorrent客户端


原文地址:https://blog.jse.li/posts/torrent/


从零开始用Go构建BitTorrent客户端

你是否好奇过,当我们访问The Pirate Bay时,是如何凭空下载到一个MP3文件的?在本文中,我们将实现足够的BitTorrent协议来下载Debian操作系统。如果你对代码感兴趣,可以查看源代码,或者直接跳到最后的部分。

这篇文章还有俄语版韩语版中文版


BitTorrent简介

BitTorrent是一种用于在互联网上下载和分发文件的协议。与传统的客户端/服务器模型(例如,在Netflix上观看电影或加载你现在阅读的网页)不同,BitTorrent网络中的参与者称为peer(对等节点),它们彼此之间下载文件片段——这就是它被称为点对点(P2P)协议的原因。接下来,我们将探讨它的原理,并构建自己的客户端来找到其他peer并交换数据。

示意图展示了客户端/服务器模型(所有客户端连接到一台服务器)和P2P模型(节点互相连接)的区别


寻找Peer

现在的问题是:我们想要使用BitTorrent下载文件,但它是点对点协议,我们不知道去哪里找到其他peer。这就像搬到一个新城市尝试结交朋友一样——也许我们会去当地的酒吧或聚会活动!集中式的场所正是**tracker(跟踪器)**的概念所在。这些中心化的服务器会将peer互相介绍给对方。

当然,如果这些集中式服务器帮助peer交换非法内容,它们可能会被执法机构查封。你可能还记得一些追踪器如TorrentSpy、Popcorn Time和KickassTorrents被没收和关闭的消息。新的方法通过使peer发现过程也变得分布式来去除中间人。虽然我们不会实现这些功能,但如果你感兴趣,可以研究一下DHTPEX磁力链接等概念。


解析.torrent文件

.torrent文件描述了要下载的文件内容以及连接到tracker的信息。它是启动下载过程所需的一切。Debian的.torrent文件如下所示:

d8:announce41:http://bttracker.debian.org:6969/announce7:comment35:"Debian CD from cdimage.debian.org"13:creation datei1573903810e9:httpseedsl145:https://cdimage.debian.org/cdimage/release/10.2.0//srv/cdbuilder.debian.org/dst/deb-cd/weekly-builds/amd64/iso-cd/debian-10.2.0-amd64-netinst.iso145:https://cdimage.debian.org/cdimage/archive/10.2.0//srv/cdbuilder.debian.org/dst/deb-cd/weekly-builds/amd64/iso-cd/debian-10.2.0-amd64-netinst.isoe4:infod6:lengthi351272960e4:name31:debian-10.2.0-amd64-netinst.iso12:piece lengthi262144e6:pieces26800:�����PS�^�� (二进制散列块)e

这段混乱的内容以Bencode格式编码(读作“bee-encode”),我们需要将其解码。Bencode可以编码类似于JSON的结构类型,如字符串、整数、列表和字典。Bencoded数据不如JSON易于人类阅读或编写,但它能高效处理二进制数据,且解析起来非常简单。

经过整理后,我们的.torrent文件如下:

d
 8:announce
  41:http://bttracker.debian.org:6969/announce
 7:comment
  35:"Debian CD from cdimage.debian.org"
 13:creation date
  i1573903810e
 4:info
  d
   6:length
    i351272960e
   4:name
    31:debian-10.2.0-amd64-netinst.iso
   12:piece length
    i262144e
   6:pieces
    26800:�����PS�^�� (每段文件的SHA-1哈希值组成的二进制块)
  e
e

在这个文件中,我们可以找到tracker的URL、创建日期(Unix时间戳)、文件名称和大小,以及一个包含每个**片段(piece)**SHA-1哈希值的大二进制块。每个片段的大小通常在256KB到1MB之间。这意味着一个大文件可能由数千个片段组成。我们将从其他peer下载这些片段,根据.torrent文件中的哈希值进行校验,然后将它们组装成完整的文件。


从Tracker获取Peer

现在我们已经拥有了关于文件及其tracker的信息,让我们联系tracker来宣布我们的存在,并检索其他peer的列表。我们只需要向.torrent文件中提供的announce URL发送GET请求,并附加一些查询参数即可。

以下是构建tracker URL的函数:

func (t *TorrentFile) buildTrackerURL(peerID [20]byte, port uint16) (string, error) {
	base, err := url.Parse(t.Announce)
	if err != nil {
		return "", err
	}
	params := url.Values{
		"info_hash":  []string{string(t.InfoHash[:])},
		"peer_id":    []string{string(peerID[:])},
		"port":       []string{strconv.Itoa(int(port))},
		"uploaded":   []string{"0"},
		"downloaded": []string{"0"},
		"compact":    []string{"1"},
		"left":       []string{strconv.Itoa(t.Length)},
	}
	base.RawQuery = params.Encode()
	return base.String(), nil
}

解析Tracker响应

我们收到的响应也是Bencode编码的:

d
 8:interval
  i900e
 5:peers
  252:(另一个长二进制块)
e
  • interval告诉我们应该多久再次连接tracker刷新peer列表。例如,900表示每隔15分钟(900秒)重新连接一次。
  • peers是一个包含每个peer IP地址的长二进制块。它由**六个字节