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", );
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(); SortedDictionary<string, object> parameters = new SortedDictionary<string, object>(StringComparer.Ordinal); int num = (s != null) ? s.Length : ;
for (int i = ; i < num; i++)
{
int startIndex = i;
int num4 = -;
while (i < num)
{
char ch = s[i];
if (ch == '=')
{
if (num4 < )
{
num4 = i;
}
}
else if (ch == '&')
{
break;
}
i++;
}
string str = null;
string str2 = null;
if (num4 >= )
{
str = s.Substring(startIndex, num4 - startIndex);
str2 = s.Substring(num4 + , (i - num4) - );
}
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进行下载测试并辅种测试:
至此我进行了做种下载测试均一切正常,如果大家在阅读此文有疑问之处还及不足之处望留言 再次感谢阅读。
BT Tracker的原理及.Net Core简单实现Tracker Server的更多相关文章
- [.NET Core] 简单读取 json 配置文件
简单读取 json 配置文件 背景 目前发现网上的 .NET Core 读取配置文件有点麻烦,自己想搞个简单点的. .NET Core 已经不使用之前的诸如 app.config 和 web.conf ...
- [.Net Core] 简单使用 Mvc 内置的 Ioc
简单使用 Mvc 内置的 Ioc 本文基于 .NET Core 2.0. 鉴于网上的文章理论较多,鄙人不才,想整理一份 Hello World(Demo)版的文章. 目录 场景一:简单类的使用 场景二 ...
- [.Net Core] 简单使用 Mvc 内置的 Ioc(续)
简单使用 Mvc 内置的 Ioc(续) 本文基于 .NET Core 2.0. 上一章<[.Net Core] 简单使用 Mvc 内置的 Ioc>已经对日常 Mvc 中的 Ioc 的简单用 ...
- ASP.NET Core 简单实现七牛图片上传(FormData 和 Base64)
ASP.NET Core 简单实现七牛图片上传(FormData 和 Base64) 七牛图片上传 SDK(.NET 版本):https://developer.qiniu.com/kodo/sdk/ ...
- P2P原理及UDP穿透简单说明(转)
源: P2P原理及UDP穿透简单说明
- P2P原理及UDP穿透简单说明
转:http://http://andylin02.iteye.com/blog/444666 P2P原理及UDP穿透简单说明 本文章出自cnntec.com的AZ猫著,如需要转发,请注明来自cnnt ...
- Fiddler-009-AutoResponder 简单的 MOCK SERVER 应用实例
在我们日常的测试中经常需要测试特定的响应对应的客户端展示样式是否正确无误,实现测试方法一般有如下三种: 创建新的测试数据(工作量较大) 修改已有测试数据(例如修改对应的状态码,若是最终需要测试的按钮状 ...
- 最简单删除SQL Server中所有数据的方法
最简单删除SQL Server中所有数据的方法 编写人:CC阿爸 2014-3-14 其实删除数据库中数据的方法并不复杂,为什么我还要多此一举呢,一是我这里介绍的是删除数据库的所有数据,因为数据之间 ...
- 编写一个简单的Web Server
编写一个简单的Web Server其实是轻而易举的.如果我们只是想托管一些HTML页面,我们可以这么实现: 在VS2013中创建一个C# 控制台程序 编写一个字符串扩展方法类,主要用于在URL中截取文 ...
随机推荐
- Myeclipse报错:“Versions of Spring facet could not be detected”的解决方法
解决方法如下: VERSION OF SPRING FACET COULD NOT BE DETECTED. The migration process needs to detect the cor ...
- python:find()函数,字符串查询
#find函数 # b中有a的元素则打印a对应的元素坐标(索引),否则打印-1 a = "abcd" b = "d" print(a.find(b))
- Tmux (转)
Tmux是一个优秀的终端复用软件,类似GNU Screen,但来自于OpenBSD,采用BSD授权.使用它最直观的好处就是,通过一个终端登录远程主机并运行tmux后,在其中可以开启多个控制台而无需再“ ...
- 封装basedao及动态创建新类型的java数组
package com.huawei.base; import java.io.Serializable;import java.lang.reflect.Array;import java.lang ...
- 数组和集合(三):Set集合的使用总结
一.概述 · 继承collection接口 · 无序(不记录添加顺序).不允许元素重复.只允许存在一个null元素 二.实现类 1. HashSet · 底层其实是包装了一个HashMap实现的 · ...
- kubernetes configmap
ConfigMaps允许您将配置工件与image内容分离,以保持容器化应用程序的便携性. 本页面提供了一系列使用示例,演示如何使用ConfigMaps中存储的数据创建ConfigMaps和配置Pod. ...
- Linux实战教学笔记43:squid代理与缓存实践(二)
第6章 squid代理模式案例 6.1 squid传统正向代理生产使用案例 6.1.1 squid传统正向代理两种方案 (1)普通代理服务器 作为代理服务器,这是SQUID的最基本功能:通过在squi ...
- 解剖Nginx·自动脚本篇(7)类型相关脚本系列
1 auto/types/sizeof 该脚本的功能,是通过测试程序获知给定的ngx_type的大小. 1.1 显示提示信息 echo $ngx_n "checking for $ngx_t ...
- Mysql 使用触发器,把插入的数据在插入到宁一张表里
CREATE TRIGGER tgr_tablea_insert AFTER //触发器名字 动作在插入数据之后 ON alertinfo //监听哪个表之后触发 FOR INSERT //监听的表的 ...
- 基于mapper插件编写的可定制代码生成基本框架(springboot)
先看一下,基本结构图: 特征,提供 最佳实践的项目结构.配置文件.精简的POM 统一响应结果封装 统一异常处理 统一接口登录认证 常用基础方法抽象封装 Controller.service.dao层基 ...