本节主要介绍一下Django框架表单(Form)的基础知识。Django框架提供了一系列的工具和库来帮助设计人员构建表单,通过表单来接收网站用户的输入,然后处理以及响应这些用户的输入。

6.1.1 HTML表单


Django框架表单是在HTML模板中设计完成的,其实类似于传统HTML Form表单的应用。在传统HTML页面中,表单是由“<form>...</form>”标签实现的,通过在其中添加相关的一些元素(例如文本输入框、单选框、复选框、文本域、重置按钮和提交按钮等),允许终端用户通过表单输入相关的数据信息,然后发送到服务端(后台)。Django框架表单也实现了相应的功能,只不过要遵循Django框架标准来设计。

在HTML中,有一些表单元素(例如文本输入框)是非常简单且内置于HTML中的,而有一些表单元素会比较复杂(例如日期选择控件、滑块控件等),一般需要通过使用JavaScript、CSS以及<input>等来实现效果。

Django框架表单同样如此,定义时需要满足以下两项常规标准:

负责响应用户输入数据的URL地址(action属性)。
数据请求时使用的HTTP方法(method属性:GET、POST)。
例如,在Django框架内置的Admin(管理员)登录表单中,就包含如下一些常规<input>元素类型:

用户名:type="text"。
密码:type="password"。
登录按钮:type="submit"。
action属性指定的URL地址:“/admin/”。
method属性指定的HTTP方法:“POST”。
当用户单击<input type="submit" value="Log in">按钮元素时,提交响应就会被触发,然后表单数据会被发送到“/admin/”地址上去。

6.1.2 HTTP方法:GET和POST


Django框架处理表单时只会用到GET和POST这两种HTTP方法。Django的登录表单需要使用POST方法传输数据。当使用POST方法时,浏览器会封装表单数据,为了传输安全还会进行必要编码,然后发送到服务端并接收其响应。

相比之下,GET方法会将提交的数据绑定到一个字符串中,并用该字符串来组成一个URL地址。该URL地址包含了数据要发送的地址以及一些键值对应的数据。例如,在Django官方文档(https://docs.djangoproject.com)中进行一次搜索,就会生成一个类似“https://docs.djangoproject.com/search/?q=forms&release=1”的URL地址,这个就是GET方式。

GET和POST这两种HTTP方法通常用于不同的目的。任何可能用于更改系统状态的请求应该使用POST方法,比如一个更改数据库的请求;GET方法应该只被用于不会影响系统状态的请求。

还有,GET方法也不适合密码表单,因为密码会出现在URL地址字符串中,自然也会被记录在浏览器的历史记录以及服务器的日志中,而且都是纯文本的形式,因此安全性就无法保证。GET方法同样也不适合处理大量的字符串数据或二进制数据,比如图片和视频这类的。

在Web应用的管理表单中使用GET请求具有安全隐患:攻击者很容易通过模拟请求来访问系统的敏感数据,因此Django Admin模块选择使用POST方法。在Django框架模板中,POST方法通过与CSRF protection这样的保护措施配合使用,能对访问提供更多的控制。

GET方法也不是完全无用武之地的。GET方法适用于类似网页搜索表单这样的场景,这时GET请求的URL地址很容易被保存为书签,便于用户分享或重新提交。因此,在Django官方文档中进行搜索,就使用了GET方法。

6.1.3 Django在表单中的角色


Django框架处理表单是一件比较复杂的事情。研究一下Django框架的Admin模块,就会发现许多不同类型的数据可能需要在一张表单中完成,然后渲染到HTML模板中呈现,还需要使用便捷的界面进行编辑、上传到服务器、验证和清理数据,最后还要保存或跳过进行下一步处理。

Django框架的表单功能可以简化和自动化上述工作的大部分内容,并且也能比大多数设计人员自己编写代码去实现表现得更安全一些。

Django框架会处理涉及表单的3个不同部分:

准备并重组数据,以便下一步的渲染。
为数据创建HTML表单。
接收并处理客户端提交的表单及数据。
虽然设计人员可以通过手动编写代码来实现上述功能,不过Django框架表单的内置功能已能够完成这些工作。

6.1.4 Form类


Django框架表单系统的核心组件是Form类,其与Django模型描述对象的逻辑结构、行为以及呈现内容的方式是大致相同的。Form类描述了表单并决定其如何工作以及如何呈现。

类似于模型类的字段映射到数据库字段的方式,ModelForm模型类的字段会通过表单类的字段映射到HTML表单的<input>元素中。Django框架的Admin模块就是基于此设计实现的。

表单字段本身也是类,用于管理表单数据并在提交表单时执行验证。DateField和FileField处理的数据类型差别很大,所以必须用来处理不同的字段。

在浏览器中,表单字段以HTML元素(控件类)的形式展现。每个字段类型都有与之相匹配的控件类,但必要时可以进行覆盖。

6.1.5 实例化、处理和渲染表单


在Django框架表单中渲染一个对象的时候,其流程通常如下:

(1)在视图中获取对象(例如从数据库中取出)。

(2)将对象传递给模板上下文。

(3)使用模板变量将对象扩展为HTML标签。

在模板中渲染表单与渲染任何其他类型的对象几乎一样,但是存在一些关键性的差异。

如果模型实例不包含数据,则在模板中对其做任何处理几乎没什么用,但完全有理由来渲染一张空表单,通常当我们希望用户来填充的时候就会这么做。因此,当在视图中处理模型实例时,一般从数据库中获取这些对象;当处理表单时,一般在视图中实例化这些对象。

实例化表单时,可以选择让表单为空或预先填充数据,数据来源可以是:

(1)用来保存模型实例的数据(例如在管理编辑表单的情况下)。

(2)从其他来源获取的数据。

(3)从前面一个HTML表单提交过来的数据。

6.1.6 创建一个表单


假设希望在网站上创建一个最简单的表单用来获取用户的名字,通常只需要在模板中使用如下类似的代码:

【代码6-1】

<form action="/get-name/" method="get">
      <label for="your_name">Your name: </label>
      <input id="your_name" type="text" name="your_name" value="{{ current_name }}">
      <input type="submit" value="OK">
</form>

【代码分析】

在第01行代码中,action属性通知浏览器将表单数据提交到URL地址"/get-name/"上,method属性定义使用GET方法。

在第03行代码中,定义了一个<input type="text" />的文本输入框,用于用户输入姓名。同时,value属性定义为一个上下文变量current_name,如果该变量存在,则其值将会预先填充到表单中。

在第04行代码中,定义了一个<input type="submit" />的提交按钮。

对于【代码6-1】中定义的表单,需要一个视图来渲染这个包含HTML表单的模板,并能提供适当的{{ current_name }}字段。提交表单时,发送给服务器的“GET”请求将包含表单数据。

然后,还需要一个与该URL地址("/get-name/")相对应的视图,该视图将在请求中找到相应的键-值对,然后对其进行处理。

同时,可能还需要浏览器在表单提交之前进行一些字段验证,或者使用更复杂的字段以允许用户做类似日期选择的操作等。这时,通过Django框架可以很容易地完成以上大部分工作。

1.简介

上一篇文章,宏哥已经在搭建的java项目环境中添加jar包实践了如何启动浏览器,今天就在基于maven项目的环境中给小伙伴们或者童鞋们演示一下如何启动浏览器。

2.eclipse中新建maven项目

1.依次点击eclipse的file - new - other ,如下图所示:

2.在搜索框输入关键字“maven”,然后选中“maven project”,如下图所示:

3.选择创建后的工作区——项目存放的地址。如下图所示:

4.选择Maven项目的模板也叫项目类型(quikstart或者webapp等等),,如果选择create a simple project,则跳过了下面的步骤,也就不存在这个问题了,但是如果需要选择项目类型,则不能勾选create a simple project)如下图所示:

5.宏哥为了省事,直接选中create a simple project,点击next,输入Group Id和Artifact Id。如下图所示:

6.点击“Finish”,查看新建的maven项目,如下图所示:

到此,创建maven项目成功!!!

3.maven项目加载playwright依赖jar

3.1加载playwright依赖

maven项目加载playwright依赖就不想上一篇java项目加载playwright那么费事需要把jar包引入到项目下,maven项目只需要将相关的jar包依赖配置到pom.xml文件中就会自动加载了。因此要给上面创建的maven项目中加载playwright依赖jar包,只需在pom.xml中引入playwright的jar包即可;具体步骤如下:

1.查看maven仓库:
http://mvnrepository.com/
如下图所示:

2.搜索playwright, 输入playwright,点击“Search”,如下图所示:

3.点击“
Playwright Main Library
”,查看自己需要的playwright版本,playwright我们都会选择最新的(宏哥这里用1.40.0举例一下,最新可能有bug,老的可能有些方法不支持,宏哥这里选择一个不新也不旧的),如下图所示:

4.下载playwright-1.40.0版本,点1.40.0进入页面后,只需要单击下边的编码就自动全选复制了。如下图所示:

<!-- https://mvnrepository.com/artifact/com.microsoft.playwright/playwright -->
<dependency>
    <groupId>com.microsoft.playwright</groupId>
    <artifactId>playwright</artifactId>
    <version>1.40.0</version>
</dependency>

5.copy到maven项目中的pom.xml中,如下图所示:

6.playwright的jar包maven会自动加载,从右边路径可以看到jar的路径在本地仓库。如下图所示:

需要其他的jar包只需配置到pom.xml中即可!是不是比之前介绍的方法简单多了哈!

3.2修改jdk版本

因为playwright的Java需要Java8以上,所以需要重新配置jdk。如下图所示:

1.右键JRE System Library[JavaSe-1.7] ->properties。如下图所示:

2.选择javaSE-1.8,如下图所示:

3.点击“OK”后,就变成JavaSE-1.8了,如下图所示:

好了,至此,基于maven的java+Playwright自动化测试环境搭建就搭建成功了。下边就开始实践Maven项目如何启动浏览器。

4.启动Chrome浏览器

大致思路:打开Chrome浏览器,访问百度网址,获取网址的title,然后再关闭Chrome浏览器。根据思路进行代码设计。

4.1代码设计

4.2参考代码

packagecom.bjhg.playwright;importcom.microsoft.playwright.Browser;importcom.microsoft.playwright.BrowserType;importcom.microsoft.playwright.Page;importcom.microsoft.playwright.Playwright;/***@author北京-宏哥
*
* @公众号:北京宏哥(微信搜索,关注宏哥,提前解锁更多测试干货)
*
* 《刚刚问世》系列初窥篇-Java+Playwright自动化测试-4-启动浏览器-基于Maven(详细教程)
*
* 2024年7月10日
*/ public classLaunchChrome {public static voidmain(String[] args) {try (Playwright playwright =Playwright.create()) {//使用chromium浏览器,# 浏览器配置,设置以GUI模式启动Chrome浏览器(要查看浏览器UI,在启动浏览器时传递 headless=false 标志。您还可以使用 slowMo 来减慢执行速度。 Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false).setSlowMo(50));//创建page Page page =browser.newPage();//浏览器打开百度 page.navigate("https://www.baidu.com/");//打印title System.out.println(page.title());//关闭page page.close();//关闭browser browser.close();
}
}

}

4.3运行代码

1.运行代码,右键Run AS->java Application,就可以看到控制台输出,如下图所示:

2.运行代码后电脑端的浏览器的动作。如下图所示:

好了,到此,在Maven项目中如何启动Chrome浏览器,就完成了,Firefox和webkit的两个浏览器和Chrome的非常相似,宏哥就不在这里进行赘述了。

5.小结

宏哥因为之前做过python、java语言和selenium,经常碰到头疼的问题就是:出现浏览器版本和驱动版本匹配的问题,新手一定要注意这个问题。但是playwright无论是Java还是python语言,无论是新手还是老鸟就都不需要担心这些问题了,而且今天讲解和分享的非常简单,就是简单换个方法就可以启动不同的浏览器了。好了,今天关于三大浏览器的驱动宏哥就分享到这里,感谢你耐心的阅读。

前言

在.NET 8中,IHostedService 和 BackgroundService 两个核心接口的引入,增强了项目开发中处理定时任务的能力。这两个接口不仅简化了定时任务、后台处理作业以及定期维护任务的实现过程,还提升了在ASP.NET Core 或任何基于.NET的宿主应用程序中的集成与管理效率。

IHostedService接口提供了一个基本的框架,允许自定义后台服务的启动和停止逻辑。通过实现该接口,可以灵活地控制服务的生命周期,确保任务在应用程序启动时自动运行,并在应用程序关闭时结束。

而 BackgroundService 类则是对 IHostedService 接口的进一步封装,它专为需要长时间运行的任务而设计。

通过继承 BackgroundService并重写其 ExecuteAsync方法,可以轻松地实现复杂的后台逻辑,如循环执行的任务、基于时间间隔的操作等。这种设计模式让代码的可读性和可维护性变的更好。

利用这些功能,可以快速构建出高效、可靠的定时任务系统,用于执行诸如消息推送、数据更新、定时发布等关键业务操作。这些任务可以在不影响应用程序主流程的情况下独立运行,从而提高了整个系统的性能和稳定性。

介绍

.NET 中的后台服务允许在后台独立于主应用程序线程运行任务。这对于需要连续或定期运行而不阻塞主应用程序流的任务至关重要。

IHostedService

IHostedService 是一个简单的接口,用于实现后台服务。当需要实现自定义的托管服务时,可以通过实现这个接口来创建。

该接口定义了两个方法:StartAsync(CancellationToken cancellationToken) 和 StopAsync(CancellationToken cancellationToken),分别用于启动和停止服务。

BackgroundService

BackgroundService 是一个抽象类,它继承自 IHostedService 并提供了更简洁的方式来编写后台服务。它通常用于需要长时间运行的任务,如监听器、工作队列处理器等。通过重写 ExecuteAsync(CancellationToken stoppingToken)方法,可以在其中编写任务的逻辑。ExecuteAsync方法会循环在后台执行,直到服务停止。

IHostedService 示例

1、注册服务

Program.cs 中添加配置,.NET 5 及以下在需要在 Startup.cs 注册服务。

//.NET 8
usingManageCore.Api;var builder =WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHostedService
<DemoHostedService>();var app = builder.Build();

2、创建服务接口

创建一个类,该类继承 IHostedService 接口,并实现该接口成员.

在不需要定时执行任务的时候,也可以在这里进行应用启动后的操作,例如创建 RabbitMQ 连接

usingMicrosoft.Extensions.Hosting;namespaceManageCore.Api
{
public classDemoHostedService : IHostedService, IDisposable
{
private Timer?_timer;publicTask StartAsync(CancellationToken cancellationToken)
{
_timer
= new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));returnTask.CompletedTask;
}
private void DoWork(object?state)
{
Console.WriteLine($
"{DateTime.Now:yyyy-MM-dd HH:mm:ss}");
}
publicTask StopAsync(CancellationToken cancellationToken)
{
Console.WriteLine(
"StopAsync");returnTask.CompletedTask;
}
public voidDispose()
{
_timer
?.Dispose();
}
}
}

上面的Demo代码非常简单,应用在运行后,会去执行 StartAsync 函数,应用关闭执行 StopAsync,由于这里使用的定时器,所以每过5秒都会执行一次 DoWork 函数。

3、运行效果

4、IHostedService 说明

注意:定时是不等待任务执行完成,只要时间一到,就会调用 DoWork 函数,所以适合一些简单、特定的场景。

以下为官方文档对 IHostedService 接口 的说明

IHostedService 接口为主机托管的对象定义了两种方法:

  • StartAsync(CancellationToken)
  • StopAsync(CancellationToken)

StartAsync(CancellationToken)
包含用于启动后台任务的逻辑。 在以下操作之前调用 `StartAsync`:已配置应用的请求处理管道。已启动服务器且已触发 IApplicationLifetime.ApplicationStarted。

StartAsync应仅限于短期任务,因为托管服务是按顺序运行的,在 StartAsync 运行完成之前不会启动其他服务。

StopAsync(CancellationToken)
在主机执行正常关闭时触发。 StopAsync`包含结束后台任务的逻辑。 实现 IDisposable 和终结器(析构函数)以处置任何非托管资源。

默认情况下,取消令牌会有五秒超时,以指示关闭进程不再正常。 在令牌上请求取消时:

  • 应中止应用正在执行的任何剩余后台操作。
  • StopAsync 中调用的任何方法都应及时返回。

但是,在请求取消后,将不会放弃任务,调用方会等待所有任务完成。

如果应用意外关闭(例如,应用的进程失败),则可能不会调用 StopAsync。 因此,在 StopAsync 中执行的任何方法或操作都可能不会发生。

若要延长默认值为 5 秒的关闭超时值,请设置:

  • ShutdownTimeout(当使用通用主机时)
  • 使用 Web 主机时为关闭超时值主机配置设置

托管服务在应用启动时激活一次,在应用关闭时正常关闭。 如果在执行后台任务期间引发错误,即使未调用 StopAsync,也应调用 Dispose。

BackgroundService 示例

1、注册服务

首先,同样需要在配置中注册服务接口。

usingManageCore.Api;var builder =WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddScoped
<IDemoTaskWorkService, DemoTaskWorkService>();

2、BackgroundService 源码

查看 BackgroundService 的源码,帮助我们理解BackgroundService 实现原理。BackgroundService 是 IHostedService的一个简单实现,内部 IHostedService 的 StartAsync 调用了 ExecuteAsync,本质上就是使用了 IHostedService。

public abstract classBackgroundService : IHostedService, IDisposable
{
privateTask _executingTask;private readonly CancellationTokenSource _stoppingCts = newCancellationTokenSource();/// <summary> ///This method is called when the<see cref="IHostedService"/>starts. The implementation should return a task that represents///the lifetime of the long running operation(s) being performed./// /// </summary> /// <param name="stoppingToken">Triggered when<see cref="IHostedService.StopAsync(CancellationToken)"/>is called.</param> /// <returns>A<see cref="Task"/>that represents the long running operations.</returns> protected abstractTask ExecuteAsync(CancellationToken stoppingToken);/// <summary> ///Triggered when the application host is ready to start the service./// </summary> /// <param name="cancellationToken">Indicates that the start process has been aborted.</param> public virtualTask StartAsync(CancellationToken cancellationToken)
{
//Store the task we're executing _executingTask =ExecuteAsync(_stoppingCts.Token);//If the task is completed then return it, this will bubble cancellation and failure to the caller if(_executingTask.IsCompleted)
{
return_executingTask;
}
//Otherwise it's running returnTask.CompletedTask;
}
/// <summary> ///Triggered when the application host is performing a graceful shutdown./// </summary> /// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param> public virtual asyncTask StopAsync(CancellationToken cancellationToken)
{
//Stop called without start if (_executingTask == null)
{
return;
}
try{//Signal cancellation to the executing method _stoppingCts.Cancel();
}
finally{//Wait until the task completes or the stop token triggers awaitTask.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
}

}
public virtual voidDispose()
{
_stoppingCts.Cancel();
}
}

3、创建服务接口

创建一个服务接口,定义需要实现的任务,以及对应的实现,如果需要执行异步方法,记得加上 await,不然任务将不会等待执行结果,直接进行下一个任务。

namespaceManageCore.Api
{
public interfaceIDemoTaskWorkService
{
/// <summary> ///测试任务/// </summary> /// <param name="stoppingToken"></param> /// <returns></returns> Task TaskWorkAsync(CancellationToken stoppingToken);
}
}
public classDemoTaskWorkService : IDemoTaskWorkService
{
/// <summary> ///任务执行/// </summary> /// <param name="stoppingToken"></param> /// <returns></returns> public asyncTask TaskWorkAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
//执行任务 Console.WriteLine($"{DateTime.Now}");//周期性任务,于上次任务执行完成后,等待5秒,执行下一次任务 await Task.Delay(500);
}
}
}

创建后台服务类,继承基类 BackgroundService,这里需要注意的是,要在 BackgroundService 中使用有作用域的服务,请创建作用域, 默认情况下,不会为托管服务创建作用域,得自己管理服务的生命周期,切记!于构造函数中注入 IServiceProvider即可。

namespaceManageCore.Api
{
public classDemoBackgroundService : BackgroundService
{
private readonlyIServiceProvider _services;publicDemoBackgroundService(IServiceProvider services)
{
_services
=services;
}
protected override asyncTask ExecuteAsync(CancellationToken stoppingToken)
{
using var scope =_services.CreateScope();var taskWorkService = scope.ServiceProvider.GetRequiredService<IDemoTaskWorkService>();awaittaskWorkService.TaskWorkAsync(stoppingToken);
}
}
}

DemoBackgroundService类也是需要注册的,注册方式与 IHostedService 接口的方式一样

builder.Services.AddHostedService<DemoBackgroundService>();

4、运行效果

5、BackgroundService 说明

BackgroundService 是用于实现长时间运行的 IHostedService 的基类。

调用 ExecuteAsync(CancellationToken) 来运行后台服务。 实现返回一个 Task,其表示后台服务的整个生存期。

在 ExecuteAsync 变为异步(例如通过调用 await)之前,不会启动任何其他服务。 避免在 ExecuteAsync 中执行长时间的阻塞初始化工作。

StopAsync(CancellationToken) 中的主机块等待完成 ExecuteAsync。

调用 IHostedService.StopAsync 时,将触发取消令牌。 当激发取消令牌以便正常关闭服务时,ExecuteAsync 的实现应立即完成。 否则,服务将在关闭超时后不正常关闭。

StartAsync 应仅限于短期任务,因为托管服务是按顺序运行的,在 StartAsync 运行完成之前不会启动其他服务。

长期任务应放置在 ExecuteAsync 中。

IHostedService 和 BackgroundService 区别

抽象级别

  • IHostedService:需要手动实现启动和停止逻辑。
  • BackgroundService:通过提供具有要重写的单个方法的基类来简化实现。

使用案例

  • IHostedService:适用于需要对服务生命周期进行精细控制的更复杂的方案。
  • BackgroundService:非常适合受益于减少样板代码的更简单、长时间运行的任务。

总结

总之.NET 8 中的 IHostedService 和 BackgroundService 提供了强大的工具集,使定时任务、后台处理以及定期维护等功能的实现变得更加直接、高效和灵活。无论是构建复杂的企业级应用还是简单的服务应用,这两个组件都能提供稳定且高效的解决方案。

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

背景

前段时间为公司的产品增加了磁力链、种子下载的能力,测试时发现网上搜到的热门种子,有时好用,有时不好用,不好用主要表现在:没速度、速度慢、速度不稳定、下载一部分后没速度等,严重拖累了测试工作。为此,想到搭建一套内网测试环境,用来保证下载速度的同时,还能自己制作测试种子,方便控制种子的文件数、总大小等,从而进行各种各样的测试。

方案

网上参考了一些案例 (附录 2),发现大部分都是在公网环境搭建的,比如一些 NAS 设备,本身有公网 IP,可以在内网直接请求。然而我仅有的两台设备,都没有公网 IP,想申请公网 IP 也非常难,其中一台 Linux 测试机还要通过跳板机访问:

主要的服务部署在 Linux 机上,这样除了我自己,测试人员也能用;下载侧就放在 Mac 上,或者移动设备,目前有一台 Android 和一台 iPhone;需要注意的是,出于安全管控,Linux 机只有 8000~9000 范围内的端口可以建立侦听,这对后面服务侦听端口的设置有一定影响。

tracker

主要提供种子的声明、查询 peer 的能力。这里需要搭建内网 tracker 的主要原因是:
如果指定公网 tracker,那么它们只能记录公司 gateway 的 IP 作为 peer 地址,通过这个地址是无法找到并连接内网供种机的,即使下载者位于内网

这里选取的是 opentracker,它本身是开源的,除去性能强大外,安装也比较简单,直接源码编译,这里归纳为以下脚本:

#! /bin/sh
if [ ! -f "libowfat.tar.gz" ]; then
    wget --no-check-certificate https://kokodayo.site/usr/uploads/Opentracker/libowfat.tar.gz
fi

if [ ! -d "libowfat" ]; then
    tar xvf libowfat.tar.gz
fi

cd libowfat
# note: add -std=gnu99 for GNUmakefile
# CFLAGS=-std=gnu99 -pipe $(WARN) $(DEFINE) $(OPT_REG)
# CFLAGS_OPT=-std=gnu99 -pipe $(WARN) $(DEFINE) $(OPT_PLUS)
make -j2
cd ..

if [ ! -f "opentracker.tar.gz" ]; then
    wget --no-check-certificate https://kokodayo.site/usr/uploads/Opentracker/opentracker.tar.gz
fi

if [ ! -d "opentracker" ]; then
    tar xvf opentracker.tar.gz
fi

cd opentracker
# note: add -D_GNU_SOURCE for Makefile
# CFLAGS+=-I$(LIBOWFAT_HEADERS) -Wall -pipe -Wextra -D_GNU_SOURCE #-ansi -pedantic
make -j2

if [ ! -f /usr/bin/opentracker ]; then
    sudo cp opentracker /usr/bin/opentracker
fi

cd ..

echo "done!"

在 CentOS 7.9 和 gcc 4.8.5 上编译这两个库,存在两个坑,需要修改配置文件解决。

问题 I:libowfat -std=gnu99

注意是加在 GNUMakefile 而不是 Makefile,后者不生效。没添加之前会报下面的编译错误:

问题 II:opentracker -D_GNU_SOURCE

没添加时会报下面的编译错误:

这两个问题,怀疑是 gcc 版本过低所致。

编译

$ make
cc -c -o opentracker.o -I../libowfat -Wall -pipe -Wextra -D_GNU_SOURCE  -O3 -DWANT_FULLSCRAPE opentracker.c
cc -o opentracker opentracker.o trackerlogic.o scan_urlencoded_query.o ot_mutex.o ot_stats.o ot_vector.o ot_clean.o ot_udp.o ot_iovec.o ot_fullscrape.o ot_accesslist.o ot_http.o ot_livesync.o ot_rijndael.o -L../libowfat -lowfat -pthread -lpthread -lz
strip opentracker
$ ls -lh opentracker
-rwxr-xr-x 1 yunhai01 DOORGOD 88K Oct 25 17:10 opentracker

编译时 opentracker 无脑找 ../libowfat 目录作为输入库,只要保证两个库同目录即可。

脚本最后将生成的 opentracker 复制到
/usr/bin
以便全局生效。

运行

将 opentracker 配置为系统服务,在 CentOS 上就是通过 systemctl,它需要一个配置文件:

[Unit]
Description=opentracker server

[Service]
User=yunhai01
ExecStart=/usr/bin/opentracker -p 8888 -P 8888
Restart=on-abort

[Install]
WantedBy=multi-user.target

将这个文件放置在:
/usr/lib/systemd/system/opentracker.service
,注意命令带的两个参数 -p 和 -P,分别表示侦听的 tcp & udp 端口,需要选取 8000~9000 之间的端口,原因这里不再赘述。

之后可以通过下面的命令启动 opentracker:

$ sudo systemctl enable opentracker.service
Created symlink from /etc/systemd/system/multi-user.target.wants/opentracker.service to /usr/lib/systemd/system/opentracker.service.
$ sudo systemctl start opentracker.service
$ systemctl status opentracker.service
● opentracker.service - opentracker server
   Loaded: loaded (/usr/lib/systemd/system/opentracker.service; disabled; vendor preset: disabled)
   Active: active (running) since Sat 2024-10-12 15:59:31 CST; 1 weeks 6 days ago
 Main PID: 107808 (opentracker)
   CGroup: /system.slice/opentracker.service
           └─107808 /usr/bin/opentracker -p 8888 -P 8888

第一条 enable 命令,设置了开机自启动;第二条 start 命令启动了服务;第三条 status 命令查看命令状态。
举一反三:停止是 stop,开机不启动就是 disable 啦。

除了在 Linux 的命令行查看服务状态,还可以通过 web 查看,地址是:
http://yunhai.bcc-bdbl.baidu.com:8888/stats

其中
yunhai.bcc-bdbl.baidu.com
是 Linux 机域名,8888 是刚才在配置文件中指定的端口。

图中输出显示 tracker 中记录了 9 个种子,14 个 peer,12 个供种 peer。想要查看详细信息,可以加上
?mode=everything
参数。

最后需要注意的是,如果开启了防火墙,需要添加相应的出入端口以保证服务可以被网络上的其它设备访问。

供种侧

主要提供种子制作、上传、做种的能力,这里选取在 Linux 上应用广泛的 Transmission 软件包,它是很多 Linux 发行版的默认 BT 下载工具,对 Linux、Mac、Windows 等 PC 设备支持的比较好。也可以选取其它流行的 BT 工具,这方面没有限制。

安装

使用 Transmission 前需要安装,这里直接通过包管理器安装:

$ sudo yum install transmission transmission-daemon transmission-cli

其中 daemon 是后台服务,用于供种;cli 是命令行工具,用来和 daemon 交互;工具包中的其它工具可以制作、查看种子文件。

运行

daemon 的配置文件位于:
/var/lib/transmission/.config/transmission-daemon/settings.json
,在启动前需要做一些配置。

{
    "alt-speed-down": 50,
    "alt-speed-enabled": false,
    "alt-speed-time-begin": 540,
    "alt-speed-time-day": 127,
    "alt-speed-time-enabled": false,
    "alt-speed-time-end": 1020,
    "alt-speed-up": 50,
    "bind-address-ipv4": "0.0.0.0",
    "bind-address-ipv6": "::",
    "blocklist-enabled": false,
    "blocklist-url": "http://www.example.com/blocklist",
    "cache-size-mb": 4,
    "dht-enabled": false,
    "download-dir": "/var/lib/transmission/Downloads",
    "download-queue-enabled": true,
    "download-queue-size": 5,
    "encryption": 2,
    "idle-seeding-limit": 30,
    "idle-seeding-limit-enabled": false,
    "incomplete-dir": "/var/lib/transmission/Downloads",
    "incomplete-dir-enabled": false,
    "lpd-enabled": false,
    "message-level": 1,
    "peer-congestion-algorithm": "",
    "peer-id-ttl-hours": 6,
    "peer-limit-global": 200,
    "peer-limit-per-torrent": 50,
    "peer-port": 8082,
    "peer-port-random-high": 9000,
    "peer-port-random-low": 8000,
    "peer-port-random-on-start": false,
    "peer-socket-tos": "default",
    "pex-enabled": true,
    "port-forwarding-enabled": true,
    "preallocation": 1,
    "prefetch-enabled": true,
    "queue-stalled-enabled": true,
    "queue-stalled-minutes": 30,
    "ratio-limit": 2,
    "ratio-limit-enabled": false,
    "rename-partial-files": true,
    "rpc-authentication-required": true,
    "rpc-bind-address": "0.0.0.0",
    "rpc-enabled": true,
    "rpc-host-whitelist": "",
    "rpc-host-whitelist-enabled": false,
    "rpc-password": "{5a5bfec5cb304bb9018c9fcf985f87eec2053f00joXosjaH",
    "rpc-port": 8081,
    "rpc-url": "/transmission/",
    "rpc-username": "admin",
    "rpc-whitelist": "127.0.0.1",
    "rpc-whitelist-enabled": false,
    "scrape-paused-torrents-enabled": true,
    "script-torrent-done-enabled": false,
    "script-torrent-done-filename": "",
    "seed-queue-enabled": false,
    "seed-queue-size": 10,
    "speed-limit-down": 100,
    "speed-limit-down-enabled": false,
    "speed-limit-up": 100,
    "speed-limit-up-enabled": false,
    "start-added-torrents": true,
    "trash-original-torrent-files": false,
    "umask": 18,
    "upload-slots-per-torrent": 14,
    "utp-enabled": true
}

主要修改的字段如下:

  • 端口范围
    • rpc-port:通过 rpc 访问的端口,设置为 8081
    • peer-port:告诉 tracker 下载侧连接的端口,设置为 8082
    • peer-port-random-low, peer-port-random-high:随机 peer-port 允许的范围,设置为 8000~9000
  • 用户账户
    • rpc-authentication-required:需要帐密校验,不设置这个直接裸访问会失败
    • rpc-username:用户名,设置为 admin
    • rpc-password:密码,设置为 abc123

密码在服务重启后会加密,所以看到一串奇怪的数字字符组合不要惊讶。

如果 daemon 已在运行,所有变更均会在退出时被服务用当前配置覆盖,所以要确保服务停止后再修改配置文件。

daemon 的启动、状态查询、开机启动和 opentracker 一致,这里不再赘述。

查询

服务启动后可通过 web 查看,地址是:
http://yunhai.bcc-bdbl.baidu.com:8081/transmission/web/index.html

注意 8081 端口就是配置文件中的 rpc-port 字段。

网上有一些美化后的 WebUI (例如 TWC),可以无缝替换官方简陋的界面:

安装也不复杂:

$ wget https://github.com/ronggang/transmission-web-control/raw/master/release/install-tr-control-cn.sh
$ sudo sh install-tr-control-cn.sh
/bin/whoami

	欢迎使用 Transmission Web Control 中文安装脚本。
	官方帮助文档:https://github.com/ronggang/transmission-web-control/wiki
	安装脚本版本:1.2.5

	1. 安装最新的发布版本(release);
	2. 安装指定版本,可用于降级;
	3. 恢复到官方UI;
	4. 重新下载安装脚本(install-tr-control-cn.sh);
	5. 检测 Transmission 是否已启动;
	6. 指定安装目录;
	9. 安装最新代码库中的内容(master);
	===================
	0. 退出安装;

	请输入对应的数字:1

具体可参考附录 4。关于 CUI 命令行的查询方式,在下一节制种中介绍。

制种

基于原文件,transmission-create 可以一键制种:

$ transmission-create -h
Usage: transmission-create [options] <file|directory>

Options:
 -h --help                    Display this help page and exit
 -p --private                 Allow this torrent to only be used with the
                              specified tracker(s)
 -o --outfile   <file>        Save the generated .torrent to this filename
 -s --piecesize <size in KiB> Set how many KiB each piece should be, overriding
                              the preferred default
 -c --comment   <comment>     Add a comment
 -t --tracker   <url>         Add a tracker's announce URL
 -V --version                 Show version number and exit

这里主要使用 -t 参数:

$ ls -lh *.mp4
-rwxr--r-- 1 yunhai01 DOORGOD 1.2G Oct 25 19:59 盲道.mp4
$ transmission-create 盲道.mp4 -t http://yunhai.bcc-bdbl.baidu.com:8888/annouce
Creating torrent "/ext/torrent/movie/盲道.mp4.torrent" ........ done!
$ ls -lh *.torrent
-rw------- 1 yunhai01 DOORGOD 24K Oct 25 20:01 盲道.mp4.torrent

-t 指定 tracker 地址,这里需要输入上面搭建的内网 tracker 地址:
http://yunhai.bcc-bdbl.baidu.com:8888/annouce

否则在加载种子的时候,Transmission 不知道向哪个 tracker 声明 (announce) 种子
,允许指定多个 -t 参来添加多个 tracker 地址;-o 可以指定输出的种子文件名,默认文件命名规则为"输入文件.torrent";-s 可以指定分片 (piece) 大小,如果不指定,transmission-create 将根据文件长度智能设定。

分片是数据校验的基础,针对每个分片,种子文件中都会存储它的 md5 以及其它的一些必要信息,方便下载完成后进行对比。如果分片太小,分片数量就会非常多,导致种子文件膨胀。因此大文件的分片通常会大一些,保证整个种子文件大小控制在几百 KB 的尺寸。

transmission-edit 可以修改种子的 tracker 信息:

$ transmission-edit -h
Usage: transmission-edit [options] torrent-file(s)

Options:
 -h --help                Display this help page and exit
 -a --add     <url>       Add a tracker's announce URL
 -d --delete  <url>       Delete a tracker's announce URL
 -r --replace <old> <new> Search and replace a substring in the announce URLs
 -V --version             Show version number and exit

方便添加遗漏的 tracker 信息,还能删改。

transmission-show 可以查看种子的详情:

$ transmission-show -h
Usage: transmission-show [options] <.torrent file>

Options:
 -h --help     Display this help page and exit
 -m --magnet   Give a magnet link for the specified torrent
 -s --scrape   Ask the torrent's trackers how many peers are in the torrent's
               swarm
 -V --version  Show version number and exit

不带参数时显示种子信息;-m 将种子转换为磁力链接;-s 查询种子所在的 tracker,获取 peer 信息:

$ transmission-show 盲道.mp4.torrent
Name: 盲道.mp4
File: 盲道.mp4.torrent

GENERAL

  Name: 盲道.mp4
  Hash: 7659ead46555d97a98e25a373d8216edc926d78a
  Created by: Transmission/2.94 (d8e60ee44f)
  Created on: Fri Oct 25 20:01:31 2024
  Piece Count: 1200
  Piece Size: 1.00 MiB
  Total Size: 1.26 GB
  Privacy: Public torrent

TRACKERS

  Tier #1
  http://yunhai.bcc-bdbl.baidu.com:8888/annouce

FILES

  盲道.mp4 (1.26 GB)

种子信息分三大块:

  • 通用
    • 文件名
    • 唯一 hash 值
    • 分片长度
    • 分片个数
    • 文件长度
    • 各种时间
  • tracker:列表
  • 文件:列表
$ transmission-show 盲道.mp4.torrent -m
magnet:?xt=urn:btih:7659ead46555d97a98e25a373d8216edc926d78a&dn=%E7%9B%B2%E9%81%93.mp4&tr=http%3A%2F%2Fyunhai.bcc-bdbl.baidu.com%3A8888%2Fannouce
$ transmission-show movie/月光宝盒.mkv.torrent -s
Name: 月光宝盒.mkv
File: movie/月光宝盒.mkv.torrent

http://1337.abcvg.info:80/scrape?info_hash=%dblG%95%3b%3b%dd%fd%bcl%8c%f0%bfrC%bad%1e%d5%19 ... error: unexpected response 502 "Bad Gateway"
http://bvarf.tracker.sh:2086/scrape?info_hash=%dblG%95%3b%3b%dd%fd%bcl%8c%f0%bfrC%bad%1e%d5%19 ... no match
http://ipv6.rer.lol:6969/scrape?info_hash=%dblG%95%3b%3b%dd%fd%bcl%8c%f0%bfrC%bad%1e%d5%19 ... error: Couldn't connect to server
http://jvavav.com:80/scrape?info_hash=%dblG%95%3b%3b%dd%fd%bcl%8c%f0%bfrC%bad%1e%d5%19 ... error: Couldn't resolve host name
http://retracker.x2k.ru:80/scrape?info_hash=%dblG%95%3b%3b%dd%fd%bcl%8c%f0%bfrC%bad%1e%d5%19 ... no match
http://yunhai.bcc-bdbl.baidu.com:8888/scrape?info_hash=%dblG%95%3b%3b%dd%fd%bcl%8c%f0%bfrC%bad%1e%d5%19 ... 1 seeders, 1 leechers

-m 转为磁力链方便在设备间传递;-s 查询 peer 信息,新制作的种子因为还未声明到 tracker,是查不到 peer 信息的,这里使用另外一个添加好的种子做个展示,它有 6 个 tracker,返回的信息中:有的解析不了、有的连不上、有的返回了错误、有的不包含这个种子、还有的正确返回了 peer 和 seeder 的数量。

如果所有原文件都位于一个目录,例如 movie,那么可以使用脚本一键生成所有种子:

for file in $(ls *.mkv); do
	echo "create $file.torrent"
	param="-t http://yunhai.bcc-bdbl.baidu.com:8888/announce"
	transmission-create ${param} $PWD/$file
done

添加

有两种方式可以将种子添加到 Transmission:WebUI 与 CUI 命令行,先说比较简单的 WebUI 方式:

这种方式需要事先将种子传递到启动 WebUI 的设备,并选取种子。注意这里的保存目录,是指启动 Transmission 服务的设备存放源文件的地方,如果设置不对,Transmission 无法进行数据检验,就会认为是一个下载任务而非做种任务。

刚添加的种子会进行检验,这个过程结束后就可以正常做种了。

另外一种方式是通过 transmission-remote,它除了添加种子,还可以列出、查看、删改种子,这个命令功能强大选项多,这里就不一一罗列了,只讲一下日常使用的几个子命令:

$ transmission-remote 8081 -n admin:abc123 -si
VERSION
  Daemon version: 2.94 (d8e60ee44f)
  RPC version: 15
  RPC minimum version: 1

CONFIG
  Configuration directory: /var/lib/transmission/.config/transmission-daemon
  Download directory: /var/lib/transmission/Downloads
  Listenport: 8082
  Portforwarding enabled: Yes
  uTP enabled: Yes
  Distributed hash table enabled: No
  Local peer discovery enabled: No
  Peer exchange allowed: Yes
  Encryption: required
  Maximum memory cache size: 4.00 MiB

LIMITS
  Peer limit: 200
  Default seed ratio limit: Unlimited
  Upload speed limit: Unlimited (Disabled limit: 100 kB/s; Disabled turtle limit: 50 kB/s)
  Download speed limit: Unlimited (Disabled limit: 100 kB/s; Disabled turtle limit: 50 kB/s)

MISC
  Autostart added torrents: No
  Delete automatically added torrents: No

-si 展示 session 信息。

$ transmission-remote 8081 -n admin:abc123 -st

CURRENT SESSION
  Uploaded:   115.6 GB
  Downloaded: None
  Ratio:      Inf
  Duration:   13 days (1139476 seconds)

TOTAL
  Started 1 times
  Uploaded:   115.6 GB
  Downloaded: None
  Ratio:      Inf
  Duration:   13 days (1139476 seconds)

-st 展示 session 状态。

$ transmission-remote -l
ID     Done       Have  ETA           Up    Down  Ratio  Status       Name
   4   100%    6.23 GB  Done         0.0     0.0    1.4  Idle         EP01-06.mkv
   5   100%    7.39 GB  Done         0.0     0.0    1.0  Idle         EP07-13.mkv
   6   100%    7.32 GB  Done         0.0     0.0    1.0  Idle         EP14-20.mkv
Sum:          20.94 GB               0.0     0.0
$ transmission-remote 8081 -n admin:abc123 -l
ID     Done       Have  ETA           Up    Down  Ratio  Status       Name
   7   100%    6.23 GB  Done         0.0     0.0    0.5  Idle         EP01-06.mkv
   8   100%    7.39 GB  Done         0.0     0.0    0.0  Idle         EP07-13.mkv
   9   100%    7.32 GB  Done         0.0     0.0    0.0  Idle         EP14-20.mkv
 163   100%    1.01 GB  Done         0.0     0.0   17.3  Idle         月光宝盒.mkv
 164   100%    1.51 GB  Done         0.0     0.0   10.9  Idle         西游伏魔篇.rmvb
 165   100%    4.61 GB  Done         0.0     0.0    6.0  Idle         大开眼界.mkv
 166   100%    1.65 GB  Done         0.0     0.0    5.5  Idle         逆转时空.mp4
 167   100%   647.2 MB  Done         0.0     0.0    8.7  Idle         大圣娶亲.rmvb
 168   100%    2.94 GB  Done         0.0     0.0    6.0  Idle         龙马精神.mp4
Sum:          33.32 GB               0.0     0.0

-l 列出所有种子。如果不指定账号密码,则只能查看本地添加的种子,通过 WebUI 添加的看不到。其中 ID 字段很重要,后面索引文件时会使用。

$ transmission-remote 8081 -n admin:abc123 -t 168 -i
NAME
  Id: 168
  Name: 龙马精神.mp4
  Hash: b46c1fa9b7e18b2fd91bdb3f6d7130a966865d99
  Magnet: magnet:?xt=urn:btih:b46c1fa9b7e18b2fd91bdb3f6d7130a966865d99&dn=%E9%BE%99%E9%A9%AC%E7%B2%BE%E7%A5%9E.mp4&tr=http%3A%2F%2F1337.abcvg.info%3A80%2Fannounce&tr=http%3A%2F%2Fbvarf.tracker.sh%3A2086%2Fannounce&tr=http%3A%2F%2Fipv6.rer.lol%3A6969%2Fannounce&tr=http%3A%2F%2Fjvavav.com%3A80%2Fannounce&tr=http%3A%2F%2Fretracker.x2k.ru%3A80%2Fannounce&tr=http%3A%2F%2Fyunhai.bcc-bdbl.baidu.com%3A8888%2Fannounce

TRANSFER
  State: Idle
  Location: /ext/torrent/movie
  Percent Done: 100%
  ETA: 0 seconds (0 seconds)
  Download Speed: 0 kB/s
  Upload Speed: 0 kB/s
  Have: 2.94 GB (2.94 GB verified)
  Availability: 100%
  Total size: 2.94 GB (2.94 GB wanted)
  Downloaded: None
  Uploaded: 17.68 GB
  Ratio: Inf
  Corrupt DL: None
  Peers: connected to 1, uploading to 0, downloading from 0

HISTORY
  Date added:       Mon Oct 14 14:35:37 2024
  Date started:     Mon Oct 14 14:36:08 2024
  Latest activity:  Fri Oct 25 11:41:15 2024
  Seeding Time:     11 days (972122 seconds)

ORIGINS
  Date created: Mon Oct 14 14:31:42 2024
  Public torrent: Yes
  Creator: Transmission/2.94 (d8e60ee44f)
  Piece Count: 1404
  Piece Size: 2.00 MiB

LIMITS & BANDWIDTH
  Download Limit: Unlimited
  Upload Limit: Unlimited
  Ratio Limit: Default
  Honors Session Limits: Yes
  Peer limit: 50
  Bandwidth Priority: Normal

-t 指定查看的种子,-i 展示详情,注意下面的命令都要搭配 -t 使用。

$ transmission-remote 8081 -n admin:abc123 -t 168 -if
龙马精神.mp4 (1 files):
  #  Done Priority Get      Size  Name
  0: 100% Normal   Yes   2.94 GB  龙马精神.mp4

-if 展示种子文件列表详情。

$ transmission-remote 8081 -n admin:abc123 -t 168 -it

  Tracker 0: http://1337.abcvg.info:80
  Active in tier 0
  Got a list of 1 peers 4 hours (17784 seconds) ago
  Asking for more peers in 4 hours (17362 seconds)
  Tracker had 0 seeders and 0 leechers 4 hours (14768 seconds) ago
  Asking for peer counts in 6 hours (24449 seconds)

  Tracker 1: http://bvarf.tracker.sh:2086
  Active in tier 1
  Got a list of 1 peers 15 minutes (948 seconds) ago
  Asking for more peers in 1 hour, 44 minutes (6268 seconds)
  Tracker had 1 seeders and 0 leechers 15 minutes (948 seconds) ago
  Asking for peer counts in 14 minutes (859 seconds)

  Tracker 2: http://ipv6.rer.lol:6969
  Active in tier 2
  Got an error "Could not connect to tracker" 13 minutes (780 seconds) ago
  Asking for more peers in 1 hour, 47 minutes (6430 seconds)
  Got a scrape error "Could not connect to tracker" 1 hour, 16 minutes (4619 seconds) ago
  Asking for peer counts in 43 minutes (2599 seconds)

  Tracker 3: http://jvavav.com:80
  Active in tier 3
  Got an error "Could not connect to tracker" 7 minutes (454 seconds) ago
  Asking for more peers in 1 hour, 53 minutes (6789 seconds)
  Got a scrape error "Could not connect to tracker" 1 hour, 18 minutes (4721 seconds) ago
  Asking for peer counts in 41 minutes (2499 seconds)

  Tracker 4: http://retracker.x2k.ru:80
  Active in tier 4
  Got a list of 2 peers 18 minutes (1101 seconds) ago
  Asking for more peers in 21 minutes (1299 seconds)
  Tracker had 1 seeders and 1 leechers 18 minutes (1101 seconds) ago
  Asking for peer counts in 11 minutes (699 seconds)

  Tracker 5: http://yunhai.bcc-bdbl.baidu.com:8888
  Active in tier 5
  Got a list of 2 peers 18 minutes (1114 seconds) ago
  Asking for more peers in 13 minutes (795 seconds)
  Tracker had 1 seeders and 1 leechers 18 minutes (1114 seconds) ago
  Asking for peer counts in 11 minutes (689 seconds)

-it 展示种子 tracker 列表。

$ transmission-remote 8081 -n admin:abc123 -t 168 -ic
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
  11111111 11111111 11111111 11111111 11111111 11111111 11111111 1111

-ic 展示种子分片下载详情,注意这里展示的下载块位图,上传没有位图可显示,因为 peer 可能有多个,且每个可能只申请一部分数据。

$ transmission-remote 8081 -n admin:abc123 -t 168 -ip
Address               Flags         Done  Down    Up      Client

-ip 展示种子 peer 详情。

$ transmission-remote 8081 -n admin:abc123 -t 168 -S
localhost:8081/transmission/rpc/ responded: "success"
$ transmission-remote 8081 -n admin:abc123 -t 168 -l
ID     Done       Have  ETA           Up    Down  Ratio  Status       Name
 168   100%    2.94 GB  Done         0.0     0.0    6.0  Stopped      龙马精神.mp4
Sum:           2.94 GB               0.0     0.0
$ transmission-remote 8081 -n admin:abc123 -t 168 -s
localhost:8081/transmission/rpc/ responded: "success"
$ transmission-remote 8081 -n admin:abc123 -t 168 -l
ID     Done       Have  ETA           Up    Down  Ratio  Status       Name
 168   100%    2.94 GB  Done         0.0     0.0    6.0  Idle         龙马精神.mp4
Sum:           2.94 GB               0.0     0.0

-S 停止种子任务;-s 启动种子任务。以此类推:-a 添加种子,-r 删除种子,下面重点看下如何借助 transmission-reomte 添加本地种子资源:

$ transmission-remote 8081 -n admin:abc123 -a 盲道.mp4.torrent -w /ext/torrent/movie/
localhost:8081/transmission/rpc/ responded: "success"
$ transmission-remote 8081 -n admin:abc123  -l
ID     Done       Have  ETA           Up    Down  Ratio  Status       Name
   7   100%    6.23 GB  Done         0.0     0.0    0.5  Idle         EP01-06.mkv
   8   100%    7.39 GB  Done         0.0     0.0    0.0  Idle         EP07-13.mkv
   9   100%    7.32 GB  Done         0.0     0.0    0.0  Idle         EP14-20.mkv
 163   100%    1.01 GB  Done         0.0     0.0   17.3  Idle         月光宝盒.mkv
 164   100%    1.51 GB  Done         0.0     0.0   10.9  Idle         西游伏魔篇.rmvb
 165   100%    4.61 GB  Done         0.0     0.0    6.0  Idle         大开眼界.mkv
 166   100%    1.65 GB  Done         0.0     0.0    5.5  Idle         逆转时空.mp4
 167   100%   647.2 MB  Done         0.0     0.0    8.7  Idle         大圣娶亲.rmvb
 168   100%    2.94 GB  Done         0.0     0.0    6.0  Idle         龙马精神.mp4
 173    27%   340.8 MB  Unknown      0.0     0.0    0.0  Verifying    盲道.mp4
Sum:          33.66 GB               0.0     0.0

要领就是通过 -w 参数指定原文件所在路径,这与 WebUI 中的"保存目录"如出一辙。下面是 -w 选项的 man 说明:

 -w   --download-dir           <path>      When used in conjunction with --add,
                                           set the new torrent's download
                                           folder. Otherwise, set the default
                                           download folder

单独使用是指定全局的下载目录了。也可以不指定 -w,直接使用全局的默认下载目录,这需要修改上面介绍过的 setting.json 文件,字段名为 donwload-dir。

注意观察上面的输出,添加后也有一个 Verify 状态。

下载侧

环境搭好了,下面在 mac 端测一下吧~

这里选取 Transmission mac 客户端做演示,首先将种子加入到 Transmission 中:

种子中记录的 tracker 信息会让 Transmission 连接内网 tracker 进行查询。

在接下来的界面中选择:

  • 要下载的文件
  • 本地保存路径
  • 任务分组
  • 任务优先级
  • ....

一会儿就能看到有速度了:

刷新 tracker 统计,看到 peer 数会增加 1:

Linux 机的 Transmission 也能观察到上传速度了:

在新版 WebUI 我没找到上传进度,老版通过 peer 信息是可以看到的:

Transmission 下载侧也能看到 peer 信息,也是没有进度:

另外还能展示块的信息:

除了下载完成的块,还能查看已分配正在下载中的:

不得不说很技术范儿~

下载完成后,mac Transmission 也会加入供种行列,查看 tracker 状态:

发现供种数也增 1。

如果不方便传递种子文件,也可以通过磁力链接下载:

磁力链未解析前很多信息 Transmission 拿不到,所以只能选择存储位置:

及任务分组与优先级:

在查询到种子信息之前,任务没有进度显示:

拉到种子文件后,就一样了。

二次开发

其实公司的产品也是基于 Transmission 库做的二次开发,库名为 btsdk。一般而言,含有内网 tracker 的种子文件和磁力链,是可以直接“喂”给 btsdk,然而实际使用中,由于各种技术原因,产品传递给 sdk 的是只包含 hash 值的磁力链,一些额外的信息被“过滤”掉了,以上面的例子来说,完整的磁力链是:

magnet:?xt=urn:btih:7659ead46555d97a98e25a373d8216edc926d78a&dn=%E7%9B%B2%E9%81%93.mp4&tr=http%3A%2F%2Fyunhai.bcc-bdbl.baidu.com%3A8888%2Fannouce

传递给 btsdk 的是:

magnet:?xt=urn:btih:7659ead46555d97a98e25a373d8216edc926d78a

导致 libtransmission 不知道要去内网 tracker 请求种子信息,从而下载超时。

为了解决这个问题,需要在 debug 版本为种子任务加入本地 tracker,下面是摸索过程。

tr_torrentSetTrackerList

这个接口可以在种子任务的类型上直接设置 tracker 列表,简单粗暴但有效,总的流程是:

  • tr_torrentNew
    创建种子任务
  • tr_torrentSetTrackerList
    添加种子内网 tracker
  • 设置超时定时器
  • ....

刚开始用是没问题的,然而隔了一天再试,就全超时了。仔细分析后发现,由于目前启用了创建就启动的选项,导致这里有线程竞争:位于调用者线程的 tr_torrentSetTrackerList,和位于 libtransmission 工作线程中使用 tracker 列表进行请求的地方 (通过 tr_torrentNew 触发) 存在竞争,后者可能先于前者被系统调度,这里插入的 tracker 就不会被用到,进而导致超时。

这个问题也有简单的解决方案,就是统一设置创建种子后不自启动,等到任务准备好后,手动调用 tr_torrentStart 再启动。

不过我没有尝试这种方案,而是寻求在全局记录额外的 tracker 信息,当 libtransmission 启动任务时,会从全局记录的信息中将额外的 tracker 添加到种子任务,从而实现内网 tracker 的访问。

TR_KEY_trackerList

在 key 枚举中搜索,得到了这个标识:

设置一下试试:

tr_variant settings = {};
tr_variantInitDict(&settings, 0);
...
tr_variantDictAddStr(&settings, TR_KEY_trackerList, BT_DEBUG_TRACKER_URLS); 
...
tr_sessionLoadSettings(&settings, param.config_dir, "btsdk");
tr_session* session = tr_sessionInit(param.config_dir, true, &settings);
...
tr_ctor* ctor = tr_ctorNew(session);
int n = tr_sessionLoadTorrents(session, ctor);
tr_ctorFree(ctor);

这段代码是初始化的骨架,主要流程是:

  • tr_variantDictAddXXX 添加设置项
  • tr_sessionLoadSettings 加载设置,上面添加的设置会覆盖本地读取的设置
  • tr_sessionInit 创建会话,一个 transmission 会话可以包含多个种子文件。设置由这里传入
  • tr_ctorNew / tr_ctorFree / tr_sessionLoadTorrents 加载本地种子,用于种子持久化后的恢复

按理说 session 通过 settings 可以拿到 tracker 设置,然而实际跑了一下,新代码没有起作用,内网种子仍然超时。

TR_KEY_default_trackers

继续搜索,发现了这个,感觉要靠谱不少:

替换上面代码中的 key 后,内网种子果然有速度啦,这两个 key 的差别就需要分析源码了,具体的就不在这里展开分析了,毕竟这只是一篇环境搭建的文章,搞的太深奥了会劝退一部分读者,哈哈~

后记

复盘一下整个探索过程,发现最困难的是确认 Transmission 是否声明 (announce) peer 信息到 tracker 这一步。以 opentracker 这简陋的输出,不要说看种子下面有哪些 peer,就连有哪些种子都看不到,打开繁琐输出 (
mode=everything
) 也是一样的抽象:

<?xml version="1.0" encoding="UTF-8"?>
<stats>
  <tracker_id>121091771</tracker_id>
  <version>
$Source$: $Revision$
$Source$: $Revision$
$Source$: $Revision$
$Source$: $Revision$
$Source$: $Revision$
$Source$: $Revision$
$Source$: $Revision$
$Source$: $Revision$
$Source$: $Revision$
$Source$: $Revision$
$Source$: $Revision$
$Source$: $Revision$
$Source$: $Revision$
$Source$: $Revision$
  </version>
  <uptime>1557624</uptime>
  <torrents>
    <count_mutex>10</count_mutex>
    <count_iterator>10</count_iterator>
  </torrents>
  <peers>
    <count>15</count>
  </peers>
  <seeds>
    <count>13</count>
  </seeds>
  <completed>
    <count>105</count>
  </completed>
  <connections>
    <tcp>
      <accept>37716</accept>
      <announce>24538</announce>
      <scrape>12655</scrape>
    </tcp>
    <udp>
      <overall>0</overall>
      <connect>0</connect>
      <announce>0</announce>
      <scrape>0</scrape>
      <missmatch>0</missmatch>
    </udp>
    <livesync>
      <count>0</count>
    </livesync>
  </connections>
  <debug>
    <renew>
      <count interval="00">20</count>
      <count interval="01">475</count>
      <count interval="02">4</count>
      <count interval="03">5</count>
      <count interval="04">8</count>
      <count interval="05">5</count>
      <count interval="06">8</count>
      <count interval="07">5</count>
      <count interval="08">3</count>
      <count interval="09">8</count>
      <count interval="10">5</count>
      <count interval="11">12</count>
      <count interval="12">3</count>
      <count interval="13">12</count>
      <count interval="14">10</count>
      <count interval="15">2</count>
      <count interval="16">6</count>
      <count interval="17">4</count>
      <count interval="18">2</count>
      <count interval="19">3</count>
      <count interval="20">1</count>
      <count interval="21">0</count>
      <count interval="22">0</count>
      <count interval="23">0</count>
      <count interval="24">0</count>
      <count interval="25">0</count>
      <count interval="26">2</count>
      <count interval="27">1957</count>
      <count interval="28">3920</count>
      <count interval="29">3833</count>
      <count interval="30">3799</count>
      <count interval="31">3716</count>
      <count interval="32">3888</count>
      <count interval="33">1875</count>
      <count interval="34">0</count>
      <count interval="35">0</count>
      <count interval="36">0</count>
      <count interval="37">1</count>
      <count interval="38">0</count>
      <count interval="39">0</count>
      <count interval="40">0</count>
      <count interval="41">0</count>
      <count interval="42">0</count>
      <count interval="43">0</count>
      <count interval="44">1</count>
    </renew>
    <http_error>
      <count code="302 Redirect">0</count>
      <count code="400 Parse Error">22</count>
      <count code="400 Invalid Parameter">1</count>
      <count code="400 Invalid Parameter (compact=0)">0</count>
      <count code="400 Not Modest">0</count>
      <count code="402 Payment Required">0</count>
      <count code="403 Access Denied">0</count>
      <count code="404 Not found">152</count>
      <count code="500 Internal Server Error">1</count>
    </http_error>
    <mutex_stall>
      <count>0</count>
    </mutex_stall>
  </debug>
</stats>

带来的问题就是无法区分连不通的原因:是 Transmisstion 没上报?还是上报了地址不行?

方案 I:libtransmission 增加日志输出

早期的一个解决方案,是在 libtransmission 源码中增加了日志,再通过 btsdk 将 libtransmission 的日志打印到文件中查看,效果如下:

2024-11-13 14:36:26 -- tracker knows of 1 seeders and 1 leechers and gave a list of 2 peers.
2024-11-13 14:36:26 -- pex 0: [10.127.82.16]:51413
2024-11-13 14:36:26 -- pex 1: [10.138.62.136]:8082
2024-11-13 14:36:26 -- peer counts: 1 seeders, 1 leechers.
2024-11-13 14:36:26 -- tracker knows of 1 seeders and 1 leechers and gave a list of 2 peers.
2024-11-13 14:36:26 -- pex 0: [10.127.82.16]:51413
2024-11-13 14:36:26 -- pex 1: [10.138.62.136]:8082
2024-11-13 14:36:26 -- peer counts: 1 seeders, 1 leechers.

tracker 返回了两条记录:

  • 51413 端口这条记录就是 sdk 自身
  • 8082 端口就是 Linux 机上的 Transmission 服务,IP 是能对的上的

增加的日志代码如下:

void publishPeersPex(tr_tier* tier, int seeders, int leechers, std::vector<tr_pex> const& pex)
{
    if (tier->tor->torrent_announcer->callback != nullptr)
    {
        auto e = tr_tracker_event{};
        e.type = tr_tracker_event::Type::Peers;
        e.seeders = seeders;
        e.leechers = leechers;
        e.pex = pex;
        tr_logAddDebugTier(
            tier,
            fmt::format(
                "tracker knows of {} seeders and {} leechers and gave a list of {} peers.",
                seeders,
                leechers,
                std::size(pex)));

        for (auto i=0; i<pex.size(); ++i)
        {
            tr_logAddDebugTier(
                    tier,
                    fmt::format(
                        "pex {}: {}",
                        i, 
                        pex[i].display_name())); 
        }


        tier->tor->torrent_announcer->callback(*tier->tor, &e);
    }
}

for 循环中的 tr_logAddDebugTier 就是了。在请求 tracker 响应中会调用这个 publishPeersPex,具体调用链也没深究,主要是根据上面这条日志顺藤摸瓜而来:

tracker knows of 1 seeders and 1 leechers and gave a list of 2 peers.

这种方式需要修改源码、编译,非常不便。

方案 II:模拟请求 tracker

另一种解决方案是直接向 tracker 发送模拟请求,这一点是受 transmission-show -s 命令的启发:

$ transmission-show -s 月光宝盒.mkv.torrent
Name: 月光宝盒.mkv
File: 月光宝盒.mkv.torrent

http://1337.abcvg.info:80/scrape?info_hash=%dblG%95%3b%3b%dd%fd%bcl%8c%f0%bfrC%bad%1e%d5%19 ... 0 seeders, 0 leechers
http://bvarf.tracker.sh:2086/scrape?info_hash=%dblG%95%3b%3b%dd%fd%bcl%8c%f0%bfrC%bad%1e%d5%19 ... no match
http://ipv6.rer.lol:6969/scrape?info_hash=%dblG%95%3b%3b%dd%fd%bcl%8c%f0%bfrC%bad%1e%d5%19 ... error: Couldn't connect to server
http://jvavav.com:80/scrape?info_hash=%dblG%95%3b%3b%dd%fd%bcl%8c%f0%bfrC%bad%1e%d5%19 ... error: Couldn't resolve host name
http://retracker.x2k.ru:80/scrape?info_hash=%dblG%95%3b%3b%dd%fd%bcl%8c%f0%bfrC%bad%1e%d5%19 ... no match
http://yunhai.bcc-bdbl.baidu.com:8888/scrape?info_hash=%dblG%95%3b%3b%dd%fd%bcl%8c%f0%bfrC%bad%1e%d5%19 ... 1 seeders, 0 leechers

opentracker 支持 scrape 查询操作,这个接口返回的信息中包含了 peer 数量与供种数量,有可能会包含 peer 列表。查询了一些公开的资料,它的主要参数有下面几个:

  • info_hash:sha1 效验码,共 20 比特
  • peer_id:BT 客户端的唯一标识,在客户端启动时产生,共 20 比特
  • ip:可选,不提供时服务端会自己找到
  • port:监听端口
  • uploaded/downloaded:上传/下载的字节数
  • left:还需下载的字节数
  • numwant:可选,客户端希望从 Tracker 服务器得到的 peer 数量
  • key:可选,一个扩展的唯一性标识,即使改变了IP地址,也可以使用该字段标识该 BT 客户机
  • compact:压缩标志。如果值为 1 表示接受压缩格式的 peer 列表,即用 6 字节表示一个 peer 地址 (前 4 字节表示 IP 地址,后 2 字节表示端口号);值为 0 表示不接受

下面是一个请求示例:

$ curl "http://yunhai.bcc-bdbl.baidu.com:8888/scape?info_hash=%dblG%95%3b%3b%dd%fd%bcl%8c%f0%bfrC%bad%1e%d5%19&peer_id=-TR2940-yhp3i52s0fyz&port=8088&uploaded=0&downloaded=0&left=1007978447&compact=0&numwant=10&event=started"
d5:filesd20:�lG�;;��l��rC�d�d8:completei1e10:downloadedi16e10:incompletei0eeee

由于返回内容是 bencode 编码的,一些二进制内容在 Console 窗口中显示为乱码。

结果解析放下不表,先看如何自动构造这个请求,后面通过脚本可以对任意一个种子文件发起请求。

请求组装

请求中可选的字段一律不填,剩下的字段都填写默认值:

  • tracker list
    地址列表
  • info_hash SHA1 填写为 torrent 中记录的值,注意是 hex 直接 url encode
  • peer_id 唯一 ID 固定设置一个随机值即可,需要 url encode
  • port 侦听端口随便设置,因为后面不请求数据不会用到,这里为 8088
  • downloaded 下载量设置为 0,表示从头下载
  • uploaded 上传量不设置
  • left 文件大小可设置为 0
  • compact 压缩标志设置为 0,方便后面解析
  • numwant 想要的 peer 数量可设置为 10,这里应该只有一两个 peer

这里必填的信息有 tracker 地址、info_hash 字段,都可以使用 transmission-show 获取到:

$ transmission-show 月光宝盒.mkv.torrent
Name: 月光宝盒.mkv
File: 月光宝盒.mkv.torrent

GENERAL

  Name: 月光宝盒.mkv
  Hash: db6c47953b3bddfdbc6c8cf0bf7243ba641ed519
  Created by: Transmission/2.94 (d8e60ee44f)
  Created on: Mon Oct 14 14:29:13 2024
  Piece Count: 1923
  Piece Size: 512.0 KiB
  Total Size: 1.01 GB
  Privacy: Public torrent

TRACKERS

  Tier #1
  http://1337.abcvg.info:80/announce

  Tier #2
  http://bvarf.tracker.sh:2086/announce

  Tier #3
  http://ipv6.rer.lol:6969/announce

  Tier #4
  http://jvavav.com:80/announce

  Tier #5
  http://retracker.x2k.ru:80/announce

  Tier #6
  http://yunhai.bcc-bdbl.baidu.com:8888/announce

FILES

  月光宝盒.mkv (1.01 GB)

下面在脚本中提取它们:

#! /bin/sh

# @brief: url encode string
# @param: text
# @return: encoded-text
function url_encode()
{
    local input=$1
    local output=""
    # yum install gridsite-clients
    # type "urlencode" > /dev/null 2>&1
    # if [ $? -ne 0 ]; then
        # output=$(echo "${input}" | tr -d '\n' | xxd -plain | tr -d '\n' | sed 's/\(..\)/%\1/g')
        local n=0
        while [ $n -lt ${#input} ];
        do
            case ${input:$n:1} in
                [a-z]|[A-Z]|[0-9]|.|-|_)
                    # regular urlencode only replace aboving characters
                    output="${output}${input:$n:1}"
                    # echo "${input:$n:1}" >> "${BASEDIR}/raw.data"
                    ;;
                *)
                    # for chinese charactor, more than one chars are replaced
                    output="${output}$(echo "${input:$n:1}" | tr -d '\n' | xxd -plain | tr -d '\n' | sed 's/\(..\)/%\1/g')"
                    # echo "${input:$n:1}" >> "${BASEDIR}/raw.data"
                    ;;
            esac
            n=$((n+1))
        done
    # linux urlencode is problemly while handling chinese characters
    # else
    #     output=$(urlencode "${input}")
    # fi

    # echo "${input} after urlencode: ${output}" >> "${BASEDIR}/raw.data"
    echo "${output}"
}

function hexstr2urlenc()
{
    bin=$(echo $1 | xxd -r -p)
    enc=$(url_encode "${bin}")
    #echo "after url encode: ${enc}"
    echo "${enc}"
}

function main()
{
    local file=$1
    infohash=$(transmission-show "${file}"  | grep Hash | awk '{print $2}')
    peerid='2d5452323934302d79687033693532733066797a' # hard coded
    port=8088
    # size=$(stat -c"%s" "${file}")
    size=0
    echo "infohash: ${infohash}, peerid ${peerid}, size ${size}"

    infohash_enc=$(hexstr2urlenc "${infohash}")
    peerid_enc=$(hexstr2urlenc "${peerid}")
    echo "after url encode: ${infohash_enc}, ${peerid_enc}"

    transmission-show "${file}" | sed -n '/TRACKERS/,/FILES/p' | sed '1d;$d;/^$/d;/Tier/d;s/announce/scrape/' > ${file}.tracker
    while read tracker; do
        echo "consult tracker $tracker"
        # echo "${tracker}?info_hash=${infohash_enc}&peer_id=${peerid_enc}&port=$port&uploaded=0&downloaded=0&left=$size&compact=0&numwant=10"
        resp=$(curl -s "${tracker}?info_hash=${infohash_enc}&peer_id=${peerid_enc}&port=$port&uploaded=0&downloaded=0&left=$size&compact=0&numwant=10&event=started")
        if [ -z "${resp}" ]; then
            echo "no data"
            continue
        fi

        echo "${resp}"
        echo
    done < ${file}.tracker
    rm "${file}.tracker"
}

main $@

需要注意的是无论是 info_hash 还是 peer_id,得到的已经是 hex string,需要先将它们转回为十六进制,再进行 url_encode,否则会请求失败。

下面是脚本的运行输出:

$ sh consult_tracker.sh 月光宝盒.mkv.torrent
infohash: db6c47953b3bddfdbc6c8cf0bf7243ba641ed519, peerid 2d5452323934302d79687033693532733066797a, size 0
after url encode: %dblG%95%3b%3b%dd%fd%bcl%8c%f0%bfrC%bad%1e%d5%19, -TR2940-yhp3i52s0fyz
consult tracker http://1337.abcvg.info:80/scrape
no data
consult tracker http://bvarf.tracker.sh:2086/scrape
d5:filesdee

consult tracker http://ipv6.rer.lol:6969/scrape
no data
consult tracker http://jvavav.com:80/scrape
no data
consult tracker http://retracker.x2k.ru:80/scrape
d5:filesd40:db6c47953b3bddfdbc6c8cf0bf7243ba641ed519d8:completei1e10:downloadedi1e10:incompletei0eee5:flagsd20:min_request_intervali300eee

consult tracker http://yunhai.bcc-bdbl.baidu.com:8888/scrape
d5:filesd20:�lG�;;��l��rC�d�d8:completei1e10:downloadedi16e10:incompletei0eeee

能请求通。过程中会生成 tracker.txt,可以看到脚本处理的中间结果:

$ cat 月光宝盒.mkv.torrent.tracker
  http://1337.abcvg.info:80/scrape
  http://bvarf.tracker.sh:2086/scrape
  http://ipv6.rer.lol:6969/scrape
  http://jvavav.com:80/scrape
  http://retracker.x2k.ru:80/scrape
  http://yunhai.bcc-bdbl.baidu.com:8888/scrape

注意已经将 announce 接口替换为了 scrape。

结果解析

opentracker 返回的信息是经过 bencode 的,bencode 是一种 BitTorrent 专用的传输格式,主要上的是减少文本的空间占用,感兴趣的可以查看附录 7。

如果想查看文本形式的内容,还需要转换一下。经过一番搜索,发现有基于 python 的解析器:bencodepy

$ pip3 install bencode.py
Collecting bencode.py
  Downloading https://files.pythonhosted.org/packages/15/9f/eabbc8c8a16db698d9c4bd24953763df2594b054237b89afe1ec56d3965e/bencode.py-4.0.0-py2.py3-none-any.whl
Installing collected packages: bencode.py
Successfully installed bencode.py-4.0.0

bencodepy 是在 Python 环境中调用的,在 shell 中使用还得封装一下:

#! /usr/bin/python3
import bencodepy
import sys

json = bencodepy.decode(sys.argv[1]) 
print ('%s' % json)

文件命名为 bdecode.py 放在脚本同名目录,待解析的内容放在第一个参数,就可以这样调用了:

$ python3 bdecode.py 'd5:filesd40:db6c47953b3bddfdbc6c8cf0bf7243ba641ed519d8:completei2e10:downloadedi2e10:incompletei0eee5:flagsd20:min_request_intervali300eee' 
{b'files': {b'db6c47953b3bddfdbc6c8cf0bf7243ba641ed519': {b'complete': 2, b'downloaded': 2, b'incomplete': 0}}, b'flags': {b'min_request_interval': 300}}

能成功解析。在脚本中增加解析代码
再次运行上面的示例:

$ sh consult_tracker.sh 月光宝盒.mkv.torrent
infohash: db6c47953b3bddfdbc6c8cf0bf7243ba641ed519, peerid 2d5452323934302d79687033693532733066797a, size 0
after url encode: %dblG%95%3b%3b%dd%fd%bcl%8c%f0%bfrC%bad%1e%d5%19, -TR2940-yhp3i52s0fyz
consult tracker http://1337.abcvg.info:80/scrape
d5:filesd20:�lG�;;��l��rC�d�d8:completei1e10:downloadedi1e10:incompletei0eee5:flagsd20:min_request_intervali41354eee

Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/bencodepy/decoder.py", line 84, in decode
    value = to_binary(value)
  File "/usr/local/lib/python3.6/site-packages/bencodepy/compat.py", line 28, in to_binary
    return s.encode('utf-8', 'strict')
UnicodeEncodeError: 'utf-8' codec can't encode character '\udcdb' in position 12: surrogates not allowed

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "./bdecode.py", line 5, in <module>
    json = bencodepy.decode(sys.argv[1])
  File "/usr/local/lib/python3.6/site-packages/bencodepy/__init__.py", line 155, in bdecode
    return DEFAULT.decode(value)
  File "/usr/local/lib/python3.6/site-packages/bencodepy/__init__.py", line 72, in decode
    return self.decoder.decode(value)
  File "/usr/local/lib/python3.6/site-packages/bencodepy/decoder.py", line 87, in decode
    raise BencodeDecodeError("not a valid bencoded string")
bencodepy.exceptions.BencodeDecodeError: not a valid bencoded string

consult tracker http://bvarf.tracker.sh:2086/scrape
no data
consult tracker http://ipv6.rer.lol:6969/scrape
no data
consult tracker http://jvavav.com:80/scrape
no data
consult tracker http://retracker.x2k.ru:80/scrape
d5:filesd40:db6c47953b3bddfdbc6c8cf0bf7243ba641ed519d8:completei1e10:downloadedi1e10:incompletei0eee5:flagsd20:min_request_intervali300eee

{b'files': {b'db6c47953b3bddfdbc6c8cf0bf7243ba641ed519': {b'complete': 1, b'downloaded': 1, b'incomplete': 0}}, b'flags': {b'min_request_interval': 300}}
consult tracker http://yunhai.bcc-bdbl.baidu.com:8888/scrape
d5:filesd20:�lG�;;��l��rC�d�d8:completei1e10:downloadedi16e10:incompletei0eeee

Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/bencodepy/decoder.py", line 84, in decode
    value = to_binary(value)
  File "/usr/local/lib/python3.6/site-packages/bencodepy/compat.py", line 28, in to_binary
    return s.encode('utf-8', 'strict')
UnicodeEncodeError: 'utf-8' codec can't encode character '\udcdb' in position 12: surrogates not allowed

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "./bdecode.py", line 5, in <module>
    json = bencodepy.decode(sys.argv[1])
  File "/usr/local/lib/python3.6/site-packages/bencodepy/__init__.py", line 155, in bdecode
    return DEFAULT.decode(value)
  File "/usr/local/lib/python3.6/site-packages/bencodepy/__init__.py", line 72, in decode
    return self.decoder.decode(value)
  File "/usr/local/lib/python3.6/site-packages/bencodepy/decoder.py", line 87, in decode
    raise BencodeDecodeError("not a valid bencoded string")
bencodepy.exceptions.BencodeDecodeError: not a valid bencoded string

看起来效果一般,可能是 info_hash 等字段解析后是二进制,python 无法直接打印。通过 xxd 转为二进制查看会好一些:

    while read tracker; do
        echo "consult tracker $tracker"
        resp=$(curl -s "${tracker}?info_hash=${infohash_enc}&peer_id=${peerid_enc}&port=$port&uploaded=0&downloaded=0&left=$size&compact=0&numwant=10")
        if [ -z "${resp}" ]; then
            echo "no data"
            continue
        fi

        echo "${resp}"
        echo
        echo "${resp}" | xxd -u -g 1
        #echo $(python3 ./bdecode.py "$resp")
    done < ${file}.tracker

下面是新的输出:

$ sh consult_tracker.sh 月光宝盒.mkv.torrent
infohash: db6c47953b3bddfdbc6c8cf0bf7243ba641ed519, peerid 2d5452323934302d79687033693532733066797a, size 0
after url encode: %dblG%95%3b%3b%dd%fd%bcl%8c%f0%bfrC%bad%1e%d5%19, -TR2940-yhp3i52s0fyz
consult tracker http://1337.abcvg.info:80/scrape
d5:filesd20:�lG�;;��l��rC�d�d8:completei0e10:downloadedi0e10:incompletei0eee5:flagsd20:min_request_intervali34648eee

0000000: 64 35 3A 66 69 6C 65 73 64 32 30 3A DB 6C 47 95  d5:filesd20:.lG.
0000010: 3B 3B DD FD BC 6C 8C F0 BF 72 43 BA 64 1E D5 19  ;;...l...rC.d...
0000020: 64 38 3A 63 6F 6D 70 6C 65 74 65 69 30 65 31 30  d8:completei0e10
0000030: 3A 64 6F 77 6E 6C 6F 61 64 65 64 69 30 65 31 30  :downloadedi0e10
0000040: 3A 69 6E 63 6F 6D 70 6C 65 74 65 69 30 65 65 65  :incompletei0eee
0000050: 35 3A 66 6C 61 67 73 64 32 30 3A 6D 69 6E 5F 72  5:flagsd20:min_r
0000060: 65 71 75 65 73 74 5F 69 6E 74 65 72 76 61 6C 69  equest_intervali
0000070: 33 34 36 34 38 65 65 65 0A                       34648eee.
consult tracker http://bvarf.tracker.sh:2086/scrape
no data
consult tracker http://ipv6.rer.lol:6969/scrape
no data
consult tracker http://jvavav.com:80/scrape
no data
consult tracker http://retracker.x2k.ru:80/scrape
d5:filesd40:db6c47953b3bddfdbc6c8cf0bf7243ba641ed519d8:completei1e10:downloadedi1e10:incompletei0eee5:flagsd20:min_request_intervali300eee

0000000: 64 35 3A 66 69 6C 65 73 64 34 30 3A 64 62 36 63  d5:filesd40:db6c
0000010: 34 37 39 35 33 62 33 62 64 64 66 64 62 63 36 63  47953b3bddfdbc6c
0000020: 38 63 66 30 62 66 37 32 34 33 62 61 36 34 31 65  8cf0bf7243ba641e
0000030: 64 35 31 39 64 38 3A 63 6F 6D 70 6C 65 74 65 69  d519d8:completei
0000040: 31 65 31 30 3A 64 6F 77 6E 6C 6F 61 64 65 64 69  1e10:downloadedi
0000050: 31 65 31 30 3A 69 6E 63 6F 6D 70 6C 65 74 65 69  1e10:incompletei
0000060: 30 65 65 65 35 3A 66 6C 61 67 73 64 32 30 3A 6D  0eee5:flagsd20:m
0000070: 69 6E 5F 72 65 71 75 65 73 74 5F 69 6E 74 65 72  in_request_inter
0000080: 76 61 6C 69 33 30 30 65 65 65 0A                 vali300eee.
consult tracker http://yunhai.bcc-bdbl.baidu.com:8888/scrape
d5:filesd20:�lG�;;��l��rC�d�d8:completei1e10:downloadedi16e10:incompletei0eeee

0000000: 64 35 3A 66 69 6C 65 73 64 32 30 3A DB 6C 47 95  d5:filesd20:.lG.
0000010: 3B 3B DD FD BC 6C 8C F0 BF 72 43 BA 64 1E D5 19  ;;...l...rC.d...
0000020: 64 38 3A 63 6F 6D 70 6C 65 74 65 69 31 65 31 30  d8:completei1e10
0000030: 3A 64 6F 77 6E 6C 6F 61 64 65 64 69 31 36 65 31  :downloadedi16e1
0000040: 30 3A 69 6E 63 6F 6D 70 6C 65 74 65 69 30 65 65  0:incompletei0ee
0000050: 65 65 0A

最后一个请求的输出,可以看到二进制 info_hash 的存在:db6c47953b3bddfdbc6c8cf0bf7243ba641ed519,从 0000000 行后半部分一直延伸到 0000010。

经过一番确认,这里仍没有返回 peer 的 IP & port 信息,想要查询它们,还得请求 announce 接口,它的请求参数与 scrape 基本相同,仅增加:

  • event:可选参数,可能取值为 started、completed、stopped,可以分别在下载开始、下载完成和停止下载时发送,这里设置为 started

因为是可选参数,就没添加,直接将 scrape 接口改为 annouce 进行请求,下面是一个示例:

$ curl http://1337.abcvg.info:80/announce?info_hash=%dblG%95%3b%3b%dd%fd%bcl%8c%f0%bfrC%bad%1e%d5%19&peer_id=-TR2940-yhp3i52s0fyz&port=8088&uploaded=0&downloaded=0&left=0&compact=0&numwant=10
d8:intervali39012e5:peersld2:ip13:10.138.62.1367:peer id25:-TR2940-yhp3i52s0fyz4:porti8088eeee

约摸能看到 ip 字段,值为:10.138.62.136,与 Linux 机地址也能对得上。

不过上面的请求内网自建的 tracker 会返回错误:

<title>Invalid Request</title>

怀疑是字段没设置对,感兴趣的读者可以排查下原因。

总结

本文记录了 BitTorrent 内网测试环境的搭建过程,特别是没有公网设备的场景。

如果已经有公网设备,可以直接使用国内一些活跃的公共 tracker,具体请参考附录 9,
作者会不定时更新。

需要注意的是,这里面一部分是 PT 站点,没有身份是不能上报种子的,PT 是 BT 的深化,即私有种子。用户注册站点时会分配一个 passkey,之后使用这个 key 做种子的上传下载。基于种子身份,站点可以做供种时长的统计,对于只下载不上传的“吸血”用户,可以进行有效治理,以提升社区的健康度。这种站点一般不公开注册,需要邀请码才能进入,但分享的资源也有速度保证。

参考

[1].
CentOS7上OpenTracker的搭建

[2].
opentracker 搭建自己的 BT Tracker 服务器

[3].
搭建自己的 BT Tracker

[4].
Transmission 搭建记录

[5].
BT(带中心Tracker)通信协议的分析

[6].
BitTorrent Tracker 协议详解

[7].
BT 协议规范文档中文版

[8].
bencode.py · PyPI

[9].
XIU2/TrackersListCollection

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

[11].
Linux | 如何挂PT:CentOS 7安装配置美化Transmission及制作种子

[12].
PT站种子制作发布新手全攻略

[13].
制作BT(BitTorrent)种子和磁力链接教程通过BT分享文件

[14].
如何用 Transmission 做种

[15].
PT作弊与反作弊

[16].
实现DHT网络上的种子爬虫

[17].
杂谈网络协议之种子与P2P

[18].
一次对BT种子的追踪小记

前言

本规范的目的是提升代码质量,提升团队协作效率,规范中出现的强制,推荐,参考含义如下:

【强制】:必须严格遵守,如有特殊情况,需架构委员会评审报备。

【推荐】:没特殊情况必须遵守,在开发组长允许下可以不遵守。

【参考】:可以参考,不做严格要求。

后台开发规范

1.1 命名规范

  1. 【强制】驼峰式命名,其他不允许,常量除外。

  2. 【强制】拼音和英文混合不允许。

    正例: alibaba / taobao / youku / hangzhou 等国际通用的名称,可视同英文。
    反例: DaZhePromotion [ 打折 ] / getPingfenByName() [ 评分 ] 
    
  3. 【强制】常量命名大写,单词以下划线隔开,语义尽量表达完整,比如MAX就语义不明确。

     正例: MAX _STOCK _COUNT
     反例: MAX _COUNT
    
  4. 【强制】抽象类使用Abstract开头或者Base结尾,异常类以Exception结尾, 测试类命名以它要测试的类的名称开始,以 Test 结尾。

  5. 【强制】除非业界通用缩写,否则不允许单词缩写。

     反例: AbstractClass “缩写”命名成 AbsClass;condition “缩写”命名成 condi ,此类随意缩写严重降低了代码的可阅读性。 
    
  6. 【推荐】工具类以Utils结尾,帮助类以Helper结尾,帮助类跟工具类的区别在于帮助类是方便业务逻辑使用的,工具类是更通用的。

     正例: 应用工具类包名为 com . yujiahui . common . util 、类名为 MessageUtils( 此规则参考spring 的框架结构 ) 
    
  7. 【推荐】枚举类使用Enum结尾。

  8. 【推荐】如果模块、接口、类、方法使用了设计模式,在命名时体现出具体模式。

     说明:将设计模式体现在名字中,有利于阅读者快速理解架构设计理念。
     正例: public class OrderFactory;
     public class LoginProxy;
     public class ResourceObserver;
    
  9. 【参考】分层命名规范

      1.  DTO命名规范,如果DTO是命令,则Cmd结尾,如果是查询,Query结尾,如果是view object,VO结尾,其他无法归类的DTO结尾。 
      2.  Service层以Service结尾,Dao层以Dao结尾。方法获取单个对象以get开头,获取多个对象以list开头,获取数量已count开头。
          插入以create开头,更新以update开头,删除以delete开头 
      3.  领域层工厂以Factory结尾,领域服务建议以DomainService结尾,实体和值对象不需要后缀,是什么名称就什么名称,比如订单实体就叫Order 
      4.  领域模型层命名尽量与数据表一致,比如表order_detail,命名为OrderDetail,如果表有统一前缀,前缀是否体现到模型对象名上在一个项目内统一。
    
  10. 【推荐】实体里面有些布尔方法如果用is开头容易被框架判断为属性,建议都用iz开头,比如izEasy。

1.2 常量规范

  1. 【强制】魔鬼数字不允许。

  2. 【强制】long 或者 Long 初始赋值时,使用大写的 L ,不能是小写的 l ,小写容易跟数字 1 混淆,造成误解。

    说明: Long a = 2 l; 写的是数字的 21,还是 Long 型的 2?。
    
  3. 【推荐】不要在一个类里面维护所有常量,比如领域模型的常量可以放到领域模型里面,也可以另外建立一个常量类,常量类以Constants结尾

     正例:缓存相关常量放在类 CacheConstants 下 ; 系统配置相关常量放在类 ConfigConstants 下 
    
  4. 【推荐】常量类共享应该按层次放置,层次分为:跨应用共享,应用内共享,模块内共享,类内共享。跨应用共享的常量类放置在一个jar的constant包下,应用内共享的常量类放置下通用模块下的constant包下,模块内共享的常量类放置在本模块的constant包下。

    反例:易懂变量也要统一定义成应用内共享常量,两位攻城师在两个类中分别定义了表示“是”的变量:
     类 A 中: public static final String YES = " yes " ;
     类 B 中: public static final String YES = " y " ;
     A . YES . equals(B . YES) ,预期是 true ,但实际返回为 false ,导致线上问题。
    

1.3 格式规范

  1. 【强制】第一个大括号不换行,单行字符120个,其他采用IDE默认格式。
  2. 【推荐】不同业务逻辑或者不同语义的代码之间需要有空行。
  3. 【强制】IDE设置文件编码为UTF-8。
  4. 【强制】一行不允许定义多个变量。

1.4 Java规范

  1. 【强制】所有覆写的方法都必须加上@Override。

  2. 【推荐】equals方法容易报空指针异常,常量放前面或者使用Objects.equals(jdk7引入)。

     正例:" test " .equals(object);
     反例: object.equals( " test " );
    
  3. 【强制】包装类的相等比较用equals,不能用==。

  4. 【推荐】基本类型和包装类型的使用标准:

      1. pojo类型的属性用包装类型
      2. RPC方法的参数和返回值用包装类型
      3. 局部变量使用基本类型
    
  5. 【强制】领域模型类必须实现toString方法

  6. 【推荐】类内方法定义的顺序是:公有方法》保护方法》私有方法》getter,setter。

  7. 【推荐】类的方法的访问控制从严。类的方法只在内部使用必须是private,只对继承类开放,必须是protected,变量跟方法类似。

  8. 【强制】不要在类里面使用静态变量存储数据,如果需要,使用线程安全的数据结构。

  9. 【强制】不能在foreach循环中删除集合元素,删除元素使用迭代器。

    	// 正面案例
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
                String item = iterator.next();
                if (删除元素的条件) {
                        iterator.remove();
                }
        }
    
    	// 反面案例
         List<String> a = new ArrayList<String>();
            list.add("1");
            list.add("2");
            for (String item : list) {
                    if ("1".equals(item)) {
                            list.remove(item);
                    }
            }
    
  10. 【强制】SimpleDateFormat线程不安全不要定义为static变量。

     	// 正例:注意线程安全,使用 DateUtils 。亦推荐如下处理:
        private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
                @ Override
                protected DateFormat initialValue() {
                        return new SimpleDateFormat("yyyy-MM-dd");
                }
        };
    
  11. 【推荐】高并发时,考虑锁的性能,尽量用无锁数据结构,能锁区块就不要锁整个方法,能锁对象及不要锁整个类。

  12. 【强制】有并发修改同一个对象的场景,需要加锁,并发修改的概率大于20%,使用悲观锁,否则使用乐观锁, 乐观锁根据业务场景考虑重试次数。

  13. 【推荐】有返回值的函数尽量不要修改入参。

  14. 【推荐】尽量少用else,使用卫语句。比如:if(condition) {return obj;} 其他逻辑; 如果实在if-else多,采用状态模式。

        //  正例:超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现,
        public void today() {
            if (izBusy()) {
                System.out.println(“change time.”);
                return;        
            }
            if (izFree()) {
                System.out.println(“go to travel.”);
                return;
            }
            return;
        }
    

1.5 业务代码规范

  1. 【强制】方法的参数不允许超过5个。

  2. 【推荐】参数和返回值不要用Map这种泛化参数。

  3. 【强制】方法的大括号层级不允许超过4层。

  4. 【推荐】一个方法只做一件事情,方法不超过30行。

  5. 【强制】方法不能有副作用,比如查询类方法,不允许改变入参的属性值。

  6. 【推荐】类不能超过500行。

  7. 【推荐】不允许大量重复代码。

  8. 【强制】批量操作必须分组,比如批量插入一千条数据,分为500一组。

 正例:List groups = Lists.partition(list,500);
  1. 【强制】业务查询返回数据过多必须分页,比如不能超过5000条返回数据。

  2. 【推荐】工具类先看项目中是否有提供,不允许随意添加,如果碰到项目中和jar内有同名工具类,优先使用项目中的类,比如StringUtils在多个jar中有,先使用本项目的StringUtils,满足不了要求,再用其他jar的类或者移植方法到本项目的StringUtils。

  3. 【推荐】重要业务流程必须有业务日志,采用BizLog注解,记录新旧值。

  4. 【推荐】Service层提供的都是业务逻辑方法,不要放大量查询方法,有多种查询的业务模型抽取出Query类,比如OrderQueryService,也可以采用CQRS模式,命令和查询分离。

  5. 【强制】业务上存在并发操作的场景,考虑方法的幂等性,或者使用乐观锁。

  6. 【推荐】时刻进行代码重构,避免代码腐化,去掉下次再改的心态(潜台词:永不再改)

  7. 【强制】业务上涉及订单、用户信息、金额等安全敏感数据文件导出并上传阿里云OSS时,必须使用文件服务的
    /api/file/uploadPrivate
    接口上传到阿里云OSS。

  8. 【强制】业务逻辑尽量避免跨数据库事务操作,严禁事务中穿插执行不同数据库的sql语句,必要时要考虑失败场景补偿方案和告警机制。

    反例:
    执行A数据库更新逻辑X
    执行B数据库更新逻辑Y
    执行A数据库更新逻辑X
    执行B数据库更新逻辑Y
    

1.6 异常规范

  1. 【推荐】不允许对大段代码进行try-catch。
  2. 【强制】对于有核心功能的死循环线程必须try-catch整个循环体,防止任何异常导致循环线程退出,如果有性能问题,可以酌情优化
  3. 【推荐】不允许捕获异常后不做任何处理,如果不想处理,抛出去。
  4. 【推荐】方法可能返回null,调用方要做非空判断防止NPE问题。
  5. 【推荐】避免直接抛RuntimeException,使用业务异常,比如BusinessException,ApplicatinException,DomainException。

1.7 日志规范

  1. 【推荐】应用的扩展日志文件命名格式 logName.时间.logType.log。logType:日志类型,比如json表示结构化日志,app表示普通日志,logName:日志名称。
  2. 【推荐】错误日志和业务日志分开。
  3. 【推荐】异常日志必须包含堆栈信息和现场参数。
  4. 【强制】严禁输出大量无效日志,比如在大循环中输出日志。

1.8 注释规范

  1. 【参考】类、类属性、类方法的注释使用 Javadoc 规范,使用/**内容*/格式,避免使用// xxx 方式。

  2. 【推荐】所有的抽象方法 ( 包括接口中的方法 ) 必须要用 Javadoc 注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。

 说明:对子类的实现要求,或者调用注意事项,请一并说明。
  1. 【推荐】所有的枚举类型字段要有注释,说明每个数据项的用途。

  2. 【推荐】代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改。

  3. 【参考】谨慎注释掉代码。在上方详细说明,而不是简单的注释掉。如果无用,则删除。

 说明:代码被注释掉有两种可能性:
  1. 后续会恢复此段代码逻辑。
  2. 永久不用。前者如果没有备注信息,难以知晓注释动机。后者建议直接删掉 ( 代码仓库保存了历史代码 ) 。
  1. 【参考】对于注释的要求:第一、能够准确反应设计思想和代码逻辑 ; 第二、能够描述业务含义,使别的程序员能够迅速了解到代码背后的信息。完全没有注释的大段代码对于阅读者形同天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路 ; 注释也是给继任者看的,使其能够快速接替自己的工作。

  2. 【参考】好的命名、代码结构是自解释的,注释力求精简准确、表达到位。避免出现注释的一个极端:过多过滥的注释,代码的逻辑一旦修改,修改注释是相当大的负担。


// 反例 put elephant into fridge
put(elephant, fridge);
方法名 put ,加上两个有意义的变量名 elephant 和 fridge ,已经说明了这是在干什么,语义清晰的代码不需要额外的注释。
  1. 【推荐】及时清理不再使用的代码段或配置信息。

     说明:对于垃圾代码或过时配置,坚决清理干净,避免程序过度臃肿,代码冗余。
     正例:对于暂时被注释掉,后续可能恢复使用的代码片断,在注释代码上方,统一规定使用三个斜杠(///)来说明注释掉代码的理由。
    

接口规范

  1. 【强制】Rest接口返回值必须是BaseResult对象及其子类对象,封装了错误码,错误描述,isSuccess,data信息。data放业务数据。业务错误码自己定义,推荐优先使用英文,避免使用通用错误码,通用错误码参考ExceptionEnum。

比如{code:”0”,msg:”操作成功”,data:{XX}}。

  1. 【推荐】Rest接口必须标明请求的content_type。比如content_type=applicaton/json

  2. 【强制】接口提供方必须考虑幂等性,防止重复调用导致严重的业务灾难。

  3. 【强制】查询接口如果数据量过多,需要分页返回。

  4. 【推荐】接口必须标明字段的类型,长度,是否必填,文字说明必须准确,反例:person:人员。

  5. 正例:person:人员编码。

  6. 【推荐】Rest接口返回值需要综合考虑实际功能、安全和性能需求,精细化按需返回业务数据。


    1. 比如,移动端接口需要考虑性能问题,要避免返回无效字段;对于中台服务移动端接口返回的多余字段场景,需要业务应用封装处理后再返回给移动端。


      PC端接口返回值要求 移动端接口返回值要求
      中台服务 满足实际功能、安全即可 满足实际功能、安全即可。性能要求需要业务应用封装处理。
      业务应用 满足实际功能、安全即可 除了满足实际功能、安全需求之外,要考虑性能,避免返回无效字段
  7. 【推荐】Feign Api提供方禁止在给下游使用方的jar中引入AutoConfiguration等影响启动的配置类

数据库规范

1. 建表相关规范

  1. 库名、表名、字段名,使用小写和下划线 _ 分割
  2. 库名、表名、字段名,不超过12个字符。默认支持64个字符。
  3. 库名、表名、字段名,见名知意,建议使用名词而不是动词。
  4. 使用 InnoDB 存储引擎。支持;事务、锁、高并发 性能好。
  5. 推荐使用 utf8mb4 可以存emoji
  6. 单表字段数,建议不超过40个

2. 字段相关规范

  1. 整型定义中不显示设置长度,如使用 INT,而不是INT(4)
  2. 存储精度浮点数,使用 DECIMAL 替代 FLOAT、DOUBLE
  3. 所有字段,都要有 Comment 描述
  4. 所有字段应定义为 NOT NULL
  5. 超过2038年,用DATETIME存储
  6. 短数据类型 0~80 选用 TINYINT 存储
  7. UUID 有全局唯一统一字段属性,适合做同步ES使用。
  8. IPV4,用无符号 INT 存储
  9. IPV6,用VARBINARY存储
  10. JSON MySql 8.x 新增特性
  11. update_time 设置 on update 更新属性

3. 索引相关规范

  1. 要求有自增ID作为主键,不要使用随机性较强的 order_id 作为主键,会导致innodb内部page分裂和大量随机I/O,性能下降。

  2. 单表索引建议控制在5个以内,单索引字段数不超过5个。注意:已有idx(a, b)索引,又有idx(a)索引,可以把idx(a)删了,浪费空间,降低更新、写入性能。* 单个索引中,每个索引记录的长度不能超过64KB

  3. 利用覆盖索引来进行查询操作,避免回表。另外建组合索引的时候,区分度最高的在最左边。

  4. select(count(distinct(字段)))/count(id) = 1
    的区分度,更适合建索引。在一些低区分度的字段,例如type、status上建立独立索引几乎没意义,降低更新、写入性能。

  5. 防止因字段不同造成的隐式转换,导致索引失效。

  6. 更新频繁的字段,不要建索引。

4. 使用相关规范

  1. 单表数据量不超过500万行,ibc 文件大小不超过 2G

  2. 水平分表用取模,日志、报表类,可以用日期

  3. 单实例表数目小于 500

  4. alter表之前,先判断表数据量,对于超过100W行记录的表进行alter table,必须在业务低峰期执行。因为alter table会产生表锁,期间阻塞对于该表的所有写入

  5. SELECT语句必须指定具体字段名称,禁止写成
    “*”select *
    会将不需要读的数据也从MySQL里读出来,造成网卡压力,数据表字段一旦更新,但model层没有来得及更新的话,系统会报错

  6. insert语句指定具体字段名称,不要写成 `insert into t1 values(…)``

  7. ``insert into…values(XX),(XX),(XX)..` 这里XX的值不要超过5000个,值过多会引起主从同步延迟变大。

  8. union all

    union
    ,不要超过5个子句,如果没有去重的需求,使用union all性能更好。

  9. in 值列表限制在500以内,例如
    select… where userid in(….500个以内…)
    ,可以减少底层扫描,减轻数据库压力。

  10. 除静态表或小表(100行以内),DML语句必须有where条件,且尽量使用索引查找

  11. 生产环境禁止使用 hint,如 sql_no_cache,force index,ignore key,straight join等。 要相信MySQL优化器。hint是用来强制SQL按照某个执行计划来执行,但随着数据量变化我们无法保证自己当初的预判是正确的。

  12. where条件里,等号左右字段类型必须一致,否则会造成隐式的类型转化,可能导致无法使用索引

  13. 生产数据库中强烈不推荐在大表执行全表扫描,查询数据量不要超过表行数的25%,否则可能导致无法使用索引

  14. where子句中禁止只使用全模糊的LIKE条件进行查找,如like ‘%abc%’,必须有其他等值或范围查询条件,否则可能导致无法使用索引

  15. 索引列不要使用函数或表达式,如
    where length(name)=10

    where user_id+2=1002
    ,否则可能导致无法使用索引

  16. 减少使用or语句 or有可能被 mysq l优化为支持索引,但也要损耗 mysql 的 cpu 性能。可将or语句优化为union,然后在各个where条件上建立索引。如
    where a=1 or b=2
    优化为
    where a=1… union …where b=2, key(a),key(b)
    某些场景下,也可优化为
    in

  17. 分页查询,当limit起点较高时,可先用过滤条件进行过滤。如
    select a,b,c from t1 limit 10000,20
    ; 优化为
    select a,b,c from t1 where id>10000 limit 20
    ;

  18. 同表的字段增删、索引增删等,合并成一条DDL语句执行,提高执行效率,减少与数据库的交互。

  19. replace into

    insert on duplicate key update
    在并发环境下执行都可能产生死锁(后者在5.6版本可能不报错,但数据有可能产生问题),需要catch异常,做事务回滚,具体的锁冲突可以关注
    next key lock

    insert intention lock

  20. TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少,但 TRUNCATE 无事务且不触发 trigger ,有可能造成事故,故不建议在开发代码中使用此语句。说明: TRUNCATE TABLE 在功能上与不带 WHERE 子句的 DELETE 语句相同。

安全规范

接口安全

  1. 【强制】
    涉及大批量敏感数据的接口增加限流和内网访问保护
  2. 【强制】
    涉及小批量敏感数据的接口增加内网访问保护
  3. 【强制】
    所有不宜公开数据都必须添加接口权限

代码安全

  1. 【强制】新加的jar要经过技术中台组安全检测评估才能使用
  2. 【强制】敏感字段禁止明文存储,需要使用统一工具类进行加密处理
  3. 【强制】mybatis中能用#{}时不要用${},#{}能防止SQL注入。禁止SQL拼接不安全参数。

密码安全

  1. 【强制】所有使用中的业务系统(包括测试环境)密码必须由大写字母+小写字母+数字+特殊字符四种组合,字符数不少于8位。

技术文档的写作规范

  1. 设计文档需要的核心要素有业务架构 ,应用架构,领域模型(数据架构),技术架构,。
  2. 业务架构描绘系统的业务流程和功能
  3. 应用架构描绘系统之间的关系
  4. 数据架构是指领域模型的关系
  5. 技术架构描绘系统实现,我们都是比较统一的技术架构,可以省略

Java开发编程军规

一、禁止循环中查询数据库,尽量在循环外一次查询

  • 说明

系统性能瓶颈很大一部分都是指向了数据库,而循环中查询数据库非常耗资源。

  • 案例

展示少量树结构数据时,循环内查询数据后进行数据组装。导致服务器在测试环境就频繁宕机。

使用java.util.Comparator#compare方法调用数据库查询接口,导致线上性能极低。

二、禁止把redis这种缓存当数据库用

  • 说明

缓存无法完全符合事务特性ACID原则,数据存在不可使用的风险比较大。

  • 案例

会员数据直接存储到redis缓存中,数据量也比较大,经常会丢数据。

三、禁止循环中创建新线程,尽量使用线程池

四、死循环必须有退出机制

  • 说明

死循环中最好有休眠语句存在,另外还要退出机制。

  • 案例

订单同步应用请求第三方平台数据时,平台方没有翻页的结束标志,同时代码中没有退出机制直接导致该平台订单同步异常。

五、共享变量必须考虑线程安全

  • 说明

尽量避免使用共享变量,无法避免时必须考虑线程安全。

  • 案例

微信抽奖功能中,每次中奖都是同一个,原因是对共享变量进行了修改操作,后面的逻辑获取的是脏数据。

六、浮点计算必须使用BigDecimal

  • 说明

如果需要精确计算,非要用String来够造BigDecimal不可。

七、批量操作必须考虑合理分组

  • 说明

数据量大时须批量操作,而批量操作必须分组,避免一次操作耗时过久导致连锁反应。

  • 案例

订单历史迁移数据时,分组为5000,导致数据库删除操作没有走索引。建议分组数量在100~500之间。

八、禁止单点部署

  • 说明

增加一台服务器部署可以降低50%的服务不可用风险。

九、禁止大表的全表扫描不加限流

  • 说明

全表扫描已经很耗数据库资源了,频繁处理请求不加限流就更雪上加霜。

  • 案例

售后问题跟踪单的导出,时间索引没有控制范围,导致全表扫描。导出数据接口没有加限流加剧服务资源消耗。

十、读写分离架构,必须考虑读到过期数据

  • 说明

读写分离在业务数据更新写入后再重现读取时会存在延迟问题,导致读到脏数据。

  • 案例

A. 双十一开启读写分离,主从同步有延迟,导致业务事件重复发送,原因是读取到历史 脏数据。

B. 会员积分服务创建数据后其他服务应用马上查询,结果是查询到空数据。

十一、事务内有外部调用,必须考虑外部不稳定和性能问题

  • 说明

事务本身是很耗资源,极易产生超时的问题,要避免再引入外部不稳定因素。

  • 案例

个人中心服务,事务内远程查询美丽分享官的积分,导致性能极低。外部接口调用需要设置超时和最长等待时间。

十二、接口提供方和Xxljob定时任务必须考虑幂等性,防止重复调用导致严重的业务灾难

  • 说明

根据墨菲定律,接口重复调用是会必现的线上问题。

  • 案例

订单付款接口,幂等逻辑不严谨导致重复付款问题。apollo报表中心由于xxljob一秒内重复调度任务,导致统计数据重复,严重影响管理层的决策判断。

十三、禁止资源操作(IO等)后未释放

十四、嵌套事务的默认传播属性是Propagation.REQUIRED,如果需要开启新事务,必须手动设置事务传播属性为Propagation.REQUIRES_NEW。尽量不要使用嵌套事务。

  • 说明

使用事务注解或者编程式事务时,需要考虑默认的事务传播属性,根据需要决定是并入同一个事务还是开启新事务。

  • 案例

OMS异步操作任务,调用第三方接口时,修改状态为确认中状态,需要先提交事务更新,后面的逻辑操作成功则需要修改为已确认,失败则修改为待审核。当第三方接口没有返回明确的成功或失败时,状态应该保持确认中不变。如果调用接口前不开启新事务,会导致后面回滚的数据有误。

十五、覆写对象的equals()方法时必须同时覆写hashCode()方法

  • 说明

equals和hashCode方法是对象在hash容器内高效工作的基础,正确的覆写这两个方法才能保证在hash容器内查找对象的正确性,同时一个好的hashCode方法能大幅提升hash容器效率。

十六、禁止含事务的循环内加线程同步锁

  • 说明

循环上层包含事务,使用synchronized锁,会导致MySQL事务锁和JVM同步锁互相等待死锁问题。

  • 案例
@Transactional(rollbackFor = Exception.class) 
public void storeData(List<Order> orderList) { 
    /**
    order_id = {1,2,3}
    线程A更新order_id为1后进入下一轮循环,事务锁还未释放,同步锁需要重新获取。
    同时线程B已获取同步锁,需要更新order_id=1的事务操作。结果就是线程A等待线程B持有的
    JVM同步锁,线程B等待线程A持有的事务锁。
    */
    for(Order order : orderList) {
        synchronized (LOCK) {
            updateOrderId(order);
        }
    }
}

十七、使用线程池时,必须设置合理的大小,禁止不加限制动态批量创建线程

  • 说明

不加限制的批量创建线程会抢占大量系统的资源,引发OOM等连锁异常,最终导致宕机

  • 案例

将new MapReduce<>(xxx)创建线程池的代码放在API接口实现方法中没有加其他限制,导致引发OOM宕机,中间触发了Redis连接超时、Kafka重复消费等异常

十八、所有公共代码(比如api包、通用工具类)或公共服务(中台服务、业务应用自身服务)的改动,必须考虑向下兼容

  • 说明·

对多个系统项目都有依赖的公共代码进行修改时,需要考虑兼容历史逻辑,除非确认所有使用方都能够接受功能修改产生的影响

  • 案例

项目A的开发人员将公共jar包中逻辑进行了修改(该修改需要开启一个新配置才和原逻辑一致),同时deploy jar包进行测试,新配置只在测试环境进行了操作。依赖了公共jar包的项目B这时进行线上发版,但是没有进行配置(而且也不知道有这个配置),导致线上事故。

十九、禁止在新建表或者增加修改表字段时设置字符集和排序规则

  • 说明·

字符集和排序规则后期修改需要耗费巨大的资源,影响业务稳定性,为了保持schema-表-表字段三者的字符集和排序规则一致,禁止在新建表或者增加修改表字段时设置字符集和排序规则。

  • 案例

A项目的某一个表的字段设置了字符集和排序规则,导致与表-schema的排序规则不一致,在联合查询时这个表作为关联字段,报关联字段排序规则不一致的错误,无法进行关联查询,只能修改,如果是一张大表修改会非常耗时,占用大量的io会影响业务。

二十、新建表或者增加修改表字段时使用TEXT/BLOD字段需要评估必要性

  • 说明·

TEXT/BLOD的大字段会产生磁盘临时表,而且不能使用全文索引,各种操作的代价都非常高昂,在业务中最好不要使用,如果实在是要使用,也要独立出一张表专门用于存储,不得跟业务表中使用。

  • 案例

A项目前期的一张表中使用了一个TEXT存储json大字段,导致这张表占用了600多G的空间,其中那一个大字段就占用90%的表空间,后续的查询,迁移,碎片整理都非常的耗资源。