一、什麼是 BT Tracker ? 在 BT 下載過程當中,我們如果拿到一個種子文件,在其內部會包含一組 BT Tracker 伺服器信息。在開始進行下載的時候,BT 下載工具會根據種子內的唯一 HASH 碼請求 Tracker 伺服器,之後 Tracker 伺服器會返回給正在 下載/做種 的 P ...
一、什麼是 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" 的編碼形式:
[字元串長度]:[字元串數據]
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 編解碼工具類。
三、整體編寫思路
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 的信息進行持久化處理。
四、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
,最後我們的項目結構如下。
添加一個 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 = 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 信息進行特定的編碼處理。
/// <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 集合的狀態。該介面的定義如下:
/// <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 數,下載完成等統計信息,這裡我們可以建立一個類存放這些統計信息以跟種子相關聯。
/// <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
介面。
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 測試效果
六、源碼下載
本 DEMO 已經托管到 Github 上,有需要的朋友可以自行前往以下地址進行 clone 。
GitHub 倉庫地址: https://github.com/GameBelial/BTTrackerDemo