BT Tracker的原理及.Net Core简单实现Tracker Server

最近很忙,自上次Blog被盗 帖子全部丢失后也很少时间更新Blog了,闲暇在国外站点查阅资料时正好看到一些Tracker 的协议资料,也就今天记录并实践了下,再次分享给大家希望可以帮到需要的小伙伴。

首先我们来了解下BT Tracker

一、做种

 

    现在很多BT软件都提供了做种功能,在做种时,我们都必须指定tracker服务器地址,如果该地址无效,则做出来的种子对BT协议来说是没有任何实际意义的。

二、bt tracker服务

    对于纯BT协议来说,每个BT网络中至少要有一台Tracker服务器(追踪服务器),tracker主要基本工作有以下几个方面:

  •  记录种子信息(torrent文件信息)
  •  记录节点信息
  •  计算并返回节点列表给BT客户端

    每次我们利用BT软件做完种子后,总要找个论坛之类的来上传自己的种子,这样别人就可以下载到这个种子。为什么要上传种子呢?原因:

  • 上传种子,其实就是把种子信息记录到tracker服务器上
  • 种子可以在论坛传播,种子的扩展程度就决定了种子的健康度和下载度

    当其他用户用BT软件打开种子后,BT软件会对种子进行解析(BDecode),主要得到种子的相关信息,包括:文件名、文件大小、tracker地址等。然后BT软件会向tracker地址发送请求报文,开始进行下载。BT向tracker发送的是Get请求,请求的内容主要有以下几个方面:

info_hash

必填

种子文件info字段的SHA1值(20字节)

peer_id

必填

节点标识,由BT客户端每次启动时随机生成

port

必填

节点端口,主要用于跟其他节点交互

uploaded

必填

总共上传的字节数,初始值为0

downloaded

必填

总共下载的字节数,初始值为0

left

必填

文件剩余的待下载字节数

numwant

必填

BT客户端期望得到的节点数

ip

选填

BT客户端IP,选填的原因是Tracker可以得到请求的IP地址,不需要客户端直接上传

event

选填

started/stopped/completed/空。当BT客户端开始种子下载时,第一个发起的请求为started,

在下载过程中,该值一直为空,直到下载完成后才发起completed请求。做种过程中,发送

的event也为空。如果BT客户端停止做种或退出程序,则会发起stopped请求。

tracker收到该请求后主要进行以下几步处理:

1. 根据info_hash查找种子信息,如果tracker没有该种子的任何信息,tracker服务器可以返回错误或返回0个种子数

2. 如果tracker找到了种子信息,接下来就会去查找是否数据库中已存在该peer_id的节点。接下来根据event的值进行相关处理。

3. 如果event是stopped,说明该节点已不可用,系统会删除tracker上关于该节点的记录信息。

4. 如果event是completed,说明种子节点+1,非种子-1。

5. 如果event是started,说明这是种子第一次连接tracker,tracker需要记录该节点信息,此外如果left=0,说明这是一个种子节点。

6. 如果event是空,则说明节点正在下载或上传,需要更新tracker服务器上该节点的信息。

7. 最后tracker从本地挑选出numwant个节点信息返回给BT客户端,实际返回的节点数不一定就是numwant,tracker只是尽量达到这个数量。

Tracker响应

Tracker正常返回的信息结构主要是:

interval

必填

请求间隔(秒)

complete

选填

种子节点数

Incomplete

选填

非种子节点数

peers

ip

必填

IP地址

peer_id

选填

节点标识

port

必填

端口

如果Tracker检查发现异常,可以返回错误信息:

failure reason

错误原因

Tracker如何挑选种子节点并返回给客户端?

最普遍也是最简单的方式,那就是随机返回,tbsource采用的就是随机返回的机制。不少研究论文也提出了相关的算法,如IP地址策略和阶段返回策略。

IP地址策略是指根据IP地址所含拓扑信息来判断两个节点的距离,从而返回距离请求节点较近的节点列表。该方法主要适用于IPV6。

阶段返回策略,根据节点的下载进度,返回下载进度相近的节点列表。

个人观点:无论tracker采用什么算法,对BT客户端来说,能够提高的下载效率都是很有限的,采用“高级”的算法有时反而会增加tracker的负载。因此随机返回还算是比较高效的。

Bt协议中,有两个策略可以用来提高整个BT网络的健壮性和下载速度,它们分别是:最少片段优先策略(BT客户端处理)和最后阶段模式。为了响应“最后阶段模式”,当种子节点的下载进度大于80%(个人指定)时,tracker服务器应该尽量返回种子节点给客户端,帮助客户端尽快完成下载,使其成为种子节点。

 

三、private tracker原理

Privatetracker简称PT,目前主要应用于高清视频下载。其实PT就是“我为人人,人人为我”这个目标的最佳实践者。在实际的BT下载过程中,用户通过种子下载完文件后,出于“自私”的考虑(怕占用自己带宽),往往会退出做种,从而降低种子的热度。这就是为什么一个种子过了一段时间后,往往下载速度很慢或下载不完。

为了真正地实现BT理念,PT强制每个下载者必须上传一定量数据后,才能进行下载。如何保证这种行为呢?

现在的PT一般存在于网络社区中,每个注册网络社区的用户都会分配到一个随机的KEY,任何从社区下载的种子,都会包含用户的KEY。每次用户通过种子下载时,都会连接到社区的tracker服务器上,tracker服务器会检查KEY对应用户的上传下载量,如果上传量不满足标准,则tracker服务器会记录相关信息,并对该用户的下载及社区活动进行相关限制。

了解的基础的一些原理后 我们从实践开始入手:

封装Tracker类及数据请求上下文:

namespace WebApplication8
{
    public class TrackerContext : DbContext
    {
        public DbSet<Tracker> Bittorrents { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlite("Data Source=Tracker.db");
        }
    }

    public class Tracker
    {
        public int Id { get; set; }
        //InfoHash
        public string InfoHash { get; set; }
        //PeerId
        public string PeerId { get; set; }
        //客户端的IP地址
        public string Ip { get; set; }
        //客户端的端口
        public int Port { get; set; }
        //上传的字节数量
        public int Uploaded { get; set; }
        //下载的字节数量
        public int Downloaded { get; set; }
        //文件剩余的待下载字节数
        public int Left { get; set; }
        //客户端 事件 started/stopped/completed/空
        public string Event { get; set; }
    }
}

服务端 简单实现:

namespace WebApplication8.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class announceController : ControllerBase
    {
        private TrackerContext _db;
        public announceController(TrackerContext db)
        {
            _db = db;
        }
        // GET api/values
        [HttpGet]
        public string Get()
        {
            try
            {
                //?info_hash=o%b8t%7c~%e86%fc2%878%5c%f5%fbj0%40%26-a&peer_id=-UT354S-%e8%ad%86%f5%0d%ee%86%40%9aXo%f9&port=53974&uploaded=0&downloaded=0&left=0&corrupt=0&key=E96680BC&event=started&numwant=200&compact=1&no_peer_id=1
                var dic = GetDic(Request.QueryString.ToString());
                var infoHash = BitConverter.ToString(HttpUtility.UrlDecodeToBytes(dic["@info_hash"].ToString())).Replace("-", "").ToLower();
                var peer_id = BitConverter.ToString(HttpUtility.UrlDecodeToBytes(dic["@peer_id"].ToString())).Replace("-", "").ToLower();

                //判断是否存在该tracker
                var entity = _db.Bittorrents.FirstOrDefault(p => p.InfoHash == infoHash && p.PeerId == peer_id);
                //不存在插入tracker信息
                if (entity == null)
                {
                    _db.Bittorrents.Add(new Tracker
                    {
                        InfoHash = infoHash,
                        Ip = Request.HttpContext.Connection.RemoteIpAddress.ToString(),
                        Left = Convert.ToInt32(dic["@left"]),
                        Uploaded = Convert.ToInt32(dic["@uploaded"]),
                        Downloaded = Convert.ToInt32(dic["@downloaded"]),
                        Event = dic["@event"].ToString(),
                        PeerId = peer_id,
                        Port = Convert.ToInt32(dic["@port"])
                    });
                    _db.SaveChanges();
                }
                else
                {
                    //存在更新Tracker信息
                    entity.Ip = Request.HttpContext.Connection.RemoteIpAddress.ToString();
                    entity.Uploaded = Convert.ToInt32(dic["@uploaded"]);
                    entity.Downloaded = Convert.ToInt32(dic["@downloaded"]);
                    entity.Left = Convert.ToInt32(dic["@left"]);
                    entity.Port = Convert.ToInt32(dic["@port"]);
                    entity.Event = dic.ContainsKey("@event") ? dic["@event"].ToString() : null;
                    _db.SaveChanges();
                }
                dic.Clear();
                //构造tracker信息列表 返回给客户端 interval 客户端心跳请求间隔 单位:秒 会间隔后自动心跳上报客户端的信息
                dic.Add("interval", 60);
                List<object> peers = new List<object>();
                _db.Bittorrents.Where(p => p.InfoHash == infoHash).ToList().ForEach(o =>
                {
                    SortedDictionary<string, object> peer = new SortedDictionary<string, object>(StringComparer.Ordinal);

                    peer.Add("peer id", o.PeerId);
                    peer.Add("ip", o.Ip);
                    peer.Add("port", o.Port);

                    peers.Add(peer);
                });
                dic.Add("peers", peers);
                return encode(dic);
            }
            catch (Exception)
            {
                throw new Exception("请遵循Tracker协议,禁止浏览器直接访问");
            }
            
        }

        public SortedDictionary<string, object> GetDic(string query)
        {
            string s = query.Substring(1);

            SortedDictionary<string, object> parameters = new SortedDictionary<string, object>(StringComparer.Ordinal);

            int num = (s != null) ? s.Length : 0;
            for (int i = 0; i < num; i++)
            {
                int startIndex = i;
                int num4 = -1;
                while (i < num)
                {
                    char ch = s[i];
                    if (ch == '=')
                    {
                        if (num4 < 0)
                        {
                            num4 = i;
                        }
                    }
                    else if (ch == '&')
                    {
                        break;
                    }
                    i++;
                }
                string str = null;
                string str2 = null;
                if (num4 >= 0)
                {
                    str = s.Substring(startIndex, num4 - startIndex);
                    str2 = s.Substring(num4 + 1, (i - num4) - 1);
                }
                else
                {
                    str2 = s.Substring(startIndex, i - startIndex);
                }

                parameters.Add("@" + str, str2);
            }
            return parameters;
        }
        public string encode(string _string)
        {
            StringBuilder string_builder = new StringBuilder();

            string_builder.Append(_string.Length);
            string_builder.Append(":");
            string_builder.Append(_string);

            return string_builder.ToString();
        }
        public string encode(int _int)
        {
            StringBuilder string_builder = new StringBuilder();

            string_builder.Append("i");
            string_builder.Append(_int);
            string_builder.Append("e");

            return string_builder.ToString();
        }
        public string encode(List<object> list)
        {
            StringBuilder string_builder = new StringBuilder();

            string_builder.Append("l");

            foreach (object _object in list)
            {
                if (_object.GetType() == typeof(string))
                {
                    string_builder.Append(encode((string)_object));
                }

                if (_object.GetType() == typeof(int))
                {
                    string_builder.Append(encode((int)_object));
                }

                if (_object.GetType() == typeof(List<object>))
                {
                    string_builder.Append(encode((List<object>)_object));
                }

                if (_object.GetType() == typeof(SortedDictionary<string, object>))
                {
                    string_builder.Append(encode((SortedDictionary<string, object>)_object));
                }
            }

            string_builder.Append("e");

            return string_builder.ToString();
        }

        public string encode(SortedDictionary<string, object> sorted_dictionary)
        {
            StringBuilder string_builder = new StringBuilder();

            string_builder.Append("d");

            foreach (KeyValuePair<string, object> key_value_pair in sorted_dictionary)
            {
                string_builder.Append(encode((string)key_value_pair.Key));

                if (key_value_pair.Value.GetType() == typeof(string))
                {
                    string_builder.Append(encode((string)key_value_pair.Value));
                }

                if (key_value_pair.Value.GetType() == typeof(int))
                {
                    string_builder.Append(encode((int)key_value_pair.Value));
                }

                if (key_value_pair.Value.GetType() == typeof(List<object>))
                {
                    string_builder.Append(encode((List<object>)key_value_pair.Value));
                }

                if (key_value_pair.Value.GetType() == typeof(SortedDictionary<string, object>))
                {
                    string_builder.Append(encode((SortedDictionary<string, object>)key_value_pair.Value));
                }
            }

            string_builder.Append("e");

            return string_builder.ToString();
        }
    }
}

Tracker 地址http://192.168.50.11:5000/announce  我是在本地部署进行了测试

sqlite数据库种的Tracker信息:

在我的另一台Nas进行下载测试并辅种测试:

至此我进行了做种下载测试均一切正常,如果大家在阅读此文有疑问之处还及不足之处望留言 再次感谢阅读。

 

 

posted @ 2018-12-09 19:39  醉代码  阅读(3563)  评论(5编辑  收藏  举报