最近有点忙,实在抽不出时间更新,而且可以预见的短期内也没有充足的时间去找素材,写文章。如果有朋友在等更新,还请见谅。

什么是aria2

aria2是一个轻量级的,可以支持多种协议(不支持ed2k)的下载软件,支持多种操作系统,同时也提供了RPC调用的能力。

部署方法很简单:

1. 下载,解压,下载地址: https://github.com/aria2/aria2/。

2. 网上抄一份配置文件再根据自己情况稍微改一改,带上指定配置文件路径的参数–conf-path=/smbdata/soft/aria2/aria2.conf运行aria2c程序;如果想无人值守运行,也可以用supervisor来跑这个任务;官方参考手册地址:https://aria2.github.io/manual/en/html/aria2c.html。

3. 下载一个前端管理项目进行任务管理和配置管理,比如AriaNG,https://github.com/mayswind/AriaNg(可以放到本地,直接打开网页文件,也可以放到服务器上发布为一个web站点),这一步在日常使用场景里是必需的,但是在本文所述场景里用不到,因为我们要做的是和这个前端管理项目一样的功能。

关于第3步,一般这种项目都是靠rpc访问服务器(运行aria2程序的主机),第一次访问前端项目的时候需要指定服务器地址,端口,rpc路径,token(与配置文件里的rpc-secret参数一致)等关键信息,填写完毕,刷新页面即可生效。

下图的配置将http://192.168.3.4:6800/jsonrpc设置为rpc服务地址。


dir=/smbdata/download
log=/smbdata/soft/aria2/aria2.log
input-file=/smbdata/soft/aria2/aria2.session
save-session=/smbdata/soft/aria2/aria2.session
save-session-interval=60
force-save=true
log-level=error

max-concurrent-downloads=5
continue=true
max-overall-download-limit=0
max-overall-upload-limit=50K
max-upload-limit=20

connect-timeout=120
lowest-speed-limit=10K
max-connection-per-server=10
max-file-not-found=2
min-split-size=1M
split=5
check-certificate=false
http-no-cache=true

bt-enable-lpd=true
#bt-max-peers=55
follow-torrent=true
enable-dht6=false
bt-seed-unverified
rpc-save-upload-metadata=true
bt-hash-check-seed
bt-remove-unselected-file
bt-request-peer-speed-limit=100K
seed-ratio=0.0

enable-rpc=true
pause=false
rpc-allow-origin-all=true
rpc-listen-all=true
rpc-save-upload-metadata=true
rpc-secure=false
rpc-secret=*******#这儿自己随便设置一个token

daemon=true
disable-ipv6=true
enable-mmap=true
file-allocation=falloc
max-download-result=120
#no-file-allocation-limit=32M
force-sequential=true
parameterized-uri=true

[program:aria2]
command=/smbdata/soft/aria2/aria2c --conf-path=/smbdata/soft/aria2/aria2.conf
directory=/smbdata/soft/aria2
autostart=true
autorestart=true
stderr_logfile=/smbdata/soft/aria2/daemon.err
stdout_logfile=/smbdata/soft/aria2/daemon.log
log_stderr=true
log_stdout=true
user=root

如何远程调用

配置文件中有一个参数 enable-rpc=true 用于打开rpc功能,rpc-secret参数用于设置鉴权所用的token。其它几个相关参数见下图:

官方手册对rpc请求有详细的介绍:https://aria2.github.io/manual/en/html/aria2c.html#json-rpc-using-http-get。

当然也可以直接用万能的F12方法分析一下几个场景的网络请求。

1. 修改参数Max Concurrent Downloads:

2. 获取服务器状态getGlobalStat

3. 获取当前正在下载的任务tellActive

从上图可以看出,所有请求都需要被构造为一个请求对象,并将这个对象以json形式向 http://192.168.3.4:6800/jsonrpc 发送。基本结构有

  • jsonrpc: “2.0”, #固定的版本号
  • method: “” #所请求的方法
  • params: [] #描述详细请求内容的数组,比如:
    • 使用getGlobalStat方法,获取服务器详情的时候,这个数组只包含一个”token”:”xxxx”参数;
    • 使用changeGlobalOption方法,修改参数Max Concurrent Downloads,需要用到token参数,以及一个键值对{“max-concurrent-downloads”:”6″}

另外,jsonrpc既可以用post请求也可以用get请求。如果使用get请求,method和id可以直接传UTF8明文,params需要用base64进行编码。

如何集成到公众号

公众号打开开发模式后,所有信息(用户消息、系统消息)都将由微信服务器发到开发者指定的监听服务地址,由用户自行处理。要和公众号集成,无非就是针对具体的某一种特征的用户消息类型进行特殊处理。用户在使用的时候将种子文件或者下载链接发到公众号,再由公众号服务器调用aria2的jsonrpc添加相应的任务。

但是这样部署会存在一定的安全隐患,aria2的jsonrpc服务直接暴露在了公网,比较安全的做法是写一个前置代理对请求进行鉴权和过滤,再将合法的请求转给aria2的rpc服务,或者使用白名单模式对jsonrpc服务的请求进行管控(因为公众号服务器的地址是固定的,所以这个场景对白名单方案特别友善)。

需要实现哪些功能

首先,需要实现查看当前服务器包括正在下载、等待下载、下载中断/停止在内的所有任务。
其次,需要可以添加常见下载类型的任务,包括http, ftp, magnet。

由于公众号不支持file类型的文件,任何非媒体文件(包括torrent)上传以后,服务器无法获取到文件内容,因此torrent类型的下载功能暂时无法直接在公众号对话窗口实现,但是在小程序或者H5应用里不会有这个问题。

aria2不支持ed2k。

aria2不支持ed2k。

aria2不支持ed2k。


    public static class DownloadHelper
    {
        const string SERVER = "http://……:6800/jsonrpc";
        /// <summary>
        /// URL类型的链接,包括http, https, ftp, 有/无前缀的magnet(32位和40位)
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        public static string DownloadLink(string url)
        {
            //去掉magnet文件名参数,有些特殊字符会造成下载失败
            url = Regex.Replace(url, "&dn=[^&]+(?=(&|$))", "");
            using WebClient client = new WebClient();
            client.Headers["Content-Type"] = "application/json;charset=UTF-8";
            var res = client.UploadData(SERVER,
                Encoding.UTF8.GetBytes(
                    JsonConvert.SerializeObject(new RPCReq("addUri", new List<object> { url }))
                    ));
            string strRes = Encoding.UTF8.GetString(res);
            return JsonConvert.DeserializeObject<DownRes>(strRes).result;
        }
        /// <summary>
        /// torrent类型的资源,将torrent文件base64编码后作为参数的一部分上传
        /// </summary>
        /// <param name="file"></param>
        /// <returns></returns>
        public static string DownloadTorrent(string file)
        {
            using WebClient client = new WebClient();
            client.Headers["Content-Type"] = "application/json;charset=UTF-8";
            var res = client.UploadData(SERVER,
                Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(
                    new RPCReq("addTorrent", new List<object> {
                        Convert.ToBase64String(Encoding.UTF8.GetBytes(file)),
                        new List<string>(),
                        new object() }))));
            string strRes = Encoding.UTF8.GetString(res);
            return JsonConvert.DeserializeObject<DownRes>(strRes).result;
        }

        /// <summary>
        /// 接受本地文件torrent路径,
        /// 带协议前缀的http, https, ftp, magnet(32位和40位)链接,
        /// 不带协议前缀的32位和40位的磁力链接,并根据类型调用下载接口
        /// </summary>
        /// <param name="input"></param>
        public static string Download(string input)
        {   //32位base32格式的磁力,或者40位Hex格式的磁力,不带协议前缀
            //带协议前缀的http,https,ftp,magnet
            if (Regex.IsMatch(input, "([a-fA-F0-9]{40}&?|^[a-zA-Z2-7]{32}&?)") ||
                Regex.IsMatch(input, "(http(s?)|ftp|magnet):"))
            {
                DownloadLink(input);
            }
            else if (Regex.IsMatch(input, @"(^/|^[C-Zc-z]:\\)"))
            {//本地文件路径
                DownloadTorrent(input);
            }
            Thread.Sleep(2000);
            return GetFullStatus();
        }
        /// <summary>
        /// 获取服务器状态,以及每个任务的状态和进度
        /// </summary>
        /// <returns></returns>
        public static string GetFullStatus()
        {
            string result = "";
            List<object> columns =
                new List<object> { "gid", "files", "totalLength", "bittorrent", "completedLength" };
            using WebClient client = new WebClient();
            foreach (var method in new List<string> { "Active", "Waiting", "Stopped" })
            {
                client.Headers["Content-Type"] = "application/json;charset=UTF-8";
                var res = client.UploadData(SERVER,
                    Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(
                        new RPCReq(
                            $"tell{method}", columns)
                        )
                    ));
                string strRes = Encoding.UTF8.GetString(res);
                result += $"【{method}】\r\n" + string.Join("\r\n",
                    JsonConvert.DeserializeObject<Tasks>(strRes).result.Select(t =>
                    $"[{t.Name}]\r\n   {t.totalLength >> 20} MB, " +
                    $"{t.completedLength * 1.0 / t.totalLength:P2}")) + "\r\n\r\n";
            }
            client.Headers["Content-Type"] = "application/json;charset=UTF-8";
            var stat = Encoding.UTF8.GetString(client.UploadData(SERVER,
                Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(
                    new RPCReq($"getGlobalStat", new List<object>())))));
            var statobj = JsonConvert.DeserializeObject<ServerStat>(stat).result;
            return $"下载:{statobj.numActive}, 等待:{statobj.numWaiting},停止:{statobj.numStopped}\r\n{result}";
        }
    }
    public class RPCReq
    {
        public string id { get; set; }
        public const string jsonrpc = "2.0";
        public string method { get; set; }
        public List<object> @params =
            new List<object> { "token:c4c……9b" };
        /// <summary>
        /// 
        /// </summary>
        /// <param name="_method"></param>
        /// <param name="_params"></param>
        /// <param name="append"></param>
        public RPCReq(string _method, List<object> _params)
        {
            id = Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()));
            method = $"aria2.{_method}";
            if (_method.ToUpper() == "ADDURI")
            {
                @params.Add(_params);
                @params.Add(new object());
            }
            else
            {
                switch (_method.ToUpper())
                {
                    case "TELLSTOPPED":
                        @params.Add(0);
                        @params.Add(1000);
                        break;
                    case "TELLWAITING":
                        @params.Add(-1);
                        @params.Add(1000);
                        break;
                    default:
                        break;
                }
                @params.Add(_params);
            }
        }
    }
    public class DownRes
    {
        /// <summary>
        /// 返回任务ID,其它不重要的字段,比如id, jsonrpc就不处理了。
        /// </summary>
        public string result { get; set; }
    }
    public class Tasks { public List<DownTask> result { get; set; } }
    public class ServerStat
    {
        public ServerStatResult result { get; set; }
    }
    public class ServerStatResult
    {
        public int numActive { get; set; }
        public int numStopped { get; set; }
        public int numWaiting { get; set; }
    }
    public class DownTask
    {
        public TorrentObj bittorrent { get; set; }
        public long completedLength { get; set; }
        public long totalLength { get; set; }
        public List<TaskFileInfo> files { get; set; }
        public string Name => bittorrent?.info is null ?
            new FileInfo(files[0].path).Name :
            new FileInfo(bittorrent.info.name).Name;
    }
    public class TorrentObj { public TorrentObjInfo info { get; set; } }
    public class TorrentObjInfo { public string name { get; set; } }
    public class TaskFileInfo { public string path { get; set; } }