前几天,在 使用docker-compose搭建nextcloud+Nginx+MySQL+Redis 这篇文章中的 这条评论 引起了我的好奇心。这位兄弟的评论内容大致是想要将 Nextcloud 与 Aria2 联动实现网盘上的离线下载功能。
在看到这个主意后,转念一想,就目前市面上来看,离线下载不是已经成为一个网盘应用的基础功能了吗?我这 Nextcloud 那也必须整上啊 🤣 !二话不说就开整。
经过几次尝试后,大致找到了一个能够与 使用docker-compose搭建nextcloud+Nginx+MySQL+Redis 这篇文章中的 nextcloud 整体方案能够完美兼容,并实现部署后简单配置即可使用的效果。
套方案是基于 使用docker-compose搭建nextcloud+Nginx+MySQL+Redis 这篇文章中搭建好的 Nextcloud 整体解决方案实现的,所以在部署上是沿用了 docker-compose 这种简单快速且方便迁移的基本方法。
而就 Nextcloud 与 Aria2 的交互方法采用的是目前已经成熟了且完美兼容 Nextcloud 的插件 ocDownload。这款插件支持 aria2、youtube-dl、curl 等网络下载组件,但我们这里仅使用 aira2 这一个功能,其他的下载组件看以后是否有充分的需求再去折腾吧 😂 。ocDownloader 与 Aria2 交互的方式是传统的 Aria2-RPC 协议,与 AriaNG 类似,也是一个可独立运行的前端,有效与否取决于 Aria2 的服务端是否正常工作。
在使用的过程中,发现 ocDownloader 在连接 Aria2 这方面几乎没有可配置的地方,只能选择在 HTTP 和磁力下载时使用 Aria2 还是 cURL,其余的类似 Aria2 的地址、端口等都找不到配置的地方。所以初步怀疑是 ocDownloader 将 Aria2 的地址和端口写死了,目标连接很可能就是经典的 127.0.0.1:6800
。
经过以上的分析,这套操作的主要思路分为以下几步:
由于之前的 nextcloud 的 docker 是使用了 docker-compose 来组织容器,所以这次也使用 docker-compose 的形式呈现。
由于 docker-compose 强依赖于 docker-compose.yml 文件中的参数配置,如果 yml 文件中的参数配置在 compose 运行过程中发生了变化,会导致 down 和 up 都发生错误。所以在开始之前,我们先将容器都 down 掉(就是删除容器的意思)。
注意,这里要确保所有重要的数据都映射到了本地磁盘空间中,否则在重启这份 compose 后造成数据丢失。当然,按照我之前文章的映射方法映射后,就不会有问题,重要数据都是映射到本地了。
这里我们可以使用常规的 down 来关闭 compose,并在配置完成后常规 up
1 | # 关闭 compose |
也可以在全部配置完成后,强行使用新的配置重新构建 compose
1 | docker-compose up --force-recreate -d |
这里我将 Aria2 的容器命名为 downloader,以下是容器的启动参数。镜像使用的是他人专门给 nextcloud 配置过的 wahyd4/aria2-ui:nextcloud
版本,当然也可以使用其他版本的,例如使用次数最多的 p3terx/aria2-pro
,只要是能够配置 uid 和 gid 的就足够了。
1 | downloader: |
/var/www/html/data
以相同路径映射到 aria2 容器中,是因为 ocDownloader 会自动将每个用户创建的下载任务的下载目的路径拼接到 nextcloud 本地的路径中,以实现单个用户下载的数据只能由当前用户使用。www-data
,而这个用户的用户 ID 和组 ID 是 33:33
,配置了相同的用户 ID 和组 ID 后,就可以让 Aria2 容器中的下载器下载下来的文件的所属权限与 nextcloud 完全相同,并使得两边的进程对这些文件都能操作。配置好 Aria2 的容器后,还需要修改一下 nextcloud 容器的配置,使得两个容器从网络上和数据上做到互通:
1 | app: |
直接从应用商店安装与自己 nextcloud 匹配的 ocDownloader,或者从 ocDownloader - Apps - App Store - Nextcloud 下载与自己 nextcloud 版本匹配的拓展包上传到服务器后解压安装到 /var/www/html/apps
中。
并启用 ocDownloader。在设置中的“其他设置”中能够看到 ocDownloader 的配置选项,即为安装并启用完成。
此时已经可以可以看到导航栏中的 ocDownloader 图标了,并且可以进入它的页面。但是此时大家会发现,它其实是无法使用的,因为它根本连不上 Aria2 的 RPC。
之前在大致思路中也讲到过,ocDownloader 在插件代码中将连接 Aria2 的地址写死了,导致除了 127.0.0.1:6800
,其他地址一律不认。好在整个 nextcloud 体系是建立在 php 语言的基础上的,所有代码都是可以修改后直接运行的,不需要重新编译。
虽然我是做 c++/python 后端开发的,不会 php,但是万变不离其宗,编码的中心思想基本都是通用的。依托于我那蹩脚阅读代码的能力,最后也还是找到了配置 Aria2 连接地址的位置,这里就不卖关子了,直接把文件路径放这里。
容器内的代码路径为
/var/www/html/apps/ocdownloader/controller/lib/aria2.php
,如果使用跟我的 docker-compose.yml 一样的映射方式,那么此时在宿主机中的位置就是./app/html/apps/ocdownloader/controller/lib/aria2.php
先进入这个目录:
1 | cd html/apps/ocdownloader/controller/lib/ |
备份一下原文件
1 | cp aria2.php aria2.php.bak |
然后使用编辑器编辑这个文件的第 34 行,或者搜索 self::$Server
,将其赋值的数据从 "127.0.0.1"
改为 "downloader"
。
1 | class Aria2 |
我这里改为 "downloader"
是因为我在 docker-compose.yml 中将 Aria2 的容器以 downloader
的名称映射到了 nextcloud 的容器 host 列表中,这样就可以直接通过 downloader 这个地址解析到 Aria2 容器在 docker 网桥内的内网地址。
保存这个文件。
重启重新构建 docker-compose 。
1 | docker-compose up -d |
等待容器完全启动,页面服务可以访问后,就可以在 nextcloud 中使用 ocDownloader 与 Aria2 容器进行交互了!
因为这个方法有点骚,每个人在环境中面对的情况可能不一样,所以这里提供一些基本的排障手段。
持续监控查看某个容器的服务日志:
1 | docker logs [容器名/容器ID] -f |
这里我们尝试从页面上的 ocDownloader 下载一个微信的 windows 端安装包。
可以看到,下载是可以正常启动,并可以看到历史的下载记录的:
从 Aria2 容器的日志中也可以看到下载的创建、进行情况:
打字太麻烦了,贴张图把
在默认下载文件夹参数中配置了目录后,就会在个人的文件页面中创建对应的目录,并下载到这里面。
]]>
sorted
函数和各类中的sort
方法都是用的 Timesort。最好的情况下可以达到 O(n),最差情况下也不过 O(n log(n)),这属于是排序算法中最好的情况了。和堆排序的优劣在于,最好情况下堆排序为O(n log(n))(比timsort慢),空间上堆排为 O(1),timsort为 O(n)。
Timsort的大致步骤可以分为3步:
run
,并将单调递减的 run 翻转。[X, Y, Z]
始终满足 len(X) + len(Y) < len(Z)
和 len(X) < len(Y)
的条件。如果不满足以上条件,则将 Y run 与 X, Z 中较短的 run 通过归并排序合并。以上三个步骤为 timsort 算法的核心思想,每个步骤的实现会因人而异,总体的核心代码如下:
1 | MIN_SORT_LEN = 32 # 可以根据数据的规模规定长度阈值 |
run
将数据按照单调递增或单调递减分割为多个独立的切片,每个切片称作一个run
,并将单调递减的 run 翻转。
1 | def splite_runs(data): |
run
定义一个长度阈值,将长度低于这个阈值的 run 与相邻的 run 合并,以提高后续归并的效率。
1 | def my_bisect_pos(data, x): |
run
将所有的 run 依次压入栈中,并使得在压入栈的过程中,三个连续的 run [X, Y, Z]
始终满足 len(X) + len(Y) < len(Z)
和 len(X) < len(Y)
的条件。如果不满足以上条件,则将 Y run 与 X, Z 中较短的 run 通过归并排序合并。
1 | def merge_run(run_a, run_b): |
1 | Welcome to Ubuntu 20.04.4 LTS (GNU/Linux 5.4.0-104-generic x86_64) |
针对每次登陆都展示相同的问候语,较为简单和通用的办法为修改 /etc/motd
这个文件,果不其然,查看其中的内容:
1 | ❯ cat motd |
先来看看几个效果图。
要实现这样的效果,需要用到3个组件:
其中,fortune 负责输出随机问候语,cowsay 用于套框和加入小动物形象,而 lolcat 负责对这些字符画着色。
先安装这些组件(此处为ubuntu的安装办法,其他发行版可以自行上网查找安装方法):
1 | # 安装 fortune 和 中文库 |
先来看看 fortune。fortune 用于输出随机的名言、诗词或者是一些程序文档。执行几次就能够明白:
1 | ❯ fortune |
接着是 cowsay。cowsay 用于将输入的内容放到小动物的说话的框中,使其更加地有观赏性和趣味性。
1 | ❯ echo abcdefg | cowsay |
通过查看 cowsay 的 usage,可以看到,不光是有牛一种小动物,还有很多内置的形象可以使用,通过 -f
参数后加内置的形象即可使用:
1 | ❯ cowsay -h |
而怎么能够知道内置了多少种小动物呢?其实每种小动物都是一个配置文件,并存在 /usr/share/cowsay/cows
中:
1 | ❯ ls /usr/share/cowsay/cows |
如此一来,既然是随机问候,为什么不随机到底呢,索性小动物的形象也随机一下吧!
可以通过以下的 shell 指令获取到一个随机的小动物配置文件,并将其的名字作为参数传入 cowsay:
1 | ❯ cow=`ls /usr/share/cowsay/cows | sort -R | tail -n 1` |
最后来看看 lolcat。lolcat 的作用就简单很多,就是对终端输出的字符进行彩虹色的着色:
经过以上的分析,我们可以通过管道符将以上三个软件组合起来使用,最终整合成这样一个流程:
1 | # 获取一个随机小动物 |
将其填到终端配置文件,比如 .bashrc 或者 .zshrc(我是用的是 zsh,所以是 .zshrc),然后在之后的每次启动终端,在终端读取这个配置文件的时候,就会执行到这个命令,从而输出问候语。
]]>1 | # 字面值创建方式 |
1 | from dis import dis |
可以看出,通过字面值去创建一个集合,在解释器实现上是通过调用了一个叫做 BUILD_SET
的方法直接创建;
而使用 set()
方法创建集合,解释器实现上会先通过 set 的名字去寻找构造方法,并且调用 BUILD_LIST
方法去创建一个 list 传入 set 构造方法,从而创建一个集合。
所以结论为:使用字面值直接创建集合要比使用 set 构造方法效率更高。
同理地,list()
构造方法和 []
字面值创建列表的效率也很容易得出高低:
1 | '[]') dis( |
起因自《流畅的python》1.1 一摞Python风格的纸牌
自实现类要支持 random.choice
方法只需要支持两个魔术方法即可。
1 | __len__ |
例如
1 | class MyClass: |
进入python内置库 random
中查看choice
的实现,可以看到实现正是使用了序列的 __len__
方法和 __getitem__
方法。
1 | def choice(seq): |
青龙面板+XDD
的时候配置了一套 Golang 的环境,导致为数不多的硬盘空间被吃干净了(没错,剩余量就是这么少)。考虑到机器还会用很长一段时间,所以决定增加一块硬盘对机器进行扩容。在阿里云页面购买了硬盘并挂载到 ecs 实例上之后,在系统中出现了一个新的设备——/dev/vdb
,表明新增的是一块 virtio
磁盘,和我们常见的 sd*
不同,sd*
表示的是 SCSI
类型的设备。
1 | ❯ ls /dev |
通过 ls
查看代表硬件的文件还是比较粗糙的办法,而且无法查看关于硬件的更详细的内容。
对于磁盘类的设备,可以通过 lshw -C disk
查看硬盘信息和通过 fdisk -l
来查看各个分区信息。
lshw
查看硬盘信息。1 | ❯ lshw -C disk |
fdisk
查看硬盘分区信息。1 | ❯ fdisk -l |
在着手分区之前,首先最重要的是确定分区方案。
我的这台阿里云 ecs 自带的硬盘是 20G 的“高效云盘”(估计就是一般的 HDD),这次发生硬盘空间不足主要是 home
目录和 var
目录,所以本次扩容+迁移的主要目标也是这两个目录。
参考 n 个经典 linux 分区方案,另外一个可以纳入考虑的是 swap
分区,使用 free -h
查看当前系统中内存的使用情况,可以看到本机物理内存只有 2G,加之之前在运行 docker 的时候忘记开了性能限制,脚本逻辑导致机器内存吃满导致 ssh 都登录不上的情况,所以适当添加虚拟内存 swap
分区在本次是一个可取的方案。
1 | ❯ free -h |
由于添加的是一个 100G 的硬盘,所以我决定的分配方案为:
序号 | 挂载点 | 大小 | 拓展分区 |
---|---|---|---|
0 | swap | 16G | |
1 | /var | 20G | √ |
2 | /home | 64G | √ |
总合 | 100G |
因为 swap 分区的灵活性比较强,所以单独划分一个主分区给它,以备不时之需的时候划分用作其他用途(虽然分配 16G 对于这台机器来说着实是绰绰有余了)。
而 var 和 home 分区意义上都属于是数据区,所以可以放在一起,由一个逻辑拓展分区分出来。
以上只是我自己不成熟的理解,如果看到这篇文章的大佬有更好的见解,希望可以在评论区给予指导,谢谢!
为了以防万一,即使系统中没有使用 swap 分区,也手动显式地关闭一下 swap 功能。
1 | ❯ swapoff -a # 关闭所有的 swap |
如之前所说,添加的是一块 virtio 类型磁盘,所以显示的是/dev/vdb
,使用 fdisk 对磁盘分区,分区具体步骤在下面,如果太长了看不下去,可以直接跳到 格式化操作。分区操作我尽量加入注释。
1 | ❯ fdisk /dev/vdb |
退出后使用fdisk -l
查看分区结果。
1 | ❯ fdisk -l |
使用 lsblk 查看块硬件信息。
1 | ❯ lsblk |
格式化 swap 分区可以使用 mkswap。这里需要注意的是,swap 分区需要在分区的时候给分区 ID 注册为 82,这样在系统启动的时候才可以正常挂载分区为 swap(具体步骤在 分区。
1 | ❯ mkswap /dev/vdb1 # 格式化为 swap |
swapon 是手动执行挂载 swap 的操作,关机重启会丢失挂载,所以我们还需要将挂载 swap 的操作添加到开机挂载目录的流程中。使用编辑器编辑/etc/fstab
,加入下面这行内容即可,当然,开头的设备文件映射符要换成实际的设备映射符。
1 | ❯ vim /etc/fstab # 添加下面这行 |
将 /dev/vdb5 和 /dev/vdb6 格式化为 ext4 格式。
1 | ❯ mkfs.ext4 /dev/vdb5 |
将 /dev/vdb6 挂载到 /mnt/tmp 下,使用 rsync 同步 /home 目录下内容到 vdb6 中。
rsync 据说是可以代替 cp 和 mv 指令的,具体使用情况可以看看 阮一峰的 rsync 教程。
1 | ❯ mkdir /mnt/tmp # 创建临时目录 |
可以看到,文件已经转移过来了,但是多了一个 lost+found
目录,这是因为在格式化分区时自动生成的目录,这次是新增的磁盘分区,所以无需理会直接删掉即可。
之后还需要将分区挂载流程像 swap 一样加入到/etc/fstab 文件中,使得开机自动挂载。
先使用 blkid 获取到每个分区的 id。
1 | ❯ blkid |
像我这里因为是用 /dev/vdb6 挂载到 /home,所以 UUID 是3fbc870d-3f3a-4182-86c2-690bcf1273b5
,获取到了 UUID 之后,在/etc/fstab 添加下面一行即可。
1 | ❯ vim /etc/fstab |
接着如法炮制 /dev/vdb5 挂载到 /var 目录。
1 | ❯ mount /dev/vdb5 /mnt/tmp |
最后,使用 df 查看,一切目录挂载占用正常即可。
1 | ❯ df -hT |
在编辑 fstab 的过程中对每个参数的意义产生了疑惑,所以查找了一些资料并且总结一下:
例如下面这条记录:
1 | UUID=3fbc870d-3f3a-4182-86c2-690bcf1273b5 /home ext4 defaults 0 2 |
我们从 fstab 的文件的头部注释中可以获取到一些信息,例如第一个参数是能够代表挂载点的硬件文件系统,第二个是挂载点,第三个是硬件分区类型,第四个是挂载选项,但是还是很难明确最后两个数字的意义。
1 | # /etc/fstab: static file system information. |
对于六个参数的意义的明确:
设备
意义:要挂载的设备
输入类型:
- 设备文件
- LABEL=
- UUID=
挂载点
swap 没有挂载点,挂载点为 swap
其余可以填写为目录名称,例如/home
文件系统类型
ext2、ext3、ext4、xfs、nfs、smb、iso9660 等
挂载选项
async、sync、_netdev
defaults( rw, suid, dev, exec, auto, nouser, async, and relatime.)
转储频度
0:从不备份
1:每日备份
2:每隔一天备份
自检次序
0: 不自检
1:首先自检,通常只能被/使用;
2:等数字为 1 的自检完成后,再进行自检
参考资料:
]]>去年使用闲置的阿里云小鸡给协会搭建了一个公用的网盘,采用的是nextcloud作为框架,选择nextcloud的原因主要是因为它可以使用docker进行部署,而且又是开源的,没有免费版、捐赠版或者企业版之类的版本区别,只要能够部署,那就能用。
之前部署的时候匆匆忙忙,单纯为了抓紧时间上线就赶鸭子上架,搭建了一个最简陋的docker版nextcloud,虽然用起来没有什么问题,但是每次自己访问都会感觉到性能捉急,想要改善一下现状,利用有限的硬件资源发挥最大的性能。
所以最近,挑了一个大家不咋使用的时间把网盘下线了一个星期,升级了网盘的部署方式和添加了一些功能。
以Ubuntu 20.04为例,其他操作系统可以自行查找资料。
首先是卸载原有的Docker。这里要讲一个题外话,就是docker在老版本中是叫做Docker的,新版就把大写D改为了小写的d。
1 | apt-get remove docker \ |
安装新Docker
1 | # 官方脚本 使用阿里云源 |
而本文需要使用docker-compose,所以这里还要安装一下,也比较简单,执行脚本就行了。
1 | # 下载 Docker Compose 的当前稳定版本 |
但是使用以上方法安装比较繁琐,也比较难以管理,所以我推荐使用python的pip安装。
1 | # 使用 pip 安装 docker-compose |
docker的优越性想必都比较清楚了,独立运维、即开即用、不影响原有环境等等。
如果本地机器性能比较高,并且使用的人数、压力都没有太大的话,那么使用nextcloud独立容器的安装就足够了。
nextcloud独立容器版本使用的是apache做web服务器,自带SQLite作为数据库,也算是够用。
下面给一下单容器安装nextcloud的运行命令:
1 | docker run -d \ |
稍微解释一下这几个参数:
1 | run # 运行镜像创建容器示例,后面跟着创建容器参数 |
等待docker执行创建容器实例完成后,则可以通过http://ip:7788
来访问nextcloud了。但是这不是本文的重点,本文的目标是一台满血的nextcloud。
自己搭建过nextcloud的人可能都知道,nextcloud不仅可以在默认配置下使用,还可以通过加入各种各样的底层组件来提高它运行的性能。
而这次搭建的这台nextcloud则是与单镜像搭建不同,采用了:
通过加入这些组件可以提高在多用户处理场景时系统的处理能力。
由于这个搭建思路并不是nextcloud官方提供的解决方案(本身也不应该官方去解决这种事情),所以需要自己配置多个容器并将其连接起来,使其之间可以互相访问和操作,运用每个组件的优势提高整体系统的性能。
本次一共会使用到以下几个镜像:
建议提前挂代理通过docker pull
来下载,以免搭建的时候还要去拉去镜像而占用过多时间。
1 | # 配置代理 |
更换镜像源可以看看这篇Docker中国源 - 简书。
其中需要明确一下每个容器在这一套系统中扮演的角色。
nextcloud作为此次的主角,也就是主体业务(app),它在存取数据的时候是会用到数据库(db)和缓存(cache)系统的,那么可以得出nextcloud依赖于mysql和redis。
而mysql和redis之间各司其职,其中一个挂了不会影响到另一个,所以优先级相同,之间没有依赖关系。
nginx作为代理业务和用户之间沟通的主体,首先是需要业务(app)正常运行才能够正常提供它自己本身的服务,所以nginx依赖于nextcloud。
1 | # 依赖关系如下 |
docker-compose是一个代理用户去管理docker的一个工具,使用docker-compose.yml配置文件就可以系统、方便地部署多容器项目,因为我们这次搭建的nextcloud也是基于多容器的,所以使用docker-compose进行部署和调试以及重建最为方便。
完整的docker-compose配置参数文档可以参考下面的这个文档,讲的还是比较细致,推荐给要深入docker-compose的同学学习。
以下就是本文输出的docker-compose配置文件。
注:参数的解释会通过备注的形式写在配置中
1 | version: '3.4' |
把这些内容保存到docker-compose.yml
文件中,然后将其放置在某个文件夹中,例如~/super_nextcloud/
,我们之后就在这个路径下做后续的操作。
接着我们要准备一下nginx的配置。需要创建两个文件夹,一个是./proxy/conf.d
,一个是./proxy/ssl_certs
。
其中conf.d
放置nginx的配置文件,可以起名叫做nextcloud.conf
,ssl_certs
放置域名对应的SSL证书的pem和key文件。
1 | mkdir -p ./proxy/conf.d ./proxy/ssl_certs |
具体可以参考以下的配置来写nextcloud.conf,其中域名和SSL证书文件的名字需要替换成你自己的。
1 | upstream php-handler { |
文件都准备好之后,文件结构如下:
1 | super_nextcloud |
此时准备工作已经完成。使用docker-compose代理对docker的容器进行操作可以使用下面的命令:
1 | docker-compose up -d # 创建所有容器并按顺序启动 |
所以这里我们使用docker-compose up -d
启动我们的服务。在docker-compose输出的提示中,所有目标都显示为done
后,我们可以使用netstat
查看nginx容器是否在监听7788端口:
1 | ❯ netstat -tnlp | grep 7788 |
如果没有输出,那就是创建容器有问题。可以通过docker ps -a
查看哪些容器在疯狂重启,然后通过docker logs [容器名or容器ID]
进到这个容器中查看容器服务日志,自行排障。
如果有输出,并且和上面的内容大致相同,那么恭喜你,构建已经成功大半了。
从地址和端口进入站点,通常是https+域名+端口,如果是内网搭建并且不在意域名以及https的小锁头的话,则可以直接通过ip来访问,但是需要注意的是,在nginx中配置了特定域名后,它会检查访问时是否是正确的域名,如果不是正确的域名,那么就会拒绝访问网盘资源。
进入后,我们就需要进行网盘首次配置的一些操作,包括管理员的配置、数据库的配置、应用程序的配置等等。
其中需要注意的是,在数据库配置部分,数据库的地址需要填写我们在docker的links中映射marindb的主机名称,即db
;而账号密码则是在配置数据库时制定的数据库用户密码以及数据库名称。
使用上一步配置好的管理员账号和密码登录网盘,点击头像进入设置,现将站点的参数配置好后再配置本账号的参数。
由于在网站上线后,需要配置一系列安全参数,所以需要我们再次去修改一下Nginx的配置文件。
在proxy/conf.d/nextcloud.conf
中找到这行(也就是上面那个nginx配置的41行),将前面的注释解开,然后重启nginx容器即可(也可以直接使用docker-compose restart
重启整个项目)。
1 | #add_header X-Frame-Options SAMEORIGIN; |
由于nextcloud默认使用的缓存机制是APCu
,我们需要到配置中修改其使用Redis作为缓存。
将app/config/config.php
中的内容按照如下方法改动:
1 | //'memcache.local' => '\\OC\\Memcache\\APCu' // 用本地式缓存使用APCu // 注释这行 |
使用以下命令进入nextcloud的docker容器,并将文件归属确认改为www-data:
1 | docker exec -it [nextcloud容器的容器ID] /bin/bash |
这时候再去访问nextcloud的页面,就是在使用redis做缓存了。
由于nextcloud内部的文件和配置会在用户操作过程中产生变化,所以需要启用一个定时任务去定期整理和归档这些数据到数据库或者应用到配置中。
先确定自己的nextcloud容器的容器ID或者容器名字,可以使用docker ps -a
查看。
然后使用crontab -e
或vim /etc/crontab
打开crontab的配置进行编辑,加入如下内容:
1 | # run nextcloud cron task every 5 min |
建议两个地方都加一次,避免配置不生效。
至此,站点的基本内容已经配置完毕,可以满足个人用于网盘、webdav等使用场景了。
nextcloud另一个吸引人的地方就是可以安装很多拓展,但是由于nextcloud是从github上安装拓展,国内的网络连接github又有许许多多的困难,所以直接在页面上点击安装按钮是绝对会报cURL的错的。
所以我想出了一个曲线救国的方法,虽然比直接点击安装要多了几步,但着实可行,实施起来也没有什么阻碍。(反倒是之前为了实现直接点页面安装而各种配置代理浪费了不少时间,而且还有搞不出来…)
手动安装拓展程序的步骤大致可以分为下面几个步骤:
tar.gz
文件;docker exec -u 33 -it [container ID or container name] /bin/bash
,其中[container ID or container name]要换成真实容器的ID或名字;/var/www/html/data/[username]/files/nextcloud_extension/metadata.tar.gz
,其中[username]要换成用户的名字。这里装的是matadata插件,用于查看图片各种元数据的;/var/www/html/apps
。cp /var/www/html/data/[username]/files/nextcloud_extension/metadata.tar.gz /var/www/html/apps
;cd /var/www/html/apps && tar -xzf metadata.tar.gz && rm metadata.tar.gz
;按照以上步骤来操作就可以做到实现手动安装插件的目的了。
经历了这次搭建网盘,算是初级入门了docker容器的部署,以及docker-compose的配置入门。
只能说,在开始一个项目之前,还有许多东西需要实现考虑和准备好,做好缺陷预防。这样在真正上手做的时候,才会尽可能的少出差错,以及处变不惊。
原版后记:本来以为这个网盘搭好之后可以养老地给自己和协会使用几年,结果最近阿里云发了续费通知,上去一看发现之前的轻量级应用服务器学生机已经不在学生优惠计划中了(只剩下了一个OSS,基本没啥用),续费一年得1k多,所以只能另寻他处或者项目搁浅了,哎。。
后来的后来,在迁移原项目到阿里云的按量付费 ECS 后,自购了扩容云磁盘,并做了扩容的记录,感兴趣的同学也可以看看。
更新了使用 docker-compose 组织的 nextcloud 与 aria2 配合实现的离线下载方案。
前往链接:基于docker的nextcloud+aria2离线下载联动解决方案
然而随之而来的问题就是,优质的资源它总得要播放才能看才能听吧?而播放过程中某个环节出了问题,则会影响整体的观感体验,没错,我就遇到了。
下载的资源大多都是1080P、2K、4K的视频格式文件,而后两者大多还是蓝光原盘的ios或者是BDMV包,1080P的视频一两个小时可以从2G到10多G不等,而到了2K、4K级别的资源,少说也是一个多小时得有个20G到80G不等,这样就对NAS这种网络存储设备使用场景产生一定的考验,或者说是整条网络链路质量的考验。
现在手上的涉及到这一套基于网络的播放、网络设备就这么几个,电信给的普通光猫,K2P,蜗牛星际A款,小米电视E55A。
为啥要说是普通光猫呢?因为它就是最典型的单千兆+三百兆配置,其中千兆桥接出来给K2P拨号上网代理家庭网络上网,其他接口暂时没有使用。
K2P这么一款经典的全千兆良心路由器就没有什么好多说的了,刷了padavan(老毛子),更新了hiboy的v5驱动版本。而作为经典矿渣的蜗牛,我的这个蜗牛星际A款是第一批上的车,里头还是原装绿色主板,J1900,16G内存,单千兆网口,刷了黑群晖6.1.7(DS3617xs)。
小米电视和K2P之间使用5G wifi连接,使用小米自带的测速功能,测试与公网之间的速度可以达到20MB/S,算是还行的程度,但是尚未知电视和NAS之间的速度,并且公网与内网之间的差别还有延迟等各种问题,以及延迟对小量数据的随机读取的影响等等。
总览一下,原来的网络拓扑如下图所示:
基于以上这一套网络拓扑,在通过小米电视观看1080P视频文件的时候就还好,而如果是观看2K以上清晰度视频的时候,就会出现播放一会,就要转圈圈读取一会的情况,频率大概是10s一次,非常影响观影体验。遇到这种情况,就算不是强迫症,也会想要去改善一番。
在尝试过将蓝光原盘拷贝到U盘中插入电视进行本地播放,并且非常顺畅之后,基本上就确认问题出现在视频文件数据传输到电视上这个环节了。
而影响视频文件从NAS通过wifi传输到电视上速度的因素也有很多,比如硬盘读速度、NAS到路由器的传输速率、路由器到电视之间的wifi传输速率、路由器各网口之间的交换速率,都有可能影响,所以我又开始了漫漫的排错之路。
在排错的策略上,我采取的是由易到难的顺序进行的。每个环节按照排错难易度来排序,以我个人的理解,可以排成下面这样:
测试链路带宽质量,最方便也最普适的方法应该就是iperf了吧。
Padavan虽说是华硕自家的系统,但是也内置了opkg,opkg是一个开源的包管理系统,是Open PacKaGe management
的缩写。直接进入K2P的后台使用opkg安装iperf3即可。
1 | opkg update |
而在群晖上使用iperf3就更加简单了。只需要从docker套件中,安装iperf3的镜像,然后在配置容器的时候按照iperf3的语法规则配置启动命令即可。附上一个张大妈的参考文章:
运行iperf3。iperf3和iperf相同,有服务端和客户端两种运行模式,在测试的时候需要自己确定哪个设备是服务端,哪个设备是客户端。
1 | # 服务端启动 |
测试之后,只能说,很遗憾,问题也不是出在路由器到NAS之间的链路上。
由于每个厂商的路由器设计不同,每个网口的工作行为虽然在用户侧看来是相同的,但是在内部实现上却是不同的。
例如大部分家用路由器只需要支持单WAN口,+N个LAN口的工作模式,这时候路由器厂商在设计时,是在CPU后方接一个带网管的交换机,通过不同的vLAN来区分WAN、LAN、DMZ等。在这种设计模式下,同属于LAN区域或者DMZ区域的流量会直接经过交换机里头的交换芯片,而不通过CPU,这样交换效率就会比较高。
而另一些工业级、企业级的路由器上,由于需要满足自定义网口配置(例如多WAN口、自定义WAN口、自定义vLAN等),这时候则可能将路由器内部实现为每个网口都是一个独立的网卡,每个网卡都直接接通CPU,在配置LAN区是某几个网口时,CPU会在系统内建立一个虚拟网卡(例如eth0)去桥接每个需要绑定在LAN区的网口(例如ge0、ge3、ge4),将其上的流量在CPU上模拟出来的交换机(虚拟网卡)上进行交换。在这种设计模式下,由于没有硬件交换机芯片,交换机功能是通过CPU模拟出来的,这时候同区域的网络流量的交换在经过CPU的时候就会产生性能的损失。
基于这种情况,所以需要看看是否是这一环节出了问题。为了排查这个影响因素,我还去专门买了一个USB3.0转2.5G RJ45口的网卡【捂脸】。
结果一顿操作猛如虎,一测,路由器到电脑有线连接跑iperf3可以跑满千兆,NAS到电脑中间经过路由器有线连接跑iperf3还是可以跑满千兆。
哦豁,这一波,白亏一个USB网卡。
由于具备了上面的条件,就可以直接通过电脑拷贝NAS上的文件到本地来测试NAS上硬盘的读取速度了。
拷贝一个3G的文件到本地,速度可以维持在100到130MB/S之间,这样的速度测试,反正看下来也是没啥问题,千兆极限妥妥的。
一个题外话,使用windows电脑就可以跑满速度,而使用mac就跑不满,估计这mac系统也有问题,mmp…
到了这里,大概可以确定问题就出现在数据到达电视的“最后一公里”——接入网络的介质——上了。
本来是想着换一个牛批一点的路由器比如华硕AC86U或者小米AX3600这种以通过高功率wifi信号传输更大的带宽稳定性到电视。但是经过仔细分析,K2P原先号称千元最强不仅仅是因为它性价比、0元薅、全千兆,更是因为K2P的wifi信号的强大。有大佬拆机分析之后发现,K2P的硬件系统,对2.4G和5G wifi模块都有各自独立的功放,我的一个朋友甚至说在一楼都能收到K2P在6楼发出的信号,连上之后还能上网,不可谓牛哉。所以,在当下有如此强大的无线信号下,还是有这样的问题,那么通过更换路由器来改善网络的程度是非常不确定的。
放弃了走无线的思路后,我把目光转向了有线连接。但是再来看看这个难顶的网络拓扑结构和户型图:
在这样的户型结构下,光猫啥的还可以放在电视柜后面或者弱电箱中,而想要把K2P挪到客厅几乎是不可能的,因为客厅还有猫(真·猫,一只豹猫,一只橘猫,一只英短),把路由器搞挂了那就是真的挂了,毕竟影响到全家的上网。
那么现在只能寄希望于光猫。光猫除了一个千兆(LAN)口桥接给到K2P做PPPoE之外,还有三个百兆口。其中一个绑定了IPTV的vlan没法改(也不是没法改,就是怕改了出问题),剩下两个可以用于光猫的LAN区域。
电视接入光猫剩余的两个网口,此时对于光猫的LAN端和K2P的WAN端以及电视的网口之间,组成了一个无外网权限,也无K2P内网权限的局域网,或者可以说是城域网(MAN)。以下是对MAN城域网的一些定义:
wikipedia
都会网域(Metropolitan Area Network,MAN)指大型的计算机网络,属于IEEE802.6标准,是介于LAN和WAN之间能传输语音与资料的公用网络。MAN是改进LAN(局域网)中的传输介质,扩大局域网的范围,达到包含一个大学校园、城市或都会区。它是较大型的局域网,需要的成本较高,但可以提供更快的传输速率。例如:某一家企业把在一个城市或同一国家内的服务据点连接起来,就可以称为一个或多个城域网。一些常用于城市区网的技术包括:以太网(10Gbps/100Gbps)、WiMAX(全球互通微波访问)。PPPoE协议和MAN访问 这个是什么东西? - 恩山
BYFAN:man是一个网络工程术语,全称是Metropolitan Area Network,意思是城域网,指大型的计算机网络,属于IEEE802.6标准,是介于LAN和WAN之间能传输语音与数据的公用网络。
光猫会给你分配一个光猫的IP,然后你的路由PPPOE拨号会拿到运营商给你的外网IP
man网关是光猫分配的地址。
在组成WAN和LAN之间的中间网络MAN城域网之后,此时就形成了同网段下,电视需要使用K2P做网关,也就是熟知的旁路由场景,但是这个旁路由又有那么点不一样。
既然说这个旁路由不那么一样,那它究竟不一样在哪里呢?
传统旁路由并非是接入广域网的最后一关,而是旁路由自身还有一个指向的网关,用于旁路由的流量的下一跳,而旁路由自己则负责同网段内其他设备流量的加密解密以及各种转发策略等。
所以说K2P一个设备既要做流量出口,又要做旁路由,这就给K2P的配置带来了一些纠结。K2P在刷了Padavan后,是有做AP的功能的,也就是可以在这个模式下用于做旁路由,但又要K2P运行pppd来拨号上网,那么又需要它运行在网关模式下。
本着优先不影响环境上网的原则,K2P是肯定要配置在网关模式下拨号上网的。基于这个原则,就开始折腾了。
注:以下提及的MAN、WAN、LAN都是从K2P的角度来讨论的。
需求:
MAN端需要访问LAN端设备。
MAN端网段为192.168.1.0/24,而LAN端网段为192.168.2.0/24,需要满足192.168.1.4/24之间192.168.2.65/24互访。
思路:
实现电视可以访问NAS,总体思路就无非以下几点,电视访问NAS的请求可以正确到达K2P,而K2P又能正确转发请求到NAS,并且反向可通。
其一,请求可达网关。这一点由于电视和K2P都属于MAN网段,即同一个广播域,电视发出的请求自然可以到达K2P,但是为了不被光猫转发到它的外部(因为我们不知道光猫外部的网络环境是什么样的),则需要手动指派发往LAN端的流量到K2P,也就是配置一个静态路由。
其二,K2P能够转发MAN端的流量到LAN端。这一点则需要手动放通防火墙规则,因为在默认情况下,路由器固件编写者是不会放通MAN到LAN的转发的,这样的场景实在是少之又少。而我们做的放通,也并非是完全放通,而是仅针对MAN端地址进行放通,因为我们不知道运营商PPPoE给到K2P的网段中是否有图谋不轨的人,对同网段进行端口扫描或者网络扫描等不良行为。毕竟安全大过天。
配置步骤:
光猫配置静态路由将MAN端(192.168.1.0/24)中目的地址为LAN端(192.168.2.0/24)的流量定向转发到K2P。
- 使用光猫的运营商管理员账号登录光猫后台,像电信的运营商管理员账号为
telecomadmin
,密码或者其他的运营商可以去网上搜索。- 进入用户侧网络配置 - 静态路由配置
- 添加静态路由:
目的地址 下一跳地址 网络接口 192.168.2.65/24
(NAS)192.168.1.2/24
(K2P)LAN
(这里的LAN代表光猫的LAN,就是K2P的MAN)192.168.2.1/24
(K2P - LAN IP)192.168.1.2/24
(K2P - MAN IP)LAN
配置K2P开放MAN端到LAN端流量的转发
在Padavan的自定义设置 - 脚本配置中 -
在防火墙规则启动后执行:
中添加以下iptables规则:
1 | ## 放通FORWARD放通K2P的流量转发,可让电视访问nas |
需求:
K2P要代理MAN端设备上网。
MAN端设备需要配置网关和DNS为K2P地址。
思路:
基于这个需求,可以看出MAN端设备的请求不仅需要能够被K2P转发,还需要能够进入K2P。为什么要进入K2P呢?因为:
这里明确一下,请求被转发和被代理是不同的概念,转发是将请求报文原封不动地再发出去,而代理则是将请求报文中的源地址从原来真实的地址换成自身的地址之后,再发出去,以起到“代理”的作用。
这里的能够进入,指的就是请求进到K2P中,被K2P接收之后处理完成后再发出去。在这个场景中就是我们常说的NAT——网络地址转换——也是一种代理方式。
明确了以上的定义之后,也就明确了,为什么需要满足流量可以进入K2P了。
配置步骤:
有了上面光猫的配置之后,我们就只需要配置电视和K2P即可。
电视配置以下网络参数:
键 值 IP 192.168.1.4 掩码 255.255.255.0 网关 192.168.2.1
或者是
192.168.1.2DNS 192.168.2.1
K2P还是在Padavan的自定义设置 - 脚本配置中 - 在防火墙规则启动后执行:
中添加以下iptables规则:
1 | ## 放通INPUT 和OUTPUT 可以让MAN端的电视访问K2P做DNS代理 |
至此,已经全部实现MAN端的电视可以访问LAN端的NAS以及通过K2P代理上网了。并且访问NAS播放蓝光原盘也可以没有卡顿得播放了。所以说这一套操作还是对了的。
需求被满足了,但是新的需求永远都会产生。例如如果说我以后需要在K2P的LAN端网络中添加一个旁路由用于代理全网流量出国留学,那么就只需要配置电视的网关为旁路由的IP即可,而不是需要配置更多的参数。
所以说这一套方案实际上是已经考虑到了未来的拓展性的。
而另外一个因素,还记得前面说的电视通过wifi进行广域网测速可以跑到20M/S吧?20MB/S,也就是200Mbps(二百兆比特每秒)而接入光猫的接口只有100Mbps(百兆比特每秒),带宽整整小了一半。但是文件播放的顺畅程度却提高了,所以可以断言,影响NAS上文件的播放的因素不仅仅只是带宽,更多的可能是时延和抖动。
如果我采取了将K2P挪到客厅,将电视连接到K2P的LAN端的方案,将来添加旁路由的时候又需要在K2P上插一个设备,而在宠物猫眼底下多添加一个设备就是多一份危险。当然,除去使用交换机的场景(我认为家里的网络设备规模还打不到使用交换机的地步哈哈哈)。
如果有大佬朋友也有这样的需求,又正好看到了我的这个记录,那么在看到有纰漏的地方,请指出,感谢!
]]>本博文仅作记录。
在博客源文件目录生成git配置和与github远端关联。
1 | git init |
这里我用了另一个仓库做博客源文件存放地,因为如果使用另一个分支来存放的话,难免有命令输错的时候,比如git push origin hexo
不小心顺手打成git push
或者git push origin master
(这都是有可能的,毕竟这些都是熟手操作了),这样子就全盘覆灭。再者就是在github被微软收购之后,每个帐号下的仓库个数已经没有限制了(private仓库也一样),所以这样做更安全,更方便。
还有一点是Hexo在创建博客之初即为支持git仓库同步,最好的证据就是在博客根目录下有一个.gitignore
文件用于非必须文件的忽略。
我用的主题是Melody,采用了Hexo的data files特性,只用在source/_data/melody.yml
中对主题配置进行改动,而不用改动主题本身文件,所以支持主题平滑升级,具体可以直接到melody官方文档里看看。
因为修改主题配置无需改动文件,已经升级主题只需要在主题目录中git pull
即可,这样索性就讲主题设置为git仓库的一个submodule即可。无需像其他的主题一样要fork一份主题文件到自己的账号下。
首先删除原来的melody主题文件(这里无需担心出问题,因为还会下回来),然后以submodule的形式将melody的文件下载到主题路径下即可。
1 | rm -rf theme/melody |
等待它下好即可重新使用博客。以下命令测试一下能不能用:
1 | hexo clean; hexo g; hexo s |
MacOS
1 | brew install node # 高版本node.js自带npm |
Linux
1 | sudo apt-get install nodejs |
Windows
前往Mode.js官网下载安装包安装。
可选操作:npm换源
1 | npm config set registry https://registry.npm.taobao.org # 更换淘宝源 |
1 | npm install hexo-cli -g # 安装hexo脚手架 |
1 | git clone 仓库链接 dir_name # 克隆本仓库 |
1 | # 在博客目录下运行 |
1 | git add . |
这样即可完成博客在多个终端中的同步问题了。
当然,这篇博文就是在家里头的台式机上写好并部署、同步到github上的~
美汁汁
因为平时都使用mac自带的mail.app来对邮箱进行管理,所以使用习惯基本就是不会关闭,最多是用command+W暂时的关闭窗口。
但是最近出现了一个问题:正常关闭mail的窗口之后,会时不时自动弹出mail的窗口;如果是在桌面还好,如果是正在全屏使用软件,突然被mail弹出占用半个屏幕,那可太难受了。
在逛v2ex的时候看到一位仁兄提到使用command+H来隐藏窗口,通过这样“关闭”的mail窗口就不会自动弹出,测试之后发现的确不会弹出来。此事到此就告一段落,但是…
在习惯中,其他软件都是使用command+W来关闭窗口,只有mail是用command+H来关闭,有时候总是记不住,还是手贱使用了command+W,这样一来,还是时常会被“半屏”打扰。
今天闲来无事,看了一下mac上快捷键修改的问题,这一看就发现了转机。
下面教程正式开始:
Application: Mail.app // 列表中手动选择
Menu Title: Hide Mail // mail中顶栏菜单中的实际操作名称
Keyboard Shortcut: ⌘W // 直接在键盘使用command+W就可以设定了
现在就能开心的使用command+W来“关闭”mail的窗口也不会被“半屏”打扰到啦~
]]>反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
进阶:
你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
来源:力扣(LeetCode)
1 | /** |
结果
执行用时 : 16 ms, 在所有 C++ 提交中击败了53.05%的用户
内存消耗 : 9.2 MB, 在所有 C++ 提交中击败了28.08%的用户
1 | /** |
结果
执行用时 : 12 ms, 在所有 C++ 提交中击败了85.51%的用户
内存消耗 : 9.1 MB, 在所有 C++ 提交中击败了47.27%的用户
这个递归法有点意思,看了很久才赚过弯来。
在读代码+一步步画图解析之后,终于明白了其中的原理。
一行一行的讲解吧:
1 | if (head == NULL || head->next == NULL) return head; |
第一行是每个递归都需要的开始返回的条件。
第一个条件head == NULL
是处理[]
这样的链表的特殊条件,并且在第二个条件head->next == NULL
之前进行运算是为了防止在[]
这样的空链表中运行head->next
导致内存访问错误。
第二个条件是正常的递归跳出。搭配第2句、第5句就可以将尾节点不断的往上层调用栈返回。
1 | ListNode* p = reverseList(head->next); |
使用一个临时指针将第一句返回的尾节点存起来,以便最后返回。
1 | head->next->next = head; |
这两句要一起看,我们先来看看每一次递归调用栈运行的时候链表的情况吧,以链表[1,2,3,4,null]
为例。n在那个节点下面就代表那个节点的next指向null,h代表head指向,p代表临时指针p指向(最后返回的p也是这个)。
初始状态:
1 | 1->2->3->4 |
第3层调用栈
1 | h p |
第2层调用栈
1 | h p |
第1层调用栈
1 | h p |
可以看出在head指向的那个节点处运算过后就会将后一个节点的next指向本节点,以此往复就会将整条链表都换向。
而将本节点的next指向null我觉得是加强算法完整性来做的,因为是一个函数自己递归,所以要保证最后返回的链表最后一个节点要指向null,所以需要这一步。
可以看出p指针永远都是指向尾节点,并且一层一层的往上返回,感觉这一点设计很巧妙就是了。
通常在C/C++中实现一个类,都是将声明放在.h文件中,将定义(实现)放在.c或者.cpp文件中,例如:
myClass.h
1 |
|
myClass.cpp
1 |
|
myClass.main.cpp
1 |
|
然后编译的时候直接运行makefile或者执行命令:
1 | g++ -c myClass.cpp |
就可以运行了。这在没有设计模板类的时候一切都会很顺利,但是只要涉及到了模板类就…
就拿一个循环队列来举例子吧。
cycleQueue.hpp
1 |
|
cycleQueue.cpp
1 |
|
cycleQueue.main.cpp
1 |
|
按照常理,正常使用g++编译即可生成main可执行文件,但是在执行最后一步链接的时候会出现问题。
1 | $ g++ -c cycleQueue.cpp |
可以看到,单独编译实现代码的时候是可以通过的,但是在两个文件链接起来的时候出现了问题。经过查找资料,貌似是因为实现文件中实现的是cycleQueue<T>
这个类型,但是main中使用了cycleQueue<int>
这个类型,所以编译器找不到这个类型的定义。
想了想,这好办啊,只要把实现的定义也加进去就好了呀。
于是乎,cycleQueue.hpp就成了:
1 |
|
而为了解决include重复问题,cycleQueue.cpp也变成了:
1 |
|
于是乎,搭配makefile就可以快速实现编译了
1 | cycleQueue: cycleQueue.o cycleQueue.main.o |
1 | $ ls -la |
一顿操作猛如虎23333
]]>给定一个字符串,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。
示例 1:
输入: “Let’s take LeetCode contest”
输出: “s’teL ekat edoCteeL tsetnoc”
来源:力扣(LeetCode)
这道题以另一题作为基础,就是上一篇leetcode题记-344.反转字符串。
这里要注意的就是,这是一个“双指针法”,在运行的过程中会有一个头指针固定,尾指针不断向前移动,遇到空格即为头指针和尾指针之间为一个单词,对这个单词进行反转即可。而使用的交换算法是反转字符串中最快速的异或交换法,实际上还是一个双指针法。
1 | class Solution { |
]]>执行用时 : 16 ms, 在所有 C++ 提交中击败了98.24%的用户
内存消耗 : 11.6 MB, 在所有 C++ 提交中击败了95.48%的用户
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。
示例 1:
输入:[“h”,”e”,”l”,”l”,”o”]
输出:[“o”,”l”,”l”,”e”,”h”]
示例 2:
输入:[“H”,”a”,”n”,”n”,”a”,”h”]
输出:[“h”,”a”,”n”,”n”,”a”,”H”]
来源:力扣(LeetCode)
1 | class Solution { |
结果
执行用时 : 72 ms, 在所有 C++ 提交中击败了54.97%的用户
内存消耗 : 15.2 MB, 在所有 C++ 提交中击败了79.02%的用户
1 | class Solution { |
结果
执行用时 : 64 ms, 在所有 C++ 提交中击败了85.40%的用户
内存消耗 : 15.1 MB, 在所有 C++ 提交中击败了90.38%的用户
双指针的确比直接诶计算下标快了不少,毕竟在算加减的时候双指针法只用了两个基本的加减,而直接计算下标(算上改变i)则需要5此加减法,消耗了两倍多的计算量。
这里使用了c++自带的swap()
函数,在底层上应该是有改进的,并且在使用的时候将自增运算运用得淋漓尽致,于是乎又加快了不少。
1 | class Solution { |
结果
执行用时 : 60 ms, 在所有 C++ 提交中击败了94.45%的用户
内存消耗 : 15 MB, 在所有 C++ 提交中击败了94.68%的用户
这算是奇技淫巧的一种了,就是两个变量来回异或就可以交换数据,而且是从bit层面进行运算的,可以说是速度特别的快了。
1 | class Solution { |
结果
]]>执行用时 : 52 ms, 在所有 C++ 提交中击败了99.76%的用户
内存消耗 : 15.3 MB, 在所有 C++ 提交中击败了75.25%的用户
你和你的朋友,两个人一起玩 Nim 游戏:桌子上有一堆石头,每次你们轮流拿掉 1 - 3 块石头。 拿掉最后一块石头的人就是获胜者。你作为先手。
你们是聪明人,每一步都是最优解。 编写一个函数,来判断你是否可以在给定石头数量的情况下赢得游戏。
示例:
输入: 4
输出: false
解释: 如果堆中有 4 块石头,那么你永远不会赢得比赛;
因为无论你拿走 1 块、2 块 还是 3 块石头,最后一块石头总是会被你的朋友拿走。
来源:力扣(LeetCode)
刚开始看到这道题的难度为“简单”,我怀疑了一下,甚至无从下手。
看到题解的时候才发现,这是一个数学推理题,或者说是一道小学生做的“找规律”题目。
在条件中,有一个关键点——你作为先手。这一点重要在于之后的推理都有一个前提。
Leetcode 官方题解:
如果石头堆中只有一块、两块、或是三块石头,那么在你的回合,你就可以把全部石子拿走,从而在游戏中取胜。而如果就像题目描述那样,堆中恰好有四块石头,你就会失败。因为在这种情况下不管你取走多少石头,总会为你的对手留下几块,使得他可以在游戏中打败你。因此,要想获胜,在你的回合中,必须避免石头堆中的石子数为 4 的情况。
同样地,如果有五块、六块、或是七块石头,你可以控制自己拿取的石头数,总是恰好给你的对手留下四块石头,使他输掉这场比赛。但是如果石头堆里有八块石头,你就不可避免地会输掉,因为不管你从一堆石头中挑出一块、两块还是三块,你的对手都可以选择三块、两块或一块,以确保在再一次轮到你的时候,你会面对四块石头。
看看 ooolize 的回答中的表格:
who\times 1 2 3 4 5 6 7 8 9 … us √ √ √ × √ √ √ × √ ··· enemy × × × √ × × × √ × ···
所以说,想要获胜,那就必须使得存在的石子数量不为4的倍数。既然获得了这一步重要的推理结论,那么代码就不是问题了。
1 | class Solution { |
结果:
其实是会在0~8ms之间浮动的,所以不够稳定的算法,leetcode的判定机状况对结果会有影响。
执行用时 : 0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗 : 8 MB, 在所有 C++ 提交中击败了52.50%的用户
算法题做着做着就有点丢了初心了。算法题归根结底还是数学题,问题还是要从数学的角度出发去解决,而找规律就是数学上最基本的一种解题思路。基础多了高大上的算法之后,拿到一道题脑海里面冒出来的思路很少就是很简单的解法了。
只能说,千里之行始于足下,好吧。
]]>给定一个没有重复数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
来源:力扣(LeetCode)
解这道题主要用了通过递归实现回溯的办法。
在处理数组的过程中,先确定当前正在遍历的位,例如第一位,那么就将每一位和第一位互换,然后在下一个递归过程中将第二位和其他数字依次替换,这样的过程和人类的思维几乎一样。
具体可以看这个leetcode 官方题解,讲的还是很具体的,还有动图可以看。
1 | class Solution { |
执行用时 : 16 ms, 在所有 C++ 提交中击败了86.15%的用户
内存消耗 : 10.2 MB, 在所有 C++ 提交中击败了13.65%的用户
我们提供了一个类:
1 | public class Foo { |
三个不同的线程将会共用一个 Foo 实例。
线程 A 将会调用 one()
方法
线程 B 将会调用 two()
方法
线程 C 将会调用 three()
方法
请设计修改程序,以确保 two()
方法在 one()
方法之后被执行,three()
方法在 two()
方法之后被执行。
示例 1:
输入: [1,2,3]
输出: “onetwothree”
解释:
有三个线程会被异步启动。
输入 [1,2,3] 表示线程 A 将会调用 one() 方法,线程 B 将会调用 two() 方法,线程 C 将会调用 three() 方法。
正确的输出是 “onetwothree”。
示例 2:
输入: [1,3,2]
输出: “onetwothree”
解释:
输入 [1,3,2] 表示线程 A 将会调用 one() 方法,线程 B 将会调用 three() 方法,线程 C 将会调用 two() 方法。
正确的输出是 “onetwothree”。
注意:
尽管输入中的数字似乎暗示了顺序,但是我们并不保证线程在操作系统中的调度顺序。
你看到的输入格式主要是为了确保测试的全面性。
来源:力扣(LeetCode)
因为是第一次做多线程的题目,之前对操作系统的锁之类的东西也没有好好学,这次做下来这题(包括看了其他人的题解)也算是复习了以下多线程的基础。
一开始并不知道锁要怎么实现(体现了我的菜),所以就用了几个bool和while实现了简单的阻塞(我并不知道线程的阻塞是怎么实现的),结果运行结果一出来就傻眼了(太菜了)。
1 | class Foo { |
执行用时 : 1484 ms, 在所有 C++ 提交中击败了5.01%的用户
内存消耗 : 9 MB, 在所有 C++ 提交中击败了100.00%的用户
当然我也不知道为啥我的占用总是超过了100%的人。
看过了别人的题解之后,我发现了多数人用了std::mutex
和std::condition_variable
这两个库用于互斥锁的实现。因为mutex比较简单,所以就顺便学了以下(还不是因为太菜)。
先来看看 std::mutex
的介绍:
构造函数,std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。
lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:
- 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
- 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
unlock(), 解锁,释放对互斥量的所有权。
try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况,(1). 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。(2). 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
reference:C++11 并发指南三(std::mutex 详解)
从用法中可以看出来,在互斥量被锁住的时候,线程执行lock()函数是会被阻塞的,只有在互斥量被unlock()之后,lock()函数才会被执行。所以可以通过设置两个互斥锁分别对second和third进行锁定。
1 | class Foo { |
执行用时 : 40 ms, 在所有 C++ 提交中击败了31.67%的用户
内存消耗 : 9.2 MB, 在所有 C++ 提交中击败了100.00%的用户
这次的结果就好了许多。
]]>昨天,也就是大疆笔试,做三道算法题的时候,调试没法用了(其实是早就没法用了,只是一直没管)。然后就计划着做完笔试之后把这个事情搞定了,于是回到家就开始一直google查查查。
这个问题可就是历史遗留问题了,Mac把gcc和g++链接到clang和clang++之后就能够感觉的出来,苹果对gnu这些东西不是很待见。
每次brew升级完gdb之后,gdb的代码签名就失效了,只好每次都重新应用签名。好在签名的命令还算简单:
1 | # gdb-code-sign keychain中gdb代码签名的名字 |
不过还是要说说这个gdb-code-sign
的这个certification要怎么generate出来。
运行Keychan Access.app,也就是钥匙串,这个东西管理了记录在系统中的所有钥匙,包括wifi密码,网络证书等等,只要有系统管理员的密码就可以随意查看(chrome里头记录的密码看不到,因为chrome不把它们记录在系统中,但是Safari的可以)。
选择 Keychain Access -> Certificate Assistant -> Create a Certifitate…,在名字处取一个自己能够分辨的名字,然后选择代码签名(Code sign),在Mojave版本的MacOS中就只要点击确定两下就可以生成了。
生成证书之后,讲生成的三个同名项目全都移到System目录下,分别是证书(Self-signed Certificate)、公钥(Public Key)和私钥(Private Key)。
接着双击生成的证书,将信任一栏改为任何时候都信任。
这时候就可以关闭Keychain了。
接着打开终端,使用sudo -i
进入root权限。
我一开始使用的是上面的那条命令,但是怎么都签不好,开始调试之后都显示(例如我调试一个叫做sleep的程序):
1 | (gdb) r |
只好又去查。发现有两个解决办法:
sudo gdb xxx.out
因为后面要给vscode用的调试器,总不能每次调试都输一次密码吧。所以不行,得一劳永逸。所以进行第二个解决办法。
将一个文件命名为gdb.xml或者别的什么都可以,然后写入下面的内容,然后保存。
1 |
|
在本目录下,使用root权限,重新运行签名命令:
1 | codesign --entitlements gdb.xml -fs gdb-code-sign /usr/local/bin/gdb |
签名完了之后,再运行gdb调试,这回就好了一些了,可以run,但是会卡住。。。显示:
1 | (gdb) r |
如果gdb能用了,但是因为什么原因导致调试无法进行,这就很难受。所以就放弃gdb了…
重新给lldb签名之后,发现lldb能用,而且跑的很欢块,并且很开心的一点就是,无论是用gcc还是clang,还是g++或是clang++编译的文件,都可以使用lldb调试,这也在之后的编译工具的选择上给出了多个选项。
其实到了这里,我就有点虚了,因为配置文件一直都是一套的,不应该存在以前能用但是现在不能用的情况,毕竟微软的东西不能说不向后兼容吧- -!
经过一番查找之后,发现最新的配置文件版本已经到了2.0.0,而我还在用0.2.0,哈哈,所以说还是过时了【手动捂脸。不过怎么说,就算是过时,那也不应该让编译和运行出错呀。
经过了一番修改,终于将vscode的运行配置更新了一遍。C和C++两套配置因为要调用不同的编译器,编译命令也不同,所以得分成两块,C用gcc或者clang或者llvm-gcc来编译,C++用g++或者clang++或者llvm-g++来编译。
1 | { |
经过这顿折腾,我也理解了一些其中的参数的意义所在,就比如:
运行调试任务前需要运行的任务,这个任务要在tasks.json中配置,而这里填写的就是名字,或者标签等标识这个task用的属性。
要运行的程序的名字,这个和tasks.json中编译选项中的文件路径是对应的,必须相同,不然vscode就无法找到这个文件来运行。
运行调试的参数,会直接传入lldb或者gdb的。
字面意思,在调试的时候会在入口处暂停,点击继续才会开始调试,例如C/C++中的main函数,或者Python中的第一条语句,或者Golang中的 func main()
外部的控制台,这个我是启用的,因为如果不启用的话,在vscode自带的DEBUG CONSOLE中只能输出程序信息,不能输入(或许是有办法的,但是我不会而已哈哈哈)。在调试程序的时候肯定会需要一些交互,所以这个还是要打开的。
调试模式,这里选择的就是调试器的种类,选择
lldb
或者是gdb
,我这里选择的是lldb,因为编译已经可以用gcc的优化了,而在MacOS上用lldb会更加的的心应手一些,毕竟也算是系统级的“亲儿子”。
1 | { |
编译使用的命令,也就是编译器,这里要根据需求改变编译器的选择,例如gcc,g++,clang,clang++,llvm-gcc,llvm-g++这些。
编译选项,这里头的选项要按照顺序来输入,例如编译一个c文件是要用到
gcc sleep.c -o sleep -g -std=c11
,那么args里面的顺序就是要按照这样来填写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88 "args": [
"${file}",
"-o",
"${workspaceFolder}/${fileBasenameNoExtension}",
"-g",
"-std=c11"
],```
### c_cpp_properties.json
虽然这个文件对编译调试不会造成什么影响,但是也放上来作为参考。这个c_cpp_properties是对vscode的自动补全有影响,它会根据系统类型去选择要引用的位置的文件,并检测联想出来的结果。
```json
{
"configurations": [
{
"name": "Mac",
"includePath": [
"${workspaceFolder}",
"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1",
"/usr/local/include",
"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/10.0.1/include",
"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include"
],
"defines": [],
"intelliSenseMode": "clang-x64",
"browse": {
"path": [
"${workspaceFolder}",
"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1",
"/usr/local/include",
"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/10.0.1/include",
"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include"
],
"limitSymbolsToIncludedHeaders": true,
"databaseFilename": ""
},
"macFrameworkPath": [
"/System/Library/Frameworks",
"/Library/Frameworks"
],
"compilerPath": "/usr/bin/clang",
"cStandard": "c11",
"cppStandard": "c++17"
},
{
"name": "Linux",
"includePath": [
"/usr/include",
"/usr/local/include",
"${workspaceFolder}"
],
"defines": [],
"intelliSenseMode": "clang-x64",
"browse": {
"path": [
"/usr/include",
"/usr/local/include",
"${workspaceFolder}"
],
"limitSymbolsToIncludedHeaders": true,
"databaseFilename": ""
}
},
{
"name": "Win32",
"includePath": [
"C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/include",
"${workspaceFolder}"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE"
],
"intelliSenseMode": "msvc-x64",
"browse": {
"path": [
"C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/include/*",
"${workspaceFolder}"
],
"limitSymbolsToIncludedHeaders": true,
"databaseFilename": ""
}
}
],
"version": 4
}
到了这一步还是不行,使用vscode运行调试的时候,调试控制器台能够显示调试信息,但是调用的Terminal.app终端却没有程序运行的信息,也无法输入变量。
在某个偶然,我发现我把iTerm关了之后,Terminal就能够承载调试的程序了,并且在重新开启iTerm之后,还是一样可以调试。
(重要的事情说三遍)
关闭iTerm等第三方终端
关闭iTerm等第三方终端
关闭iTerm等第三方终端
虽然问题解决了(可以在vscode上愉快的调试了,并且可以输入输出变量和结果),但是让我还是有点匪夷所思,决定继续追查下去。
我在vscode中看到一个配置选项:"terminal.external.osxExec": "iTerm.app"
这里原先的值是Terminal.app,后来我发现之后,根据字面意思,意思就是外部终端调用的是哪个程序,索性我改成了iTerm.app。
改成iTerm.app之后,我把iTerm关了,准备让vscode自动调用打开iTerm运行程序的时候,我发现系统弹出了一个辅助控制的窗口显示“是否允许iTerm控制Terminal”,我选了“Not Allow”。选了之后,系统调用出来的还是Terminal.app,并且上面变成了什么都没有,和之前一模一样。
为了做个对比,我特意去装了个Hyper终端(后来莫名发现这东西颜值高又好用除了有点不稳定),然后将terminal.external.osxExec
改为Hyper.app,同样的,在运行调试的时候,和之前一样弹出了窗口,我选择了Allow。这次和相同的是,弹出来的还是Terminal.app,但是不同的是可以输入输出了!这让我really开心。
于是乎,我来到辅助功能这里一探究竟:
System Preferences -> Security & Privacy -> Privacy -> Automation
终于让我“盲生”发现了“华点”:
在打开iTerm.app下面对Terminal.app的自动控制之后,再将terminal.external.osxExec
改为iTerm的时候,同样的,也能进行正常的调试了。
我的理解是,系统没有允许iTerm去控制Terminal,但是vscode调用了iTerm并且权限下放的不够多,所以就没法使得Terminal“接住”这个调试任务(可能是被系统拦截了),所以就什么都看不到。
还想吐槽的是,微软的这个terminal.external.osxExec
简直就是摆设,设置了也没法更换调试用的终端,并且还会导致这种玄妙的权限问题。
有机会再说说Hyper的安装和设置,嘿嘿,还挺有意思的~
]]>假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
你可以假设数组中不存在重复的元素。
你的算法时间复杂度必须是 O(log n) 级别。
示例 1:
输入: nums = [4,5,6,7,0,1,2], target = 0
输出: 4
示例 2:
输入: nums = [4,5,6,7,0,1,2], target = 3
输出: -1
来源:力扣(LeetCode)
这道题可以直接用二分法解决,但是这个二分法需要魔改左右边界迭代的判断。而这个魔改的思想,有点类似反证法。
在进行二分法的时候,数组的反转点或许在中点的左边或者右边,这让我们对数据位置的确定会产生一些困难。
这时候就需要换个思路,例如:
我们可以确定左边或者右边中的某一边是肯定按照顺序排列的,如果目标在这个按照顺序的个区间内,则将二分查找区域放到这边;
反之所有的情况都是在另一边,我们也将二分查找区域放到这另一边,这样不断进行下去,就会不断的无视这个翻转点进行二分查找了。
1 | class Solution { |
执行用时 : 4 ms, 在所有 C++ 提交中击败了92.20%的用户
内存消耗 : 8.5 MB, 在所有 C++ 提交中击败了98.16%的用户
给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
示例 1:
给定数组 nums = [1,1,2],
函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,0,1,1,1,2,2,3,3,4],
函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。
你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。for (int i = 0; i < len; i++) { print(nums[i]);
来源:力扣(LeetCode)
做了这题才发现自己是naive。
一开始只想到了遍历,遍历到和之前一样的元素的时候直接将数组中的元素删除掉就完事儿了,结果出来之后的结果惨不忍睹。
执行用时 :264 ms, 在所有 C++ 提交中击败了15.21%的用户
内存消耗 : 9.9 MB, 在所有 C++ 提交中击败了78.41%的用户
遍历的代码如下,一开始还觉得自己用了迭代器,骚的一批,结果溢出来给自己骚断了腰【手动捂脸
1 | class Solution { |
在看了题解之后,才发现题目中早就已经给出了明确的提示:
你不需要考虑数组中超出新长度后面的元素。
基于这句话,也就是说并不需要将原数组中多余的元素真正的“删除”,而是只需要将后面的元素都放到前面来,令得返回的长度之内都不同和正确就可以了。
所以题解中给出的双指针的确是最优解了。
1 | class Solution { |
结果:
]]>执行用时 : 24 ms, 在所有 C++ 提交中击败了98.03%的用户
内存消耗 : 10.1 MB, 在所有 C++ 提交中击败了73.17%的用户