在 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