使用 .NET Core 开发 BT Tracker 服务器

一、什么是 BT Tracker ?

在 BT 下载过程当中,我们如果拿到一个种子文件,在其内部会包含一组 BT Tracker 服务器信息。在开始进行下载的时候,BT 下载工具会根据种子内的唯一 HASH 码请求 Tracker 服务器,之后 Tracker 服务器会返回给正在 下载/做种 的 Peer 信息,下载工具获得了其他的 Peer 信息之后就会与其他的 Peer 建立通讯下载数据。

整个过程的时序图如下:

—

在这里 BT Tracker 充当的角色就是一个通讯员的角色,它的构造很简单,最简构造的情况下只需要一个 HTTP API 接口即可。其作用就是在 BT 下载工具请求 Peer 信息的时候,返回相应的信息即可。

二、BT 协议与 BEncode 编码

在 BT 协议通讯的过程当中,所有的数据都是通过 B Encode 进行编码的。这种编码方式类似于 JSON 的数据组织形式,它可以表示字符串与整形这两种基本类型,也可以表示列表与字典这两种数据结构,其语法规则很简单。

字符串 “hello” 的编码形式:

1
2
[字符串长度]:[字符串数据]
5:hello

整数 10 的编码形式:

1
2
i[整数]e
i10e

列表这种数据结构,可以包含任意 B 编码的类型,包括 字符串整形字典(dictionary)列表(list)

包含两个字符串元素 “hello”、“world” 的 列表 编码形式:

1
2
I[内容]e
I5:hello5:world

字典的概念与我们 C# 当中 BCL 所定义的 Dictionary<string,T> 一样,它是由一个键值对组成,其键的类型必须为 B 编码的字符串,而其值可以为任意的 B 编码类型,包括 字符串整形字典(dictionary)列表(list)

1
2
d[内容]e
d4:Name4:Lisa3:Agei15ee

上述内容以 JSON 为例,则表示为:

1
2
3
4
{
    "Name":"Lisa",
    "Age":15
}

在本篇文章的示例当中,没有自行编写 B Encode 的编码与解码工具类,而是使用的第三方库 BencodeNET 来进行操作。

当然,针对于 B Encode 的编解码工具类的编写并不复杂,有了上述的解析,你也可以尝试自己编写一个 B Encode 编解码工具类。

三、整体编写思路

BT Tracker 服务器本质上就是一个 Web Api 项目,BT 客户端携带种子的唯一 HASH 值,去请求某个接口,从而获得正在工作的 Peer 列表。剩下的事情就与 Tracker 服务器无关了,Tacker 服务器的职责就是为 BT 下载工具提供正在工作的其他 BT 客户端。

因此我们第一步就需要建立一个基于 .NET Core 的 Web Api 项目,并编写一个控制器接口用于响应 BT 下载工具的请求。除此之外,我们还需要一个字典用来存储种子与对应的 Peer 集合信息,在 BT 下载工具请求 Tracker 服务器的时候,能够返回相应的 Peer 集合信息。

除了返回给 BT 下载工具 Peer 信息之外,Tracker 还可以根据 Client 请求时携带的附加数据来更新 Peer 的统计信息。(这些信息常用于 PT 站进行积分统计)

Tracker 服务器针对于返回的 Peer 集合有两种处理方式,第一种则是 紧凑模式 ,这个时候 Tracker 服务器需要将 Peer 的 IP 与 Port 按照 [IP 地址(4 byte)][端口号(2 byte)] 的形式进行编码,返回二进制流。另一种则是直接将 Peer 集合的信息,通过 BDictionary 进行编码,其组织形式如下。

1
2
3
{PeerIdKey,PeerId 值},
{IpKey,IP 值}{PortKey,Port 值}

最后总结来说,如果要实现最简的 Tracker 服务器,只需要管理好 Peer (BT 客户端) 的状态,并且响应 Peer 的请求即可。如果需要实现积分制,那么就需要针对 Peer 的信息进行持久化处理。

四、BT Tacker 服务器接口的定义

BT 下载工具向 Tracker 接口请求的参数与返回的参数已经在 BT 协议规范 当中有说明,下面我们就来介绍一下请求参数与返回参数的含义。

4.1 请求参数

参数名称 具体含义 类型 必填
info_hash 种子的唯一 HASH 标识。 string
peer_id BT 下载端的唯一标识,由客户端生成。 string
ip 客户端的 IP 地址。 string
port 客户端监听的端口。 int
uploaded 客户端已经上传的数据大小,以 byte 为单位。 long
downloaded 客户端已经下载的数据大小,以 byte 为单位。 long
left 客户端待下载的数据大小,以 bytes 为单位。 long
event 当前事件,一般有三个值代表客户端现在的状态,已开始 、已停止、已完成。 string
compact 是否启用紧凑模式,如果为 1 则启动,为 0 则正常编码。 int
numWant 客户端想要获得的 Peer 数量。 int

Tracker 的接口返回参数其 Content-Type 的值必须为 text/plain ,并且其结果是通过 B Encode 进行编码的。最外层是一个 BDictionary 字典,内部的数据除了一个 Peer 集合之外,还包含了以下的固定键值对。

4.2 返回参数

字典键 字典值类型 含义 必填
peers BList/BString Peer 列表,根据 compact 参数不同,其值类型不一样。
interval BNumber 客户端发送规则请求到 Tracker 服务器之后的强制等待 时间,以秒为单位。
min interval BNumer 最小的发布时间间隔,客户端的重发间隔不能小于此值,也 是以秒为单位。
tracker id BString Tracker 服务器的 Id,用于标识服务器。
complete BNumber 当前请求的种子,已经完成的 Peer 数量(做种数)。
incomplete BNumber 当前请求的种子,非做种状态的用户。
failure reason BString Tracker 处理失败的原因,为可选参数。

五、编码实现 BT Tracker 服务器

5.1 基本架构

首先新建立一个标准的 Web API 模板项目,删除掉其默认的 ValuesController ,建立一个新的控制器,其名字为 AnnounceController ,最后我们的项目结构如下。

1551669213230

添加一个 GetPeersInfo 接口,其 HTTP Method 为 GET 类型,建立一个输入 DTO 其代码如下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class GetPeersInfoInput
{
    /// <summary>
    /// 种子的唯一 Hash 标识。
    /// </summary>
    public string Info_Hash { get; set; }

    /// <summary>
    /// 客户端的随机 Id,由 BT 客户端生成。
    /// </summary>
    public string Peer_Id { get; set; }

    /// <summary>
    /// 客户端的 IP 地址。
    /// </summary>
    public string Ip { get; set; }

    /// <summary>
    /// 客户端监听的端口。
    /// </summary>
    public int Port { get; set; }

    /// <summary>
    /// 已经上传的数据大小。
    /// </summary>
    public long Uploaded { get; set; }

    /// <summary>
    /// 已经下载的数据大小。
    /// </summary>
    public long Downloaded { get; set; }

    /// <summary>
    /// 事件表示,具体可以转换为 <see cref="TorrentEvent"/> 枚举的具体值。
    /// </summary>
    public string Event { get; set; }

    /// <summary>
    /// 该客户端剩余待下载的数据。
    /// </summary>
    public long Left { get; set; }

    /// <summary>
    /// 是否启用压缩,当该值为 1 的时候,表示当前客户端接受压缩格式的 Peer 列表,即使用
    /// 6 字节表示一个 Peer (前 4 字节表示 IP 地址,后 2 字节表示端口号)。当该值为 0
    /// 的时候则表示客户端不接受。
    /// </summary>
    public int Compact { get; set; }

    /// <summary>
    /// 表示客户端想要获得的 Peer 数量。
    /// </summary>
    public int? NumWant { get; set; }
}

上面仅仅是 PT 客户端传递给 Tracker 服务器的参数信息,为了在后面我们方便使用,我们还需要将其转换为方便操作的充血模型。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
public class AnnounceInputParameters
{
    /// <summary>
    /// 客户端 IP 端点信息。
    /// </summary>
    public IPEndPoint ClientAddress { get; }

    /// <summary>
    /// 种子的唯一 Hash 标识。
    /// </summary>
    public string InfoHash { get; }

    /// <summary>
    /// 客户端的随机 Id,由 BT 客户端生成。
    /// </summary>
    public string PeerId { get; }

    /// <summary>
    /// 已经上传的数据大小。
    /// </summary>
    public long Uploaded { get; }

    /// <summary>
    /// 已经下载的数据大小。
    /// </summary>
    public long Downloaded { get; }

    /// <summary>
    /// 事件表示,具体可以转换为 <see cref="TorrentEvent"/> 枚举的具体值。
    /// </summary>
    public TorrentEvent Event { get; }

    /// <summary>
    /// 该客户端剩余待下载的数据。
    /// </summary>
    public long Left { get; }

    /// <summary>
    /// Peer 是否允许启用压缩。
    /// </summary>
    public bool IsEnableCompact { get; }

    /// <summary>
    /// Peer 想要获得的可用的 Peer 数量。
    /// </summary>
    public int PeerWantCount { get; }

    /// <summary>
    /// 如果在请求过程当中出现了异常,则本字典包含了异常信息。
    /// </summary>
    public BDictionary Error { get; }

    public AnnounceInputParameters(GetPeersInfoInput apiInput)
    {
        Error = new BDictionary();

        ClientAddress = ConvertClientAddress(apiInput);
        InfoHash = ConvertInfoHash(apiInput);
        Event = ConvertTorrentEvent(apiInput);
        PeerId = apiInput.Peer_Id;
        Uploaded = apiInput.Uploaded;
        Downloaded = apiInput.Downloaded;
        Left = apiInput.Left;
        IsEnableCompact = apiInput.Compact == 1;
        PeerWantCount = apiInput.NumWant ?? 30;
    }

    /// <summary>
    /// <see cref="GetPeersInfoInput"/> 到当前类型的隐式转换定义。
    /// </summary>
    public static implicit operator AnnounceInputParameters(GetPeersInfoInput input)
    {
        return new AnnounceInputParameters(input);
    }

    /// <summary>
    /// 将客户端传递的 IP 地址与端口转换为 <see cref="IPEndPoint"/> 类型。
    /// </summary>
    private IPEndPoint ConvertClientAddress(GetPeersInfoInput apiInput)
    {
        if (IPAddress.TryParse(apiInput.Ip, out IPAddress ipAddress))
        {
            return new IPEndPoint(ipAddress,apiInput.Port);
        }

        return null;
    }

    /// <summary>
    /// 将客户端传递的字符串 Event 转换为 <see cref="TorrentEvent"/> 枚举。
    /// </summary>
    private TorrentEvent ConvertTorrentEvent(GetPeersInfoInput apiInput)
    {
        switch (apiInput.Event)
        {
            case "started":
                return TorrentEvent.Started;
            case "stopped":
                return TorrentEvent.Stopped;
            case "completed":
                return TorrentEvent.Completed;
            default:
                return TorrentEvent.None;
        }
    }

    /// <summary>
    /// 将 info_hash 参数从 URL 编码转换为标准的字符串。
    /// </summary>
    private string ConvertInfoHash(GetPeersInfoInput apiInput)
    {
        var infoHashBytes = HttpUtility.UrlDecodeToBytes(apiInput.Info_Hash);
        if (infoHashBytes == null)
        {
            Error.Add(TrackerServerConsts.FailureKey,new BString("info_hash 参数不能为空."));
            return null;
        }

        if (infoHashBytes.Length != 20)
        {
            Error.Add(TrackerServerConsts.FailureKey,new BString($"info_hash 参数的长度 {{{infoHashBytes.Length}}} 不符合 BT 协议规范."));
        }

        return BitConverter.ToString(infoHashBytes);
    }
}

上述代码我们构建了一个新的类型 AnnounceInputParameters ,该类型会将部分参数转换为我们便于操作的类型。这里需要注意的是,我们在 TrackerServerConsts 当中定义了所用到了大部分 BDictionary 关键字。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public enum TorrentEvent
{
	/// <summary>
	/// 未知状态。
	/// </summary>
	None,
	/// <summary>
	/// 已开始。
	/// </summary>
	Started,
	/// <summary>
	/// 已停止。
	/// </summary>
	Stopped,
	/// <summary>
	/// 已完成。
	/// </summary>
	Completed
}

/// <summary>
/// 常用的字典 KEY。
/// </summary>
public static class TrackerServerConsts
{
    public static readonly BString PeerIdKey = new BString("peer id");
    public static readonly BString PeersKey = new BString("peers"); 
    public static readonly BString IntervalKey = new BString("interval");
    public static readonly BString MinIntervalKey = new BString("min interval");
    public static readonly BString TrackerIdKey = new BString("tracker id");
    public static readonly BString CompleteKey = new BString("complete");
    public static readonly BString IncompleteKey = new BString("incomplete");

    public static readonly BString Port = new BString("port");
    public static readonly BString Ip = new BString("ip");

    public static readonly string FailureKey = "failure reason";
}

5.2 Peer 的定义

每一个 Peer 我们定义一个 Peer 类型进行表示,我们可以通过 BT 客户端传递的请求参数来实时更新每个 Peer 对象的信息。

除此之外,根据 BT 协议的规定,在返回 Peer 列表的时候可以返回紧凑型的结果和正常 B 编码结果的 Peer 信息。所以我们也会在 Peer 对象中,增加两个方法用于将 Peer 信息进行特定的编码处理。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/// <summary>
/// 每个 BT 下载客户端的定义。
/// </summary>
public class Peer
{
	/// <summary>
	/// 客户端 IP 端点信息。
	/// </summary>
	public IPEndPoint ClientAddress { get; private set; }

	/// <summary>
	/// 客户端的随机 Id,由 BT 客户端生成。
	/// </summary>
	public string PeerId { get; private set; }

	/// <summary>
	/// 客户端唯一标识。
	/// </summary>
	public string UniqueId { get; private set; }
	
	/// <summary>
	/// 客户端在本次会话过程中下载的数据量。(以 Byte 为单位)
	/// </summary>
	public long DownLoaded { get; private set; }

	/// <summary>
	/// 客户端在本次会话过程当中上传的数据量。(以 Byte 为单位)
	/// </summary>
	public long Uploaded { get; private set; }

	/// <summary>
	/// 客户端的下载速度。(以 Byte/秒 为单位)
	/// </summary>
	public long DownloadSpeed { get; private set; }

	/// <summary>
	/// 客户端的上传速度。(以 Byte/秒 为单位)
	/// </summary>
	public long UploadSpeed { get; private set; }

	/// <summary>
	/// 客户端是否完成了当前种子,True 为已经完成,False 为还未完成。
	/// </summary>
	public bool IsCompleted { get; private set; }

	/// <summary>
	/// 最后一次请求 Tracker 服务器的时间。
	/// </summary>
	public DateTime LastRequestTrackerTime { get; private set; }

	/// <summary>
	/// Peer 还需要下载的数量。
	/// </summary>
	public long Left { get; private set; }

	public Peer() { }

	public Peer(AnnounceInputParameters inputParameters)
	{
		UniqueId = inputParameters.ClientAddress.ToString();
		
		// 根据输入参数更新 Peer 的状态。
		UpdateStatus(inputParameters);
	}

	/// <summary>
	/// 根据输入参数更新 Peer 的状态。
	/// </summary>
	/// <param name="inputParameters">BT 客户端请求 Tracker 服务器时传递的参数。</param>
	public void UpdateStatus(AnnounceInputParameters inputParameters)
	{
		var now = DateTime.Now;

		var elapsedTime = (now - LastRequestTrackerTime).TotalSeconds;
		if (elapsedTime < 1) elapsedTime = 1;

		ClientAddress = inputParameters.ClientAddress;
		// 通过差值除以消耗的时间,得到每秒的大概下载速度。
		DownloadSpeed = (int) ((inputParameters.Downloaded - DownLoaded) / elapsedTime);
		DownLoaded = inputParameters.Downloaded;
		UploadSpeed = (int) ((inputParameters.Uploaded) / elapsedTime);
		Uploaded = inputParameters.Uploaded;
		Left = inputParameters.Left;
		PeerId = inputParameters.PeerId;
		LastRequestTrackerTime = now;
		
		// 如果没有剩余数据,则表示 Peer 已经完成下载。
		if (Left == 0) IsCompleted = true;
	}

	/// <summary>
	/// 将 Peer 信息进行 B 编码,按照协议处理为字典。
	/// </summary>
	public BDictionary ToEncodedDictionary()
	{
		return new BDictionary
		{
			{TrackerServerConsts.PeerIdKey,new BString(PeerId)},
			{TrackerServerConsts.Ip,new BString(ClientAddress.Address.ToString())},
			{TrackerServerConsts.Port,new BNumber(ClientAddress.Port)}
		};
	}

	/// <summary>
	/// 将 Peer 信息进行紧凑编码成字节组。
	/// </summary>
	public byte[] ToBytes()
	{
		var portBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short) ClientAddress.Port));
		var addressBytes = ClientAddress.Address.GetAddressBytes();

		var resultBytes = new byte[portBytes.Length + addressBytes.Length];
		
		// 根据协议规定,首部的 4 字节为 IP 地址,尾部的 2 自己为端口信息
		Array.Copy(addressBytes,resultBytes,addressBytes.Length);
		Array.Copy(portBytes,0,resultBytes,addressBytes.Length,portBytes.Length);

		return resultBytes;
	}
}

5.3 管理种子与其 Peer 集合

BT 客户端请求 Tracker 服务器的目的只有一个,就是获取正在 下载同一个种子的 Peer 列表 ,明白了这一点之后就知道我们需要一个字典来管理种子与可用 Peer 集合的关系。

在上一节我们知道,客户端在请求 Tracker 服务器的时候会带上正在下载的种子唯一 Hash 值,而我们则可以根据这个 Hash 值来索引我们 Peer 列表。

PT 站的原理也是类似,会有一个种子表,这个表以种子的唯一 Hash 值作为主键,并添加某些扩展字段。(IMDB 评分、描述、视频信息等…)

这里我们定义一个 IBitTorrentManager 管理器对象,通过该对象来管理种子的状态,以及种子与 Peer 集合的状态。该接口的定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/// <summary>
/// 用于管理 BT 种子与其关联的 Peer 集合。
/// </summary>
public interface IBitTorrentManager
{
	/// <summary>
	/// 添加一个新的 Peer 到指定种子关联的集合当中。
	/// </summary>
	/// <param name="infoHash">种子的唯一标识。</param>
	/// <param name="inputParameters">BT 客户端传入的参数信息。</param>
	Peer AddPeer(string infoHash,AnnounceInputParameters inputParameters);
	
	/// <summary>
	/// 根据参数删除指定种子的 Peer 信息。
	/// </summary>
	/// <param name="infoHash">种子的唯一标识。</param>
	/// <param name="inputParameters">BT 客户端传入的参数信息。</param>
	void DeletePeer(string infoHash,AnnounceInputParameters inputParameters);

	/// <summary>
	/// 更新指定种子的某个 Peer 状态。
	/// </summary>
	/// <param name="infoHash">种子的唯一标识。</param>
	/// <param name="inputParameters">BT 客户端传入的参数信息。</param>
	void UpdatePeer(string infoHash, AnnounceInputParameters inputParameters);

	/// <summary>
	/// 获得指定种子的可用 Peer 集合。
	/// </summary>
	/// <param name="infoHash">种子的唯一标识。</param>
	/// <returns>当前种子关联的 Peer 列表。</returns>
	IReadOnlyList<Peer> GetPeers(string infoHash);

	/// <summary>
	/// 清理指定种子内部不活跃的 Peer 。 
	/// </summary>
	/// <param name="infoHash">种子的唯一标识。</param>
	/// <param name="expiry">超时周期,超过这个时间的 Peer 将会被清理掉。</param>
	void ClearZombiePeers(string infoHash,TimeSpan expiry);

	/// <summary>
	/// 获得指定种子已经完成下载的 Peer 数量。
	/// </summary>
	/// <param name="infoHash">种子的唯一标识。</param>
	int GetComplete(string infoHash);

	/// <summary>
	/// 获得指定种子正在下载的 Peer 数量。
	/// </summary>
	/// <param name="infoHash">种子的唯一标识。</param>
	int GetInComplete(string infoHash);
}

前四个方法都是用于管理种子关联的 Peer 数据的,就是一些 CRUD 操作。由于某些用户可能不再做种,这个时候他的 Peer 信息就是无用的,就需要进行清理,所以我们也提供了一个 ClearZombiePeers() 方法来清理这些无效的 Peer 。

最后两个方法是用于更新种子的最新状态,每一个种子除了它关联的 Peer 信息,同时也有一些统计信息,例如已经完成的 Peer 数,正在下载的 Peer 数,下载完成等统计信息,这里我们可以建立一个类存放这些统计信息以跟种子相关联。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/// <summary>
/// 用于表示某个种子的状态与统计信息。
/// </summary>
public class BitTorrentStatus
{
	/// <summary>
	/// 下载完成的 Peer 数量。
	/// </summary>
	public BNumber Downloaded { get; set; }

	/// <summary>
	/// 已经完成种子下载的 Peer 数量。
	/// </summary>
	public BNumber Completed { get; set; }

	/// <summary>
	/// 正在下载种子的 Peer 数量。
	/// </summary>
	public BNumber InCompleted { get; set; }

	public BitTorrentStatus()
	{
		Downloaded = new BNumber(0);
		Completed = new BNumber(0);
		InCompleted = new BNumber(0);
	}
}

接下来我们就来实现 IBitTorrentManager 接口。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
public class BitTorrentManager : IBitTorrentManager
{
	private readonly ConcurrentDictionary<string, List<Peer>> _peers;
	private readonly ConcurrentDictionary<string, BitTorrentStatus> _bitTorrentStatus;

	public BitTorrentManager()
	{
		_peers = new ConcurrentDictionary<string, List<Peer>>();
		_bitTorrentStatus = new ConcurrentDictionary<string, BitTorrentStatus>();
	}

	public Peer AddPeer(string infoHash, AnnounceInputParameters inputParameters)
	{
		CheckParameters(infoHash, inputParameters);

		var newPeer = new Peer(inputParameters);
		
		if (!_peers.ContainsKey(infoHash))
		{
			_peers.TryAdd(infoHash, new List<Peer> {newPeer});
		}
		
		_peers[infoHash].Add(newPeer);
		
		UpdateBitTorrentStatus(infoHash);
		
		return newPeer;
	}

	public void DeletePeer(string infoHash, AnnounceInputParameters inputParameters)
	{
		CheckParameters(infoHash, inputParameters);

		if (!_peers.ContainsKey(infoHash)) return;

		_peers[infoHash].RemoveAll(p => p.UniqueId == inputParameters.ClientAddress.ToString());
		
		UpdateBitTorrentStatus(infoHash);
	}

	public void UpdatePeer(string infoHash, AnnounceInputParameters inputParameters)
	{
		CheckParameters(infoHash, inputParameters);
		
		if (!_peers.ContainsKey(inputParameters.InfoHash)) _peers.TryAdd(infoHash, new List<Peer>());
		if (!_bitTorrentStatus.ContainsKey(inputParameters.InfoHash)) _bitTorrentStatus.TryAdd(infoHash, new BitTorrentStatus());

		// 如果 Peer 不存在则添加,否则更新其状态。
		var peers = _peers[infoHash];
		var peer = peers.FirstOrDefault(p => p.UniqueId == inputParameters.ClientAddress.ToString());
		if (peer == null)
		{
			AddPeer(infoHash, inputParameters);
		}
		else
		{
			peer.UpdateStatus(inputParameters);
		}
		
		// 根据事件更新种子状态与 Peer 信息。
		if (inputParameters.Event == TorrentEvent.Stopped) DeletePeer(infoHash,inputParameters);
		if (inputParameters.Event == TorrentEvent.Completed) _bitTorrentStatus[infoHash].Downloaded++;
		
		UpdateBitTorrentStatus(infoHash);
	}

	public IReadOnlyList<Peer> GetPeers(string infoHash)
	{
		if (!_peers.ContainsKey(infoHash)) return null;
		return _peers[infoHash];
	}

	public void ClearZombiePeers(string infoHash, TimeSpan expiry)
	{
		if (!_peers.ContainsKey(infoHash)) return;

		var now = DateTime.Now;

		_peers[infoHash].RemoveAll(p => now - p.LastRequestTrackerTime > expiry);
	}

	public int GetComplete(string infoHash)
	{
		if (_bitTorrentStatus.TryGetValue(infoHash, out BitTorrentStatus status))
		{
			return status.Completed;
		}

		return 0;
	}

	public int GetInComplete(string infoHash)
	{
		if (_bitTorrentStatus.TryGetValue(infoHash, out BitTorrentStatus status))
		{
			return status.InCompleted;
		}

		return 0;
	}

	/// <summary>
	/// 更新种子的统计信息。
	/// </summary>
	private void UpdateBitTorrentStatus(string infoHash)
	{
		if (!_peers.ContainsKey(infoHash)) return;
		if (!_bitTorrentStatus.ContainsKey(infoHash)) return;

		// 遍历种子所有的 Peer 状态,对种子统计信息进行处理。
		int complete = 0, incomplete = 0;
		var peers = _peers[infoHash];
		foreach (var peer in peers)
		{
			if (peer.IsCompleted) complete++;
			else incomplete++;
		}

		_bitTorrentStatus[infoHash].Completed = complete;
		_bitTorrentStatus[infoHash].InCompleted = incomplete;
	}

	/// <summary>
	/// 检测参数与种子唯一标识的状态。
	/// </summary>
	private void CheckParameters(string infoHash,AnnounceInputParameters inputParameters)
	{
		if (string.IsNullOrEmpty(infoHash)) throw new Exception("种子的唯一标识不能为空。");
		if (inputParameters == null) throw new Exception("BT 客户端传入的参数不能为空。");
	}
}

5.4 响应客户端请求

上述工作完成之后,我们就需要来构建我们的响应结果了。根据 BT 协议的规定,返回的结果是一个字典类型(BDictionary) ,并且还要支持紧凑模式与非紧凑模式。

现在我们可以通过 IBitTorrentManager 来获得所需要的 Peer 信息,这个时候只需要将这些信息按照 BT 协议来组装即可。

来到 GetPeersInfo() 接口开始编码,首先我们编写一个方法用于构建 Peer 集合的结果,这个方法可以处理紧凑/非紧凑两种模式的 Peer 信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 /// <summary>
 /// 将 Peer 集合的数据转换为 BT 协议规定的格式
 /// </summary>
 private void HandlePeersData(BDictionary resultDict, IReadOnlyList<Peer> peers, AnnounceInputParameters inputParameters)
 {
	 var total = Math.Min(peers.Count, inputParameters.PeerWantCount);
	 //var startIndex = new Random().Next(total);
	 
	 // 判断当前 BT 客户端是否需要紧凑模式的数据。
	 if (inputParameters.IsEnableCompact)
	 {
		 var compactResponse = new byte[total * 6];
		 for (int index =0; index<total; index++)
		 {
			 var peer = peers[index];
			 Buffer.BlockCopy(peer.ToBytes(),0,compactResponse,(total -1) *6,6);
		 }
		 
		 resultDict.Add(TrackerServerConsts.PeersKey,new BString(compactResponse));
	 }
	 else
	 {
		 var nonCompactResponse = new BList();
		 for (int index =0; index<total; index++)
		 {
			 var peer = peers[index];
			 nonCompactResponse.Add(peer.ToEncodedDictionary());
		 }
		 
		 resultDict.Add(TrackerServerConsts.PeersKey,nonCompactResponse);
	 }
 }

处理完成之后,在 GetPeersInfo() 方法内部针对返回结果的字典结合 Peer 列表进行构建,构建完成之后写入到响应体当中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
 [HttpGet]
 [Route("/Announce/GetPeersInfo")]
 public async Task GetPeersInfo(GetPeersInfoInput input)
 {
	 // 如果 BT 客户端没有传递 IP,则通过 Context 获得。
	 if (string.IsNullOrEmpty(input.Ip)) input.Ip = _httpContextAccessor.HttpContext.Connection.RemoteIpAddress.MapToIPv4().ToString();

     // 本机测试用。
	 input.Ip = "127.0.0.1";
	 
	 AnnounceInputParameters inputPara = input;
	 var resultDict = new BDictionary();

	 // 如果产生了错误,则不执行其他操作,直接返回结果。
	 if (inputPara.Error.Count == 0)
	 {
		 _bitTorrentManager.UpdatePeer(input.Info_Hash,inputPara);
		 _bitTorrentManager.ClearZombiePeers(input.Info_Hash,TimeSpan.FromMinutes(10));
		 var peers = _bitTorrentManager.GetPeers(input.Info_Hash);
	 
		 HandlePeersData(resultDict,peers,inputPara);
	 
		 // 构建剩余字段信息
		 // 客户端等待时间
		 resultDict.Add(TrackerServerConsts.IntervalKey,new BNumber((int)TimeSpan.FromSeconds(30).TotalSeconds));
		 // 最小等待间隔
		 resultDict.Add(TrackerServerConsts.MinIntervalKey,new BNumber((int)TimeSpan.FromSeconds(30).TotalSeconds));
		 // Tracker 服务器的 Id
		 resultDict.Add(TrackerServerConsts.TrackerIdKey,new BString("Tracker-DEMO"));
		 // 已完成的 Peer 数量
		 resultDict.Add(TrackerServerConsts.CompleteKey,new BNumber(_bitTorrentManager.GetComplete(input.Info_Hash)));
		 // 非做种状态的 Peer 数量
		 resultDict.Add(TrackerServerConsts.IncompleteKey,new BNumber(_bitTorrentManager.GetInComplete(input.Info_Hash)));
	 }
	 else
	 {
		 resultDict = inputPara.Error;
	 }
				  
	 // 写入响应结果。
	 var resultDictBytes = resultDict.EncodeAsBytes();
	 var response = _httpContextAccessor.HttpContext.Response;
	 response.ContentType = "text/plain;";
	 response.StatusCode = 200;
	 response.ContentLength = resultDictBytes.Length;
	 await response.Body.WriteAsync(resultDictBytes);
 }

5.5 测试效果

1551953743862

1551953765483

1551953787850

20190125141004

1551954191181

六、源码下载

本 DEMO 已经托管到 Github 上,有需要的朋友可以自行前往以下地址进行 clone 。

GitHub 仓库地址: https://github.com/GameBelial/BTTrackerDemo

Built with Hugo
主题 StackJimmy 设计