(给DotNet加星标,提升.Net技能)

转自:myzony

cnblogs.com/myzony/p/10493707.html

一、什么是 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[整数]ei10e

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

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

I[内容]eI5:hello5:world

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

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

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

三、整体编写思路

BT Tracker 服务器本质上就是一个WebAPI项目,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 请求参数

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

4.2 返回参数

五、编码实现 BT Tracker 服务器

5.1 基本架构

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

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

public class GetPeersInfoInput{    ///     /// 种子的唯一 Hash 标识。    ///     public string Info_Hash { get; set; }    ///     /// 客户端的随机 Id,由 BT 客户端生成。    ///     public string Peer_Id { get; set; }    ///     /// 客户端的 IP 地址。    ///     public string Ip { get; set; }    ///     /// 客户端监听的端口。    ///     public int Port { get; set; }    ///     /// 已经上传的数据大小。    ///     public long Uploaded { get; set; }    ///     /// 已经下载的数据大小。    ///     public long Downloaded { get; set; }    ///     /// 事件表示,具体可以转换为  枚举的具体值。    ///     public string Event { get; set; }    ///     /// 该客户端剩余待下载的数据。    ///     public long Left { get; set; }    ///     /// 是否启用压缩,当该值为 1 的时候,表示当前客户端接受压缩格式的 Peer 列表,即使用    /// 6 字节表示一个 Peer (前 4 字节表示 IP 地址,后 2 字节表示端口号)。当该值为 0    /// 的时候则表示客户端不接受。    ///     public int Compact { get; set; }    ///     /// 表示客户端想要获得的 Peer 数量。    ///     public int? NumWant { get; set; }}

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

public class AnnounceInputParameters{    ///     /// 客户端 IP 端点信息。    ///     public IPEndPoint ClientAddress { get; }    ///     /// 种子的唯一 Hash 标识。    ///     public string InfoHash { get; }    ///     /// 客户端的随机 Id,由 BT 客户端生成。    ///     public string PeerId { get; }    ///     /// 已经上传的数据大小。    ///     public long Uploaded { get; }    ///     /// 已经下载的数据大小。    ///     public long Downloaded { get; }    ///     /// 事件表示,具体可以转换为  枚举的具体值。    ///     public TorrentEvent Event { get; }    ///     /// 该客户端剩余待下载的数据。    ///     public long Left { get; }    ///     /// Peer 是否允许启用压缩。    ///     public bool IsEnableCompact { get; }    ///     /// Peer 想要获得的可用的 Peer 数量。    ///     public int PeerWantCount { get; }    ///     /// 如果在请求过程当中出现了异常,则本字典包含了异常信息。    ///     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;    }    ///     ///  到当前类型的隐式转换定义。    ///     public static implicit operator AnnounceInputParameters(GetPeersInfoInput input)    {        return new AnnounceInputParameters(input);    }    ///     /// 将客户端传递的 IP 地址与端口转换为  类型。    ///     private IPEndPoint ConvertClientAddress(GetPeersInfoInput apiInput)    {        if (IPAddress.TryParse(apiInput.Ip, out IPAddress ipAddress))        {            return new IPEndPoint(ipAddress,apiInput.Port);        }        return null;    }    ///     /// 将客户端传递的字符串 Event 转换为  枚举。    ///     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;        }    }    ///     /// 将 info_hash 参数从 URL 编码转换为标准的字符串。    ///     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{    ///     /// 未知状态。    ///     None,    ///     /// 已开始。    ///     Started,    ///     /// 已停止。    ///     Stopped,    ///     /// 已完成。    ///     Completed}/// /// 常用的字典 KEY。/// 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 信息进行特定的编码处理。

/// /// 每个 BT 下载客户端的定义。/// public class Peer{    ///     /// 客户端 IP 端点信息。    ///     public IPEndPoint ClientAddress { get; private set; }    ///     /// 客户端的随机 Id,由 BT 客户端生成。    ///     public string PeerId { get; private set; }    ///     /// 客户端唯一标识。    ///     public string UniqueId { get; private set; }        ///     /// 客户端在本次会话过程中下载的数据量。(以 Byte 为单位)    ///     public long DownLoaded { get; private set; }    ///     /// 客户端在本次会话过程当中上传的数据量。(以 Byte 为单位)    ///     public long Uploaded { get; private set; }    ///     /// 客户端的下载速度。(以 Byte/秒 为单位)    ///     public long DownloadSpeed { get; private set; }    ///     /// 客户端的上传速度。(以 Byte/秒 为单位)    ///     public long UploadSpeed { get; private set; }    ///     /// 客户端是否完成了当前种子,True 为已经完成,False 为还未完成。    ///     public bool IsCompleted { get; private set; }    ///     /// 最后一次请求 Tracker 服务器的时间。    ///     public DateTime LastRequestTrackerTime { get; private set; }    ///     /// Peer 还需要下载的数量。    ///     public long Left { get; private set; }    public Peer() { }    public Peer(AnnounceInputParameters inputParameters)    {        UniqueId = inputParameters.ClientAddress.ToString();                // 根据输入参数更新 Peer 的状态。        UpdateStatus(inputParameters);    }    ///     /// 根据输入参数更新 Peer 的状态。    ///     /// BT 客户端请求 Tracker 服务器时传递的参数。    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;    }    ///     /// 将 Peer 信息进行 B 编码,按照协议处理为字典。    ///     public BDictionary ToEncodedDictionary()    {        return new BDictionary        {            {TrackerServerConsts.PeerIdKey,new BString(PeerId)},            {TrackerServerConsts.Ip,new BString(ClientAddress.Address.ToString())},            {TrackerServerConsts.Port,new BNumber(ClientAddress.Port)}        };    }    ///     /// 将 Peer 信息进行紧凑编码成字节组。    ///     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 集合的状态。该接口的定义如下:

/// /// 用于管理 BT 种子与其关联的 Peer 集合。/// public interface IBitTorrentManager{    ///     /// 添加一个新的 Peer 到指定种子关联的集合当中。    ///     /// 种子的唯一标识。    /// BT 客户端传入的参数信息。    Peer AddPeer(string infoHash,AnnounceInputParameters inputParameters);        ///     /// 根据参数删除指定种子的 Peer 信息。    ///     /// 种子的唯一标识。    /// BT 客户端传入的参数信息。    void DeletePeer(string infoHash,AnnounceInputParameters inputParameters);    ///     /// 更新指定种子的某个 Peer 状态。    ///     /// 种子的唯一标识。    /// BT 客户端传入的参数信息。    void UpdatePeer(string infoHash, AnnounceInputParameters inputParameters);    ///     /// 获得指定种子的可用 Peer 集合。    ///     /// 种子的唯一标识。    /// 当前种子关联的 Peer 列表。    IReadOnlyListGetPeers(string infoHash);    ///     /// 清理指定种子内部不活跃的 Peer 。     ///     /// 种子的唯一标识。    /// 超时周期,超过这个时间的 Peer 将会被清理掉。    void ClearZombiePeers(string infoHash,TimeSpan expiry);    ///     /// 获得指定种子已经完成下载的 Peer 数量。    ///     /// 种子的唯一标识。    int GetComplete(string infoHash);    ///     /// 获得指定种子正在下载的 Peer 数量。    ///     /// 种子的唯一标识。    int GetInComplete(string infoHash);}

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

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

/// /// 用于表示某个种子的状态与统计信息。/// public class BitTorrentStatus{    ///     /// 下载完成的 Peer 数量。    ///     public BNumber Downloaded { get; set; }    ///     /// 已经完成种子下载的 Peer 数量。    ///     public BNumber Completed { get; set; }    ///     /// 正在下载种子的 Peer 数量。    ///     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> _peers;    private readonly ConcurrentDictionary<string, BitTorrentStatus> _bitTorrentStatus;    public BitTorrentManager()    {        _peers = new ConcurrentDictionary<string, List>();        _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 {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());        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 IReadOnlyListGetPeers(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;    }    ///     /// 更新种子的统计信息。    ///     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;    }    ///     /// 检测参数与种子唯一标识的状态。    ///     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 信息。

///  /// 将 Peer 集合的数据转换为 BT 协议规定的格式 ///  private void HandlePeersData(BDictionary resultDict, IReadOnlyList 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         {             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         {             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

推荐阅读

(点击标题可跳转阅读)

.NET开源快速开发框架Colder发布

最大限度地降低多线程 C# 代码的复杂性

.NET Core下支持分表分库、读写分离的通用Repository

看完本文有收获?请转发分享给更多人

关注「DotNet」加星标,提升.Net技能

喜欢就点一下「好看」呗~

tracker服务器_.NET Core 开发 BT Tracker 服务器相关推荐

  1. netcore开源框架_.NET Core开发精选文章目录,持续更新,欢迎投稿!

    收集的一些.NET Core开发的文章,持续更新,欢迎投稿! 0.文章目录 布莱恩特:@.NET程序员,请了解这8种.NET 内存泄露方式! 布莱恩特:ASP.NET Core 性能优化22条最佳实践 ...

  2. 构建静态服务器_为静态网站构建无服务器联系表

    构建静态服务器 介绍 (Introduction) A few years ago AWS launched static hosting service S3, which was a paradi ...

  3. arm集群服务器_高密度ARM集群服务器的应用场景

    1 什么是高密度ARM集群服务器 ARM集群服务器,是在一个机箱内设计N个高性能ARM处理器,每个ARM处理器配置独立的内存及存储,所有的处理器通过高速网络(千兆或万兆网络)进行相互之间以及对外通信的 ...

  4. cefsharp 发送请求服务器_使用 WPF 版简易 SIP 服务器向 GB28181 摄像头发送直播请求...

    使用 WPF 版简易 SIP 服务器向 GB28181 摄像头发送直播请求 目录 一.引言 二.项目渊源 三.软件使用及 SIP INVITE 流程 (一) 注册和心跳 (二) 直播 INVITE 四 ...

  5. 前端ajax数据提交到服务器_详解前端如何让服务器主动向浏览器推送数据

    前言 前面我们已经聊了ajax,它的特点是浏览器必须先发起请求,服务器才能给出对应的响应,想一想能不能让服务器主动向浏览器推送数据呢?那么这篇文章我们来聊一聊服务器推送功能. 轮询 假设你现在需要去做 ...

  6. 组态软件运行在云服务器_哪些软件可以在云服务器上运行?

    哪些软件可以在云服务器上运行?可以在云服务器上运行的软件很多,使用云服务器具有完全的独立性和灵活性,用户可以根据自身需求,配置所需要的服务器环境.安装各类应用软件. 比如,Windows系统的云服务器 ...

  7. 网页测试本地服务器_音视频开发搭建一个直播服务器

    现在抖音.快手等直播实在是太火了,因此对音视频的开发非常感兴趣.查阅了相关资料,使用Nginx搭建一个简单的直播跟点播流媒体服务器,能够实时推流到服务器,同时在网页端播放直播的视频. 先上效果 使用O ...

  8. 笔记合并_.NET Core开发实战(第23课:静态文件中间件:前后端分离开发合并部署骚操作)学习笔记(上)...

    23 | 静态文件中间件:前后端分离开发合并部署骚操作 我们先来看一下静态文件中间件有哪些能力 1.支持指定相对路径 2.支持目录的浏览 3.支持设置默认文档 4.支持多目录映射 源码链接: http ...

  9. arduino服务器_如何使用Arduino检查Web服务器的响应状态

    arduino服务器 by Harshita Arora 通过Harshita Arora 如何使用Arduino检查Web服务器的响应状态 (How to use Arduino to check ...

最新文章

  1. 妈呀,终于搞定VIM的复制粘贴问题了!
  2. 专访腾讯徐汉彬:日请求高达3.5亿+平台的架构设计及演变
  3. python找指定内容_python查找指定具有相同内容文件的方法
  4. peripheralStateNotificationCB
  5. OpenJudge NOI 1.7 34:回文子串
  6. html设置功能区菜单,html – 具有“3D”效果的功能区
  7. ClickHouse安装部署与SQL实战
  8. Java Stream flatMap()
  9. 串口UART学习笔记(一)
  10. ubuntu c/c++ 生成.so 并被python调用
  11. 90后华人教授夫妇斩获IEEE the best 论文和ACM Demo冠军,团队1年连发4篇Nature子刊
  12. armbian 斐讯n1_[流水账记录] 斐讯N1刷armbian到emmc并安装Docker!
  13. ffmpeg学习笔记1-----udp流多路节目实现分离
  14. jenkins更换插件安装源
  15. coffeescript html5,CoffeeScript函数
  16. 51nod 1108.距离之和最小 V2 - 曼哈顿距离
  17. 网易传媒数据指标体系搭建实战!
  18. 3t硬盘哪个服务器系统识别,3T硬盘为何不能被系统完全识别
  19. Hibernate_9_Person和IdCard实例_一对一关系:基于主键
  20. C++_输入一个字符串,并逆序输出

热门文章

  1. 最近发现一个爬虫开源项目weixin_crawler
  2. append()方法_超详细教程 | pandas合并之append和concat
  3. 服务器不能安装exe文件,云服务器安装exe文件
  4. Exalogic Elastic Cloud
  5. Redis:从应用到底层,都在这儿了!
  6. 硬刚 Kafka,Apache 顶级项目背后的公司完成数百万美元 Pre-A 轮融资
  7. 一文带你认清数据仓库“维度模型设计”与“分层架构” | 原力计划
  8. 评测 | 千元以下的扫拖一体机器人,到底值不值得买?
  9. 招人了!MySQL 面试必须掌握的 8 个知识点!
  10. 支付宝解释 2019 年账单总额较高;腾讯 QQ 回应新功能可显示对方实时电量;Python 2.7 结束支持 | 极客头条...