最近很忙,自上次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進行下載測試並輔種測試:

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