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進行下載測試並輔種測試:

至此我進行了做種下載測試均一切正常,如果大家在閱讀此文有疑問之處還及不足之處望留言 再次感謝閱讀。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM