从零开始编写一个BitTorrent下载器

BT协议

简介

BT协议Bit Torrent(BT)是一种通信协议,又是一种应用程序,广泛用于对等网络通信(P2P)。曾经风靡一时,由于它引起了巨大的流量,对因特网的运营、维护和管理都产生了重要的影响。

BT协议的典型特征就是没有中心服务器。BT协议中,作为参与者的机器被称为peerspeer之间的通信协议又被称为peer wire protocal,即peer连线协议,是一个基于TCP协议的应用层协议。

BT协议在20年里不断发展(从2001年开始),加入加密、私有种子等设计,也扩展了搜寻peer主机的方法。

连接

由于没有中心服务器,参与者需要使用另外的方法取得他人的地址,以建立对等连接,确定自己的机器应当从何处下载需要的文件。传统的BT协议使用中介服务器trackers来告知每个参与者如何进行下载。trackers服务器是基于HTTP的,这类服务器本身不托管文件资源,仅为每个参与者分配peers。

在BT协议网络中传播违法资源的现象十分常见,这导致其中介服务器常常会受到法律制裁,查封事件屡见不鲜。要解决这一问题,就需要将主机搜寻的工作下放到每个参与者的机器,即分布式处理(distributed process)。BT协议未来的核心就是DHT、PEX、磁力链

.torrent文件解析

以debian发布的镜像文件种子为例。

image

一个.torrent文件描述了可下载文件的内容以及需要连接到的tracker中介服务器的信息,其编码格式为Bencode

文件的头部信息可以直接以文本形式查看:

  1. d8:announce41:http://bttracker.debian.org:6969/announce7:comment35:"Debian CD from cdimage.debian.org"13:creation datei1612616380e9:httpseedsl146:https://cdimage.debian.org/cdimage/release/edu//srv/cdbuilder.debian.org/dst/deb-cd/weekly-builds/amd64/iso-cd/debian-edu-10.8.0-amd64-netinst.iso146:https://cdimage.debian.org/cdimage/archive/edu//srv/cdbuilder.debian.org/dst/deb-cd/weekly-builds/amd64/iso-cd/debian-edu-10.8.0-amd64-netinst.isoe4:infod6:lengthi425721856e4:name35:debian-edu-10.8.0-amd64-netinst.iso12:piece lengthi262144e6:pieces32480:[每个部分的hash,以二进制表示]

之后的内容为二进制,无法直接查看。

美化一下这个部分的信息,可以发现清晰的结构特征:

  1. d
  2. 8:announce
  3. 41:http://bttracker.debian.org:6969/announce
  4. 7:comment
  5. 35:"Debian CD from cdimage.debian.org"
  6. 13:creation date
  7. i1612616380e
  8. 9:httpseeds
  9. l
  10. 146:https://cdimage.debian.org/cdimage/release/edu//srv/cdbuilder.debian.org/dst/deb-cd/weekly-builds/amd64/iso-cd/debian-edu-10.8.0-amd64-netinst.iso
  11. 146:https://cdimage.debian.org/cdimage/archive/edu//srv/cdbuilder.debian.org/dst/deb-cd/weekly-builds/amd64/iso-cd/debian-edu-10.8.0-amd64-netinst.iso
  12. e
  13. 4:info
  14. d
  15. 6:length
  16. i425721856e
  17. 4:name
  18. 35:debian-edu-10.8.0-amd64-netinst.iso
  19. 12:piece length
  20. i262144e
  21. 6:pieces
  22. 32480:[每个部分的hash,以二进制表示]
  23. e
  24. e

其中包含了tracker服务器的URL、创建事件(Unix时间戳)、文件名和文件大小、以及一系列表示每个文件块的SHA-1哈希值的二进制片段(文件块是指文件被等量拆分后形成的几个部分)。每个种子中文件被拆分的大小依据是不同的,但基本处在一个区间内(256KB到1MB)。因为这样的设计,大型文件将会被拆分成众多碎片。在实际下载中,下载执行者会从能够连接的那些peers主机下载文件块,并且根据种子文件校验其哈希值,最后拼接成完整的文件。

这种机制能够确保每个文件块的完整性,抵御设备故障或恶意投毒(torrent poisoning)造成的损害。如果攻击者不能破解SHA-1进行原像攻击(preimage attack),那么下载取得的文件就是安全可靠的。

Bencode编码

从已知的信息可以看出,.torrent文件中的元数据均以“键:值”形式存储,故可以将整个内容理解为一个经过特殊编码的字典,或者一个近似的JSON。

Bencode中,数字采用十进制编码,相比纯二进制编码显得效率较低,但保证了良好的跨平台性(无大小端存储问题)。

Bencode支持四种类型的数据:string、int、Dictionary<string, object>、List<object>。

  • string类型

    string类型的编码格式为[length]:[string],以字符串长度开头,以字符串内容结束。示例:

    1. "abc" => 3:abc
  • int类型

    int类型的编码格式为i[int]e,以i开头,以e结尾。示例:

    1. 123 => i123e
  • Dictionary<string, object>类型

    Dictionary<string, object>类型的编码格式为d[Key-Value Pair]e,以d开头,以e结尾。示例:

    1. Dictionary<{"name":"create chen"},{"age":23}> => d4:name11:create chen3:agei23ee
  • List<object>类型

    List<object>类型的编码格式为l[object]e,以l开头,以e结尾。示例:

    1. List<"abc", 123> => l3:abci123ee

Bencode实现

编码

  1. public static string Encode(object obj)
  2. {
  3. var sb = new StringBuilder();
  4. if(obj is Dictionary<string,object>)
  5. {
  6. var parseObj = obj as Dictionary<string, object>;
  7. sb.Append("d");
  8. foreach (var o in parseObj)
  9. {
  10. sb.AppendFormat("{0}:{1}{2}", o.Key.Length,o.Key, Encode(o.Value));
  11. }
  12. sb.Append("e");
  13. }
  14. if ((obj as int?) != null)
  15. {
  16. var parseObj = (int) obj;
  17. sb.AppendFormat("i{0}e", parseObj);
  18. }
  19. if (obj is List<object>)
  20. {
  21. var parseObj = obj as List<object>;
  22. sb.Append("l");
  23. foreach (var o in parseObj)
  24. {
  25. sb.Append(Encode(o));
  26. }
  27. sb.Append("e");
  28. }
  29. if (obj is string)
  30. {
  31. var parseObj = obj as string;
  32. sb.AppendFormat("{0}:{1}", parseObj.Length, parseObj);
  33. }
  34. return sb.ToString();
  35. }

解码

  1. public static object Decode(string s)
  2. {
  3. return DecodeObject(s, ref _index, EncodeState.Value);
  4. }
  5. private enum EncodeState
  6. {
  7. Key,
  8. Value
  9. }
  10. private static int _index;
  11. private static object DecodeObject(string str,ref int index, EncodeState state)
  12. {
  13. var obj = new Dictionary<string, object>();
  14. var c = str[index];
  15. while (c != 'e')
  16. {
  17. if (c == 'd')
  18. {
  19. index++;
  20. return DecodeObject(str, ref index,EncodeState.Key);
  21. }
  22. if (c == 'i')
  23. {
  24. var value = "";
  25. index++; c = str[index];
  26. while (c != 'e')
  27. {
  28. value += c.ToString(CultureInfo.InvariantCulture);
  29. index++;
  30. c = str[index];
  31. }
  32. return Convert.ToInt32(value);
  33. }
  34. if (c == 'l')
  35. {
  36. index++;
  37. var value = new List<object>();
  38. while (str[index]!='e')
  39. {
  40. value.Add(DecodeObject(str, ref index, EncodeState.Value));
  41. index++;
  42. }
  43. return value;
  44. }
  45. if ('0' < c && c <= '9')
  46. {
  47. string strLength = "";
  48. while (c != ':')
  49. {
  50. strLength += c.ToString(CultureInfo.InvariantCulture);
  51. c = str[++index];
  52. }
  53. var length = Convert.ToInt32(strLength);
  54. var strContent = "";
  55. for (int i = 0; i < length; i++)
  56. {
  57. strContent += str[index + 1].ToString(CultureInfo.InvariantCulture);
  58. index++;
  59. }
  60. if (state == EncodeState.Value)
  61. {
  62. return strContent;
  63. }
  64. index++;
  65. obj.Add(strContent, DecodeObject(str, ref index, EncodeState.Value));
  66. state = EncodeState.Key;
  67. index++;
  68. }
  69. c = str[index];
  70. }
  71. return obj;
  72. }

编写项目

这里使用Go来编写,也是首次使用Go完成网络工具。仅包含主要代码,完整项目见Github

寻找

解析种子(~/torrentfile/torrentfile.go)

  1. import (
  2. "github.com/jackpal/bencode-go"
  3. )

这里省略了自带库文件的导入。

  1. type bencodeInfo struct {
  2. Pieces string `bencode:"pieces"`
  3. PieceLength int `bencode:"piece length"`
  4. Length int `bencode:"length"`
  5. Name string `bencode:"name"`
  6. }
  7. type bencodeTorrent struct {
  8. Announce string `bencode:"announce"`
  9. Info bencodeInfo `bencode:"info"`
  10. }
  1. // Open函数用于解析种子
  2. func Open(path string) (TorrentFile, error) {
  3. file, err := os.Open(path)
  4. if err != nil {
  5. return TorrentFile{}, err
  6. }
  7. defer file.Close()
  8. bto := bencodeTorrent{}
  9. err = bencode.Unmarshal(file, &bto)
  10. if err != nil {
  11. return TorrentFile{}, err
  12. }
  13. return bto.toTorrentFile()
  14. }

处理时,将pieces对应的值(原先为哈希值的字符串)变成哈希值切片(每个长度为20 bytes),以便后续调用每个独立的哈希值。另外,计算info对应的整个字典(含有名称、大小、文件块哈希值)的SHA-1哈希值,存储在infohash,在与trackers服务器和peers主机交互时表示所需的文件。

  1. type TorrentFile struct {
  2. Announce string
  3. InfoHash [20]byte
  4. PieceHashes [][20]byte
  5. PieceLength int
  6. Length int
  7. Name string
  8. }
  1. func (bto bencodeTorrent) toTorrentFile() (TorrentFile, error) {
  2. // ...
  3. }

从trackers服务器获取peers主机地址(~/torrentfile/tracker.go)

处理完种子后,就可以向trackers服务器发起请求:作为一台peer主机,需要获取同一网络中的其它peers主机的列表。只需要对announce对应URL发起GET请求(需要设置几个请求参数)。

  1. // buildTrackerURL函数用于构成请求peers列表的序列
  2. func (t * TorrentFile) buildTrackerURL(peerID [20]byte, port uint16) (string, error) {
  3. base, err:= url.Parse(t.Announce)
  4. if err != nil {
  5. return "", err
  6. }
  7. params := url.Values{
  8. "info_hash": []string{string(t.InfoHash[:])},
  9. "peer_id": []string{string(peerID[:])},
  10. "port": []string{strconv.Itoa(int(port))},
  11. "uploaded": []string{"0"},
  12. "downloaded": []string{"0"},
  13. "compact": []string{"1"},
  14. "left": []string{strconv.Itoa(t.Length)},
  15. }
  16. base.RawQuery = params.Encode()
  17. return base.String(), nil
  18. }

其中重要的参数有:

  • info_hash:用以标识需要下载的文件,其值就是之前由info对应值计算出的infohash。trackers服务器基于这个值返回能够为下载提供资源的peers主机。
  • peer_id:20字节长的数据,用于向peers主机和trackers服务器标识自己的身份。具体实现仅仅是产生随机的20个字节。真实的BitTorrent客户端ID形如-TR2940-k8hj0wgej6ch,标出了客户端软件及其版本(TR2940表示Transmission Client 2.94)。

处理trackers服务器的响应(~/peers/peers.go)

服务器响应也是采用Bencode编码的:

  1. d
  2. 8:interval
  3. i900e
  4. 5:peers
  5. 252:[很长的二进制块]
  6. e

interval表示本地应当在多长的时间间隔后再次向tracker服务器请求以刷新peers主机列表,900的单位是秒。peers包含了每个peer主机的IP地址,以二进制表示,由若干个6字节元组成,前4个字节表示主机IP,后2个字节表示端口号(大端存储的16位无符号整型,uint16)。大端存储,即big-endian,是网络中所采用的存储方式(相对于小端存储),故被称为network order。运算时可以直接将一组字节从左至右拼接以形成所要表达的整数,如0x1A0xE1能拼接成0x1AE1,即十进制的6881。

  1. type Peer struct {
  2. IP net.IP
  3. Port uint16
  4. }
  1. // Unmarshal函数从缓冲区解析IP及其端口
  2. func Unmarshal(peerBin []byte)([]Peer, error) {
  3. const peerSize = 6
  4. numPeers := len(peerBin) / peerSize
  5. if len(peerBin) % peerSize != 0 {
  6. err := fmt.Errorf("received malformed peers")
  7. return nil, err
  8. }
  9. peers := make([]Peer, numPeers)
  10. for i := 0; i < numPeers ; i++ {
  11. offset := i * peerSize
  12. peers[i].IP = net.IP(peerBin[offset : offset+4])
  13. peers[i].Port = binary.BigEndian.Uint16(peersBin[offset+4 : offset+6])
  14. }
  15. return peers, nil
  16. }

下载

在取得peers主机的地址后,就可以进行下载了。对每台peer主机的连接,有如下的几个步骤:

  1. 与目标peer建立TCP连接;
  2. 完成BitTorrent握手;
  3. 交换信息(告知对方本地需要的资源)。

TCP连接(~/client/client.go)

设定一个超时检测机制,防止消耗过多网络资源。

  1. conn, err := net.DialTimeout("tcp", peer.String(), 3*time.Second)
  2. if err != nil {
  3. return nil, err
  4. }

握手(~/handshake/handshake.go)

通过达成握手,以确定某peer主机具有期望的功能:

  • 能够使用BT协议通信;
  • 能够理解本机发出的信息,并作出响应;
  • 持有本机需要的文件资源,或者持有文件资源在网络中位置的索引。

BitTorrent握手行为需要传输的信息由5个部分构成:

  1. 协议标识(表明这是BitTorrent协议)的长度,即19,十六进制表示为0x13
  2. 协议标识,被称为pstr,即BitTorrent protocol
  3. 8个保留字节,默认全为0,如果客户端支持BT协议的某些扩展,则需要将其中一些设置为1;
  4. infohash,基于种子中info对应的全部信息计算得出的哈希值,用于标明本机需要的文件;
  5. PEER ID,用于标明本机身份。

这些信息组合起来,就是达成握手需要的序列:

  1. \x13BitTorrent protocol\x00\x00\x00\x00\x00\x00\x00\x00\x86\xd4\xc8\x00\x24\xa4\x69\xbe\x4c\x50\xbc\x5a\x10\x2c\xf7\x17\x80\x31\x00\x74-TR2940-k8hj0wgej6ch

本机发出这些信息后,peers主机应当以相同形式响应,且返回的infohash应当与本机持有的一致。

使用一个结构体表示握手包,并添加一些序列化、读取函数。

  1. // 握手包结构体
  2. type Handshake struct {
  3. Pstr string
  4. InfoHash [20]byte
  5. PeerID [20]byte
  6. }
  1. //Serialize函数用于序列化握手信息
  2. func (h *Handshake) Serialize() []byte {
  3. buf := make([]byte, len(h.Pstr)+49)
  4. buf[0] = byte(len(h.Pstr))
  5. curr := 1
  6. curr += copy(buf[curr:], h.Pstr)
  7. curr += copy(buf[curr:], make([]byte, 8)) //即8个保留字节
  8. curr += copy(buf[curr:], h.InfoHash[:])
  9. curr += copy(buf[curr:], h.PeerID[:])
  10. return buf
  11. }
  12. func Read(r io.Reader) (* Handshake, error) {
  13. // ...
  14. }

信息

完成握手后就将开始正式的收发信息。如果远端的peers主机未能做好收发的准备,本机仍旧无法发送信息,此时本机会被远端认定为阻塞的(choked)。在peers主机完成准备后,会向本机发送解除阻塞(unchoke)信息。代码设计中,默认需要杰出阻塞才能进行下载。

解析(~/message/message.go)

信息包含三个部分:长度、ID、payload。

长度为32位整型,是大端存储形式的4个字节。ID用以表示信息类型,这在代码中进行了详细定义。

  1. type messageID uint8
  2. const (
  3. // MsgChoke表示阻塞
  4. MsgChoke messageID = 0
  5. // MsgUnchoke表示解除阻塞
  6. MsgUnchoke messageID = 1
  7. // MsgInterested表示信息相关
  8. MsgInterested messageID = 2
  9. // MsgNotInterested表示信息不相关
  10. MsgNotInterested messageID = 3
  11. // MsgHave表示提醒接收者,发送者拥有资源
  12. MsgHave messageID = 4
  13. // MsgBitfield表示发送者拥有资源的哪些部分
  14. MsgBitfield messageID = 5
  15. // MsgRequest表示向接收方请求数据
  16. MsgRequest messageID = 6
  17. // MsgPiece表示发送数据以完成请求
  18. MsgPiece messageID = 7
  19. // MsgCancel表示取消一个请求
  20. MsgCancel messageID = 8
  21. )
  22. //Message结构体储存ID和包含信息的payload
  23. type Message struct {
  24. ID messageID
  25. Payload []byte
  26. }
  1. // Serialize函数用于执行序列化
  2. // 信息依次为前缀、信息的ID、payload
  3. // 需要将`nil`解释为`keep-alive`
  4. func (m *Message) Serialize() []byte {
  5. if m == nil {
  6. return make([]byte, 4)
  7. }
  8. length := uint32(len(m.Payload) + 1)
  9. buf := make([]byte, 4+length)
  10. binary.BigEndian.PutUint32(buf[0:4], length)
  11. buf[4] = byte(m.ID)
  12. copy(buf[5:], m.Payload)
  13. return buf
  14. }

为读取信息,也需要依照信息格式编写函数。先读取4个字节并作为一个uint32以表示长度length,然后依据这个数字读取相应位数的数据,这部分中的第一个字节表示ID,剩下的表示payload

  1. // Read函数用于解析信息
  2. func Read(r io.Reader) (*Message, error) {
  3. lengthBuf := make([]byte, 4)
  4. _, err := io.ReadFull(r, lengthBuf)
  5. if err != nil {
  6. return nil, err
  7. }
  8. length := binary.BigEndian.Uint32(lengthBuf)
  9. // keep-alive
  10. if length == 0 {
  11. return nil, nil
  12. }
  13. messageBuf := make([]byte, length)
  14. _, err = io.ReadFull(r, messageBuf)
  15. if err != nil {
  16. return nil, err
  17. }
  18. m := Message{
  19. ID: messageID(messageBuf[0]),
  20. Payload: messageBuf[1:],
  21. }
  22. return &m, nil
  23. }
位域(~/bitfield/bitfield.go)

peers主机使用位域来高效地编码自身能够提供的资源分块。位域类似基于字节的数组,被标为1的位即代表拥有这个资源分块。因为使用单个的位即能完成标注,位域有极高的压缩能力,这意味着在一个布尔(bool)空间内完成了8次布尔类型的操作。

当然这样的思路需要一定的代价:可以寻址的最小内存单位是字节,处理单个的位就需要额外的函数设计。

  1. // Bitfield用以表示一台peer主机拥有的资源分块
  2. type Bitfield []byte
  1. // HasPiece用以表明一个位域(bitfield)是否有特定的索引集
  2. func (bf Bitfield) HasPiece(index int) bool {
  3. byteIndex := index / 8
  4. offset := index % 8
  5. if byteIndex < 0 || byteIndex >= len(bf) {
  6. return false
  7. }
  8. return bf[byteIndex] >> uint(7-offset)&1 != 0
  9. }
  10. // SetPiece用以在位域设置单个位
  11. func (bf Bitfield) SetPiece(index int) {
  12. byteIndex := index / 8
  13. offset := index % 8
  14. // 撇除不合规的索引
  15. if byteIndex < 0 || byteIndex >= len(bf) {
  16. return
  17. }
  18. bf[byteIndex] |= 1 << uint(7-offset)
  19. }

组装

至此完成了所有下载种子文件的工具:

  • 从trackers服务器获得了peers主机列表;
  • 与peers主机达成TCP连接;
  • 与peers主机进行握手;
  • 与peers主机收发信息。

现在面临的问题是如何解决下载必然造成的高并发(concurrency),并且需要统一管理每个连接的peer主机的状态(state)

高并发(~/p2p/p2p.go)

Effective Go中对并发的描述中有这样一句话:

Do not communicate by sharing memory; instead, share memory by communicating.

官网给出了解释。

这里将Go中重要的Channel类型作为简洁且线程安全的队列。Channel可以被认为是管道,通过并发核心单元就可以发送或者接收数据进行通讯(communication)。

建立两个Channel来同步并发工作:一个用于在peers主机间分派工作(要下载的资源分块),另一个用于已下载的分块。

  1. workQueue := make(chan *pieceWork, len(t.PieceHashes))
  2. results := make(chan *pieceResult)
  3. for index, hash := range t.PieceHashes {
  4. length := t.calculatePieceSize(index)
  5. workQueue <- &pieceWork{index, hash, length}
  6. }
  7. // 执行下载
  8. for _, peer := range t.Peers {
  9. go t.startDownloadWorker(peer, workQueue, results)
  10. }
  11. // 收集分块
  12. buf := make([]byte, t.Length)
  13. donePieces := 0
  14. for donePieces < len(t.PieceHashes) {
  15. res := <- results
  16. begin, end := t.calculateBoundsForPiece(res.index)
  17. copy(buf[begin:end], res.buf)
  18. donePieces ++
  19. percent := float64(donePieces) / float64(len(t.PieceHashes)) * 100
  20. numWorkers := runtime.NumGoroutine() - 1
  21. log.Printf("(%0.2f%%) downloaded piece #%d from %d peers\n", percent, res.index, numWorkers)
  22. }
  23. close(workQueue)

为取得的每个peer主机都生成一个goroutine(轻量级线程)。每个线程连接peer主机并握手,然后从workQueue中抽取任务,尝试进行下载,并把下载得到的分块传至名为resultschannel

可以用流程图表示这个过程:

image

  1. func (t *Torrent) startDownloadWorker(peer peers.Peer, workQueue chan *pieceWork, results chan *pieceResult) {
  2. c, err := client.New(peer, t.PeerID, t.InfoHash)
  3. if err != nil {
  4. log.Printf("could not handshake eith %s\ndisconnecting\n", peer.IP)
  5. return
  6. }
  7. defer c.Conn.Close()
  8. log.Printf("completed handshake with %s\n", peer.IP)
  9. c.SendUnchoke()
  10. c.SendInterested()
  11. for pw := range workQueue {
  12. if !c.Bitfield.HasPiece(pw.index) {
  13. workQueue <- pw // 将分块重新放入队列
  14. continue
  15. }
  16. // 下载分块
  17. buf, err := attemptDownloadPiece(c, pw)
  18. if err != nil {
  19. log.Println("exiting", err)
  20. workQueue <- pw // 将分块重新放入队列
  21. return
  22. }
  23. err = checkIntegrity(pw, buf)
  24. if err != nil {
  25. log.Printf("piece #%d failed integrity check\n", pw.index)
  26. workQueue <- pw // 将分块重新放入队列
  27. continue
  28. }
  29. c.SendHave(pw.index)
  30. results <- &pieceResult{pw.index, buf}
  31. }
  32. }

状态(~/p2p/p2p.go)

除了上述的流程控制,还需要对每台peer主机跟踪状态。这里使用一个结构体记录跟踪信息,并且需要实时更改跟踪记录,如:从该主机下载的量、向该主机发起请求的量、是否被该主机认定为阻塞。如果足够专业,可以把这种监控扩展为一种有限状态机,但限于项目体量,一个结构体就足以完成任务。

  1. type pieceProgress struct {
  2. index int
  3. client *client.Client
  4. buf []byte
  5. downloaded int
  6. requested int
  7. backlog int
  8. }
  1. func (state *pieceProgress) readMessage() error {
  2. msg, err := state.client.Read()
  3. if err != nil {
  4. return err
  5. }
  6. if msg == nil { // keep-alive
  7. return nil
  8. }
  9. switch msg.ID {
  10. case message.MsgUnchoke:
  11. state.client.Choked = false
  12. case message.MsgChoke:
  13. state.client.Choked = true
  14. case message.MsgHave:
  15. index, err := message.ParseHave(msg)
  16. if err != nil {
  17. return err
  18. }
  19. state.client.Bitfield.SetPiece(index)
  20. case message.MsgPiece:
  21. n, err := message.ParsePiece(state.index, state.buf, msg)
  22. if err != nil {
  23. return err
  24. }
  25. state.downloaded += n
  26. state.backlog --
  27. }
  28. return nil
  29. }

一般而言,BT客户端的任务队列就是5个,所以这里也设定为5个。任务队列的大小在不同网络环境中表现不同,常常提升至10个左右时表现会更好,因此较新的BT的客户端都会弹性调整队列大小。

  1. // MaxBlockSize表示单个请求最多可以获取的字节数
  2. const MaxBlockSize = 16384
  3. // MaxBacklog表示客户端在管道中能够保有的最多未完成请求数
  4. const MaxBacklog = 5
  1. func attemptDownloadPiece(c *client.Client, pw *pieceWork) ([]byte, error) {
  2. state := pieceProgress{
  3. index: pw.index,
  4. client: c,
  5. buf: make([]byte, pw.length),
  6. }
  7. // 设定超时检测以帮助删去不能正常运行的peers主机
  8. c.Conn.SetDeadline(time.Now().Add(30 * time.Second))
  9. defer c.Conn.SetDeadline(time.Time{})
  10. for state.downloaded < pw.length {
  11. if !state.client.Choked {
  12. for state.backlog < MaxBacklog && state.requested < pw.length {
  13. blockSize := MaxBlockSize
  14. if pw.length - state.requested < blockSize {
  15. blockSize = pw.length - state.requested
  16. }
  17. err := c.SendRequest(pw.index, state.requested, blockSize)
  18. if err != nil {
  19. return nil, err
  20. }
  21. state.backlog ++
  22. state.requested += blockSize
  23. }
  24. }
  25. err := state.readMessage()
  26. if err != nil {
  27. return nil, err
  28. }
  29. }
  30. return state.buf, nil
  31. }

主函数

最后来到了主函数。

  1. func main() {
  2. inPath := os.Args[1]
  3. outPath := os.Args[2]
  4. tf, err := torrentfile.Open(inPath)
  5. if err != nil {
  6. log.Fatal(err)
  7. }
  8. err = tf.DownloadToFile(outPath)
  9. if err != nil {
  10. log.Fatal(err)
  11. }
  12. }

项目运行截图

将项目打包为单程序,这里仅展示Windows平台:

image

现在使用下图中的文件进行下载测试:

image

该程序只有一种命令格式:

  1. > Tiny-BT-Client [种子文件名] [最终产物文件名]

image

image

下载成功:

image

image

从零开始编写一个BitTorrent下载器的更多相关文章

  1. 手把手从零开始---封装一个vue视频播放器组件

    现在,在网页上播放视频已经越来越流行,但是网上的资料鱼龙混杂,很难找到自己想要的,今天小编就自己的亲身开发体验,手把手从零开始---封装一个vue视频播放器组件. 作为一个老道的前端搬砖师,怎么可能会 ...

  2. CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL

    CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL +BIT祝威+悄悄在此留下版了个权的信息说: 开始 本文用step by step的方式,讲述如何使 ...

  3. 如何编写一个JSON解析器

    编写一个JSON解析器实际上就是一个函数,它的输入是一个表示JSON的字符串,输出是结构化的对应到语言本身的数据结构. 和XML相比,JSON本身结构非常简单,并且仅有几种数据类型,以Java为例,对 ...

  4. Python 网络爬虫 005 (编程) 如何编写一个可以 下载(或叫:爬取)一个网页 的网络爬虫

    如何编写一个可以 下载(或叫:爬取)一个网页 的网络爬虫 使用的系统:Windows 10 64位 Python 语言版本:Python 2.7.10 V 使用的编程 Python 的集成开发环境:P ...

  5. 从零开始编写一个vue插件

    title: 从零开始编写一个vue插件 toc: true date: 2018-12-17 10:54:29 categories: Web tags: vue mathjax 写毕设的时候需要一 ...

  6. 使用tkinter打造一个小说下载器,想看什么小说,就下什么

    前言 今天教大家用户Python GUI编程--tkinter 打造一个小说下载器,想看什么小说,就下载什么小说 先看下效果图 Tkinter 是使用 python 进行窗口视窗设计的模块.Tkint ...

  7. DIY:从零开始写一个 SQL 构建器

    最近在项目中遇到了一个棘手的问题,因为 EF Core 不支持直接生成 Update 语句,所以这个项目就用到了 EFCore.Plus 来实现这个功能,但是 EFCore.Plus 对 SQLite ...

  8. Android简单的编写一个txt阅读器(没有处理字符编码),适用于新手学习

    本程序只是使用了一些基本的知识点编写了一个比较简单粗陋的txt文本阅读器,效率不高,只适合新手练习.所以大神勿喷. 其实想到编写这种程序源自本人之前喜欢看小说,而很多小说更新太慢,所以本人就只能找一个 ...

  9. 使用C#+XPath+HtmlAgilityPack轻松搞一个资源下载器

    HtmlAgilityPack简介 HtmlAgilityPack是一个开源的解析HTML元素的类库,最大的特点是可以通过XPath来解析HMTL,如果您以前用C#操作过XML,那么使用起HtmlAg ...

随机推荐

  1. codeforces 11B Jumping Jack

    Jack is working on his jumping skills recently. Currently he's located at point zero of the number l ...

  2. Git使用指南(上)

    1 Git简介 学习一门技术老师更加倾向于看官网的. 度娘看完了,官网看完了,大家还是很懵逼 学生成绩管理系统 登录模块   3.2 登录模块进一步完善    缺一个验证码的功能    3.3 登录模 ...

  3. favicon.ico All In One

    favicon.ico All In One link rel="icon" type="image/x-icon" href="http://exa ...

  4. 从GitHub Jobs! 看技术发展趋势! 程序员进阶必备!

    0. https://jobs.github.com/positions GitHub Jobs: 1. https://jobs.github.com/positions/38bb8dc8-b5b4 ...

  5. how to read the system information by using the node cli tool?

    how to read the system information by using the node cli tool? node cli & get system info demos ...

  6. React vs Vue in 2020

    React vs Vue in 2020 技术选型 React // UserProfile.jsx function UserProfile({id, showAvatar, onFollowCli ...

  7. GitHub & GitHub Package Registry

    GitHub & GitHub Package Registry npm https://github.blog/2019-05-10-introducing-github-package-r ...

  8. DevOps tools

    DevOps tools mozart & ansible https://www.ansible.com/integrations/devops-tools websocket jenken ...

  9. TypeScript Generics

    TypeScript Generics https://www.typescriptlang.org/docs/handbook/generics.html 泛型 1 Generic Interfac ...

  10. Flutter: MobX和flutter_mobx状态管理器

    MobX.dart网站上的 " 入门指南" mobxjs video 组织Stores 安装依赖 dependencies: mobx: flutter_mobx: dev_dep ...