使用 .NET Core 開發 BT Tracker 伺服器

来源:https://www.cnblogs.com/myzony/archive/2019/03/08/10493707.html
-Advertisement-
Play Games

一、什麼是 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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1、萬惡的”+“號字元串拼接 字元串中的連接符+”會開闢一個新的空間,多一個“+“就會多開闢一個空間,影響性能 2、字元串格式化 ”%S“ :字元類型 ”%D“ ”數字類型 “%F” :浮點類型 3、字元串的常用操作 移除空白 strip 分割 split 長度 len 索引 obj [index] ...
  • 正則表達式的作用:用來匹配字元串 一、字元串方法字元串提供的方法是完全匹配,不能進行模糊匹配 s = 'hello world' # 字元串提供的方法是完全匹配,不能進行模糊匹配 print(s.find('ll')) # 2 查找ll的位置,輸出的是第一個l的位置 ret = s.replace(... ...
  • 第一,談談final, finally, finalize的區別。 最常被問到。final修飾符(關鍵字)如果一個類被聲明為final,意味著它不能再派生出新的子類,不能作為父類被繼承。因此一個類不能既被聲明為 abstract的,又被聲明為final的。將變數或方法聲明為final,可以保證它們在 ...
  • python版本:3.6 python編輯器:pycharm 最新版本 整理成代碼如下: ...
  • #1寫在前面的話 我覺得這樣學習或許能夠在學習的過程中事半功倍 第一道簡單的python編寫代碼,輸出10行帶標號的“Hello,world.”,具體效果參閱輸入輸出示例 1:Hello,world. 2:Hello,world. 3:Hello,world. 4:Hello,world. 5:He ...
  • 一、配置文件 web.xml <?xml version="1.0" encoding="UTF-8"?><web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/200 ...
  • 環境:pyspider0.3.9 PhantomJS2.1.1,均為最新版 進程用supervisor托管的。 其中需要加的幾個地方: webui進程: processor進程: fetcher進程: phantomjs進程: 以上需要註意的是,webui、processor 、fetcher 都加 ...
  • 原創文章,轉載請標註出處: "《Java設計模式系列 工廠方法模式》" 一、概述 工廠,就是生產產品的地方。 在Java設計模式中使用工廠的概念,那就是生成對象的地方了。 本來直接就能創建的對象為何要增加一個工廠類呢? 這就需要瞭解工廠方法要解決的是什麼問題了,如果只有一個類,我們直接new一個對象 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...