在 BT 下载过程中,咱们若是拿到一个种子文件,在其内部会包含一组 BT Tracker 服务器信息。在开始进行下载的时候,BT 下载工具会根据种子内的惟一 HASH 码请求 Tracker 服务器,以后 Tracker 服务器会返回给正在 下载/作种 的 Peer 信息,下载工具得到了其余的 Peer 信息以后就会与其余的 Peer 创建通信下载数据。
整个过程的时序图以下:
在这里 BT Tracker 充当的角色就是一个通信员的角色,它的构造很简单,最简构造的状况下只须要一个 HTTP API 接口便可。其做用就是在 BT 下载工具请求 Peer 信息的时候,返回相应的信息便可。
2、BT 协议与 BEncode 编码
在 BT 协议通信的过程中,全部的数据都是经过 B Encode 进行编码的。这种编码方式相似于 JSON 的数据组织形式,它能够表示字符串与整形这两种基本类型,也能够表示列表与字典这两种数据结构,其语法规则很简单。
字符串 "hello" 的编码形式:
[字符串长度]:[字符串数据]
5:hello
整数 10 的编码形式:
i[整数]e
i10e
列表这种数据结构,能够包含任意 B 编码的类型,包括 字符串、整形、字典(dictionary)、列表(list) 。
包含两个字符串元素 "hello"、"world" 的 列表 编码形式:
I[内容]e
I5:hello5:world
字典的概念与咱们 C# 当中 BCL 所定义的 Dictionary<string,T> 同样,它是由一个键值对组成,其键的类型必须为 B 编码的字符串,而其值能够为任意的 B 编码类型,包括 字符串、整形、字典(dictionary)、列表(list) 。
在本篇文章的示例当中,没有自行编写 B Encode 的编码与解码工具类,而是使用的第三方库 BencodeNET 来进行操做。
固然,针对于 B Encode 的编解码工具类的编写并不复杂,有了上述的解析,你也能够尝试本身编写一个 B Encode 编解码工具类。
3、总体编写思路
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 进行编码,其组织形式以下。
{PeerIdKey,PeerId 值},
{IpKey,IP 值},
{PortKey,Port 值}
最后总结来讲,若是要实现最简的 Tracker 服务器,只须要管理好 Peer (BT 客户端) 的状态,而且响应 Peer 的请求便可。若是须要实现积分制,那么就须要针对 Peer 的信息进行持久化处理。
4、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 处理失败的缘由,为可选参数。 否
5、编码实现 BT Tracker 服务器
5.1 基本架构
首先新创建一个标准的 Web API 模板项目,删除掉其默认的 ValuesController ,创建一个新的控制器,其名字为 AnnounceController ,最后咱们的项目结构以下。
添加一个 GetPeersInfo 接口,其 HTTP Method 为 GET 类型,创建一个输入 DTO 其代码以下。
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 服务器的参数信息,为了在后面咱们方便使用,咱们还须要将其转换为方便操做的充血模型。
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 关键字。
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 www.yongshi123.cn= new BString("min interval");
public static readonly BString TrackerIdKey = new BString("tracker id");
public static readonly BString CompleteKey = new BString(www.cmeidi.cn"complete");
public static readonly BString IncompleteKey = new BString(www.mhylpt.com"incomplete");
public static readonly BString Port = new BString("port");
public static readonly BString Ip = new BString("ip");
public static readonly string FailureKey = www.huayi157.com"failure reason";
}
5.2 Peer 的定义
每个 Peer 咱们定义一个 Peer 类型进行表示,咱们能够经过 BT 客户端传递的请求参数来实时更新每一个 Peer 对象的信息。
除此以外,根据 BT 协议的规定,在返回 Peer 列表的时候能够返回紧凑型的结果和正常 B 编码结果的 Peer 信息。因此咱们也会在 Peer 对象中,增长两个方法用于将 Peer 信息进行特定的编码处理。
/// <summary>
/// 每一个 BT 下载客户端的定义。
/// </summary>
public class Peer
{
/// <summary>
/// 客户端 IP 端点信息。
/// </summary>
public IPEndPoint ClientAddress www.yingka178.com{ 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 www.yongshiyule178.com BString(ClientAddress.Address.ToString(www.dfgjpt.com ))},
{TrackerServerConsts.Port,new BNumber(ClientAddress.Port)}
};
}
/// <summary>
/// 将 Peer 信息进行紧凑编码成字节组。
/// </summary>
public byte[] ToBytes()
{
var portBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short) ClientAddress.Port));
var addressBytes =www.michenggw.com ClientAddress.Address.GetAddressBytes();
var resultBytes = new byte[portBytes.Length + addressBytes.Length];
// 根据协议规定,首部的 4 字节为 IP 地址,尾部的 2 本身为端口信息
Array.Copy(addressBytes,resultBytes,addressBytes.Length);
Array.Copy(portBytes,0,www.huarenyl.cn resultBytes,addressBytes.Length,portBytes.Length);
return resultBytes;
}
}
5.3 管理种子与其 Peer 集合
BT 客户端请求 Tracker 服务器的目的只有一个,就是获取正在 下载同一个种子的 Peer 列表 ,明白了这一点以后就知道咱们须要一个字典来管理种子与可用 Peer 集合的关系。
在上一节咱们知道,客户端在请求 Tracker 服务器的时候会带上正在下载的种子惟一 Hash 值,而咱们则能够根据这个 Hash 值来索引咱们 Peer 列表。
PT 站的原理也是相似,会有一个种子表,这个表以种子的惟一 Hash 值做为主键,并添加某些扩展字段。(IMDB 评分、描述、视频信息等...)
这里咱们定义一个 IBitTorrentManager 管理器对象,经过该对象来管理种子的状态,以及种子与 Peer 集合的状态。该接口的定义以下:
/// <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(www.taoyang2vip.com 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 数,下载完成等统计信息,这里咱们能够创建一个类存放这些统计信息以跟种子相关联。
/// <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(www.jiuzhoyulpt.cn);
InCompleted = new BNumber(0);
}
}
接下来咱们就来实现 IBitTorrentManager 接口。
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 信息。
/// <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 列表进行构建,构建完成以后写入到响应体当中。
[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 测试效果
6、源码下载
本 DEMO 已经托管到 Github 上,有须要的朋友能够自行前往如下地址进行 clone 。api