eboer's Boat

Neboer's Blog isn't Only About Technique
dummy

用图床做网盘的那点事……
隐写术 622

如题,和大家分享一点点奇怪的知识(奇怪的知识增加了.jpg),备注:封面的大图是路过图床的背景,右侧看起来随机的色块其实很有趣!

图床,为了保证访问速度,通常的特点都是高度的可靠和很快的存取。国内的图床有很多家,这里不一一列举了,介绍图床也不是这篇文章的重点。这篇文章主要讲一讲一个很有趣的想法,用图床来做网盘。

这个想法其实也并不新鲜,道理也非常浅显易懂,大概就是把文件拆成小块,然后把每个小块包装成一个某种格式的图片,这个过程可以通过把小块数据按照二进制像素格式(RGB/RGBA等)解析的方法实现。再把所有的图片依次上传到图床,然后按照顺序保存取回的url,最后把这个url列表分享出来,这个列表就成了快速取回文件的“钥匙”。

我一开始听说bilibili drive这个项目的时候,第一反应就是“哦,有人做出来了啊,我看看”,然后第二反应是“哦,python写的啊,海星。”。实际用起来还是很香的,本地上传和下载几乎都能达到极高的速度,毕竟国内这些大企业(阿里云,哔哩哔哩,新浪微博等)的图床和用户浏览体验息息相关,肯定不会慢。后来这个项目被封锁之后,它升级到了bdrive-ex,再到现在的cdndrive,这个作者一直和cdn图床搏斗,我甚至觉得有点小帅(。

这个图床网盘非常强大,甚至在服务器上(日本东京都的某vultr机房)都可以达到4~6M/s的上传速度(国外下载没有测试,实测线程越多跑得越快,使用阿里云的图床)。本地下载更是可以飙到10M/s,虽然图床那边不一定能把图片保存多久,但是用来快速在本地和服务器高速传输文件已经足够了。下载文件的优秀操作应该是 在国外或暗网站点上找种子->在VPS上下载好(推荐用transmission或者aria2)->cdndrive发往本地->本地用cdndrive下载。

这个流程简直是完美的,用这种方法下载电影的资源之类的非常舒服,速度极快,同时假如服务器空间足够,可以一直保种直到世界末日,但是这也有一些缺点,其中有一个非常让人绝望:cdndrive的python代码有毒。

我第一次用cdndrive在服务器上传输一个很大的文件的时候,它把我的内存全部占满了。这是因为__main__.py里面有一个很浪费的循环逻辑 ,这里一定要狠狠吐槽一下:(在同文件下面还有一处)

blocks = read_in_chunk(file_name, size=args.block_size * 1024 * 1024)
for i, block in enumerate(blocks):
    hdl = trpool.submit(tr_upload, i, block, block_dicts[i])
    hdls.append(hdl)
for h in hdls: h.result()
if not succ: return

这里的block是一个generator,分かった。但是能不能解释一下为什么要把所有的任务都疯狂推到trpool里……要知道concurrent.futures这个库几乎可以做到python里真正的“多线程”(可以释放GIL锁),因此当blocks生成个不停的时候,其中所包含的文件数据块就被源源不断地拷贝到不同的进程中,同时存入内存中的还有创建一个新进程的所有开销。虽然进程执行完毕之后,操作系统会自动回收它占用的内存空间,但是对于一个缓慢的上传任务来讲,可能还没等到第一个任务结束,就已经有无数个任务产生了,小内存的服务器肯定是受不了的。这和你把文件整个读到内存里没有两样(真的没有两样,相比之下多线程的操作还大量消耗了os的线程资源)。这个问题也好修,加一个计数器就可以了,并不会影响速度,建议服务器用户可以暂时采用我的fork(好久不更新了),下面是一个修改后的例子。

nblocks = math.ceil(path.getsize(file_name) / (args.block_size * 1024 * 1024))
block_dicts = [{} for _ in range(nblocks)]
trpool = ThreadPoolExecutor(args.thread)
workers_pool = []

blocks = read_in_chunk(file_name, size=args.block_size * 1024 * 1024)
for i, block in enumerate(blocks):
    hdl = trpool.submit(tr_upload, i, block, block_dicts[i])
    workers_pool.append(hdl)
    if len(workers_pool) == args.thread:
        workers_pool[0].result()
        del workers_pool[0]
for h in workers_pool: h.result()
if not succ: return

我并没有报bug、发pr或者发布我的版本,因为我虽然对cdndrive这个想法很感兴趣,但是很不喜欢这个项目,这个任务应该让更快速轻量的语言比如C++来完成,python确实比较臃肿了。编码的过程中涉及到大量的内存拷贝操作,中间产生的垃圾变量就被白白浪费了,明明有更好的解决方法的(就地转换并不难实现)……这正好也说明了经过精心设计的程序可以极大的提升上传的速度和传输文件的体验(比如一边上传一边下载,刻意强化cdndrive“快速传输”一面)。

因此我找了一段时间,决定开发我自己的程序。这个程序将会基于一个库,围绕这个核心库可以做GUI或tui/cli程序。这个核心库我打算叫做libnbtp,大概就是neboer's toPNG的意思……当然它不仅仅可以转换到png,它最重要的功能应该是上传队列。有一些converter线程可以用来把文件切片然后不断转换成图片(这里甚至可以使用mmap来在os层面减少不必要的内存复制),然后推到队列中,剩下的uploaders进程则不断的把图片发送到网络上,下载也是一个道理,和多线程爬虫的实现一样,也是利用队列来安排一切。

今天简单的试了一下,用C++做了一个小程序,可以把一个二进制文件转换成图片,也可以反过来把图片转换成二进制文件。整个过程并不复杂,除了编码几乎全都可以就地转换,开销极小,速度极快,这个demo可以在我的gist上面找到。程序基于lodepng,编译也非常方便,并且从设计上兼容cdndrive生成的图片格式,理论上可以生成cdndrive那样的链接,但是我更倾向于生成所有链接的gzip压缩文件作为key,这样可以增加传输的难度(一定程度上抵御滥用)并且节省了一次传输的开销。

最后跟大家玩个可能有点意思的游戏(低 水 平 c t f):这个博客在列表中的右侧小图就是用nbtp编码得到的一张png图片,请你把它还原吧!如果方便的话可以邮件和我说一下你得到了什么(邮件即可,或者你可以选择你觉得合适的任意方法),谢谢各位捧场!

2020/9/26 更新:

用这篇文章做了一道题,大家很多人浏览,真的让我受宠若惊。我仔细验证了一下这些程序,发现C++版本的nbtp在Windows上编译运行会出现很大问题,运行的结果存在错误。我对给大家带来的困惑表示非常抱歉!发现问题之后,我迅速用C语言重写了这个程序,这次程序运行的结果让人满意,终于没有问题了。大家在看我的gist的时候,请选择我的nbtp.c那个程序进行编译操作,C++那个就不用管了吧,再次和大家道歉,希望大家玩的开心!