├── .gitignore
├── BTTrackerDemo.sln
├── BTTrackerDemo
├── BTTrackerDemo.csproj
├── Controllers
│ ├── AnnounceController.cs
│ └── Dtos
│ │ └── GetPeersInfoInput.cs
├── Program.cs
├── Properties
│ └── launchSettings.json
├── Startup.cs
├── Tracker
│ ├── AnnounceInputParameters.cs
│ ├── BitTorrentManager.cs
│ ├── BitTorrentStatus.cs
│ ├── IBitTorrentManager.cs
│ ├── Peer.cs
│ ├── TorrentEvent.cs
│ └── TrackerServerConsts.cs
├── appsettings.Development.json
└── appsettings.json
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### JetBrains template
3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
5 |
6 | # User-specific stuff
7 | .idea/**/workspace.xml
8 | .idea/**/tasks.xml
9 | .idea/**/dictionaries
10 | .idea/**/shelf
11 |
12 | # Sensitive or high-churn files
13 | .idea/**/dataSources/
14 | .idea/**/dataSources.ids
15 | .idea/**/dataSources.local.xml
16 | .idea/**/sqlDataSources.xml
17 | .idea/**/dynamic.xml
18 | .idea/**/uiDesigner.xml
19 | .idea/**/dbnavigator.xml
20 |
21 | # Gradle
22 | .idea/**/gradle.xml
23 | .idea/**/libraries
24 |
25 | # CMake
26 | cmake-build-debug/
27 | cmake-build-release/
28 |
29 | # Mongo Explorer plugin
30 | .idea/**/mongoSettings.xml
31 |
32 | # File-based project format
33 | *.iws
34 |
35 | # IntelliJ
36 | out/
37 |
38 | # mpeltonen/sbt-idea plugin
39 | .idea_modules/
40 |
41 | # JIRA plugin
42 | atlassian-ide-plugin.xml
43 |
44 | # Cursive Clojure plugin
45 | .idea/replstate.xml
46 |
47 | # Crashlytics plugin (for Android Studio and IntelliJ)
48 | com_crashlytics_export_strings.xml
49 | crashlytics.properties
50 | crashlytics-build.properties
51 | fabric.properties
52 |
53 | # Editor-based Rest Client
54 | .idea/httpRequests
55 | ### VisualStudio template
56 | ## Ignore Visual Studio temporary files, build results, and
57 | ## files generated by popular Visual Studio add-ons.
58 | ##
59 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
60 |
61 | # User-specific files
62 | *.suo
63 | *.user
64 | *.userosscache
65 | *.sln.docstates
66 |
67 | # User-specific files (MonoDevelop/Xamarin Studio)
68 | *.userprefs
69 |
70 | # Build results
71 | [Dd]ebug/
72 | [Dd]ebugPublic/
73 | [Rr]elease/
74 | [Rr]eleases/
75 | x64/
76 | x86/
77 | bld/
78 | [Bb]in/
79 | [Oo]bj/
80 | [Ll]og/
81 |
82 | # Visual Studio 2015/2017 cache/options directory
83 | .vs/
84 | # Uncomment if you have tasks that create the project's static files in wwwroot
85 | #wwwroot/
86 |
87 | # Visual Studio 2017 auto generated files
88 | Generated\ Files/
89 |
90 | # MSTest test Results
91 | [Tt]est[Rr]esult*/
92 | [Bb]uild[Ll]og.*
93 |
94 | # NUNIT
95 | *.VisualState.xml
96 | TestResult.xml
97 |
98 | # Build Results of an ATL Project
99 | [Dd]ebugPS/
100 | [Rr]eleasePS/
101 | dlldata.c
102 |
103 | # Benchmark Results
104 | BenchmarkDotNet.Artifacts/
105 |
106 | # .NET Core
107 | project.lock.json
108 | project.fragment.lock.json
109 | artifacts/
110 |
111 | # StyleCop
112 | StyleCopReport.xml
113 |
114 | # Files built by Visual Studio
115 | *_i.c
116 | *_p.c
117 | *_i.h
118 | *.ilk
119 | *.meta
120 | *.obj
121 | *.iobj
122 | *.pch
123 | *.pdb
124 | *.ipdb
125 | *.pgc
126 | *.pgd
127 | *.rsp
128 | *.sbr
129 | *.tlb
130 | *.tli
131 | *.tlh
132 | *.tmp
133 | *.tmp_proj
134 | *.log
135 | *.vspscc
136 | *.vssscc
137 | .builds
138 | *.pidb
139 | *.svclog
140 | *.scc
141 |
142 | # Chutzpah Test files
143 | _Chutzpah*
144 |
145 | # Visual C++ cache files
146 | ipch/
147 | *.aps
148 | *.ncb
149 | *.opendb
150 | *.opensdf
151 | *.sdf
152 | *.cachefile
153 | *.VC.db
154 | *.VC.VC.opendb
155 |
156 | # Visual Studio profiler
157 | *.psess
158 | *.vsp
159 | *.vspx
160 | *.sap
161 |
162 | # Visual Studio Trace Files
163 | *.e2e
164 |
165 | # TFS 2012 Local Workspace
166 | $tf/
167 |
168 | # Guidance Automation Toolkit
169 | *.gpState
170 |
171 | # ReSharper is a .NET coding add-in
172 | _ReSharper*/
173 | *.[Rr]e[Ss]harper
174 | *.DotSettings.user
175 |
176 | # JustCode is a .NET coding add-in
177 | .JustCode
178 |
179 | # TeamCity is a build add-in
180 | _TeamCity*
181 |
182 | # DotCover is a Code Coverage Tool
183 | *.dotCover
184 |
185 | # AxoCover is a Code Coverage Tool
186 | .axoCover/*
187 | !.axoCover/settings.json
188 |
189 | # Visual Studio code coverage results
190 | *.coverage
191 | *.coveragexml
192 |
193 | # NCrunch
194 | _NCrunch_*
195 | .*crunch*.local.xml
196 | nCrunchTemp_*
197 |
198 | # MightyMoose
199 | *.mm.*
200 | AutoTest.Net/
201 |
202 | # Web workbench (sass)
203 | .sass-cache/
204 |
205 | # Installshield output folder
206 | [Ee]xpress/
207 |
208 | # DocProject is a documentation generator add-in
209 | DocProject/buildhelp/
210 | DocProject/Help/*.HxT
211 | DocProject/Help/*.HxC
212 | DocProject/Help/*.hhc
213 | DocProject/Help/*.hhk
214 | DocProject/Help/*.hhp
215 | DocProject/Help/Html2
216 | DocProject/Help/html
217 |
218 | # Click-Once directory
219 | publish/
220 |
221 | # Publish Web Output
222 | *.[Pp]ublish.xml
223 | *.azurePubxml
224 | # Note: Comment the next line if you want to checkin your web deploy settings,
225 | # but database connection strings (with potential passwords) will be unencrypted
226 | *.pubxml
227 | *.publishproj
228 |
229 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
230 | # checkin your Azure Web App publish settings, but sensitive information contained
231 | # in these scripts will be unencrypted
232 | PublishScripts/
233 |
234 | # NuGet Packages
235 | *.nupkg
236 | # The packages folder can be ignored because of Package Restore
237 | **/[Pp]ackages/*
238 | # except build/, which is used as an MSBuild target.
239 | !**/[Pp]ackages/build/
240 | # Uncomment if necessary however generally it will be regenerated when needed
241 | #!**/[Pp]ackages/repositories.config
242 | # NuGet v3's project.json files produces more ignorable files
243 | *.nuget.props
244 | *.nuget.targets
245 |
246 | # Microsoft Azure Build Output
247 | csx/
248 | *.build.csdef
249 |
250 | # Microsoft Azure Emulator
251 | ecf/
252 | rcf/
253 |
254 | # Windows Store app package directories and files
255 | AppPackages/
256 | BundleArtifacts/
257 | Package.StoreAssociation.xml
258 | _pkginfo.txt
259 | *.appx
260 |
261 | # Visual Studio cache files
262 | # files ending in .cache can be ignored
263 | *.[Cc]ache
264 | # but keep track of directories ending in .cache
265 | !*.[Cc]ache/
266 |
267 | # Others
268 | ClientBin/
269 | ~$*
270 | *~
271 | *.dbmdl
272 | *.dbproj.schemaview
273 | *.jfm
274 | *.pfx
275 | *.publishsettings
276 | orleans.codegen.cs
277 |
278 | # Including strong name files can present a security risk
279 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
280 | #*.snk
281 |
282 | # Since there are multiple workflows, uncomment next line to ignore bower_components
283 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
284 | #bower_components/
285 |
286 | # RIA/Silverlight projects
287 | Generated_Code/
288 |
289 | # Backup & report files from converting an old project file
290 | # to a newer Visual Studio version. Backup files are not needed,
291 | # because we have git ;-)
292 | _UpgradeReport_Files/
293 | Backup*/
294 | UpgradeLog*.XML
295 | UpgradeLog*.htm
296 | ServiceFabricBackup/
297 | *.rptproj.bak
298 |
299 | # SQL Server files
300 | *.mdf
301 | *.ldf
302 | *.ndf
303 |
304 | # Business Intelligence projects
305 | *.rdl.data
306 | *.bim.layout
307 | *.bim_*.settings
308 | *.rptproj.rsuser
309 |
310 | # Microsoft Fakes
311 | FakesAssemblies/
312 |
313 | # GhostDoc plugin setting file
314 | *.GhostDoc.xml
315 |
316 | # Node.js Tools for Visual Studio
317 | .ntvs_analysis.dat
318 | node_modules/
319 |
320 | # Visual Studio 6 build log
321 | *.plg
322 |
323 | # Visual Studio 6 workspace options file
324 | *.opt
325 |
326 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
327 | *.vbw
328 |
329 | # Visual Studio LightSwitch build output
330 | **/*.HTMLClient/GeneratedArtifacts
331 | **/*.DesktopClient/GeneratedArtifacts
332 | **/*.DesktopClient/ModelManifest.xml
333 | **/*.Server/GeneratedArtifacts
334 | **/*.Server/ModelManifest.xml
335 | _Pvt_Extensions
336 |
337 | # Paket dependency manager
338 | .paket/paket.exe
339 | paket-files/
340 |
341 | # FAKE - F# Make
342 | .fake/
343 |
344 | # JetBrains Rider
345 | .idea/
346 | *.sln.iml
347 |
348 | # CodeRush
349 | .cr/
350 |
351 | # Python Tools for Visual Studio (PTVS)
352 | __pycache__/
353 | *.pyc
354 |
355 | # Cake - Uncomment if you are using it
356 | # tools/**
357 | # !tools/packages.config
358 |
359 | # Tabs Studio
360 | *.tss
361 |
362 | # Telerik's JustMock configuration file
363 | *.jmconfig
364 |
365 | # BizTalk build output
366 | *.btp.cs
367 | *.btm.cs
368 | *.odx.cs
369 | *.xsd.cs
370 |
371 | # OpenCover UI analysis results
372 | OpenCover/
373 |
374 | # Azure Stream Analytics local run output
375 | ASALocalRun/
376 |
377 | # MSBuild Binary and Structured Log
378 | *.binlog
379 |
380 | # NVidia Nsight GPU debugger configuration file
381 | *.nvuser
382 |
383 | # MFractors (Xamarin productivity tool) working folder
384 | .mfractor/
385 |
386 |
--------------------------------------------------------------------------------
/BTTrackerDemo.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTTrackerDemo", "BTTrackerDemo\BTTrackerDemo.csproj", "{4BFC69B8-0099-454A-B667-E823F47B1465}"
4 | EndProject
5 | Global
6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
7 | Debug|Any CPU = Debug|Any CPU
8 | Release|Any CPU = Release|Any CPU
9 | EndGlobalSection
10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
11 | {4BFC69B8-0099-454A-B667-E823F47B1465}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
12 | {4BFC69B8-0099-454A-B667-E823F47B1465}.Debug|Any CPU.Build.0 = Debug|Any CPU
13 | {4BFC69B8-0099-454A-B667-E823F47B1465}.Release|Any CPU.ActiveCfg = Release|Any CPU
14 | {4BFC69B8-0099-454A-B667-E823F47B1465}.Release|Any CPU.Build.0 = Release|Any CPU
15 | EndGlobalSection
16 | EndGlobal
17 |
--------------------------------------------------------------------------------
/BTTrackerDemo/BTTrackerDemo.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/BTTrackerDemo/Controllers/AnnounceController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using BencodeNET.Objects;
5 | using BTTrackerDemo.Controllers.Dtos;
6 | using BTTrackerDemo.Tracker;
7 | using Microsoft.AspNetCore.Http;
8 | using Microsoft.AspNetCore.Mvc;
9 |
10 | namespace BTTrackerDemo.Controllers
11 | {
12 | public class AnnounceController : Controller
13 | {
14 | private readonly IHttpContextAccessor _httpContextAccessor;
15 | private readonly IBitTorrentManager _bitTorrentManager;
16 |
17 | public AnnounceController(IHttpContextAccessor httpContextAccessor, IBitTorrentManager bitTorrentManager)
18 | {
19 | _httpContextAccessor = httpContextAccessor;
20 | _bitTorrentManager = bitTorrentManager;
21 | }
22 |
23 | [HttpGet]
24 | [Route("/Announce/GetPeersInfo")]
25 | public async Task GetPeersInfo(GetPeersInfoInput input)
26 | {
27 | // 如果 BT 客户端没有传递 IP,则通过 Context 获得。
28 | if (string.IsNullOrEmpty(input.Ip)) input.Ip = _httpContextAccessor.HttpContext.Connection.RemoteIpAddress.MapToIPv4().ToString();
29 |
30 | // 本机测试用。
31 | input.Ip = "127.0.0.1";
32 |
33 | AnnounceInputParameters inputPara = input;
34 | var resultDict = new BDictionary();
35 |
36 | // 如果产生了错误,则不执行其他操作,直接返回结果。
37 | if (inputPara.Error.Count == 0)
38 | {
39 | _bitTorrentManager.UpdatePeer(input.Info_Hash,inputPara);
40 | _bitTorrentManager.ClearZombiePeers(input.Info_Hash,TimeSpan.FromMinutes(10));
41 | var peers = _bitTorrentManager.GetPeers(input.Info_Hash);
42 |
43 | HandlePeersData(resultDict,peers,inputPara);
44 |
45 | // 构建剩余字段信息
46 | // 客户端等待时间
47 | resultDict.Add(TrackerServerConsts.IntervalKey,new BNumber((int)TimeSpan.FromSeconds(30).TotalSeconds));
48 | // 最小等待间隔
49 | resultDict.Add(TrackerServerConsts.MinIntervalKey,new BNumber((int)TimeSpan.FromSeconds(30).TotalSeconds));
50 | // Tracker 服务器的 Id
51 | resultDict.Add(TrackerServerConsts.TrackerIdKey,new BString("Tracker-DEMO"));
52 | // 已完成的 Peer 数量
53 | resultDict.Add(TrackerServerConsts.CompleteKey,new BNumber(_bitTorrentManager.GetComplete(input.Info_Hash)));
54 | // 非做种状态的 Peer 数量
55 | resultDict.Add(TrackerServerConsts.IncompleteKey,new BNumber(_bitTorrentManager.GetInComplete(input.Info_Hash)));
56 | }
57 | else
58 | {
59 | resultDict = inputPara.Error;
60 | }
61 |
62 | // 写入响应结果。
63 | var resultDictBytes = resultDict.EncodeAsBytes();
64 | var response = _httpContextAccessor.HttpContext.Response;
65 | response.ContentType = "text/plain";
66 | response.StatusCode = 200;
67 | response.ContentLength = resultDictBytes.Length;
68 | await response.Body.WriteAsync(resultDictBytes);
69 | }
70 |
71 | ///
72 | /// 将 Peer 集合的数据转换为 BT 协议规定的格式
73 | ///
74 | private void HandlePeersData(BDictionary resultDict, IReadOnlyList peers, AnnounceInputParameters inputParameters)
75 | {
76 | var total = Math.Min(peers.Count, inputParameters.PeerWantCount);
77 | //var startIndex = new Random().Next(total);
78 |
79 | // 判断当前 BT 客户端是否需要紧凑模式的数据。
80 | if (inputParameters.IsEnableCompact)
81 | {
82 | var compactResponse = new byte[total * 6];
83 | for (int index =0; index
6 | /// 种子的唯一 Hash 标识。
7 | ///
8 | public string Info_Hash { get; set; }
9 |
10 | ///
11 | /// 客户端的随机 Id,由 BT 客户端生成。
12 | ///
13 | public string Peer_Id { get; set; }
14 |
15 | ///
16 | /// 客户端的 IP 地址。
17 | ///
18 | public string Ip { get; set; }
19 |
20 | ///
21 | /// 客户端监听的端口。
22 | ///
23 | public int Port { get; set; }
24 |
25 | ///
26 | /// 已经上传的数据大小。
27 | ///
28 | public long Uploaded { get; set; }
29 |
30 | ///
31 | /// 已经下载的数据大小。
32 | ///
33 | public long Downloaded { get; set; }
34 |
35 | ///
36 | /// 事件表示,具体可以转换为 枚举的具体值。
37 | ///
38 | public string Event { get; set; }
39 |
40 | ///
41 | /// 该客户端剩余待下载的数据。
42 | ///
43 | public long Left { get; set; }
44 |
45 | ///
46 | /// 是否启用压缩,当该值为 1 的时候,表示当前客户端接受压缩格式的 Peer 列表,即使用
47 | /// 6 字节表示一个 Peer (前 4 字节表示 IP 地址,后 2 字节表示端口号)。当该值为 0
48 | /// 的时候则表示客户端不接受。
49 | ///
50 | public int Compact { get; set; }
51 |
52 | ///
53 | /// 表示客户端想要获得的 Peer 数量。
54 | ///
55 | public int? NumWant { get; set; }
56 | }
57 | }
--------------------------------------------------------------------------------
/BTTrackerDemo/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using BencodeNET.Objects;
7 | using Microsoft.AspNetCore;
8 | using Microsoft.AspNetCore.Hosting;
9 | using Microsoft.Extensions.Configuration;
10 | using Microsoft.Extensions.Hosting;
11 | using Microsoft.Extensions.Logging;
12 |
13 | namespace BTTrackerDemo
14 | {
15 | public class Program
16 | {
17 | public static void Main(string[] args)
18 | {
19 | CreateHostBuilder(args).Build().Run();
20 | }
21 |
22 | public static IHostBuilder CreateHostBuilder(string[] args) =>
23 | Host.CreateDefaultBuilder(args)
24 | .ConfigureWebHostDefaults(webBuilder =>
25 | {
26 | webBuilder.UseStartup()
27 | .UseKestrel(op=>op.ListenAnyIP(5000));
28 | });
29 | }
30 | }
--------------------------------------------------------------------------------
/BTTrackerDemo/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:63438",
8 | "sslPort": 44395
9 | }
10 | },
11 | "profiles": {
12 | "IIS Express": {
13 | "commandName": "IISExpress",
14 | "launchBrowser": true,
15 | "launchUrl": "api/values",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | }
19 | },
20 | "BTTrackerDemo": {
21 | "commandName": "Project",
22 | "launchBrowser": true,
23 | "launchUrl": "api/values",
24 | "applicationUrl": "https://localhost:5001;http://localhost:5000",
25 | "environmentVariables": {
26 | "ASPNETCORE_ENVIRONMENT": "Development"
27 | }
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/BTTrackerDemo/Startup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using BTTrackerDemo.Tracker;
6 | using Microsoft.AspNetCore.Builder;
7 | using Microsoft.AspNetCore.Hosting;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.AspNetCore.HttpsPolicy;
10 | using Microsoft.AspNetCore.Mvc;
11 | using Microsoft.Extensions.Configuration;
12 | using Microsoft.Extensions.DependencyInjection;
13 | using Microsoft.Extensions.DependencyInjection.Extensions;
14 | using Microsoft.Extensions.Logging;
15 | using Microsoft.Extensions.Options;
16 |
17 | namespace BTTrackerDemo
18 | {
19 | public class Startup
20 | {
21 | public Startup(IConfiguration configuration)
22 | {
23 | Configuration = configuration;
24 | }
25 |
26 | public IConfiguration Configuration { get; }
27 |
28 | // This method gets called by the runtime. Use this method to add services to the container.
29 | public void ConfigureServices(IServiceCollection services)
30 | {
31 | services.AddMvc()
32 | .AddNewtonsoftJson();
33 | services.TryAddSingleton();
34 | services.TryAddSingleton();
35 | }
36 |
37 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
38 | public void Configure(IApplicationBuilder app, IHostingEnvironment env)
39 | {
40 | if (env.IsDevelopment())
41 | {
42 | app.UseDeveloperExceptionPage();
43 | }
44 | else
45 | {
46 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
47 | app.UseHsts();
48 | }
49 |
50 | app.UseHttpsRedirection();
51 |
52 | app.UseRouting(routes => { routes.MapApplication(); });
53 |
54 | app.UseAuthorization();
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/BTTrackerDemo/Tracker/AnnounceInputParameters.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using System.Web;
4 | using BencodeNET.Objects;
5 | using BTTrackerDemo.Controllers.Dtos;
6 |
7 | namespace BTTrackerDemo.Tracker
8 | {
9 | public class AnnounceInputParameters
10 | {
11 | ///
12 | /// 客户端 IP 端点信息。
13 | ///
14 | public IPEndPoint ClientAddress { get; }
15 |
16 | ///
17 | /// 种子的唯一 Hash 标识。
18 | ///
19 | public string InfoHash { get; }
20 |
21 | ///
22 | /// 客户端的随机 Id,由 BT 客户端生成。
23 | ///
24 | public string PeerId { get; }
25 |
26 | ///
27 | /// 已经上传的数据大小。
28 | ///
29 | public long Uploaded { get; }
30 |
31 | ///
32 | /// 已经下载的数据大小。
33 | ///
34 | public long Downloaded { get; }
35 |
36 | ///
37 | /// 事件表示,具体可以转换为 枚举的具体值。
38 | ///
39 | public TorrentEvent Event { get; }
40 |
41 | ///
42 | /// 该客户端剩余待下载的数据。
43 | ///
44 | public long Left { get; }
45 |
46 | ///
47 | /// Peer 是否允许启用压缩。
48 | ///
49 | public bool IsEnableCompact { get; }
50 |
51 | ///
52 | /// Peer 想要获得的可用的 Peer 数量。
53 | ///
54 | public int PeerWantCount { get; }
55 |
56 | ///
57 | /// 如果在请求过程当中出现了异常,则本字典包含了异常信息。
58 | ///
59 | public BDictionary Error { get; }
60 |
61 | public AnnounceInputParameters(GetPeersInfoInput apiInput)
62 | {
63 | Error = new BDictionary();
64 |
65 | ClientAddress = ConvertClientAddress(apiInput);
66 | InfoHash = ConvertInfoHash(apiInput);
67 | Event = ConvertTorrentEvent(apiInput);
68 | PeerId = apiInput.Peer_Id;
69 | Uploaded = apiInput.Uploaded;
70 | Downloaded = apiInput.Downloaded;
71 | Left = apiInput.Left;
72 | IsEnableCompact = apiInput.Compact == 1;
73 | PeerWantCount = apiInput.NumWant ?? 30;
74 | }
75 |
76 | ///
77 | /// 到当前类型的隐式转换定义。
78 | ///
79 | public static implicit operator AnnounceInputParameters(GetPeersInfoInput input)
80 | {
81 | return new AnnounceInputParameters(input);
82 | }
83 |
84 | ///
85 | /// 将客户端传递的 IP 地址与端口转换为 类型。
86 | ///
87 | private IPEndPoint ConvertClientAddress(GetPeersInfoInput apiInput)
88 | {
89 | if (IPAddress.TryParse(apiInput.Ip, out IPAddress ipAddress))
90 | {
91 | return new IPEndPoint(ipAddress,apiInput.Port);
92 | }
93 |
94 | return null;
95 | }
96 |
97 | ///
98 | /// 将客户端传递的字符串 Event 转换为 枚举。
99 | ///
100 | private TorrentEvent ConvertTorrentEvent(GetPeersInfoInput apiInput)
101 | {
102 | switch (apiInput.Event)
103 | {
104 | case "started":
105 | return TorrentEvent.Started;
106 | case "stopped":
107 | return TorrentEvent.Stopped;
108 | case "completed":
109 | return TorrentEvent.Completed;
110 | default:
111 | return TorrentEvent.None;
112 | }
113 | }
114 |
115 | ///
116 | /// 将 info_hash 参数从 URL 编码转换为标准的字符串。
117 | ///
118 | private string ConvertInfoHash(GetPeersInfoInput apiInput)
119 | {
120 | var infoHashBytes = HttpUtility.UrlDecodeToBytes(apiInput.Info_Hash);
121 | if (infoHashBytes == null)
122 | {
123 | Error.Add(TrackerServerConsts.FailureKey,new BString("info_hash 参数不能为空."));
124 | return null;
125 | }
126 |
127 | if (infoHashBytes.Length != 20)
128 | {
129 | Error.Add(TrackerServerConsts.FailureKey,new BString($"info_hash 参数的长度 {{{infoHashBytes.Length}}} 不符合 BT 协议规范."));
130 | }
131 |
132 | return BitConverter.ToString(infoHashBytes);
133 | }
134 | }
135 | }
--------------------------------------------------------------------------------
/BTTrackerDemo/Tracker/BitTorrentManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 |
6 | namespace BTTrackerDemo.Tracker
7 | {
8 | public class BitTorrentManager : IBitTorrentManager
9 | {
10 | private readonly ConcurrentDictionary> _peers;
11 | private readonly ConcurrentDictionary _bitTorrentStatus;
12 |
13 | public BitTorrentManager()
14 | {
15 | _peers = new ConcurrentDictionary>();
16 | _bitTorrentStatus = new ConcurrentDictionary();
17 | }
18 |
19 | public Peer AddPeer(string infoHash, AnnounceInputParameters inputParameters)
20 | {
21 | CheckParameters(infoHash, inputParameters);
22 |
23 | var newPeer = new Peer(inputParameters);
24 |
25 | if (!_peers.ContainsKey(infoHash))
26 | {
27 | _peers.TryAdd(infoHash, new List {newPeer});
28 | }
29 |
30 | _peers[infoHash].Add(newPeer);
31 |
32 | UpdateBitTorrentStatus(infoHash);
33 |
34 | return newPeer;
35 | }
36 |
37 | public void DeletePeer(string infoHash, AnnounceInputParameters inputParameters)
38 | {
39 | CheckParameters(infoHash, inputParameters);
40 |
41 | if (!_peers.ContainsKey(infoHash)) return;
42 |
43 | _peers[infoHash].RemoveAll(p => p.UniqueId == inputParameters.PeerId);
44 |
45 | UpdateBitTorrentStatus(infoHash);
46 | }
47 |
48 | public void UpdatePeer(string infoHash, AnnounceInputParameters inputParameters)
49 | {
50 | CheckParameters(infoHash, inputParameters);
51 |
52 | if (!_peers.ContainsKey(inputParameters.InfoHash)) _peers.TryAdd(infoHash, new List());
53 | if (!_bitTorrentStatus.ContainsKey(inputParameters.InfoHash)) _bitTorrentStatus.TryAdd(infoHash, new BitTorrentStatus());
54 |
55 | // 如果 Peer 不存在则添加,否则更新其状态。
56 | var peers = _peers[infoHash];
57 | var peer = peers.FirstOrDefault(p => p.UniqueId == inputParameters.PeerId);
58 | if (peer == null)
59 | {
60 | AddPeer(infoHash, inputParameters);
61 | }
62 | else
63 | {
64 | peer.UpdateStatus(inputParameters);
65 | }
66 |
67 | // 根据事件更新种子状态与 Peer 信息。
68 | if (inputParameters.Event == TorrentEvent.Stopped) DeletePeer(infoHash,inputParameters);
69 | if (inputParameters.Event == TorrentEvent.Completed) _bitTorrentStatus[infoHash].Downloaded++;
70 |
71 | UpdateBitTorrentStatus(infoHash);
72 | }
73 |
74 | public IReadOnlyList GetPeers(string infoHash)
75 | {
76 | if (!_peers.ContainsKey(infoHash)) return null;
77 | return _peers[infoHash];
78 | }
79 |
80 | public void ClearZombiePeers(string infoHash, TimeSpan expiry)
81 | {
82 | if (!_peers.ContainsKey(infoHash)) return;
83 |
84 | var now = DateTime.Now;
85 |
86 | _peers[infoHash].RemoveAll(p => now - p.LastRequestTrackerTime > expiry);
87 | }
88 |
89 | public int GetComplete(string infoHash)
90 | {
91 | if (_bitTorrentStatus.TryGetValue(infoHash, out BitTorrentStatus status))
92 | {
93 | return status.Completed;
94 | }
95 |
96 | return 0;
97 | }
98 |
99 | public int GetInComplete(string infoHash)
100 | {
101 | if (_bitTorrentStatus.TryGetValue(infoHash, out BitTorrentStatus status))
102 | {
103 | return status.InCompleted;
104 | }
105 |
106 | return 0;
107 | }
108 |
109 | ///
110 | /// 更新种子的统计信息。
111 | ///
112 | private void UpdateBitTorrentStatus(string infoHash)
113 | {
114 | if (!_peers.ContainsKey(infoHash)) return;
115 | if (!_bitTorrentStatus.ContainsKey(infoHash)) return;
116 |
117 | // 遍历种子所有的 Peer 状态,对种子统计信息进行处理。
118 | int complete = 0, incomplete = 0;
119 | var peers = _peers[infoHash];
120 | foreach (var peer in peers)
121 | {
122 | if (peer.IsCompleted) complete++;
123 | else incomplete++;
124 | }
125 |
126 | _bitTorrentStatus[infoHash].Completed = complete;
127 | _bitTorrentStatus[infoHash].InCompleted = incomplete;
128 | }
129 |
130 | ///
131 | /// 检测参数与种子唯一标识的状态。
132 | ///
133 | private void CheckParameters(string infoHash,AnnounceInputParameters inputParameters)
134 | {
135 | if (string.IsNullOrEmpty(infoHash)) throw new Exception("种子的唯一标识不能为空。");
136 | if (inputParameters == null) throw new Exception("BT 客户端传入的参数不能为空。");
137 | }
138 | }
139 | }
--------------------------------------------------------------------------------
/BTTrackerDemo/Tracker/BitTorrentStatus.cs:
--------------------------------------------------------------------------------
1 | using BencodeNET.Objects;
2 |
3 | namespace BTTrackerDemo.Tracker
4 | {
5 | ///
6 | /// 用于表示某个种子的状态与统计信息。
7 | ///
8 | public class BitTorrentStatus
9 | {
10 | ///
11 | /// 下载完成的 Peer 数量。
12 | ///
13 | public BNumber Downloaded { get; set; }
14 |
15 | ///
16 | /// 已经完成种子下载的 Peer 数量。
17 | ///
18 | public BNumber Completed { get; set; }
19 |
20 | ///
21 | /// 正在下载种子的 Peer 数量。
22 | ///
23 | public BNumber InCompleted { get; set; }
24 |
25 | public BitTorrentStatus()
26 | {
27 | Downloaded = new BNumber(0);
28 | Completed = new BNumber(0);
29 | InCompleted = new BNumber(0);
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/BTTrackerDemo/Tracker/IBitTorrentManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace BTTrackerDemo.Tracker
5 | {
6 | ///
7 | /// 用于管理 BT 种子与其关联的 Peer 集合。
8 | ///
9 | public interface IBitTorrentManager
10 | {
11 | ///
12 | /// 添加一个新的 Peer 到指定种子关联的集合当中。
13 | ///
14 | /// 种子的唯一标识。
15 | /// BT 客户端传入的参数信息。
16 | Peer AddPeer(string infoHash,AnnounceInputParameters inputParameters);
17 |
18 | ///
19 | /// 根据参数删除指定种子的 Peer 信息。
20 | ///
21 | /// 种子的唯一标识。
22 | /// BT 客户端传入的参数信息。
23 | void DeletePeer(string infoHash,AnnounceInputParameters inputParameters);
24 |
25 | ///
26 | /// 更新指定种子的某个 Peer 状态。
27 | ///
28 | /// 种子的唯一标识。
29 | /// BT 客户端传入的参数信息。
30 | void UpdatePeer(string infoHash, AnnounceInputParameters inputParameters);
31 |
32 | ///
33 | /// 获得指定种子的可用 Peer 集合。
34 | ///
35 | /// 种子的唯一标识。
36 | /// 当前种子关联的 Peer 列表。
37 | IReadOnlyList GetPeers(string infoHash);
38 |
39 | ///
40 | /// 清理指定种子内部不活跃的 Peer 。
41 | ///
42 | /// 种子的唯一标识。
43 | /// 超时周期,超过这个时间的 Peer 将会被清理掉。
44 | void ClearZombiePeers(string infoHash,TimeSpan expiry);
45 |
46 | ///
47 | /// 获得指定种子已经完成下载的 Peer 数量。
48 | ///
49 | /// 种子的唯一标识。
50 | int GetComplete(string infoHash);
51 |
52 | ///
53 | /// 获得指定种子正在下载的 Peer 数量。
54 | ///
55 | /// 种子的唯一标识。
56 | int GetInComplete(string infoHash);
57 | }
58 | }
--------------------------------------------------------------------------------
/BTTrackerDemo/Tracker/Peer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using BencodeNET.Objects;
4 |
5 | namespace BTTrackerDemo.Tracker
6 | {
7 | ///
8 | /// 每个 BT 下载客户端的定义。
9 | ///
10 | public class Peer
11 | {
12 | ///
13 | /// 客户端 IP 端点信息。
14 | ///
15 | public IPEndPoint ClientAddress { get; private set; }
16 |
17 | ///
18 | /// 客户端的随机 Id,由 BT 客户端生成。
19 | ///
20 | public string PeerId { get; private set; }
21 |
22 | ///
23 | /// 客户端唯一标识。
24 | ///
25 | public string UniqueId { get; private set; }
26 |
27 | ///
28 | /// 客户端在本次会话过程中下载的数据量。(以 Byte 为单位)
29 | ///
30 | public long DownLoaded { get; private set; }
31 |
32 | ///
33 | /// 客户端在本次会话过程当中上传的数据量。(以 Byte 为单位)
34 | ///
35 | public long Uploaded { get; private set; }
36 |
37 | ///
38 | /// 客户端的下载速度。(以 Byte/秒 为单位)
39 | ///
40 | public long DownloadSpeed { get; private set; }
41 |
42 | ///
43 | /// 客户端的上传速度。(以 Byte/秒 为单位)
44 | ///
45 | public long UploadSpeed { get; private set; }
46 |
47 | ///
48 | /// 客户端是否完成了当前种子,True 为已经完成,False 为还未完成。
49 | ///
50 | public bool IsCompleted { get; private set; }
51 |
52 | ///
53 | /// 最后一次请求 Tracker 服务器的时间。
54 | ///
55 | public DateTime LastRequestTrackerTime { get; private set; }
56 |
57 | ///
58 | /// Peer 还需要下载的数量。
59 | ///
60 | public long Left { get; private set; }
61 |
62 | public Peer() { }
63 |
64 | public Peer(AnnounceInputParameters inputParameters)
65 | {
66 | UniqueId = inputParameters.PeerId;
67 |
68 | // 根据输入参数更新 Peer 的状态。
69 | UpdateStatus(inputParameters);
70 | }
71 |
72 | ///
73 | /// 根据输入参数更新 Peer 的状态。
74 | ///
75 | /// BT 客户端请求 Tracker 服务器时传递的参数。
76 | public void UpdateStatus(AnnounceInputParameters inputParameters)
77 | {
78 | var now = DateTime.Now;
79 |
80 | var elapsedTime = (now - LastRequestTrackerTime).TotalSeconds;
81 | if (elapsedTime < 1) elapsedTime = 1;
82 |
83 | ClientAddress = inputParameters.ClientAddress;
84 | // 通过差值除以消耗的时间,得到每秒的大概下载速度。
85 | DownloadSpeed = (int) ((inputParameters.Downloaded - DownLoaded) / elapsedTime);
86 | DownLoaded = inputParameters.Downloaded;
87 | UploadSpeed = (int) ((inputParameters.Uploaded) / elapsedTime);
88 | Uploaded = inputParameters.Uploaded;
89 | Left = inputParameters.Left;
90 | PeerId = inputParameters.PeerId;
91 | LastRequestTrackerTime = now;
92 |
93 | // 如果没有剩余数据,则表示 Peer 已经完成下载。
94 | if (Left == 0) IsCompleted = true;
95 | }
96 |
97 | ///
98 | /// 将 Peer 信息进行 B 编码,按照协议处理为字典。
99 | ///
100 | public BDictionary ToEncodedDictionary()
101 | {
102 | return new BDictionary
103 | {
104 | {TrackerServerConsts.PeerIdKey,new BString(PeerId)},
105 | {TrackerServerConsts.Ip,new BString(ClientAddress.Address.ToString())},
106 | {TrackerServerConsts.Port,new BNumber(ClientAddress.Port)}
107 | };
108 | }
109 |
110 | ///
111 | /// 将 Peer 信息进行紧凑编码成字节组。
112 | ///
113 | public byte[] ToBytes()
114 | {
115 | var portBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short) ClientAddress.Port));
116 | var addressBytes = ClientAddress.Address.GetAddressBytes();
117 |
118 | var resultBytes = new byte[portBytes.Length + addressBytes.Length];
119 |
120 | // 根据协议规定,首部的 4 字节为 IP 地址,尾部的 2 自己为端口信息
121 | Array.Copy(addressBytes,resultBytes,addressBytes.Length);
122 | Array.Copy(portBytes,0,resultBytes,addressBytes.Length,portBytes.Length);
123 |
124 | return resultBytes;
125 | }
126 | }
127 | }
--------------------------------------------------------------------------------
/BTTrackerDemo/Tracker/TorrentEvent.cs:
--------------------------------------------------------------------------------
1 | namespace BTTrackerDemo.Tracker
2 | {
3 | public enum TorrentEvent
4 | {
5 | ///
6 | /// 未知状态。
7 | ///
8 | None,
9 | ///
10 | /// 已开始。
11 | ///
12 | Started,
13 | ///
14 | /// 已停止。
15 | ///
16 | Stopped,
17 | ///
18 | /// 已完成。
19 | ///
20 | Completed
21 | }
22 | }
--------------------------------------------------------------------------------
/BTTrackerDemo/Tracker/TrackerServerConsts.cs:
--------------------------------------------------------------------------------
1 | using BencodeNET.Objects;
2 |
3 | namespace BTTrackerDemo.Tracker
4 | {
5 | ///
6 | /// 常用的字典 KEY。
7 | ///
8 | public static class TrackerServerConsts
9 | {
10 | public static readonly BString PeerIdKey = new BString("peer id");
11 | public static readonly BString PeersKey = new BString("peers");
12 | public static readonly BString IntervalKey = new BString("interval");
13 | public static readonly BString MinIntervalKey = new BString("min interval");
14 | public static readonly BString TrackerIdKey = new BString("tracker id");
15 | public static readonly BString CompleteKey = new BString("complete");
16 | public static readonly BString IncompleteKey = new BString("incomplete");
17 |
18 | public static readonly BString Port = new BString("port");
19 | public static readonly BString Ip = new BString("ip");
20 |
21 | public static readonly string FailureKey = "failure reason";
22 | }
23 | }
--------------------------------------------------------------------------------
/BTTrackerDemo/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "System": "Information",
6 | "Microsoft": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/BTTrackerDemo/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Warning",
5 | "Microsoft.Hosting.Lifetime": "Information"
6 | }
7 | },
8 | "AllowedHosts": "*"
9 | }
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Zony
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------