之前在 Vultr 充值了 25 刀,加上给的 25 刀 Credit,简单搭了一个 VPN,用起来还算可以;只是每月最低 5 刀的价格(2.5 刀仅在美国地区有,欧洲的访问速度太差)让这 50 刀很快就花完了。出于节省成本的原因,我找了另一家俄罗斯本土的服务提供商,他们在赫尔辛基提供的机器很符合要求:
但是他们这种垃圾机器仅提供 IPv6 单栈网络,要在上面部署代理服务需要花点时间。
因为 GitHub 等站点仅有 v4 地址,所以如果想要部署 GitHub 上的软件就需要给机器添加一个 IPv4 网络栈,这一点用 Warp 可以方便实现。同时可以使用 Zero Trust 版本的 Warp 来组建内网。
首先在服务器上安装 warp-cli
:
1 | # Add cloudflare gpg key |
默认 Warp 的工作模式是类似 TUN 的模式,但实际上只需要让它提供一个 Socks 的代理接口。为了在 Zero Trust 中使用这个功能,可以在 Cloudflare Zero Trust 管理面板中新建一个设备 Profile(Settings - Device Settings - Create Profile),规则按照自己的条件来选,我这里选择了用户 + Linux 操作系统。之后在 Service mode 中选择 Proxy mode(默认是 Gateway with WARP):
代理端口使用默认的 40000 即可。
之后在服务器上部署 Zero Trust Warp:
在随便一个浏览器里面打开 https://[teams-id].cloudflareaccess.com/warp
这个链接,会要求你登陆自己的组织,登陆成功后会打开这样一个页面:
把 Authentication 按钮 href
单引号内的内容复制出来 com.cloudflare.warp://...
,之后在服务器上执行:
1 | warp-cli teams-enroll-token [href] |
如果如果没什么问题,服务器上的 Warp 就部署好了。
连接上服务:
1 | warp-cli connect |
并在 ~/.bashrc
中增加代理:
1 | export ALL_PROXY="socks5://127.0.0.1:40000" |
验证一下:
1 | curl api.ipify.org |
如果没什么问题,应该可以看到服务器已经有了一个 IPv4 的地址。
俄罗斯的落后是全方位的,2023 年了,我的 ISP 还没有提供 IPv6。上面的操作都是在 noVNC 里面进行的,由于没有复制粘贴,十分不便。好在 Cloudflare Zero Trust 提供了一个 Tunnel 的功能,相当于一个内网穿透隧道,于是边可以将 SSH 服务映射出来:
首先创建一个 Tunnel(Access - Tunnels - Create a Tunnel),随便给一个域名或者子域,并在 Configure 里配置到 SSH 服务的映射:
记得这里的 Path 需要留空,这样公网上对给定域的访问就会被 Tunnel 转发到服务器的 22 端口上。
之后需要在服务器上安装 cloudflared
,这是 Cloudflare Tunnel 的管理软件:
1 | # Add cloudflare gpg key |
在 Tunnel 的管理页面会有一个 Token,使用下面这个命令连接到你刚刚创建的 Tunnel:
1 | sudo cloudflared service install [your-tunnel-token] |
很重要的,由于垃圾机器是 IPv6 单栈的,Tunnel 的默认链接使用 IPv4,所以使用 sudo systemctl status cloudflared
会发现链接创建失败:
1 | Failed to create new quic connection error="failed to dial to edge with quic: INTERNAL_ERROR: write udp [::]:35102->198.41.200.113:7844: sendto: network is unreachable" connIndex=0 ip=198.41.200.113 |
需要在 /etc/systemd/system/cloudflared.service
配置文件中增加默认使用 IPv6 的选项:
1 | ExecStart=/usr/bin/cloudflared --edge-ip-version 6 --no-autoupdate --config /etc/cloudflared/config.yml tunnel run |
然后重新启动 cloudflared
服务:
1 | sudo systemctl daemon-reload |
最后一步,在需要登陆的客户端机器上也安装 cloudflared
,并编辑 ~/.ssh/config
文件,对指定的 Host 使用 Cloudflared 软件连接 SSH:
1 | Host [tunnel domain] |
最后使用 SSH 验证一下:
1 | ssh [tunnel domain] |
如果没有什么问题,就可以登陆到服务器上了。这样我们使用 Cloudflare Tunnel 从 443 端口转发了 SSH 流量到机器了,可以看到,每次登陆的地址都是 ::1
,这是因为使用了隧道的缘故:
1 | Last login: Mon Oct 16 18:38:28 2023 from ::1 |
部署 xray-core:
1 | wget https://github.com/XTLS/Xray-install/raw/main/install-release.sh && sudo bash install-release.sh |
因为需要使用 Tunnel 将流量转发到机器上代理,能够穿透 Cloudflare 这样 CDN 的传输方式一般采用 websocket,并且由于 Cloudflare 代理了流量,甚至都不需要配置 SSL 证书;并且,因为使用 Tunnel 的缘故,这些端口只需要侦听本地地址即可。
下面给出一份可用的模板,这里在 80 端口上使用 VLess 协议,并在 /ws
路径上回落到本地的 8081 端口处理 websocket 流量:
1 | { |
之后在 Tunnel 的配置面板里面增加一个 Public hostname 用来转发代理流量。
很巧妙的是,由于使用了 websocket,所有的代理流量只会匹配到 /ws
这个路径下,而刚刚设置无路径的 SSH 服务流量只会匹配到 /
根目录下,所以其实可以使用同一个子域:
因为 ChatGPT 还有很多流媒体都 ban 掉了很多 VPS 的 IP 段,所以想要访问这些服务,还需要把这些流量转发到 Warp 上。操作比较简单,只需要把上面 xray 配置文件中的 outbounds
改为 Warp 即可:
1 | { |
这里还可以使用 xray 的 routing
功能配置路由实现更多媒体解锁以及广告屏蔽之类的功能。
通过 Cloudflare Tunnel 代理的代理基本可以跑满家里的 100Mbps 带宽了:
相比之前直连瑞典 Stockholm 代理的速度,甚至还要更快一些:
并且 ChatGPT、Bard 等服务也都可以正常解锁;100ms 之内的延迟,使用起来也比较丝滑。猜测这是因为 Zero Trust Tunnel 是基于 Argo 的,它可以提供更加优质的回源路径:
proxy_pass
到源服务器上。经过上面的步骤,我们实现了一个很有意思的网络结构(这张图是从 Cloudflare 官网偷的),其中我们的 Server 到 Internet 的流量也是经过 WARP 的:
这也就意味着在用户看来,服务器被 Cloudflare 的网络保护了;而对于经由代理访问的网站看来,请求又是从 Cloudflare 发来的 —— 整个过程中我们的服务器被完全包裹起来了,所有外人能知道的唯一信息就是服务器所处的大致地理位置。
而如果打开 Zero Trust - Settings - Network 里面的 WARP to WARP 以及 UDP、ICMP 转发的话,还可以在客户端上登陆到 Zero Trust Teams,并配置 Private Network,就可以实现一个内网了。
在 Applications 里面还可以添加一些 Self-hosted 应用,比如 Web-based SSH 服务,网上有很多教程,在此便不赘述,我个人就在这里部署了一个配置转换服务,这样新的用户就可以通过申请一个域邮箱登陆,之后一键下载配置代理。另外,还可以在 Gateway 里面添加自己的 DNS 服务器,实现内网解析。
不得不说,Zero Trust 真的是 Cloudflare 提供的一个非常好用的功能,并且最关键的,这个服务是免费的。想起上篇关于 DNSSEC 的介绍文章中就曾提及 Cloudflare 所提供的很多优质服务(例如 1.1.1.1 DNS),以及为推动互联网进步所做的很多努力。希望中国的互联网公司也可以少些内卷,多创造出一些这样令人兴奋的,造福大家的基础服务。
]]>而大多数机场提供的 Clash 订阅链接都包含了 Rules 规则 —— 这是为了方便国内用户,减少用户的配置负担;但是如果按照机场给定的 Rules 代理,很多在俄罗斯本没有必要代理的网站也会使用走代理,这不但会浪费流量,很多情况下也会减慢网速。
CFW 提供了名为 Parser 的功能用于解决这个问题。
在 Settings - Parsers 中编写如下代码:
1 | parsers: |
在 url
处将自己的订阅链接填入,这样当 CFW 定时更新订阅时,code
里的 Javascript 代码就会被执行。
回调函数的具体参数如下:
raw
:获取到的订阅文件内容,一般都需要用 yaml
库解析;{ axios, yaml, notify, console }
:会用到的工具,这里使用 axios
从 GitHub 上动态拉取了一份我维护的俄罗斯政府屏蔽网站的列表;{ name, url, interval, selected }
:订阅的属性信息。下面的 Javascript 代码主要是解析了获取到的 YAML 文件并替换了一些内容(包括 DNS 配置,订阅组之类的),最后使用 yaml
重新格式化后返回。
同时,我还使用了 notify
在 macOS 上给了一个通知,最后的结果如下:
如有需要可以自行更改代码内容。
]]>还记得 19 年底的时候,我刚从充电宝的项目中脱身,那时的我几乎耗尽了所有的精力;体重骤降,长期的失眠和食欲不振让我无论是精神还是身体都出现了问题。无法继续学业只得回国暂时修养,那个时候开始渐渐被 Eason 的歌吸引,无论是他的声音还是词中深意都时时让我感动。
一次跑步的时候听到这个歌,初听只是觉得曲子很好听,因为不懂粤语的关系,其实一句也没听懂;后来再听尽管仔细看了看歌词,但是仍不解其意。
如果说我人生做过哪个决定对我影响最大,应该就是放弃西农的学位选择出国吧。那个时候我觉得这是我愿意追梦的一个例证,我为做出这个决定自豪;尽管无论是朋友还是老师都曾劝我三思,但是那个时候我满心只有一个想法:我一定要做自己喜欢的事情,无论付出多大代价。
当朋友们谈起我的事情的时候,我是骄傲的。我有时会想:朋友们在那个没有前途的草业科学专业待下去,有什么意思呢 —— 整个专业每级只有极少数人是对它感兴趣的,大多数人都是抱着混一个学历的心态来的;本科毕业的同学,不是去做销售就是考研;有关系的尽量考公抱住铁饭碗,没关系的在社会上打拼多有不易。
可是经过这么些年,我才发觉:只有大多数人走的路才是最好走的路。
当踏上回国的飞机的时候,完全没有想到会遇到疫情。而疫情这两年,几乎可以被称作是我失去的两年:尽管攒了一些钱,但是在学业上几乎没有任何进步,没有抓紧转型的机会好好学习新知识,反而是着急的用技能去换钱 —— 诚然,这和我的家庭条件有关系,但不可否认的是,这让我的心态难以再调整回之前上学时的状态了。
去年真可以算是不幸的一年,不知道和所谓的本命年是否有关:尽管早早的就提交了复学手续,但直到3月多才拿到签证;尽管在晚来半个多学期的情况下艰难地通过了大多数考试,但剩下最不拿手的物理却又让我栽了大跤;兼职赚钱的快感让我在暑期有了懈怠,加上补考正逢自己新冠感染最难受的时候 —— 不意外的,再次失败让我只得再留一级;开始利用闲暇时间寻找工作,然而这时才发现自己手上的技能并不像是自己想象般那样吃香。最让我难过的是,挺过了华为的前两轮技术面,却在最后 HRBP 的三面开始前 20 分钟被放了鸽子,至今也不知道为什么会这样。
现在已经 24 岁了,面对着本科仍未毕业的尴尬情况,我对那时自己做出的决定再也高兴不起来了;朋友们工作的工作,结婚的结婚,曾经说不屑于那种普通人生活的我开始慌了。
曾迷途才怕追不上滿街趕路人 無人理睬如何求生
頑童大了沒那麼笨 可以聚腳於康莊旅途然後同沐浴溫泉
為何在雨傘外獨行
今天办完学籍变动手续,半夜躺在床上无法入眠,耳边又响起了那首熟悉的《任我行》:往事种种再现眼前 —— 那个抱着吉他在深夜歌唱的少年,现在好像一个老妪,只能时时的叹息。
看着曾经的“满街赶路人”现都已聚脚康庄旅途、同沐温泉,好像自己才是无法融入进去的人 —— 在雨伞外独行,浑身湿透、吃尽苦头却又什么都没得到;穷尽青春追寻的所谓“自由”,却充满了条条框框。
從何時開始忌諱空山無人 從何時開始怕遙望星塵
原來神仙魚橫渡大海會斷魂 聽不到世人愛聽的福音
是啊,从何时开始忌讳空山无人,从何时开始怕遥望星辰?梦想 —— 那个曾时时挂在口边的词,现在却像是蹩脚的情话一般说不出口。看到 Eason 在现场流下眼泪,我的枕巾不知觉也已被打湿。
顽童大了别再追问,可以任我走怎么到头来又随着大队走?
人群是那么像羊群。
]]>0x5f3759df
而闻名:1 | float InvSqrt(float x) { |
事实上,这个算法使用的方法是传统的牛顿迭代法 —— 之所以它能够在常数时间内给出结果,是因为这个魔法数字可以让迭代开始时的值就已经很接近实际根了。
下面我从牛顿迭代开始,一步步实现一个快速平方根算法,并探究这个魔法数字到底是怎么来的。
要求一个数 $a$ 的平方根 $x$,实际上求的就是方程 $f(x) = x^2 - a = 0$ 的根;而计算机中求解这类方程数值解最常用的方法就是牛顿迭代,具体方法如下:
首先选择一个接近方程根的点 $x_0$,计算得到 $f(x_0)$ 与 $f’(x_0)$
计算函数在该点处的切线与 $x$ 轴的交点:
$$
(x - x_0) \cdot f’(x_0) + f(x_0) = 0
$$
解出 $x$,并将其记为 $x_1$,这个点通常会比 $x_0$ 更接近方程根,于是可以不断的迭代上面的方法进行逼近
对于给定的精度 $\epsilon$,重复上面的方法,直到:
$$
x_n - x_{n - 1} \leq \epsilon
$$
上面的切线方程其实也可以看做用 Taylor 展开的前两项,线性近似函数并求解:
$$
f(x) = \sum_{n = 0}^{\infty} \frac{ f^{(n)}(x_0) }{n!} (x - x_0)^n = f(x_0) + f’(x_0)(x - x_0) + \dots
$$
下面的动图可以很好的解释这个过程:
假定第一次迭代是从 $x = x_0$ 处开始的,那么:
$$
f’(x_n) = 2x_n \Rightarrow x_{n + 1} = x_n - \frac{f(x_n)}{f’(x_n)} = x_n - \frac{x_n}{2} + \frac{a}{2x_n} = \frac{1}{2} \left( x_n + \frac{a}{x_n} \right)
$$
其中 $a$ 是要求平方根的数。
利用上面的方法,已经可以写出一个简单版本的 sqrt
函数了:
1 |
|
可以看出,随着 $a$ 的增大,根到 $x_0$ 的距离也越来越远,那么需要迭代的次数也越来越多:
1 | sqrt(1) = 1.000000 - 1 iters |
现代计算机在浮点数的内存布局方面使用的标准为 IEEE 754,其本质是二进制浮点数的科学计数法:
其中:sign
是符号位,exponent
是指数部分,fraction
是小数部分;为了方便快速比较浮点数指数的大小,需要对实际的指数加 $2^{10} - 1 = 1023$ 进行保存。
例如:
$$
3.125_\text{d} = 2^{0} + 2^{1} + 2^{-3} = 11.001_\text{b} = 1.1001 ^ {1}_{b}
$$
由于小数点前一定为 $1$,所以实际保存的二进制数为(以 double
为例):
$$
0_{\text{sign}} \ 10000000000_{\text{exp}} \ 100100000000000000000000000000000000000000000000000_{\text{frac}}
$$
这种表示方法给优化上面的算法提供了空间:如果可以先将指数部分直接原来的一半,那么就可以将初次迭代的位置向根方向大大靠近。
有了上面的想法,那么便可以先进行实现:浮点数保存的指数位为实际的指数加 1023,如果要将指数除以 2 的话,可以先将其增加 1023,之后再除以 2:
1 | exp := real_exp + 1023; |
根据上面 IEEE 754 的 64 位内存布局,指数位上的 1023 对应的二进制为:0x3ff000...
将 double
取地址转为 64 位整形指针,除以 2 则用位移实现:
1 | unsigned long *exponent = (unsigned long *)&x; |
这是使用魔法数字 0x3ff0000000000000
优化之后的结果:
1 | sqrt(1) = 1.000000 - 1 iters |
可以看到,相比较之前版本的算法,在面对大数字时求解所需的迭代次数减少了很多。
但是,魔法数字还可能带来更多的优化吗?
想要得到更多的优化,那么可以对整个魔法数字的状态空间进行一遍搜索,看看相对来说哪一个数字表现更优秀;而之前魔法数字的最后一位为 1,向右位移之后可能覆盖掉小数部分的第一位,所以为了保证搜索的完整性,状态空间的边界应该为:
$$
(\text{0x3fe0000000000000}, \text{0x3fffffffffffffff})
$$
但是整个空间太大了,如果完全存储下来(64 位整型),至少需要 6PB 的空间;想要看出整体的走向,可以先采样之后再分析:
对上面的 C 程序先进行一些修改,以方便使用 matplotlib
绘图:
ctypes
使用的函数,并使用 numpy
进行包装之后按照如下方法得到每一个魔法数字对应的误差值:
1 | lib.set_magic(int(magic)) |
其中 sqrts
是刚刚修改过的求根函数。
这是得到的误差-魔法数函数的大致走势(抽样间隔为 $2^{40}$):
可以看出,存在上个版本其实还有优化空间,一个更好的魔法数字甚至可以将一次牛顿迭代的平均绝对误差降低到很接近 0 的位置。
之前看到网上有用二分法求取魔法数字的;而相对于底部较为平滑的那小段来说,整体上图像的陡峭程度还是较均匀的,所以我决定尝试用梯度下降来求解更好的魔法数字。
要想使用梯度下降,首先得规定这个函数的梯度怎么求取,这里使用了如下方法:
1 | if const.MagicStart + delta < magic and const.MagicEnd - delta > magic: |
其中 self
是误差类的 __call__
函数,也就是用于计算给定魔法数字误差的函数。
依然使用之前的抽样间隔,得到的导数走势大致如下:
之后就是梯度下降了,对于这样的一元函数,梯度值就是导数值,只要不断沿着负梯度方向,就可以不断减小误差函数的值;而训练的终止条件设置为导数值小于某一个给定的参数值即可:
1 | epsilon, lr = 1e-5, 2**32 |
这里设置的导数临界值为 $10^{-5}$,学习速率设置为 $2^{32}$,之后开始训练:
1 | ...... |
这是误差函数随训练轮数的走势:
因为训练的误差函数选用的数字是随机选择的,而底部区间可能会有很多局部极值(其实并没有看起来那么平滑),所以每次训练所得到的最终值可能都是不同的;但是经过一段时间的“调参”之后,得到的绝对误差值基本都稳定在了 $0.012 - 0.015$ 之间。
上面训练得到的相对好的魔法数字值为:0x3feed597d60a871e
,我们将其带入之前版本的函数,测试一下误差值大概有多少:
1 | Best magic number: 3feed597d60a871e with error=0.00020297946576832972, d=-3.3881317890172014e-06 |
可以看到相对误差已经达到了万分之 2 左右;这对于常数时间的算法来说,还是很不错的。
我将测试代码放在了这里,因为是随便写的,所以质量比较糙:https://github.com/guiqiqi/quick-sqrt
本文将主要介绍和讨论 DNSSEC 的工作原理、复习一些较为容易混淆的 DNS 中的概念、最后将会介绍一些 DNS 新技术。
您可能需要有对传统 DNS 协议的工作流程有基本的了解;以及对 TCP/IP 协议、数字签名技术有一个大致的了解(不用十分深入)作为知识基础。
普通的 DNS 请求基于面向无状态的 UDP 协议,并且不会对响应结果进行检查,这就给了攻击者们可乘之机:最常见的 DNS 污染就是对工作在 UDP 53 端口上的流量进行 IDS 入侵检测,并直接返回一个虚假的地址。
更严重的情况是,由于 DNS Cache 的存在,错误的记录可能在很长一段时间内使得用户无法访问到真正的服务器。
这种情况下,有必要以某种方式对返回结果进行 Validation,于是就有了 DNSSEC 技术。我们首先介绍一些术语:
RRSet 是 Resource Record Set 的简称,在这个资源集里面包含了在该域下所有的相同类型记录资源;举个例子,init.blog 域下面有三条 A 类型的记录,分别为:
这三条记录就组成了一个 RRSet。
在 DNSSEC 中,每一个域都有一对公私钥对被称为 Zone-Singing Key (ZSK)。 其中的私钥用来签名上面我们提到的 RRSets;而获得的数字签名将被存储到 RRSIG 类型的 DNS 记录中。
这样 DNS 记录就可以在服务器端被签名,那么如何验证呢?
ZSK 的公钥将被存储在一个叫做 DNSKEY 的新类型 DNS 记录中。当 DNS Resolver 查询到包含 RRSIG 的记录时,就可以访问 Authorized NS 的 DNSKEY 记录,从中获取 ZSK 的公钥,用以验证签名。
下面是当我查询 init.blog 域的 MX 记录时给出的响应,可以看到,相同类型的 MX 资源组成的 RRSet 被签名,并随响应一同返回了 RRSIG:
那么问题就来了,如何验证返回 DNSKEY 记录是否被篡改了呢?
除了 ZSK,DNSSEC 中还有一对被称为 Key-Singing Key (KSK) 的密钥对。KSK 的私钥被用来对在 DNSKEY 记录中的 ZSK 公钥进行签名,并单独为 DNSKEY 记录创建一个 RRSIG。
之后最重要的一步,我们需要将 KSK 也放入 DNSKEY 记录中。这样,在域的内部,我们就拥有了一套可信任的 DNS 查询机制。
下面是一个完整的 DNSKEY 查询响应,可以看到包含了 KSK 以及 ZSK:
不可避免的,我们会有疑问:如果攻击者完全伪造了一套 KSK 与 ZSK,那我们的验证手段就依然失效了吗?
这里 DNSSEC 引入了最后一个概念,DS 记录:该类型的记录保存了域 KSK 公钥的哈希值;和上面在当前 Zone 进行验证的方式不同,这条 DS 记录被存储在上一级 Zone 中。通过每一级的 DS 记录,就可以对下一级 DNSKEY 的 RRSIG 进行验证,这样就可以构成一条信任链。
如果觉得很难理解的话,可以参考 X.509 体系中的 PKI 建设 —— DNSSEC 的验证流程其实与证书信任链十分相似,都是从顶向下的树状结构。
有细心的朋友可能会发现,在这个过程中,其实完全没有必要使用一套单独的 KSK;在这种情况下,我们只需要对 DNSKEY 也使用 ZSK 签名生成 RRSIG,并将 ZSK 的 Hash 作为 DS 存储即可。
那么为什么需要另外一套单独的 KSK 用来签名 DNSKEY 记录呢?
事实上,对 KSK 的任何更改都需要更新父域中的 DS 记录;而 DNSKEY 记录是会被缓存的 —— 这是为了减轻 Authorized NS 的压力;这样一来,新的 KSK 需要等到父域中旧的 DS 记录 TTL 到期之后才能被启用。
另外一方面,ZSK 将会被用来签名非常多的 RRSet,尤其是在一些庞大的域中;一旦 ZSK 的私钥出现损坏或者丢失,那么如果我们仅仅使用 ZSK,这将使得 DNS 的维护变得异常困难。
上面的过程听起来可能有些凌乱,我们自上而下的重新梳理一遍 DNSSEC 的工作流程,假设我们需要获取 init.blog 的 A 记录地址,并且该域已经启用了 DNSSEC:
在实际的递归查询过程中,该过程是自顶向下的,这里为了方便理解,我将整个过程倒过来叙述。
当我们从根域名开始查询 init.blog 的 A 记录响应时,就可以发现除根域、本域之外的任意父域都包含了子域的 DS Record,这样就可以形成一个信任链:
不难发现,这样一条信任链最终回追溯到 .
根域的 DNSKEY 记录可靠性 —— 也就是根域的 KSK 可信度。这就是下面要介绍的根域签名仪式。
为了保证根域的绝对可靠性,ICANN (也就是互联网号码分配局)会每三个月进行一次新的根域签名仪式,这被称为 KSK 轮转计划。
这个仪式每个季度都会在美国东西海岸轮转执行一次;而参加这个会议的人则是由 ICANN 推选出的绝对中立的互联网信任社群代表 —— TCRs。他们一共有 21 人,被分为三组:7 位成员在美国西海岸的数据中心、7 位在美国东海岸、还有 7 位是 RKSH (Recovery Key Share Holder) 恢复密钥共享持有人,来自中国的姚健康博士也是其中之一。
在每个季度的签名仪式上,都会有至少 5 个 TCRs 到场,他们需要经过各种生物信息的识别才能进入会议室,通过自己的 USB 设备在一台气隙隔离的计算机上进行签名,对具体流程感兴趣的朋友可以参考 Cloudflare 的 这篇日志
你甚至可以在 Youtube 上收看 ICANN 的签名仪式直播:Root KSK Ceremony 42 —— 因为新冠疫情的影响,可以看到大家都带上了口罩(希望疫情快点结束)。
这是根域签名仪式人员在 ICANN 的合影:
重启密钥系统 RKSH 的设计是 ICANN 在 2010 年提出的,当这 7 位恢复密钥共享持有人中的任意 5 位同时拿出自己的密钥(存储在一张 Smart Card 上)时,就可以恢复根密钥。当然这只会在极端情况下出现 —— 类似出现核战争导致东西海岸的 DNS 根服务系统都出现严重损坏时。希望不会有这么一天 )。
上面提到的 DNS 响应都是基于能够查询到该记录的情况 —— DNS Resolver Validation 是基于对 RRSIG 的检查;那么当不存在一条 Record 时,自然也就没有该记录的 RRSIG;如果不规定这种情况,就会给攻击者以可乘之机 —— 他们只需要返回一个固定的 NXDOMAIN Response,就可以骗过 DNS Resolver。
需要注意的是,为了保证 ZSK 的安全性以及 NS 服务器的性能,所有的签名都是事先生成的,而不是针对每个请求单独签发的。所以如何处理 “不存在” 的记录响应,就成为了一个难题。
为了解决这个问题,DNSSEC 规定了特殊的 NSEC (NextSECure) 响应,当收到查询不存在的请求时,会按照字母序返回最近一条存在的记录,并且将响应的资源记录类型指定为 NSEC,并在响应中添加该条记录的 RRSIG。
这是当查询一个不存在子域时的返回记录,可以看到响应中指明了最近一条存在记录的 NSEC 类型,并且附带了该响应的 RRSIG;除此之外,根据 RFC 7129 的描述,在响应中还需要附带该域的 SOA 以及对应的 RRSIG:
但这种处理方式引发了另外一个问题:攻击者可以从 a.exmaple.com 开始进行测试,很快就可以遍历出该域下所有的子域信息 —— 这对于某些需要高安全性的站点来说也是难以接受的。
于是在 Authenticated Denial of Existence in the DNS 这篇 RFC 中除了 NSEC,还提出了一个叫做 NSEC3 的记录类型,它在原有的 NSEC 机制上进行了修改,当查询到不存在的域时,返回其 Label 的 Hash 值作为响应,这样就可以降低子域泄漏的风险:
In NSEC3, every name is hashed, including the owner name. This means that the NSEC3 chain is sorted in hash order, instead of canonical order. Because the owner names are hashed, the next owner name for “example.org” is unlikely to be “a.example.org”. Because the next owner name is hashed, zone walking becomes more difficult.
DNSSEC 技术是保证了 DNS Resolver 与 Authorized NS 之间响应查询的可信度;但是可惜的是,由于这个技术的正式标准化距离现在时间并不算很长,国内大部分的 DNS Resolver 还不支持 DNSSEC;即使这项技术在国内准备大范围铺开,是否会受到相关政策阻力,也是一个未知数。
而更重要的是,在国内的网络用户如果使用国外的 DNS Resolver,其收到的解析结果也有很大几率被污染,因为 DNSSEC 并不是一个保证 Client 到 Resolver 之间通讯安全性的协议;即使使用国内某些支持 DNSSEC 的解析器,其与国外 NS 的通讯过程也会被干扰,导致用户依然无法查询到解析结果。
需要注意的是,现有的 DNSSEC 也仅仅是对查询结果的 Validation —— 也就是验证,而对于整个路由路径上的任何一个节点,你的查询都是透明的,没有 Encryption。
在这种背景下,诞生了 DNSCrypt、DoT (DNS over TLS) 甚至 DoH (DNS over HTTPS) 这样的技术,将 DNS 查询包裹在可以保证通讯安全性的协议下 —— 它们虽能够保证 Client 到 Resolver 的通讯安全,但是相应的,过多层协议的包裹带来的性能消耗也是一个不可忽略的问题。
这个部分将介绍几个 DNS 的新技术,他们大多都与这次介绍的 DNSSEC 技术相关。
随着互联网的发展,很多复杂的业务需要在 DNS 中添加各种各样的字段,而最初的 DNS 的 UDP 包大小被限制在 512 Bytes,并且 DNS 包内部的某些字段资源也被使用的差不多了。
在这种背景下就有了 ENDS 技术,它可以通过一个虚拟的 OPT 类型的 DNS 伪资源记录,它本身并不包含 DNS 数据,也不能被转发、缓存;而是被放在 DNS 消息的 Additional Data 区域。
事实上,我们刚刚介绍的 DNSSEC 技术就依赖 EDNS 运作。
这个技术被标准化在 RFC 2671 中,在 DNS flag day 2019 之后,目前大部分的主要 DNS 解析器以及服务提供商都已经支持了这个标准。
这个技术则是上面说的 EDNS 除了 DNSSEC 的另一个应用;很多时候用户会将自己的 DNS Resolver 设置为类似 Google Public DNS、1.1.1.1 这类的大型公共 DNS。
而在迭代查询过程中,目标域的 Authorized NS 会接收到来自这个 “本地 DNS 服务器” 的请求,假如目标网站在全球部署了很多节点,并且会根据 DNS 请求 IP 进行调度,返回距离用户最近服务器的 IP,那么你的位置就会大概率被解析到美国加州。
这样的糟糕体验显然不是我们想要的,那怎么办呢?
有了刚刚的 EDNS 扩展,我们可以在请求中添加一个 Additional RRs,并在其中添加一个 Address 字段,将客户端的 IP/IP Subnet 信息转发给域的 Authorized NS,用以方便调度。这里为了节省篇幅就不贴出包结构了,感兴趣的朋友可以在下面的 RFC 中阅读。
该技术被标准化在 RFC 7871 中。有意思的是,该技术的主要提案者是 Google,而作为全球最大的权威 DNS 网络提供者的 CloudFlare 在他们的 1.1.1.1 服务中明确提到:
1.1.1.1 is a privacy centric resolver so it does not send any client IP information and does not send the EDNS Client Subnet Header to authoritative servers.
可以想见,该技术将使得网站拥有者在 DNS 层了解到访客的信息,而对于想要隐藏自己真实 IP 的人们来说,这显然不是一个好消息。
使用过 CloudFlare 的用户应该会注意到他们提供了一个叫做 DNS Flatten 的功能,这个功能是干什么用的呢?
现在很多的朋友喜欢将根域作为 Web 服务,而不是像古早时代的人们一样添加一条子域记录 “www” 表示万维网。而随着 CDN 的兴起,很多时候我们需要把 Web 服务使用 CNAME 映射到 CDN 提供商所提供的域上去。
这就带来了一个问题,如果我们在根域上设置了 CNAME,用户查询该域的 MX 记录时该怎么返回呢?这就产生了歧义:
事实上,MX 记录的冲突不是最严重的问题,在 DNS 的 RFC 1912 和 RFC 2181 中规定了:
SOA and NS records are mandatory to be present at the root domain
CNAME records can only exist as single records and can not be combined with any other resource record ( DNSSEC SIG, NXT, and KEY RR records excepted)
在根域上设置 CNAME 会导致域本身失去意义,而仅仅作为一个其他域的别名,这时在 SOA 和 NS 记录中所记录的域信息则会失效。
所以 CloudFlare 以及一些比较有名的 DNS 提供商会提供这种叫做 DNS Flatten 的功能,其实就是将你的 CNAME 目标地址预先解析成一个 IP,并在用户查询根域 A 记录时返回。
DNS 作为互联网最古老的协议之一,在设计之初并没有考虑到安全性的问题;而随着现在的互联网发展,它在这个方面的问题也就逐渐暴露出来。事实上,不仅仅是 DNS,TCP/IP、HTTP 等等协议都有着各种各样的安全问题,相应的,人们也设计了许许多多的新的机制对它们进行补充,以保护信息与通讯安全。
而如何让中国的互联网变得更可信、易用,事实上是一个政策问题,而不是技术问题。在各种污染、阻断、劫持、入侵检测不断的今天,除了应用日益复杂的工具,我们只能寄希望于未来会更好。
]]>首先我们要知道什么是树:
任意两个顶点间只存在唯一一条路径的图被称为树。
之后我们要知道什么是生成树:
无向图 $G$ 的生成树是具有 $G$ 的全部顶点,但边最少的连通子图。
之前我们介绍过连通性的概念:对于图 $G(V, E)$ 来说,选取顶点集合的两个不同顶点形成的偶对 $(v_a, v_b)$,若在该图中同时存在一条路径能过连接这两个顶点,那么称这两个顶点具有连通性。
同样的,$\forall v_i, v_j \in V$ 如果 $v_i, v_j$ 都具有连通性,那么称这个图是一个连通图。简单的说,就是没有孤立点的图被称为连通图;那么类似的,连通子图就是某个图的连通分量。
有了这个概念,我们就可以考虑这个“最小”的定义;很容易的,我们就能够想到这个最小应该是定义在路径的长度之上,因为只有在一个图中只有权值能够形成偏序关系:
最小生成树其实是 最小权重生成树 的简称。
给出它的正式定义:
在一个无向图 $G = (V, E)$ 中使用函数 $w(u, v)$ 计算相邻节点间边的权重,如果存在 $T \subset E$,使得图 $(V, T)$ 为树,并且满足:
$$
w(T) = \sum_{(u, v) \in T} w(u, v)
$$
最小,那么称图 $(V, T)$ 为 $G$ 的最小生成树。
下来思考一个树的特殊性质;我们都知道对于一个顶点数为 $v$ 的图来说,其最大的边数量可能为 $v(v-1)$,但是这个值是不确定的;而对于顶点数为 $v$ 的树来说,由于任意两个顶点之间只存在唯一一条路径,那么这棵树的边数量是确定的:
$$
|E| = v - 1
$$
并且根据树的定义,如果在某一个图中存在环 - 也就是从某个顶点到另外一个顶点不只存在一条路径,那么这个图不能够被称为树,所以我们又得到了一个很有用的性质:
树中不存在环。
接下来我们思考一个连通图可能有多少个最小生成树。事实上,最小生成树的数量与边函数的值有关:
如果图的每一条边权值都不相同,那么最小生成树将只有一个。
证明过程这里不表,贴下相关内容的链接:
(其实我也是看 Wikipedia 上的嘿嘿😁)
有了这些性质,我们就可以很简单的理解 Kruskal 算法了。
这个算法可能是图论中比较直观的算法了 - 至少是我一遍就能看明白的并且理解的算法。它就是利用了上面我们说的树的几个特殊性质来求解最小生成树的,步骤很简单:
下面是 Wikipedia 上一个很直观的 Gif,基本看一遍就能明白了。
简单是简单,那么为什么这样就能找到最小生成树呢?
首先,我们寻找了 $n-1$ 条边,并且保证这个生成子图中没有环,所以它至少是一个生成树;
其次,回填的过程是按照权值进行的,那么最后“使用”的边的权值和一定是最小的。
有朋友可能会有疑问,在上面所述过程的第2步,该怎么检查是否出现环呢?难道要跑一遍DFS/BFS吗?
其实根本不用,用并查集就可以解决,因为这个添加过程是逐步进行的,我们只需要在内存中维护一份“已经添加节点”集合:当每次回填边时,将边的两个顶点添加进这个集合即可。
当下一次添加时,检查一下现在操作的这条边的两个顶点是否都在该集合中即可,当发现都在时意味着出现了环。
关于 Kruskal 算法就介绍这么多。
]]>由于 没有服务器只能白嫖 想要体验世界上最流行、先进的 Serverless 函数计算,正好博客部署在 Vercel,所以看了看 Vercel 的官方 Documentation,发现他们也可以部署一些函数计算,支持的语言有:
我对 Python 比较熟悉,于是就顺手用 Flask 写了一个服务;部署时候遇到了一些坑,在这里记录一下。
和其他专门提供 Lambda 计算的云厂商不一样,Vercel 这个 Serverless 有点像是 “顺便” 做的;根据官方文档,如果我们需要部署服务,只需要创建一个 api
文件夹,然后把自己的服务文件放进去就好。
但如果我们直接把文件扔上去,就会发现什么都跑不了;要是想让我们的服务正常运行,还需要避开以下几个坑。
除了 Python 开箱即用的包,我们的服务经常还会用到些其他的第三方库;就像我的服务中就用到了 Pillow/Flask 库;要想让 Vercel 提供我们需要的环境,就需要用依赖描述文件告诉它我们需要的第三方库。
方法倒也不复杂:在根目录创建一个 requirements.txt
文件即可:
1 | Flask>=1.1.2 |
当服务部署起来之后,还需要在 vercel.json
中将这个路径 Rewrite 到服务文件里;这里假设我们的服务文件名叫做 api.py
,那么在 vercel.json
中需要这样写:
1 | "rewrites": [ |
但是最坑爹的是,这个在这个 JSON 文件中如果使用了我在上篇文件中介绍的 routes
属性,那么添加 rewrites
属性就会编译报错。
没有办法,我只能照着那坑爹的文档尝试着慢慢把之前的路由属性改成使用重定向 redirects
属性:如果我们想要实现跳转,可以在配置文件中这样写:
1 | "redirects": [ |
这个是真的坑,在 Documentation 中没有写,但确实是折磨了我一会儿:
API 中服务的路径就是对外访问的路径!
就是说,如果服务相对外提供路径:/api/function
,那么 Flask 中需要绑定的路由路径就是:
1 |
为什么这个路径后面有一个尾斜杠呢?因为如果对于 api
目录下提供的服务来说,/api/function
和 /api/function/
是两个路径(真的烦)
如果我们想要实现自动添加/去除尾斜杠,那么可以在 vercel.json
中添加这个配置:
1 | "trailingSlash": true |
而这个配置将会影响到全局,在同一个 Project 中部署所有文件都会受它影响;所以如果想我一样也部署了 Hexo 之类的其他服务,可能还需要调整其他的目录配置。
在避开上面的一些坑之后就可以部署静态文件,实现简单的前后端分离,使用我们刚刚的 API;静态文件还会被 Hexo 进行编译,如果想要禁止还需要在 _config.yml
中配置跳过。
等年中如果有时间了我想看看是不是能在上面写一个简单的 Proxy,方便梯子抽风的时候凑活使用;毕竟现在的网络环境越来越差了。
]]>首先我们要清楚,既然用户给定的任务是一个个无穷循环,所以操作系统肯定不能指望任务自己放弃 CPU 的使用权,让给其他的函数去执行;相反的,当一个任务执行一段时间之后,我们的 OS 就需要“强行的”从它手里拿到处理器的时间,这样的操作系统被称为 抢占式 的,相反那种不同任务之间 Co-operate 的,就叫做 非抢占式 的。
可我们的 OS 是怎么做到 “抢占” 的动作呢?
回到 Cortex 内核,我们发现,整个处理器的运行时可以分为两个模式:
Thread 模式
这种模式用于处理我们的后台代码,也就是无穷循环内的代码
Handler 模式
当有系统异常/外部中断发生时,处理器陷入 Handler 模式,像是进入子函数调用一样将当前的上下文入栈,并切换 MSP/PSP,之后从中断向量表中更新用户函数地址到 PC
寄存器
uCOS-II 就是使用了这样的中断模式进行任务切换的,具体来说,它使用了 SysTick 作为系统时基,并且在 SysTick 的中断处理函数内进行操作系统的任务调度,这样做的原因主要有两个:
SysTick 默认使用 HCLK 时钟
Cortex 内核的大部分组件都使用这个时钟,它是由 SYSCLK 时钟经过 AHB 预分频之后得到的,其实在 SysTick 的配置中我们还可以对这个时钟源在进行一次分频:
CLKSOURCE 配置为 1 时使用 HCLK
CLKSOURCE 配置为 0 时进行 8 分频
但是默认在 SysTick 的初始化中使用了 1 倍频,也就是 HCLK,这样操作系统就能得到一个和 Cortex 内核相同频率的稳定时钟源
SysTick 属于 Cortex 的内核设备
内核既然提供了这样一个专门用的时钟,当然没有必要再去使用其他的外设,浪费MCU的资源
正是因为使用了 SysTick 作为系统时基,所以我们需要仔细的配置它的时间间隔(也就是24位的递减寄存器),这个时间如果配置的过长会导致系统的实时性能下降;配置的过短又会不断的触发中断调度任务,浪费处理的大量资源用于切换上下文。
除了上面的基于时间片的任务调度之外,还有两种情况需要系统进行任务调度:
对于外部中断处理函数,uCOS-II 自然是没有办法追踪处理器的状态,所以用户需要手动的添加代码让 OS 能够知道我们当前在处理中断,并把当前的任务状态转移至中断态:
1 | OSIntEnter(); |
当 ISR 执行完毕之后,调用的 OSIntExit
函数会触发一次中断级别的任务调度,具体的行为我们放在以后分析。
如果任务代码中调用了类似 OSTimeDly
之类的 API 函数,则会触发一次任务级别的调度,通过 OS_Sched
函数检测是否存在更高级别的已就绪任务,如果有的话则进行一次切换,这个切换的行为我们也放在之后去讲。
简单来说,uCOS-II 的系统调度分为了三个部分:
通过这些调度方法,OS 可以尽可能的让具有最高优先级的任务处于运行状态之中,这也就是 RTOS 中 实时 性的保证。
]]>uCOS-II 的任务通常就是一个无限的循环,就像我们在裸机编程时使用的循环一样:
1 | void task(void* pdata) { |
那么问题就来了,既然每个任务都是无穷循环,如何从一个任务跳转到另一个任务中呢?
不知道大家是否还记得 Cortex-M3 中的 PC
指针,只要我们更改这个寄存器的值,就可以改变处理器的流水线,因为这个指针永远指向下一条指令的地址;事实上 uCOS-II 也是通过该方法在不同的任务之间切换实现时间片切分的,这部分我们之后再说。
需要注意的是,在 uCOS-II 中定义的任务函数的参数一定要是 void*
类型的,这个指针其实是一个万金油,有编程基础的朋友应该知道我们可以将其转换为任意类型的指针,这样某一个固定的函数配合不同的参数就可以实现不同的功能。
uCOS-II 的任务有几种状态:
实际上整个操作系统的框架就像是一个FSM,OS通过对任务的状态转换实现调度。
对于 uCOS-II 来说,最多可以管理 64 个任务,每一个任务都需要对应一个独一无二的优先级;那么相应的,任务的编号 - 这里可以类比为其他 OS 的 PID,就可以看成是该任务的优先级。
事实上我们也许不需要那么多的任务,这个任务数量可以在 os_cfg.h
文件中通过宏去定义:
1 |
注意,当我们确定了最大任务数量之后,建议不要使用优先级为:0 - 4、OS_LOWEST_PRIO - OS_LOWEST_PRIO - 3 这些优先级,原因是他们已经/有可能被系统的某系关键服务所占用,这个我们在后面会说到。
在系统调度器工作时,它会从所有的任务中选取处于就绪态且 优先级最高 的任务进行运行,所以在安排多个任务时,需要仔细考虑他们的优先级安排。
在多任务的 OS 中还有一个很重要的事情就是区分不同任务的堆栈:
SP
指针,任务 B 很有可能会在任务 A 刚刚操作过的内存区域进行复写,于是任务 A 的数据丢失而在 uCOS-II 中可以静态/动态的创建堆栈;只是在动态堆栈创建时需要注意内存碎片的问题,并且在 uCOS 系统中提供了一个 OSTaskStkChk
函数用以检查任务是继续要的堆栈大小,这些问题我们放在之后内存管理的时候再讲。
需要注意的是,这里的堆栈并不能实现类似“进程内存隔离”这样的功能,它存在的目的仅仅是为了让不同任务操作的内存区域隔离开。
有了上面的这些概念之后我们就可以介绍最重要的一个概念:任务控制块 TCB。
事实上,所有关于任务的信息,包括上面提到的优先级、堆栈、任务代码地址、以及后面我们要接触的消息队列等等,这些信息都存储在任务控制块里;在创建任务时,OS 会为每一个任务创建一个 TCB,并且这些 TCB 将组成一个双联表的数据结构。
当发生任务调度时,系统会检查这个 TCB 组成的双链表,用以判断接下来需要运行的任务,并且为任务恢复上下文;在现在这个阶段,我们只介绍 TCB 结构体中最简单的一些成员变量,后期在介绍其他功能时,我们在向其中添加相对应的成员值。
1 | typedef struct os_tcb { |
其实最基础的 TCB 组成就是这些:
OSTCBNext, OSTCBPrev
- 用以组成双链表OSTCBStckPtr
- 存储任务栈底OSTCBDly
- 保存任务延迟时间OSTCBStat
- 任务状态OSTCBPrio
- 任务优先级实际应用中,我们还会用到 TCB 的其他信息,这也体现出了 uCOS 良好的可剪裁性与可拓展性。
]]>我不想像其他能查到的大部分介绍该算法的中文文章一样,直接贴算法代码;相反,我想和大家一起讨论这个算法的思想,并且证明它的正确性。
对于已经知道这个算法解决了什么问题的朋友,这部分可以跳过。
首先我们要知道 Floyd-Warshall 算法解决了什么样的问题:什么是多源最短路径?
对某个图 $G \langle {V, E} \rangle$定义一个序列结构 $P_l$,从顶点 $v_1$ 开始,到节点 $v_l$ 结束:
$$
(v_1, e_1, v_2, e_2, …, e_{l-1}, v_l)
$$
其中 $v_i \in V$ 式图中的顶点,而 $e_i \in E$ 是图中的边,并且符合条件:
这样的序列结构我们称为路径;对于无权图,路径中的边数量称为路径的长度,相应的在赋权图中,长度定义为路径中边的权值之和。
显然,对于图中的两个顶点,可能存在不止一条路径将其连接起来,最短路径就是在两个顶点形成的路径集合中长度最短的那条路径。
在图论算法中有两类最短路径算法(Shortest Path Algorithm):
单源最短路径问题
这类问题需要求解的是图从某一个给定的顶点 $v$ 到图中剩下所有点的最短路径
多源最短路径问题
这类问题需要求解的是图中任意两顶点的最短路径问题
一般来说,在计算最短路径之前,我们都需要判断两顶点的可达性问题,也就是两顶点间是否存在路径,我们可以用BFS/DFS算法去做,之后有机会我们介绍一下这两个算法,这里我们假设两个顶点间存在可达性。
在明确了我们要解决什么问题之后,我们思考问题如何解决。
我们构造一个距离变量 $d_{k, u, v}$ 用于描述顶点 $u, v$ 之间的最短路径长度,其中:
对于顶点 $u, v$ 来说,其之间的最短路径无外乎就两种情况:
最短路径为 $(u, e_{uv}, v)$
这种情况下,两个顶点是相邻的,并且最短路径就是他们之间的邻边,最短路径中不经过任何其他顶点
最短路径为 $(u, e_1, e_2, …, e_k, v)$
这种情况下,两个顶点的最短路径中间经过了 $k$ 个顶点 $(k \geq 1)$
那么,如果我们要查找两个节点之间的最短路径,就可以这样办:
这样,当顶点集合 $V - {u, v}$ 中的所有节点都被检查过后,就能找到 $u \rightarrow v$ 的最短路径。
如果用公式表示的话,我们可以得到一个经过节点 $k$ 的最短路径状态转移方程:
$$
d_{k, i, j} = \min { f_{k - 1, i, j}, f_{k - 1, i, k} + f_{k - 1, k, j} }
$$
既然我们要寻找每一对顶点之间的最短距离,那么我们就使用邻接矩阵保存图中的权值关系,节点自身到自身的距离设置为 $0$,到没有邻接关系节点的距离设置为 $\infty$ 之后:
很显然,根据我加粗的文本能够看到,这是一个三重循环,所以 Floyd 算法的时间复杂度为 $O(n^3)$
具体的代码实现大概如下:
1 | for relay in vertices: |
其中 distance
为邻接矩阵,vertices
为顶点集。
不知道有没有人和我才开始时有一样的问题:如果我们首先发现了 $u$ 到 $i$ 的最短路径,并且 $u \rightarrow v$ 这条直接路径更短,会不会导致 $u$ 到 $v$ 的最短路径计算出错呢?
实际上是不会的,Floyd 算法的核心思想是动态规划,而任何一个动态规划要想保证正确性需要有两个性质:
无后效性/马尔可夫性 - 也就是上层问题的解不会影响子问题的解
根据上面的状态转移方程我们就能够看出,$k$ 次节点的状态完全由 $k - 1$ 次转化而来,无后效性 得以保证
最优子结构 - 也就是最优解的子解也是最优解
图论中我们可以显而易见的得到一个定理 —— 最短路径的子路径仍然是最短路径,什么意思呢?
假设从顶点 $u \rightarrow v$ 的最短路径是:$u \rightarrow x \rightarrow y \rightarrow z \rightarrow v$
那么我们一定可以得到从 $u \rightarrow y$ 的最短路径是 $u \rightarrow x \rightarrow y$
换句话说,如果某一条最短路径一定要经过一个中继节点,那么从源节点到中继节点的最短路径也是被确定的,这样就保证了 最优子结构
事实上,如果我们手动的模拟 Floyd 算法的过程,就会发现,如果经过某一个节点的距离比直接走的距离还要长,那么这个距离根本就不会更新;同样的,如果经过某 $n + 1$ 个节点的路径比 $n$ 个中继节点时短,该距离也不会更新。
在运行完算法之后,我们仅仅得到了任意两点之间的最短 距离,而不是最短路径;如果我们想要得到路径,那么就需要在上面更新距离的同时更新最短路径。
我们可以在运行算法之前,保存一个路径映射(在 Python 中可以看作是字典)其中的元素是节点列表,并按照如下规则将其初始化:
[src, dest]
之后在更新最短距离时候,按照算法的思想,把从源节点到中继节点的路径拿出来,后面 extend
从中继节点到目标节点的路径即可。
Floyd 算法可以处理负权边,因为上面的状态转移方程并不限制两条边之间的权大小;但是图中不能够出现 负权环,这是因为当图中有负权值的环时,最短路径便失去了意义:因为我们只需要沿着负权值环不断循环,就能够得到一个 $-\infty$ 的路径。
]]>但是有的时候我们希望某些代码的执行过程中不要被中断,这些代码被称为临界段代码 Critical Section;那么,在 uCOS 中,系统又是如何做到的呢?
临界段代码是指那些需要连续运行,不可以被打断的代码;一般在STM32上我们有两种需要关注的临界段代码:
外设初始化相关代码
部分外设的初始化强依赖于时序,如果在这些初始化代码的执行过程中触发了中断可能会导致外设初始化失败或者不可预测的行为。
不可重入的函数
我们看一个例子就可以明白什么叫做不可重入函数了:
1 | static int temp; |
现在某一个低优先级的任务正在执行 swap
函数,并且已经执行完 temp = *x
这条指令了,假设这个时候 temp
存储在栈上并且被赋值为 1
;
此时发生了中断,某一个高优先级的任务抢占了 CPU,并且该高优先级任务也调用了 swap
函数,将 temp
赋值为了 3
;
当高优先级任务释放了 CPU 使用权,奇怪的事情发生了,原本应该被赋值为 1
的 temp
变量被刷写为了 3
,这就导致变量 y
的值在该函数调用完成后出现了错误。
而应对临界段代码,在 uCOS-II 中可以首先关闭中断,当临界段代码执行完毕后在重新开启中断;在实际使用中,我们只需要使用两个宏函数包裹需要的临界段代码即可:
1 | OS_ENTER_CRITICAL(); |
uCOS-II 中提供了三种方法保护临界段代码:
第一种方法是使用一条指令关闭中断,在退出临界段代码时重新开启中断。
这种方法不可以嵌套,如果用户在进入临界段代码之前就已经关闭了中断,那么无论如何在退出时中断都会被重新开启,这往往不是我们想看到的。
第二种方法是将 xPSR
寄存器的状态入栈之后在关闭中断,退出临界段代码时出栈即可。
但是这种方法有时会出现很严重的问题:因为 Cortex-M3 内核是允许从 SP
寄存器相对寻址的,并且大部分的编译器在函数内部都是这么做的,那么如果我们使用堆栈去保存 xPSR
寄存器状态时,编译器未必能够察觉到我们操作了 SP
指针,而在临界段的代码可能会因此出现严重的问题。
举个例子:
1 | int a = 0xABCD1234; |
假设在某个任务函数内的局部变量 a
保存在了栈上,我们进入临界段代码之后栈指针被推动了(为了保存 xPSR
的内容),但是编译器却并没有察觉到我们修改了 SP
指针,从而变量 a
仍然是按照推动之前的栈指针进行相对寻址,这会导致严重的问题。
第三个方法是我们最常用的方法,在这种方法我们使用一个变量保存当前处理器的中断使能状态,在退出临界段代码时恢复之前的状态;这样就避免了前面说到的两个问题。
在 uCOS-II 中通过定义 OS_CRITICAL_METHOD
可以选中我们想要的保护方式;因为前两种方法会出现各种各样的问题,事实上我们在 Cortex-M3 中只使用方法三完成临界段保护。
然而无论如何,关闭中断的函数也需要一定的时间去完成,当调用关闭中断的宏函数 OS_ENTER_CRITICAL
后,为了尽快的完成关闭中断的任务,方法三在 STM32 中 uCOS-II 使用了三条汇编指令:
1 | CPU_SR_Save |
在使用时,我们需要定义一个名为 cpu_sr
的变量,用于保存当前的中断使能状态;在保存状态之后使用 CPSID I
指令禁止中断,需要注意的是硬件失败仍然会被响应,这里禁止的仅仅是 ISR;最后跳转至我们需要执行的临界段代码即可。
另外一个需要注意的事情就是在临界段代码内 不能够 使用任何的中断资源,有朋友可能会觉得这是没有必要强调的,但事实是,我们仍然需要小心的对待。例如我们在临界段内使用了一个 delay
延迟函数,而很不巧,这个延迟的时基是由 SysTick
中断提供的,这就会导致我们的代码 hang 在 delay
函数内部。
坐了两个小时的公交车,终于回到了这个天花板在漏水的家。
一路上我就在想,我要是有钱该多好啊,那我就不用住在这偏远的快要离开西安市的地方;我可以有一辆车,再也不用每天通勤都花3、4个小时,这样我就能有更多时间写写自己的项目,也可以有更多时间跑步,哪怕这些时间拿去睡觉也好啊;要是家里有暖气、老妈不用瑟瑟发抖,那该多好啊。
害,想了半天,发现自己想的都是些不切实际的东西。
为什么?因为穷啊。穷,就是原罪,是刻在骨子里,从出生就带来的一种卑劣。
我经常告诉自己说,很多事情都是可以改变的;作为生在红旗下,长在新中国的新一代社会主义接班人,我们应该充分强调人的主观能动性,动手改变自己的生活。
可事实是那样嘛?根本就不是的。
我快23岁了,虽然现在能凭自己的双手吃上饭,穿上衣服,甚至为家里分担一些压力。但是回想这近20年来,我们这个家的生活水平并没有因为我/家庭成员(其实只有我妈)的付出而变的更好。
最初我还住在北稍门那个危房的时候,我们家能吃上饭;后来就越来越不行了,混得最惨的时候我印象很清楚:父亲吸毒嫖娼家暴,最后父母离婚,恰逢姥姥生病得癌症,去中心医院 ICU 几天就花光了家里的所有积蓄。在欠了一堆外债的大年三十夜,我和老妈去秋林买了一包凉菜做为年夜饭吃;后来因为没钱治病,回到社区医院所谓“保守治疗” —— 其实就是等死。老人去世之后,欠的各种外债几年前才还清。
虽说我们现在住的不是那个裂缝能透风的危房了,但是这漏着水、让人坐公交坐到恶心的廉租房,其实体验并不比那里强多少;我们现在吃的起的东西,和小时候那段时间也并无太大区别。可能这个家里唯一值钱的东西,就是我靠技能变现得来的这一套苹果设备,用它们当然也不是为了炫富 —— 我切实的感受到因为他们给我带来的生产力提高。
所以说这么些年,我们改变了什么?基本上什么都没改变。
那你能说我们不够努力吗?也许吧。但我觉得和我身上这种贫穷也有关系。
我有个同学,在彼得堡读预科期间认识的,我们称它为 L 吧,这个 L 的父亲据他自己说是兰州市什么检察院的检察官?在彼得堡的时候我和他关系还不错,这个小伙子属于那种人蛮不错,但是稍微有点憨憨的人(这里没有贬义);他曾经带我去过他在外面租的房子 —— 他在墙上挂了一大面中国国旗,没记错的话旁边还有毛主席/习主席的相片,一张大橡木桌上放了一台电脑,俨然一派官僚办公室装扮。虽说房子面积到也不大,但是比我在宿舍那吸血虫到处乱爬的处境要好上很多,不免让我羡慕。
后来有次和他出去吃麦当劳,他得意洋洋的给我炫耀自己新买的 iPhone 手机和 Apple Watch,说是他爸处置一家非法营业店时收缴的罚款还是什么的。当时我就觉得不能忍 —— 要知道电子产品,这可是男生的口红啊!我暗下决心,自己一定要凭本事买来一只 Apple Watch。
时间很快就到了预科毕业,我感觉他确实是没有语言天赋,当时在预科结业的俄语口语考试上,口试老师要求让他做自我介绍,他做不出来,于是就有了下面的对话:
Как Вас зовут? 你叫什么名字?
Меня зовут L. 我叫 L。
Сколько Вам лет? 你多大了?
… 答不上来
预科毕业考试的老师让我给他翻译,说是不会给他过考试的,他这样上学是在浪费父母的钱;但是后来不知道他走了什么途径,竟然通过了考试,并且进入了物理系 —— 我猜测可能跟他哥在那边是中介有关系;不过好景不长,他上了一个学期之后就被开除了,开除的时候还是我去系主任办公室给他做的翻译。
前一段时间 L 又联系我,说是有个专利,问我要不要把我的名字写上去;我其实到现在也没搞明白这里面都有些什么猫腻,但是写了总比不写好嘛,于是我欣然同意。但是我没想明白的是他到底怎么做出专利创新的。没有鄙视人的意思,只是觉得想要有专利创新需要有一定的知识储备,但就我对他的观察而言,我不认为他有这个能力。具体这里面发生了什么故事,我不清楚,也不敢问。
上个月他开着车来西安找我,说是专利文件申请下来了,把专利材料给我。我去请他吃饭,住的酒店都是四星级的;走之前,他还特意去回民坊买了 3、4 千块钱的茶叶、点心作为礼物,说是要送给他爸爸的朋友。我和他在曲江那边转悠的时候,他还不忘给我说,要换车了,现在的帕萨特不好开,要换成宝马。我笑了一下,说:
我也要换车了。
哦?
我的青桔单车骑行卡到期了,我要换成美团单车了。
曾经在麦当劳下的决心实现了,我却总觉得欠点什么:在用学习之余时间被迫用技能换钱的时候,比我拥有多得多的人却能毫不费力的滚出更大的雪球;在我洋洋得意以为自己走上致富道路的时候,却还是得在这没有暖气、天花板滴水的房子里和家人一起瑟瑟发抖。
所以你说穷不是原罪吗?
大多数时候我不会因为穷而感到不自在,相反,和大多数普通人一样,我们都过着在公交上体验拥挤、在无聊的工作中浪费时间的生活,这样的日子实在是令人感到舒适。但是你向窗外看去的时候,总会发现坐在私家车中的人舒适的摇下车窗,轻吐烟圈。人总是要比较的,想到戴着口罩被迫听周围外放抖音、闻韭菜盒子味道的自己,实在内心不免有些失落。
于是这种失落感变成了一种嫉妒。我开始仇视那些开私家车的人、那些生活过得比我好的人;他们把车停在路边、人行道、斑马线,让路人们甚至没有办法从狭小的缝隙中挤出去。我愤怒的看着这些车,却不能做任何事情。
这种失落感变成了一种自卑。我甚至没有勇气向自己心仪的女孩开口说话,我觉得我不配;我支持女孩子要彩礼,因为这个社会的婚姻就是一种资源交换,像我这种没有任何资源的人根本不可能有人喜欢我。在我的观念里,你没有车,没有房,没有存款,你不配谈恋爱,更不配生孩子。就像我经常给我妈说的一句话:
穷人没有生育权!
穷人哪里有生育权呢?想想吧,就假设你能遇到一个愿意与你同甘共苦的女孩,你们组成了家庭,你的女儿有音乐天赋,走过钢琴店的橱窗,给你说:“爸爸,我想弹钢琴!”,你却只能摇摇头叹叹气拉着她离开。每当这种场景在我脑海中浮现,我都会菊花一紧、虎躯一震。我告诉自己,别多想了,你不配。
这种失落感还变成了一种冷漠。当看到那些与我一样处在社会底层的人受苦受难的时候,我竟有时会觉得高兴,我庆幸于自己比他们幸运,没有受到更多的压榨与不公;可是我也会突然惊醒,我发觉自己像是猪圈中的一只猪一样,在这狭小阴暗的环境中互相抢夺食物,甚至不惜将对方践踏致死。
是啊,我时时刻刻都能感到自己内心的卑鄙,我是一个小人。我向深渊中越滑越深,但面临现实却总是束手无策。我像一个祥林嫂一样,一遍一遍的重复着自己悲惨的遭遇,却在别人遭遇不幸时幸灾乐祸。
有的时候我觉得自己是一个乐观向上的人。我热爱运动、热衷于技术研究。我在遇到能帮助的人时会尽自己全力的去帮助他们。我可以在考试之前通宵达旦的复习,那是因为我对自己的未来有着详尽的规划,我相信只要努力,自己的生活会走上正轨。
但有的时候我又会被这样卑劣的自己所控制,我越来越害怕这样黑暗的自己会把光明的自己反噬。我不是一个心理强大的人,这是我的弱点。但面对贫穷这样的原罪时,又有谁能够做到所谓乐观向上呢?
今天习主席宣布消灭了中国的绝对贫困,我为那些在山村中原先吃不起饭但现在脱贫致富的人感到高兴。但回头看看被强迫加班却没有加班费、辛苦操劳一辈子的老妈,突然发现自己还不能与他们一起高兴,我需要做的还有太多。
]]>这首歌是俄罗斯有名的乐队柳拜创作的,普京都是他们的歌迷,他们还曾经在克林姆林宫旁的红场演出过。
这种“俄罗斯”式的军旅音乐,让我想起彼得堡宽阔的涅瓦河,想起莫斯科那飘扬的三色旗,想起摩尔曼斯克天空中瞬息万变的极光;在一个地方呆一段时间总会有些感情,不论那里的人给我留下什么印象,芬兰湾清澈的海水、冬日的皑皑白雪、高耸的白桦林,总是让我时常怀念。
在网上没有找到这首歌比较不错的中文翻译,于是自己翻译一份。
因为水平有限,翻译中有错误还请大家指出。
Через тернии к звездам, через радость и слезы
Мы проложим дорогу, и за все слава Богу.
经历荆棘密布,我们以星光为引、以泪水与欢笑为伴
我们不惧坎坷,感谢危难中总有上苍眷顾
И останутся в песнях наши лучшие годы,
И останется в сердце этот ветер свободы.
那些美好的回忆珍藏在一首首经典中
这自由的风却仍在我心田中吹动
副歌开始
Головы вверх гордо поднять, за тебя - Родина-мать!
Мы до конца будем стоять, за тебя - Родина-мать!
我们昂首挺胸,只因有我们的祖国
我们屹立于民族之林,为了亲爱的母亲
Мы будем петь, будем гулять, за тебя - Родина-мать!
И за страну - трижды “Ура!” За тебя - Родина!
我们阔步高歌,感恩于我们的祖国
我们骄傲地高呼万岁,只为您 —— 祖国母亲!
副歌结束
Я люблю тебя, мама. Со мною прошла ты этот путь.
Ты ведь верила, знала - терпение и воля все перетрут.
我爱您,我的母亲;您伴我一路走来
您用坚定的信念,指引我们跨过一切艰难困苦
Мама, как ты учила - я верил, я бился, я шел до конца.
Мама, мы победили! Я верил, знал, что так будет всегда.
妈妈,就像您教会我的:我坚韧不拔,笃定前行
妈妈,我们终将胜利!我坚信真理,破晓定会来临
Через тернии к звездам, через радость и слезы
Гордо реет над нами нашей Родины знамя.
历经万千坎坷,我们仍心向远方、未知将有多少欢乐与悲伤
当我抬头仰望,那是祖国的旗帜在迎风飘扬
副歌开始
Головы вверх гордо поднять, за тебя - Родина-мать!
Мы до конца будем стоять, за тебя - Родина-мать!
我们昂首挺胸,只因有我们的祖国
我们屹立于民族之林,为了亲爱的母亲
Мы будем петь, будем гулять, за тебя - Родина-мать!
И за страну - трижды “Ура!” За тебя - Родина!
我们阔步高歌,感恩于我们的祖国
我们骄傲地高呼万岁,只为您 —— 祖国母亲!
副歌结束
По полю иду, про себя шепчу: “Я тебя люблю…”
По морю иду, тихо напою: “Я тебя люблю…”
在田野中漫步,我细语呢喃:“我爱您…”
在海岸边徘徊,我轻声吟唱:“我爱您…”
По небу летит журавлиный клин.
Родина - я твой навеки сын!
就像鹤群翱翔于天空
祖国,我永远是您的孩子!
Где бы ни был я в сердце у меня -
Родина, ты моя!
无论我身在何方,您永远在我心中 —— 祖国,我的母亲!
最后附上歌曲链接:
]]>在朋友的推荐之下选择了这款 ACRH17 路由。
买来新的路由器之后第一件事就是刷了 Merlin 固件,前三点都很好实现;但是第三方固件刷入之后官方的 Time Machine 支持却消失了。
在一番查找之后决定安装 Netatalk 服务启用 AFP 协议的支持,这是一个基于 Linux 的开源 AFP 实现,它依赖几个包使用:
在进行安装之前,我们有几个准备工作需要做:
准备我们的 Time Machine 磁盘进行分区格式化
扩展 JFFS 分区 - ACRH 默认的 JFFS 分区是内置的只有 15MB,在第三方固件下不仅无法启用软件中心,并且我们需要安装的这些包至少需要占用 40MB 的空间,所以我们需要对 JFFS 分区进行扩展
安装 entware 和软件包环境 - 我们需要安装的依赖需要一个 entware 包管理器进行管理
接下来我们就开始吧!
首先准备一块磁盘,我们的 Time Machine 最好准备两倍于磁盘大小的的分区,我这里使用了一个 500GB 的机械硬盘,并分区,因为在 ACRH17 上只有一个 USB 接口,所以后期我们的 JFFS 分区也需要拓展到这上面,所以我们需要对这块硬盘进行两个分区:
有的朋友可能想在路由上做这件事,我不建议,因为这可能会花你很多时间,路由器的性能不好,并且用命令行还是不如 GUI 界面方便嘛。
在磁盘准备完成之后,我们就开始拓展 JFFS 分区,它的原理是:
在内置的 JFFS 分区的 scripts 目录下放置脚本,这些脚本在 boot 时会自动执行,脚本将我们的给定分区重新挂载到 JFFS 分区并复制内容
使用方式就是在 /jffs/scripts
复制两个脚本:
post-mount
- 用于挂载分区并复制内容,内容很长,我放在了 OSS 上,大家可以下载
1 | wget https://oss.init.blog/scripts/post-mount |
unmount
- 用于反挂载
1 |
|
对上面的两个脚本给定运行权限
1 | chmod a+x post-mount |
最后在我们新的 JFFS 扩展分区创建 jffs
文件夹,以便脚本能够识别我们要使用哪个分区进行扩展
1 | mkdir /mnt/xxx/jffs |
这里把 xxx
换成你的卷标即可
最后重启,我们就能看到分区以及被挂载:
在管理页面也能够看到分区被扩展:
网上都说 Merlin 固件自带 Entware 的安装脚本 entware-install.sh,如果有的话,可以这样安装:
1 | entware-setup.sh |
但是我这个三方固件中没有,所以我们需要手动安装
1 | mkdir /jffs/entware-ng.arm |
这个脚本做的主要事情就是安装 entware,并且将其链接到 /opt/etc
目录,这样我们就可以使用其中的组件;之后在管理脚本中添加了启用和禁用的功能。
最后我们安装 busybox 和 vim 包,方便后面的使用:
1 | opkg update |
这个包有些 ROM 内置有安装,有些没有,我们需要分情况处理
如果系统 ROM 内已经安装,那么我们就无需再次安装了,需要配置用户/用户组:
查看配置文件 /etc/dbus-1/system.conf
:
1 | <!-- Run as special user --> |
修改你的用户名为 <user>
节点内的用户,或者创建一个用户
查看配置文件 /etc/dbus-1/system.d/*.conf
:
1 | <policy group="lp"> |
查看 group
属性,并新建用户组,例如这里:
addgroup lp
很多 ROM 内置的没有安装,那么使用 opkg install dbus
安装,并在下面的配置文件中的修改对应属性为你现在使用的用户名:
/opt/etc/dbus-1/system.conf
重启,使用命令进行测试:
1 | dbus-daemon --session --print-address --fork --print-pid |
当看到类似这样的输出就说明配置成功了:
1 | unix:abstract=/tmp/dbus-CSy0dphkTM,guid=24e009e82bece7928f58cc4b5b39c4f6 |
这说明我们的 dbus 已经可以成功创建虚拟总线。
我的这个第三方固件很奇怪,内置的也有 avahi 服务,并且是可以启用的,但是我找了半天也没有找到他使用的配置文件,并且 afpd 服务也事实上不能正常使用,于是我还是使用 opkg 安装了 avahi 包
使用包管理器安装:
1 | opkg install avahi-daemon avahi-utils |
这个 avahi 包需要一个叫 nobody/nogroup
的用户与组身份运行 daemon 守护进程,大家根据情况运行创建用户用户组的命令就好:
1 | adduser nobody |
之后我们创建一个 avahi 的服务配置文件 /opt/etc/avahi/services/afpd.service
:
1 | <!--*-nxml-*--> |
这指定守护进程监听 548 端口,之后我们重启服务即可:
1 | /opt/etc/init.d/S42avahi-daemon restart |
如果遇到问题发现不能启动,或者启动之后使用 check
发现守护进程 dead 了,那么使用 avahi-daemon --debug
命令查看错误日志,一般都是因为 dbus 的服务出现问题。
安装很简单:
1 | opkg install netatalk |
编辑配置文件:
1 | [Global] |
我们需要给 Netatalk 服务创建一个新的用户,我这里用户名是 timemachine
:
1 | adduser timemachine |
把新建的用户添加到你的管理员组:
1 | adduser -G timemachine admin |
重启服务:
1 | /opt/etc/init.d/S27afpd restart |
上面的步骤如果没有错误,那么就可以挂载磁盘了,在 FInder 中使用 Ctrl+K 连接到我们的 afpd 服务,在连接时输入刚刚创建的 timamachine 用户名和密码:
之后在 FInder 中就可以看到挂载成功的磁盘:
右边的是 afpd 服务,左边的是 smb 服务,哈哈,库克就觉得 Windows 真的丑 :)
之后在 Time Machine 中就可以指定刚刚的磁盘并开始备份了,首次备份比较久,我花了一个晚上,速度可以用 30MB/s 左右,换算过来大概是 200Mbps 的速度:
但至少我们不用再每天拔插硬盘备份了,折腾一次还是比较有意义的:
之前看到网上说可以用 smb 服务创建虚拟镜像盘进行备份,我尝试过,并不可行,并且想想在 Linux 系统是使用 smb 服务就感觉不靠谱。
祝大家使用愉快 :)
]]>之前的博客访问起来速度只能说一般般,毕竟离开大陆的服务器少有访问速度快的,Cloudflare 的 Anycast 在国内经常被绕到美国,这也是没办法的,国内的网络环境太复杂了。
但这些都不是迁移博客的原因,前一段时间在更新了 Wordpress 之后,貌似它们又搞了一个什么新的编辑器?在那之前我还可以把在本地写好的 Markdown 进行少量修改的复制过去发布文章,这次之后就完全不能了;而且对 LaTex 的支持还必须启用插件。这对只想安安静静写两篇帖子的我来说简直是太难过了 —— 折腾来折腾去,过一段时间还得升级,升级之后你也不知道会发生什么。
那就迁移吧,Hexo 是个不错的选择;毕竟折腾一次,以后轻松。
迁移牵扯到几个问题:
WordPress 是动态的博客程序,我将它部署在VPS上;但是对于 Hexo 这种静态的博客程序来说,一个 VPS 来说就太浪费了;经过查询之后,我选择了 Vercel —— 它支持免费的静态页面部署,并且大陆的访问速度也还不错。
博客内包含了大量的图片,如果将这些直接部署在静态内容托管平台上,在进行 Markdown 写作的时候会有不少的麻烦,因此我们需要一个自建的图床。
之前 WordPress 的文章需要迁移至 Hexo,尽管 Hexo 有插件从 WordPress 导出的 XML 可以生成博客文章,但是这些恢复的文章经过实测,有着严重的格式问题;
并且更重要的,这样生成的文章没有办法从旧站点生成 301 跳转,这对于依赖搜索引擎结果的访问者们来说,无异于重新建站。
博客我还是希望启用评论的,但是对于 Hexo 这样的静态博客程序,需要动态处理的评论程序往往是一个难题;另外,之前的评论我也不想直接丢掉,这也是我 7 年博客记忆的一部分。
这几个问题我们来一步步解决。
这可能是最简单的一步,网上有大量部署 Hexo 的文章;部署到 Vercel 的步骤几乎差不多:
首先在本地部署一个 Hexo,这个如果不会可以参考 Hexo 官网的部署教程
之后在你的 GitHub 上新建一个 repository 可以是 Private 的,也可以是 Public 的;我这里选择了 Private 的,因为这里面会有很多隐私信息
将你的本地 Hexo 程序 Push 到刚才的 Repos 上,但是一定注意:
node_modules
文件夹,这个文件夹是本地的 Hexo 程序,没有必要推送给托管方,并且这个文件夹的存在会导致 Vercel 的部署出现问题Public
文件夹,这个是你在本地使用 hexo generate
生成的静态文件,没有必要上传git clone
从线上克隆主题程序,请一定删除 themes
对应文件夹下的 Git
仓库信息,否则会导致你的本地项目无法正常 Push 到 GitHub 上之后在 Vercel 上创建一个账户,使用 New Project 新建项目,并按照指导授权你的 GitHub 信息访问,并选择你刚刚创建的博客 Repos 给 Vercel 访问
之后在 settings 部分更改项目的 build / server
命令如下:
之后完成 Import,如果没有问题,Vercel 就会开始对项目进行部署并分配给你一个他们的二级域名,你也可以在 settings - domain 部分绑定自己的域名;
我使用 Cloudflare 只需要在 Console 里面修改 DNS A 记录到 Vercel 的地址即可
完成了上面的步骤之后,你的 Hexo 博客就部署在了 Vercel 上,并且在你本地更新完成之后,只需要将改动 Push 到你的 GitHub 仓库,Vercel 类似 Travis CI 的 Action 就会自动执行,更新部署你的博客。
在绑定了自己的域名之后你就可以看到自己的博客正常运行了。
这里我使用了阿里云 OSS 与 CDN 自建图床,这样可以最小化成本,使用 OSS 一年的成本也就 9 元,40GB 的存储空间我相信足够大部分博客使用了;
为了最小化流量成本,我们在外面再套一层 CDN,这样按照阿里云的计费方式,我们每小时几 MB 的流量是不会收费的。
使用 OSS 和 CDN 的教程太多了,如果你不会的话更可以联系阿里云的售前支持,就说你是企业用户,我相信他们会很高兴给你解答的。
但是一样的,我需要提醒几个点:
OSS 需要设置防盗链的 Referer,但是不要相信阿里云的鬼话:
他们的防盗链对通配符支持很差!
他们的防盗链对通配符支持很差!
他们的防盗链对通配符支持很差!
重要的事情说三遍,例如:
你想允许 Referer 为 https://init.blog
的请求通过,如果编写规则为 https://*init.blog
那么恭喜你,你会得到一大堆的 403 错误。
并且在设置 Referer 时记得需要完全的匹配:协议、端口号、域名
因为我们的图床域名往往与博客主域名不同,那么在 CDN 的设置里面需要解决 CORS 问题;
我们可以在 CDN 设置里面设置 HTTP 的指定 Headers,类似如下设置即可:
之后我们可以开启压缩之类的优化,进一步节省流量
在这之后我们的自建图床应该就可以使用了,因为是图床,我使用 PicGo 管理上传;如果在前面的防盗链里面我们没有允许空 Referer,那么可能上传之后在本地查看会出现裂图的情况,但这并不影响我们在博客部署后的正常使用。
首先,我想告诉大家的是,旧文章迁移只能够手动进行,至少我没有发现有什么好的方法。
好在我们可以直接从原站 Copy HTML 的内容,像是 Typora 这样的 Markdown 对粘贴内容的支持很好。
下来就是需要从旧站点设置文章跳转,我这里的方法仅限于原站使用 postid
的方式:
首先在迁移文章时,根据 Hexo-Migrate 插件导出的原文章 id
,修改文件名为这个 id
,这样在 generate
之后我们就有了如下的 URL:
/postid
对比之前文章的 URL:
/archives/postid
我相信大家已经有了思路了
在进行如上的文章迁移之后,我们有两种方式进行原文章 URL 跳转:
使用 Cloudflare 的朋友可以使用内建的 Page Rules 进行规则跳转:
我们这里选择 301 进行永久跳转告诉搜索引擎我们已经废弃了原来的地址,部署规则之后就可以实现跳转了。
但是需要注意的是,要使用 Page Rules 必须要使得流量经过 Cloudflare,如果部署的静态托管有国内或者香港之类更快的节点,这样就会对我们的博客速度造成很大影响。
所以我选择了仅 DNS 的服务,并使用下面的方法解决跳转问题。
如果使用的静态托管是 Vercel,我们可以使用 vercel.json
进行跳转,它类似 Apache Server 的 rewrite
功能,我们在站点根目录新建一个 vercel.json
,同样的有两种方法进行设置:
使用 redirect
进行,它的配置式类似:
1 | { |
使用 routes
进行,我使用的配置式如下:
1 | { |
这样就可以实现跳转,但是需要注意两个点:
如果启用了 redirects
或者其他的高级功能,那么不能使用 routes
功能,这是因为 routes
属于较为低等的功能。
routes
的多个功能需要仔细调整顺序,因为他们的执行顺序是按照你的编写顺序来执行的,不正确的执行顺序会导致错误的跳转问题。
如果需要正确的配置式,那么需要将更广泛的配置式放在后面,我使用的整体配置式实现了:
两个管理页面的 Redirect & Rewrite
404 页面自定义 - 在 404/index.html
使用 hexo pages 404
生成
CORS 问题解决
整体的配置式如下:
1 | { |
管理页面的具体信息隐去了,大家可以自由更改。
我更推荐第二种方式,因为这样更方便我们管理,并且可以自定义 404 页面之类的。
如果源站点使用了其他的 Permanent-link
格式,可能需要修改上面的方法。
建议大家部署完成之后先测试一下在绑定自己的域名,有什么问题也方便继续调整。
才开始我使用 Disqus 启用评论支持,但是后来发现 Disqus 使用 JS 劫持站内的外部跳转链接到一个 Redirect 域名:
1 | redirect.viglink.com |
这就意味着你站内的外部链接被全部劫持了,这就很恶心了,我在 Disqus 的后台也没有找到如何关闭它,所以放弃。
之后发现了一个不错的自托管评论程序:Valine
它基于 Node.js 开发,基于 LeanCloud 的存储功能实现了无后端的评论支持,具体的使用方法大家可以去 Valine 的官网进行查看,有很多支持它的主题,一般来说我们只需要配置 API Key 即可。
需要注意的是,在申请 LeanCloud 时一定要使用国际版,因为国内版:
这会给我们带来很多不必要的麻烦。
但是对于评论的管理与迁移,Valine 就没有办法帮到我们了,因为这是一个无后端的系统,我们如果需要管理评论,可以使用这个项目:Valine-Admin
将它部署在与 Valine 同一个 Web 实例上,具体的部署方法按照上面的 README.md 说明即可,在这之前可能需要申请自己邮箱的 SMTP 服务,具体的步骤就不详述了,我这里使用的是 QQ 企业邮箱,在 SMTP_SERVICE
使用 exQQ
即可。
如果需要使用管理功能的 Akismet 垃圾评论审查,那么去申请一个免费的 API Key 填入即可。
下面就是原站评论数据的迁移了,我想到的这个方法只能够恢复所有的评论数据,而对于评论的层级关系(即回复关系)则进行了舍弃,具体的操作方法倒也不复杂:
因为 WordPress 没有办法直接导出评论数据,我们需要一个插件:WordPress Comments Import&Export
安装之后在导出数据处对 Columns 进行如下别名设置并导出
导出之后的应该是一个 CSV 文件,我使用了 Python 的以下库进行数据处理:
csv
库,使用其中的 DictReader
类读取数据
datetime
库,处理 insertedAt
列的数据进行设置,其中的 strptime/strftime
的 format
设置为:
1 | "%Y/%m/%d %H:%M" |
json
库,导出文件
任务很简单,代码量也不多,大家可以使用自己喜欢的语言进行数据处理,处理之后的数据结构大概如下:
之后将导出的 Json 文件导入到 LeanCloud 上评论的 Web 实例即可,注意导入的时候选择合适的 Class - 在这里我们选择 Comment
类即可:
导入之后如果没有问题就可以在 Valine Admin 的后台看到自己之前站点的评论了。
现在 LeanCloud 对免费版的自唤醒机制进行了限流,可能会有部分的时候评论邮件难以发送出去,我们有空可以自己看看后台,手动管理一下。
现代的互联网服务越来越 “分布” 了,想想在整个部署过程中用到了哪些服务?
这样的方式相比古老的一台 VPS 管全部更加复杂,但也使得我们的服务更难在出现单点故障时全部瘫痪。
这里面最重要的是 Vercel,但是如果它出现问题,我们可以迅速的回退使用 GitHub Pages / Netlify 同类的静态内容托管服务。
迁移之后我放弃了部分(其实有很大部分)的古老文章,它们大部分都是在2017年之前的,放弃他们有两个原因:
希望以后我能有动力和时间继续更新博客,也感谢大家的关注与支持。
]]>对于我个人来说,2020亦是不平凡的一年:
2019年年底我因为一次失败的创业经历回到国内,12月在一家药店打工的我从没有想到在1个月后一场造成上百万人死亡的大瘟疫即将来临;而我却因祸得福,恰巧赶在这之前安全的回到家中。
在这一年里我实现了完全的经济独立(甚至已经开始为家里进行一些支出);我更换了自己的工作学习用设备,购买了 Apple Watch、MacBook Pro、AirPods Pro,凑齐了苹果🍎全家桶,这让我的工作和学习变得更加容易。而这一切都是由我自己的双手创造出来的,这让我感到骄傲。
过去的一年我养成了跑步的习惯,从最初的2公里都比较难以坚持到现在的10公里日常训练量,这是让我感到高兴的事情,它也切切实实让我的身体变得更好。
在年中的时候我完成了 Leaf
- 也就是我的微信 CMS 系统的编写,虽说留了一堆坑,并且我基本也不会进行继续的维护,但也算了结了我一桩心愿,结束了长达两年的持续工作;在这个过程中我收获了很多,技术能力也有了质的变化,我很感谢当年的自己有这样的想法💡和动力。
我的家里迎来了三个新的生命 —— 我的两只狗子🐶(汪汪 & Oreo):他们之前都是流浪狗,在被好心人救走之后被我们收养,他们给我带来了很多的欢乐;
以及我的猫咪🐱(咪咪):我收养她其实是今年的事情了,但是也没有差很多天。虽说她有的时候很调皮,让我感到心烦意乱,但是看到她那么可爱温柔的躺在我身边睡觉的时候,感觉真的很棒。毕竟就像大家说的:小猫咪能有什么坏心眼呢?
上一个学期的考试月我平稳的度过了,这是我从大一以来度过的最平稳的 Сессия 了,这要感谢我上一个学期的努力,没有那些深夜依旧忙碌的背影,我可能难以获得今天的成绩(虽说依然有很多的 Тройка,但我高兴于自己学有所得,并且切切实实的努力过了)
我的考试月在昨天(也就是22号)才结束,也正是因为此,我的2021年其实从今年才开始起步。
我决定将博客从 WordPress 迁移至 Hexo,原因有几个:
我的阿里云服务器上除了博客,其实没有运行任何程序,这无疑是一种浪费;在迁移之后我将使用 OSS 作为图床,而 Hexo 作为静态的博客程序,可以部署在 Vercel 或者 GitHub Pages 这样的免费平台,以便减少支出
WordPress 对 Markdown&LaTex 的支持真的很差,而我喜欢 Markdown 这样更为稳健的编辑感,对于 WordPress 那变来变去的编辑界面实属接受不能
迁移之后我会慢慢的将之前的部分文章导出到新的平台,这项工作需要大量的时间,而对于之前一些质量很低的文章我则会放弃他们;是时候和他们说再见,拥抱新的自己了
正如以上所说,博客的迁移需要很多时间来完成,我会尽快的完成它(反正我之前的文章也没什么人看😂)
…
写完这些才发现自己在2020年完成了很多,也收获了很多。感谢家人和狗子,猫咪们的陪伴,感谢朋友的支持,你们支持我走过了这近400个日日夜夜,我要感谢你们。
希望自己在2021年能够更加努力,希望老妈可以身体健康,希望我的朋友们能够一切顺利;真心的希望在这心的一年里坏事不要再发生,疫情快快的过去,上帝的子民们都能够和平、幸福的相处。
]]>这一节我们介绍的重要的 Picard 存在唯一性定理就给出了 Cathy 问题存在唯一解的充分不必要条件。
定理的主要证明思路来自:
下面我们就开始吧!
定义函数 $f(x, y)$ 在区域 $D$ 上满足 Lipschitz 条件:
$$
\newcommand\d[1]{\text{d}#1}
|f(x, y_1) - f(x, y_2)| \leq L |y_1 - y_2|
$$
其中常数 $L > 0$,则称函数 $f(x, y)$ 在区域 $D$ 上满足 Lipschitz 条件
可以得到,如果函数 $f(x, y)$ 在凸形区域 $D$ 内对 $y$ 有连续的偏导数,则 $f(x, y)$ 在 $D$ 内对 $y$ 满足 Lipschitz 条件,反之则不一定成立,例如:
$$
f(x, y) = |y|
$$
对 $y$ 满足 Lipschitz 条件,但是当 $y = 0$ 时该函数对 $y$ 没有微商
关于凸形区域概念的补充
在某个区域 $D$ 内任取两个点 $(x_0, y_0), (x_1, y_1)$,如果他们的构成的直线段的任意部分都在该区域内,那么称该区域为一个凸区域。实际上,可以通过计算得到这个直线段的方程:
$$
\frac{x - x_0}{x_1 - x_0} = \frac{y - y_0}{y_1 - y_0} ,;(x_0 \leq x \leq x_1, y_0 \leq y \leq y_1)
$$
直观的来说就是该区域的边界弯曲方向是一致的偏微分和偏导数(偏微商)
微分和导数在单元函数的概念中就有区别:
微分指的是某个函数在某个方向上的一个微小变化量;注意这里指的是函数值的微小变化量,使用自变量的一个微小变化量代表,实际上是一个线性函数
导数指的是函数在某一自变量方向上微小的函数值变化量与自变量变化量的比值,从这个概念上可以得到微分实际是导数与微小变化量的乘积,以二元函数 $z = F(x, y)$ 举例:
$$
\d{z} = \frac{\partial{F(x, y)}}{\partial{x}} \d{x} + \frac{\partial{F(x, y)}}{\partial{y}} \d{y}
$$
例题
若存在一个函数 $f(x, y)$,其对于 $y$ 的偏导数 $f_y(x,y)$ 在区域 $R$ 上有界,试证明函数在区域 $R$ 上满足 Lipschitz 条件。
根据题目条件:
$$
\exists L > 0 , \forall (x, y_0) \in R \rightarrow f_y(x,y_0) < L
$$
那么根据拉格朗日中值定理,在任意的两个 $(x, y_1), (x, y_2) \in R$ 之间存在一个点 $(x, \xi)$,使得:
$$
|f(x, y_1) - f(x, y_2)| < |f(x, \xi)(y_1 - y_2)| \leq L|y_1 - y_2|
$$
则函数在区间 $R$ 上满足 Lipschitz 条件
由此可以得到,如果一个函数满足导数在区间内有界,那么它一定满足 Lipschitz 条件
满足 Lipschitz 条件的函数我们称它在区间上 Lipschitz 连续
连续、一致连续、Lipschitz 连续
函数 $f(x)$ 在区间 $(a, b)$ 上连续:
$$
\forall x_0 \in (a, b) ,; \forall \epsilon > 0 ,; \exists \delta(\epsilon, x_0) ,; |x - x_0| < \delta \rightarrow |f(x) - f(x_0)| < \epsilon
$$
函数 $f(x)$ 在区间 $(a, b)$ 上一致连续:
$$
\forall x_1, x_2 \in (a, b) ,; \forall \epsilon > 0 ,; \exists \delta(\epsilon) ,; |x_1 - x_2| < \delta \rightarrow |f(x_1) - f(x_2)| < \epsilon
$$
Lipschitz 连续:
$$
\forall x_1, x_2 \in (a, b) ,; \forall \epsilon > 0 ,; \exists \delta = \frac{\epsilon}{L} \rightarrow |f(x_1) - f(x_2)| < \epsilon
$$
这三者条件逐渐变强,因此有:导数有界 > Lipschitz 条件 > 一直连续 > 连续
但注意,函数可导性与 Lipschitz 并无直接关系,不能相互推出
设某一个 Cathy 问题:
$$
(E): \frac{\d{y}}{\d{x}} = f(x, y), y(x_0) = y_0
$$
其中 $f(x, y)$ 在矩形区域
$$
R: |x - x_0| \leq a, |y - y_0| \leq b
$$
内 连续,并且对变量 $y$ 满足 Lipschitz 条件, 则 $(E)$ 在区间 $I = |x_0 - h, x_0 + h|$ 上又且仅有一个解 $y = \phi(x)$,其中:
$$
h = \min { a, \frac{b}{M} }, M > \max\limits_{(x, y) \in R} |f(x, y)|
$$
思路
首先分析 $h$ 的取值范围中必须要有 $a$ 是因为 $f(x, y)$ 是一个限定在矩形区域 $R$ 上的函数,而 $x$ 的范围由 $a$ 确定
而在闭区间上连续的函数 $f(x, y)$ 根据连续函数的有界性定理必定存在一个 $M$ 使得:
$$
M > \max\limits_{(x, y) \in R} |f(x, y)|
$$
之后我们采取如下步骤:
将微分方程转化成一个积分方程,并构造一个序列(Picard 序列)
$$
\phi_{n + 1}(x) = y_0 + \int_{x_0}^{x} f(t, \phi_n(t)) \d{t} ,;(x\in I) ,; (n = 0, 1, 2, \cdots) ; \wedge ; \phi_0(x) = y_0
$$
用数学归纳法保证构造的序列有意义,即:$|\phi_n(t) - y_0| \leq b, n = 0, 1, 2\cdots$
对两侧的 $n$ 同时取极限,保证 $\phi_n(x)$ 一致收敛到 $\phi(x)$,使得极限号能够穿过积分运算
关于点态收敛与一致收敛
首先我们来看点态收敛:设 ${ f_n }$ 是一组在 $D$ 上“收敛”的函数序列,如果:
$$
\forall x_0 \in D, \forall \epsilon > 0, \exists N, \forall n > N, |f_n(x_0) - f(x_0)| < \epsilon
$$
那么这种收敛被称为点态收敛,再来看一致收敛:
$$
\forall \epsilon > 0, \exists N, \forall n > N, \forall x \in D, |f_n(x) - f(x)| < \epsilon
$$
可以看到,在点态收敛中,$N$ 的取值与 $x_0$ 有关;而在一致收敛中,$N$ 的取值仅与 $\epsilon$ 有关。这就是点态收敛与一致收敛最本质的区别。换句话说,点态收敛要求在某一点上满足这个条件 —— 其根本是首先找到一个点,之后找到满足这个条件的 $N$,之后将所有满足该条件的点聚合起来称为一个收敛域;而一致收敛是找到一个 $N$,之后找到满足这个条件的所有 $x$,称为收敛域。
从几何上来说:
点态收敛要求函数列在收敛域上某个点的 $\epsilon$ 临域内都无限趋近收敛函数 $f(x)$
一致收敛要求函数列在收敛域上全部的图像都落在带状区域:
$$
{ (x, y) | x \in D, S(x) - \epsilon < y < S(x) + \epsilon }
$$通过这样的方式,一致收敛就能保证函数序列的连续性;相对应的点态收敛则不能保证。
证明一致收敛的函数 $\phi(x)$ 就是积分方程的解(存在性)
证明这个解是唯一的解(唯一性)—— 任取一个积分方程的解 $\Phi(x)$ 并证明 $\Phi(x) - \phi_n(x) \to 0 \text{ as } n \to \infty$
证明
Cathy 问题 $(E)$ 等价于积分方程:
$$
y(x) = y_0 + \int_{x_0}^{x} f(t, y(t)) \d{t}
$$
事实上,假设原方程的解为 $y = y(x)$,那么:
$$
y’(x) = f(x, y(x)) (x \in I) ; \wedge ; y(x_0) = y_0
$$
对上面的等式两侧在区间 $x_0, x$ 上进行积分:
$$
\int_{x_0}^{x} \d{y} = \int_{x_0}^{x} f(x, y(x)) \d{x} \Rightarrow y(x) = y(x_0) + \int_{x_0}^{x} f(t, y(t)) \d{t}
$$
积分区间选取 $x_0, x$ 是因为解是一个关于 $y = y(x)$ 的显函数,确定 $x$ 区间后可以将 $f(x, y)$ 限制在区域 $I$ 内
反过来,假设 $y = y(x)$ 是积分方程的解,那么:
$$
y(x) = y_0 + \int_{x_0}^x f(t, y(t)) \d{t} \Rightarrow y’(x) = f(x, y(x))
$$
因此, Cathy 问题 $(E)$ 解的存在及唯一性等价于积分方程在区间 $I$ 上有且仅有一个解
构造一个函数序列:
$$
\phi_{n + 1}(x) = y_0 + \int_{x_0}^{x} f(t, \phi_n(t)) \d{t} ,;(x\in I) ,; (n = 0, 1, 2, \cdots) ;\wedge ; \phi_0(x) = y_0
$$
关注命题 $|\phi_n(x) - y_0| \leq b ,, (n = 0, 1, 2, \cdots)$
当 $n = 0$ 时:$|\phi_0(x) - y_0| = 0 \leq b$ 成立
假设 $n = k$ 时: $|\phi_k(x) - y_0| \leq b$ 成立
当 $n = k + 1$ 时:
$$
|\phi_{k+1}(x) - y_0| \leq \int_{x_0}^{x} |f(t, \phi_k(t))| \d{t}
$$
因为函数 $f(t, \phi_k(t))$ 在区域 $R$ 上连续,那么根据闭区间的有界性定理,函数有界,既存在 $M > 0$,使得:
$$
\int_{x_0}^{x} f(t, \phi_k(t)) \leq M|x - x_0|
$$
而根据定理条件,$|x - x_0| \leq h \leq \frac{b}{M}$,代入上式:
$$
\int_{x_0}^{x} f(t, \phi_k(t)) \leq M|x - x_0| \leq Mh \leq b
$$
那么根据数学归纳法能够得到:
$$
\forall n , |\phi_n(x) - y_0| \leq b
$$
下来证明 $\phi_n(x)$ 一致收敛到 $\phi(x)$ 上,使用级数:
$$
\phi_n(x) = \sum_{k = 1}^{n} |\phi_k(x) - \phi_{k - 1}(x)| + \phi_0(x)
$$
对于第一项 $|\phi_1(x) - \phi_0(x)|$,$\phi_0(x) = y_0$
$$
|\phi_1(x) - \phi_0(x)| \leq \int_{x_0}^{x} |f(t, y_0)| \d{t} \leq M(x - x_0)
$$
对于第二项 $|\phi_2(x) - \phi_1(x)|$
$$
|\phi_2(x) - \phi_1(x)| \leq \int_{x_0}^{x} |f(t, \phi_1(t)) - f(t, y_0)| \d{t}
$$
使用定理中的 Lipschitz 条件:$|f(t, \phi_1(t)) - f(t, y_0)| \leq L|\phi_1(t) - y_0|$:
$$
\int_{x_0}^{x} |f(t, \phi_1(t)) - f(t, y_0)| \d{t} \leq L \int_{x_0}^{x} |\phi_1(t) - y_0| \d{t} = L \int_{x_0}^{x} |\phi_1(t) - \phi_0(t)| \d{t} \leq LM \int_{x_0}^{x} (t - x_0) \d{t} \
\leq \frac{1}{2}ML (x - x_0)^2
$$
对于第三项 $|\phi_3(x) - \phi_2(x)|$ 同理可得
$$
|\phi_3(x) - \phi_2(x)| \leq L \int_{x_0}^{x} |\phi_2(t) - \phi_1(t)| \d{t} \leq \frac{1}{2} M L^2 \int_{x_0}^{x} (t - x_0)^2 \d{t} \
= \frac{ML^2}{3!} (x - x_0)^3
$$
以此类推 可以得到:
$$
|\phi_n(x) - \phi_{n - 1}(x)| \leq \frac{M}{L} \cdot \frac{L^n (x - x_0)^n}{n!} \leq \frac{M}{L} \cdot \frac{(Lh)^n}{n!}
$$
对不等式右侧的数项级数进行判别:
$$
\sum_{n = 1}^{\infty} \frac{M}{L} \frac{(Lh)^n}{n!} = \frac{M}{L} \left( \sum_{n = 0}^{\infty} \frac{(Lh)^n}{n!} - 1 \right) = \frac{M}{L} (e^{Lh} - 1)
$$
收敛,那么级数 $\phi_n(x)$ 一致收敛于 $\phi(x)$
根据上面的结论:
$$
\lim_{n \to \infty} \phi_{n + 1}(x) = y_0 + \lim_{n \to \infty} \int_{x_0}^{x} f(t, \phi_n(t)) \d{t} \
f(t, \phi_n(t)) \rightrightarrows f(t, \phi(t)) \
\Rightarrow \phi(x) = y_0 + \int_{x_0}^{x} f(t, \phi(t)) \d{t}
$$
所以 $\phi(x)$ 为积分方程的解
任取一个微分方程的解 $\Phi(x)$,那么与上面的级数证明方法类似有:
$$
\Phi(x) = y_0 + \int_{x_0}^{x} f(t, \Phi(t)) \d{t}\
|\Phi(x) - \phi_{0}(x)| \leq \int_{x_0}^{x} |f(t, \Phi(t)) - \Phi(t)| \d{t} \leq M(x - x_0) \
|\Phi(x) - \phi_{1}(x)| \leq … \leq \frac{ML}{2}(x - x_0)^2 \
|\Phi(x) - \phi_{n}(x)| \leq … \leq \frac{(Lh)^{n + 1}}{(n + 1)!} \to 0
$$
可以得到 $\phi_n(x)$ 一致收敛到 $\Phi(x)$
所以 $\phi(x)$ 是原积分方程的唯一解
注意
Picard 定理又称为 存在唯一性定理,分析以下几个问题:
假设函数 $f$ 在 $R$ 上的最大值 $M$ 较小,满足:
$$
a \leq \frac{b}{M} \Rightarrow M \leq \frac{b}{a} \Rightarrow h = a
$$
而根据定义函数 $f = \frac{\d{y}}{\d{x}}$ 即反映了解在区间上的导数(也就是斜率)比较小
而如果最大值 $M$ 比较大,使得:
$$
h = \frac{b}{M}
$$
那么解在区间内的斜率就比较大
实际上,满足 Lipschitz 连续的函数,存在一个双圆锥(图中为白色)其顶点可以沿着曲线平移,使得曲线总是完全在这两个圆锥外:
函数连续 + Lipschitz 能够推出 存在唯一,但这是一个 充分不必要条件
这也就意味着,函数 $f$ 有可能是不连续的但是其解可能是存在唯一的;同样的,Lipschitz 条件也无法推出解的存在唯一性
当给定的矩形区域条件 $|y - y_0| \leq b \to \infty$ 时,$f(x, y)$ 在带状区域 $R: {(x, y) | x \in [\alpha, \beta]}$ 连续切满足 Lipschitz 条件,那么解 $y = \phi(x)$ 在区间 $[\alpha, \beta]$ 有定义
假设给定的微分方程有如下形式:
$$
F(x, y, y’) = 0 \wedge
y(x_0) = y_0, y’(x_0) = y_0’
$$
如果给定的隐函数可以显化,则可以使用上面得到的 Picard 存在唯一性定理
隐函数存在性定理
如果函数 $F(x, y)$ 满足:
- $F(x_0, y_0) = 0$
- $F(x, y)$ 在点 $(x_0, y_0)$ 的某一个邻域内有连续偏导数
- $F’(x_0, y_0) \neq 0$
那么方程 $F(x, y) = 0$ 在 $(x_0, y_0)$ 的某个邻域内存在一个唯一的函数 $y = f(x)$ 满足 $F(x, y(x)) = 0, y_0 = y(x_0)$,并且有:
$$
\frac{\d{y}}{\d{x}} = - \frac{\partial{F(x, y)}}{\partial{x}} \cdot \frac{\partial{y}}{\partial{F(x, y)}}
$$
而根据隐函数定理,在某一个邻域内的连续偏导数肯定是有界的,则 Lipschitz 条件被保证,所以只要保证隐函数存在定理就可以保证存在唯一解,综上所述,我们能够得到:
如果在 $(x_0, y_0, y’_0)$ 的某一个邻域中
则问题存在唯一解
对于满足 Lipschitz 条件的微分方程,我们可以使用 Picard 序列对解进行估计,例如:
$$
\frac{\d{y}}{\d{x}} = y \wedge y(0) = 1
$$
我们逐项分析 Picard 序列:
$$
\phi_0(x) = 1, \phi_1(x) = y_0 + \int_{x_0}^{x} f(t, \phi_0(t)) \d{t} = 1 + \int_{0}^{x} (1 + t) \d{t} = 1 + x + \frac{x^2}{2!} \cdots \
\phi_n(x) = 1 + x + \frac{x^2}{2!} + \cdots + \frac{x^n}{n!}
$$
显然结果是 $e^x$ 的泰勒级数
对于某些很难求解的微分方程,我们可以使用 Picard 序列对解写出级数形式进行逼近;而根据上面对解的唯一性的推导(证明的第五部分):
$$
|\phi_n(x) - \Phi(x)| \leq \frac{M}{L} \cdot \frac{(Lh)^{n + 1}}{(n + 1)!}
$$
我们可以对求得的解进行误差估计,或者求出指定精度的近似解,例如:
$$
\frac{\d{y}}{\d{x}} = x^2 + y^2 \wedge y(0) = 0
\
D = {(x, y) | |x| \leq 1, |y| \leq 1 } \Rightarrow a = b = 1 \
\frac{\partial{f}}{\partial{y}} = 2y \Rightarrow L = 2, M = \max_\limits{(x, y) \in D} = 1^2 + 1^2 = 2 \Rightarrow h = \min { a, \frac{b}{M} } = \frac{1}{2}
$$
如果我们要求解的误差不超过 5%
$$
|\phi_n(x) - \Phi(x)| \leq \frac{M}{L} \cdot \frac{(Lh)^{n + 1}}{(n + 1)!} = \frac{1}{(n + 1)!} \leq 0.05 \Rightarrow n = 3 \
\phi_0(x) = 0 \
\phi_1(x) = \int_{0}^{x} (t^2 + 0) \d{t} = \frac{1}{3} x^3 \
\phi_2(x) = \int_{0}^{x} (t^2 + \frac{1}{9} t^6) \d{t} = \frac{1}{3} x^3 + \frac{1}{56} x^7 \
\phi_3(x) = \int_{0}^{x} (t^2 + \frac{1}{9} t^6 + \frac{1}{168}t^{10} + \frac{1}{3136} t^{14}) \d{t} = \frac{1}{3} x^3 + \frac{1}{56} x^7 + \frac{1}{1848} x^{11} + \frac{1}{47040} x^{15}
$$
我们观察到,我们给定的定义域在 $[-1, 1]$ 上,而解却落在了 $[-\frac{1}{2} , \frac{1}{2}]$ 上;而如果我们给定的区间拓宽至 $[-2, 2]$ 上,那么:
$$
h = \min { 2, \frac{2}{2^2 + 2^2} } = \frac{1}{4}
$$
而根据函数定理,在某一个邻域内的连续偏导数肯定是有界的,则 Lipschitz 条件被保证,所以只要保证隐函数存在定理就可以保证存在唯一解。
可以看到,给定的连续定义域越大,解的域反而越小,这说明我们的解区间给的太小了。
下一节我们会对使用 Picard 序列求得近似解的误差进行估计,并探讨解的存在区间,借此引出解的延拓概念。
]]>首先,由于圆的内街三角形是在圆上任意取得三个点,我们需要按步骤确定每一个点对最终概率的影响。并且,显而易见,由于圆的大小并不影响这个概率,所以我们取一个半径为1的单位元方便运算。对于第一个点$P1$,显而易见不管这个点在哪里,对最终的三角形形状是没有任何影响的,那我们就如图1所示取一个方便观察的位置。
然后我们就需要考虑$P2$对三角形的影响了,首先我们把圆的边进行微分,然后把$P2$的位置看成是任意一个微分出来的小边$\Delta l$中的一个点,如图2。
我们可以得出,$P2$在圆任意位置的概率就等于当小边长度$\Delta l$趋近于无穷小的时候小边和圆周长之比:$$\lim_{\Delta l \to 0}\frac{\Delta l}{2\pi}=\frac{dl}{2\pi}$$接下来,我们就需要分析在$P2$固定的情况下,$P3$需要放在哪些地方才能使这三个点形成的三角形为锐角三角形了。
首先,我们如图3所示做几条辅助线。$P1P2Q2Q1$为一个圆的内接矩形,观察这个矩形我们可以看出当$P3$坐落于$Q1$和$Q2$时,$\triangle P1P2P3$是一个直角三角形。而当$P3$是优弧$Q1P1Q2$上的点时,$\triangle P1P2P3$为一个钝角三角形。由此可得,当且仅当$P3$是红线所标的劣弧$Q1Q2$(不包含端点)上的点时,$\triangle P1P2P3$为一个锐角三角形。
现在三个点的位置都已经分析清楚了,我们只需要对其进行运算就可以了。由余弦定理我们可以得到劣弧$Q1Q2$和劣弧$P1P2$是等长的,并且这个弧的长度与$P2$的位置相关,且可以看作是无数微分出的小边$\Delta l$从$P1$为起点累加所得,因此这个长度我们直接设置为$l$。因此在确定了$P2$的情况下(也就是确定了$l$),$P3$使得$\triangle P1P2P3$为锐角三角形的概率为$Q1Q2$的弧长比上圆的周长即$\frac{l}{2\pi}$。
现在就差最后一步进行积分了,但是我们要注意$P2$这个点一但从$P1$顺时针旋转到圆的另一半时,弧$Q1Q2$的长度就不等于$l$了。如图4所示,当$P2$移动到圆的左半边时,$P1P1$从劣弧变为了优弧(红线所标),其长度$l$不再是劣弧$Q1Q2$的长度,所以我们进行积分时只能取右半边做积分,但是圆左右部分是对称的,所以左半部分的概率可以看成是$P2$从$P1$逆时针旋转所得,所以$P2$不论在左边还是右边最后的概率是相同的,并且$P2$在左半部分的概率与在右半部分的概率是相等的,假设右半部分积分出的概率为$p$,那么实际的概率就是$\frac{p}{2}$,左边同样是$\frac{p}{2}$,因此总的概率就是$\frac{p}{2}+\frac{p}{2}=p$。也就是说只用对右半部分进行积分就可以得到总的概率了。
根据之前所得的式子我们直接进行积分得:$$p=\int_0^{\pi}\frac{dl}{\pi}\frac{l}{2\pi}=\frac{1}{4}$$
可以注意到这个式子中之前的$\frac{dl}{2\pi}$被换成了$\frac{dl}{\pi}$,这正是因为我们只对右半部分进行积分所进行的改变,其积分上限$\pi$而不是$2\pi$也对应着这个改变。
为了验证这个结果,我还专门写了一段代码来检测。如图5所示,程序不断随机在一个园内生成一个内接三角形,并且判断其是否为锐角三角形,如果是上面数字的右边的点会变为蓝色,如果不是就变为红色。Rate后面的数字分别是目前所有随机出的三角形中锐角三角形的个数和总的生成的三角形的个数以及其比例。
我们加快速度,并且截取超过5000次随机过程之后的比例。如图6所示,这个比值不断趋近于0.25与我们的运算结果相符。
附代码:
1 | import numpy as np |
因此我构建了这个系列文章,以及 Flaks 项目(没错就是 Flaks – 高射炮),它模仿了一些 Flask 框架的特性(路由、可配置、…)并添加了一个简单的 并行/异步 HTTP 服务器与 CGI 支持;在这个系列文章中会较为详细的讲解该框架的构建流程以及思路,希望大家喜欢。
这是这个系列的第三篇文章,本篇文章我们构建 HTTP 响应类。
本人才疏学浅,如果在文章中有任何错误,还请大家不吝指正。
这个系列文章将会由以下几篇文章组成:
socket
到 selectors
选择器HTTP
请求解析WSGI
与 CGI
支持HTTP
响应asyncio
的协程异步 I/O上一篇文章中我们介绍了 WSGI
与 CGI
的支持,在结尾小结处的框架图中我们可以看到,在应用层处理完毕之后我们需要对应用层的结果进行包装,生成HTTP
响应,进而交予底层的 socket
连接处理数据发送。
回忆一下,在第一篇文章中,我们使用了一个固定的 Hello World 字符串来向客户端返回一个简单的问候;借此,我们来分析一下生成 HTTP
响应所需要的信息,以及思考如何构建我们的响应类。
1 | RESPONSE = \ |
可以看到,如同 HTTP
请求一样,响应遵循着大致相同的格式:
HTTP
版本号HTTP
状态码CR-LF
字符之后添加需要的相应数据总结起来大致如此:
1 | [HTTP Version] [Response Code] [Response Message] |
因为我们这里只做 HTTP/1.1
版本的支持,而响应消息由响应代码可以确定,所以我们的响应类应该具有如下的功能:
int
dict
bytes/str
bytes
在 RFC2616 中已经详细的规定了 HTTP
响应头的相关规范,我们在 Flaks
项目的服务器部分不强制指定缓存与认证相关的功能,而是交由更上层的应用部分去处理;由此,根据标准,在头部必须指定的两个信息分别为:
Content-Type
- 指定响应内容类型Content-Length
- 指定响应的长度因此,这两个信息我们单独将处理:
结合上面所得到的功能,可以给出如下实现:
1 | class Response: |
在这里,我们在程序内部的常量定义中实现了几乎所有可能的 HTTP
状态码及其说明消息,所以在类的构造函数中可以对传入的状态码进行检查;当状态码不正确/未知时,向应用层掷出一个 InvalidHTTPResponseCode
错误。
Response
类的构造函数允许仅仅传入单个的状态码,这是因为在任何非 2xx 的响应中,根据约定,不应该有任何的返回数据被传输。
在最后,我们的 Response
类提供了一个 done
方法,该方法的返回值是字节流 - 也就是打包好之后的响应,它将被交付给 socket
层进行客户端传输:
1 | def done(self): |
到此为止我们这个简单 HTTP
服务器的所有基础架构都已经完成。现在,它可以实现以下功能:
socket
Request
类实例Response
类实例CGI
与 WSGI
应用支持但是需要注意的是,除了第四点,剩下的三个部分是相互分离的的。这也就意味着,我们最重要的一个部分还没有完成 - Application
应用层,它将负责把以上的所有功能进行耦合,并提供给上层用户一个接口,用以实现“复杂的” Web
应用程序构建。
换句话说,我们已经实现了一个功能完备的 HTTP
伺服器。在下一篇文章中,我们将对前三篇文章的所有产出结果进行调用,并增加最重要的路由绑定与视图函数处理功能。
他们还是他们,我还是我,不过走上了截然不同的路。
恰同学少年,风华正茂,书生意气,挥斥方裘
《沁园春 · 长沙》
记得当时我还是一个刚上大学的小伙子,有着无穷无尽的想法。虽说那阵子比较胖,但是仿佛还是拥有无限的活力,做什么都是有动力的。
抱着一定学计算机想法和梦想,也算是一种执念吧;我光荣的混完了大一,以挂科植物学的“代价”换来了转系考试不错的成绩。
但是人生就总是这样:努力追求、一直想得到的却偏偏得不到;因为原专业挂科一门的原因,我最终还是没能去的了自己想去的专业。
作出出国的决定也是很突然,当时觉得自己反正也已经一无所有,再出去闯闯也不会再怎么样。现在想起来,书生意气这个词说的可是真的准确。
不知不觉,四年已经过去了,不得不让人感慨时间过去的太快。
时间之矢从来都是向前飞驰,何时曾为某人停下来过?
看着他们在操场上放声高歌,青春的欢乐混合着离别的伤感仿佛能透过屏幕。说不定在另一个平行世界中,我也是其中的一员。
如果真的是这样,那个我会是什么样子呢?
和同学前一段时间走在彼得堡街头上,他说了一句,有时候真的是恍如隔世,好像根本就不知道自己是怎么来到这里的。
是啊,一个人在外面待的时间长了,经常会有这种恍如隔世的不真实感;好像另外一个平行世界的我打破了那未知的隔阂,在这个世界与我相遇,诉说着他现在的生活。
要是真能遇到他,我倒是好想和他说道说道:在这条路上的我,相比他而言,有何得到,又失去了什么呢?
有时真的得感叹命运的安排,如此巧妙。
因为想要高考之后留在本地大学,选择了西农的我,却被迫离家万里求学;因为项目和学习压力归来的我,却偏偏赶上了史无前例的疫情爆发。
但不知是否冥冥之中有上苍保佑,令我多避开各种障碍,虽然走在弯路上,却也不比直道上的行人慢下许多。
再过一两个月就要走进23岁的生活了,不知不觉博客也写了7年了。新的日子里,不求命运之轮偏待我,只愿自己能不忘初心,上下求索吧。
]]>因此我构建了这个系列文章,以及 Flaks 项目(没错就是 Flaks – 高射炮),它模仿了一些 Flask 框架的特性(路由、可配置、…)并添加了一个简单的 并行/异步 HTTP 服务器与 CGI 支持;在这个系列文章中会较为详细的讲解该框架的构建流程以及思路,希望大家喜欢。
这是这个系列的第三篇文章,本篇文章我们实现服务器的 CGI 与 WSGI 支持。
本人才疏学浅,如果在文章中有任何错误,还请大家不吝指正。
这个系列文章将会由以下几篇文章组成:
socket
到 selectors
选择器HTTP
请求解析WSGI
与 CGI
支持HTTP
响应asyncio
的协程异步 I/O上篇文章我们对从 socket
服务器发送过来的请求数据进行了处理,并将其封装成了 Request
类;在这里,首先关注一下类内部都封装了哪些信息:
1 | class Request: |
在类的构造函数 docstring
中,我描述了封装的全部信息以及其意义。其实 WSGI&CGI
要做的也就是将这些信息传递给程序的其余组件/其他进程。
CGI
- Common Gateway Interface.
它并不是一种什么的特殊编程语言,只是编程语言之间用来通讯的一组协议。后端程序经常会涉及到多个组件,而这些组件很有可能是由不同的编程语言构成。我们一般在不同的编程语言之间进行通讯的方法有以下几种:
Web
接口,使用 HTTP
协议发送 JSON
数据socket
接口进行通讯CGI
和上面的这些方法一样,用于在不同的程序之间传递信息,只不过它传递信息的载体比较特殊一些 —— 环境变量。
文章的开头我们详细的列出了在 Request
类中所封装的信息,而 CGI
标准需要传递的信息有如下这些:
名称 | 意义 | 对应 Request 类中的封装变量 |
---|---|---|
CONTENT_TYPE | 所传递来的信息的MIME类型 | self.headers["Content-Type"] |
CONTENT_LENGTH | 标准输入STDIN中可以读到的有效数据的字节数 | self.hedaers["Content-Length"] |
HTTP_COOKIE | 客户机内的 COOKIE 内容 | self.cookie |
HTTP_USER_AGENT | 包含版本数或其他专有数据的客户浏览器信息 | self.headers["User-Agent"] |
PATH_INFO | 紧接在CGI程序名之后的其他路径信息 | self.path |
QUERY_STRING | 如果服务器与CGI程序信息的传递方式是GET,变量的值即使所传递的信息 | self.query |
REQUEST_METHOD | 提供脚本被调用的方法 | self.method |
SCRIPT_FILENAME | CGI脚本的完整路径 | self.path |
SCRIPT_NAME | CGI脚本的的名称 | self.path.split('/')[-1] |
SERVER_NAME | WEB 服务器的主机名、别名或IP地址 | self.headers["Host"] |
SERVER_SOFTWARE | 调用CGI程序的HTTP服务器的名称和版本号 | settings.SERVER_NAME |
在以上信息全部都提取到出来之后,只需要将他们全部放在 os.environ
即可(所有的信息全部转化为字符串)。这样我们就完成了 CGI
协议的支持。
但是有朋友可能会问,那到底怎么运行程序呢?下面,我们使用 subprocess
并行化的处理 CGI
脚本的执行。
首先,CGI
脚本可能以任何语言写成,一般在解释型语言的脚本首行,会看到以 #!
开头的一行代码;这个东西叫做 shebang
,它的本意就是指示 exec
函数以什么程序去执行这段代码。在 CGI
标准中这个东西同样适用。
所以我们需要读取首行的 shebang
获取执行的程序:
1 | with open(scriptfile, "r") as handler: |
获取到 excuter
变量之后,通过 subprocess.Popen
去执行它即可:
1 | process = subprocess.Popen((executer, scriptfile), |
可以看到,通过 Popen
函数的 env
变量将上面的环境变量传递给了新的进程,而 stdout
使用管道将 CGI
脚本的输出返回给我们程序(简单地说就是那边打印什么,这边得到什么)
之后,我们设置 CGI
脚本最大的执行时间为 settings.CGI_TEMOUT
,这样可以防止脚本中的死循环/无意义长时间I/O等待。当超时时,首先杀掉执行的进程,之后向客户端发送 408
响应;
而如果脚本的执行发生了错误,process.returncode
则会不为 0
。这时向客户端发送 502
响应;
如果一切正常,构建一个 HTTP 200
的响应并将脚本执行的输出返回给客户端即可。
这样我们就构建了一个并行化执行的 CGI
服务器,而关于响应类的封装,我们下一篇文章再讲。
WSGI
和上文所述的 CGI
本质上都是一样的,也是通过系统的环境变量传递 Request
类所封装的信息(并且需要的信息大多也都相同);唯一的区别点在于 WSGI
仅针对 Python
程序进行通讯,所以可能会多出几个字段(wsgi.multithread
, wsgi.multiprecess
, …)来描述关于并行的相关信息。
所以和上面的流程一样,只需要将符合 WSGI
规范的信息全部导出到环境变量即可,为了节省篇幅我不再重复。
而 WSGI
其实是定义在 PEP 3333
中的:PEP 3333 – Python Web Server Gateway Interface. 我在 request.py
中的 _set_environ
函数中也有较为详细的描述,有兴趣的同学可以看 这里。
在程序的扩展性方面来说,WSGI
与 CGI
不过是程序之间通讯的一种协议,通过对它们的支持,我们的程序可以轻松的与很多现有的框架/程序进行对接,大大的增强了我们程序的扩展性。
而在 Web
服务器的架构方面,它们更重要的意义在于:实现了 HTTP
服务器与 Web
服务器的完全解耦。我制作了一张简单的图片来说明到我们框架的大概构架:
我们大部分的工作业已完成,下一篇文章将对 HTTP
的响应进行封装。
文章图片较多,但是我相信大家都有钱,不担心流量问题。
我是16号在 Apple 官网下的单,18号早上就收到邮政的快递了。这次中国邮政的投递效率令我瞠目结舌。那天下午从快递柜拿到它的时候,心里还是很激动的,所以回家消毒之后一连拍了好几张图片:
不得不说,苹果在外包装上还是做的蛮用心的,包括底部的贴条还有拆箱时候的阻尼感,一直都让我有种 花钱 舒适的感觉。
外观方面,我这次购买的是与 Nike 的联名款,所以盒子和外包装都是黑色的。而在手表的主体背面上也印有 Nike 的标志(背面太难拍到了故放弃…)
至于手表的配色嘛,如果大家不差钱可以随便购买;但是如果向我一样贫穷的话,还是推荐铝合金的深空灰配色。
有同学可能会好奇 Nike 版本和普通版的区别,我才开始下单的时候就买成了普通版,后来想着买多不买少,就又撤单后改成了 Nike+ 版本。这个版本的唯N区别就是:
关于表带我们在最后一部分外设中聊。
先说结论:Apple Watch 的日常使用我觉得还是比较满意的,配合上苹果调教的 Taptic Engine 马达,转动 Digital Crown 的感觉十分完美。我下面再几个方面谈谈这块表的日常体验。
我买这块手表的最重要原因就是个人有运动的习惯,平常涉及到的项目会有力量训练、慢跑、还有徒步。在运动时候我比较希望能了解到自己身体的一些状态信息(心率,燃脂量,配速,步伐…)
而这块手表很好的满足了我的需要,在 Watch 自带的运动训练 App 中有很多种的运动类型(一般运动训练时我选择瑜伽进行记录),如果没有的话还可以从内建的选项中添加,基本上你能想到的运动项目苹果都帮你考虑到了。
你还可以像我一样设置运动模板,例如燃脂量、速度等等指标… 在达到目标/低于指定强度的时候会给你通知,这一点我也是很喜欢。
运动时候的统计也是比较准确的,watch使用心率数据和身体其他的数据预估燃脂量,只是令人摸不着头脑的是,我总是能收到低心率通知…
还有一点我比较喜欢的地方 —— 在静坐一个小时后,手表也会提醒你起来站一会。用 Water Minder 之类的软件提醒自己没事多喝点水,这对我们这种经常坐着工作的程序猿来说还是很有用处了(可以每个小时给自己10min的摸鱼时间哈哈)。
总之这块表在运动与健康方面的使用体验让我觉得无可挑剔,看着每天的三个圆环渐渐的填满,真的会有一种兴奋的感觉,而这种感觉会持续的推动你去进行体育锻炼,保持健康的生活状态(对我来说最难的反而是站立时间 —— 太爱睡懒觉了)。
如果硬说有什么问题的话,就是走路时对于开始运动提醒的敏感度太高了,经常走一会路就让你提醒开始记录运动,没办法我就把这个功能给关闭喽。
受限于国内的使用环境,能够直接使用 Apple Pay 的地方真的很少,所以安装一个支付宝是必须的,支付宝在 WatchOS 端做的还是可以的,网上普遍说的付款码加载慢的问题我还没有遇到过,基本在一秒之内就可以加载出来,确实比拿手机支付要方便多了。
现有的问题就是用手表去扫码看起来怪怪的(超市大妈白眼已经准备好),而且如果是固定扫码枪,那姿势可是对手臂柔韧性有点要求…
而关于 Apple Pay 我在国外的时候一直都在使用,现在在国内的使用也仅限于用那个京津冀一卡通坐个小巴或者地铁了,这一点还是蛮方便的(再次吐槽垃圾扫码,不明白为什么会有这种脱裤子放屁的技术)
AW 另外一个对我很重要的功能就是大大减少了我把手机拿出来再放回去的操作,所有的 iPhone 的通知可以自定义的同步到 Watch 上,而因为是抬手唤醒,在唤醒的时候可以直接查看到通知,如果是非重要通知可以直接选择无视(例如联通和银行的垃圾短信),而如果需要回复则可以拿出手机。
WatchOS 端也有微信,可以进行一些简单的收发操作,但是表情之类的暂时还不行。哦对了,微信支付在手表端也是不可用的。
但是长期使用我发现了一个问题 —— 通知的有效时间很短。我们常常会遇到一种情况:戴手表的那只手正在被占用,这时来了一条消息,但我们可能得过半分钟才能查看,等手闲下来了之后,消息通知还得从通知栏中下拉读取,确实有些不方便,希望之后的 WatchOS 可以加入用户的设置项改进这个问题。
由于我购买的是GPS版本,所以不能在没有手机的情况下直接连接到网络听歌 —— 我购买非蜂窝的版本是因为想着自己将来主要是在健身房进行室内跑步的。但是由于这段时间疫情,健身房全部都关门了,所以只能在室外进行跑步。
但是理论上来说是可以把歌曲下载到 WatchOS 版本的网易云音乐离线听歌的,可是我平常并不使用网易云,而是Youtube Music(奈何买了 Youtube Premium)
在这种情况下用 Watch 去控制音乐就没有多大意义了,因为手机就在手上。不过可以在 “正在聆听” 这个 App 中控制音量,这在平时可以不把手机拿出来控制音量,算是一个比较方便的点吧。
另外我曾经尝试把账户转回国区用来开通 Apple Music,可是账号被锁区了,也不知道为什么,遂放弃。
这个可以说是 Apple Watch 最受人诟病的点了,但是我个人觉得其实还好。因为我个人的习惯是每天睡觉前把电都充上;而且最近一直在家,我一般的策略是晚上一直佩戴,第二天早上上厕所的时候充上电,之后去睡懒觉😂
而且 Apple Watch 的充电速度还是比较快的,虽说是祖传的五福一安,但是因为电池比较小的缘故,基本一个半个小时左右就充电差不多了(从30%左右)所以一般还是不担心电量问题的。
我分享一下自己的使用:每天进行1h左右的训练,中午11点左右戴上100%,到第二天早上准备充电的时候基本还有40~50%左右的电量。
我现在手上的苹果设备除了上面说的一部 iPhone,主要就是 AirPods 了,所以分享一下和它协同使用体验。然后聊聊表带。
不得不说,苹果把自己产品的切换使用体验做的真的是很完美了,如果你用 AirPods 听歌,音源从手表来的话,你身边的苹果设备其实都可以控制你的耳机音量并且切换歌曲。就像我上面所说关于音乐的使用体验。
而 AirPods 本身就可以双击用来控制歌曲播放,所以总体来说的使用是很棒的。
表带方面,我强烈推荐Nike的荧光尼龙表带,很漂亮,还有夜光效果。并且佩戴也比较方便、没有皮肤刺激性、透气性也好。
后来因为表带脏了需要清洗更换,但官网的价格确实比较贵,于是再狗东上购买了第三方的,价格很便宜。本来没想体验多好,可是到了之后的实际使用感觉还不错,就是比较短。
还是推荐大家购买透气性好一些的表带,当时去体验店佩戴默认的表带,不一会就出汗了,感觉不舒服。
以上就是我一个月的 Apple Watch 5 使用体验。总体来说,如果朋友有比较多的运动/健康需求,并且希望摆脱手机控制的话,这是一个非常不错的设备。如果朋友们仅仅是需要NFC、接受消息提醒而不回复,也没有运动需要,就没有必要购买这个设备了。
没有很迫切需求,我推荐大家可以再等等,毕竟过一段时间 S6 就出来了哈哈。
]]>因此我构建了这个系列文章,以及 Flaks 项目(没错就是 Flaks – 高射炮),它模仿了一些 Flask 框架的特性(路由、可配置、…)并添加了一个简单的 并行/异步 HTTP 服务器与 CGI 支持;在这个系列文章中会较为详细的讲解该框架的构建流程以及思路,希望大家喜欢。
这是这个系列的第二篇文章,本篇文章我们使用探讨 HTTP 请求的解析过程。
本人才疏学浅,如果在文章中有任何错误,还请大家不吝指正。
这个系列文章将会由以下几篇文章组成:
socket
到 selectors
选择器HTTP
请求解析WSGI
与 CGI
支持HTTP
响应asyncio
的协程异步 I/O上一篇文章我们已经构建了一个简单的 socket
服务器,通过 selectors
和 threading
的使用使他能够异步+并行处理请求。而从服务器中获取到的活动连接以及客户端信息作为参数传递给了 _handler
函数,并由它向应用层传递。
从这篇文章开始,就进行到框架的设计过程。今天,就来实现应用层的重要功能之一 —— HTTP
包解析,并将其封装到我们的 Request
类中。
除此之外,今天将会介绍 WSGI
与 CGI
标准,并在 Request
类中实现对它们的支持。
首先我们来关注标准的 HTTP
请求格式:
1 | [Method] [URL] [HTTP Version] |
可以看到,HTTP 请求分为三个部分:
Request Info
信息Headers
信息Request Data
需要注意的是,这里所有的换行 实质上是两个字符:CR-LF
,也就是\r\n
第一行的请求信息组成很简单,在这里给出一个例子:
1 | GET /user?name=%e6%a1%82%e5%b0%8f%e6%96%b9&age=21 HTTP/1.1 |
可以看到,以空格分隔的第一个信息位请求方法,第二个而请求路径,第三个指定协议版本。在这里设计 Request
类的时候需要只注意两个点:
Method
是否在全部允许的 HTTP Methods
里URL
中含有非 ASCII
字符时,需要进行 URLDecode
对于第一点,我们可以简单的在框架中加入一个字面量集合,我在 const.py
中加入了这组常量:
1 | ACCEPT_METHODS = { |
多说一句,为什么要设置为集合呢?因为集合的本质是 Hash Table
拥有 O(1)
的访问时间。
对于第二点,我们既然是从零开始制作框架,想要在尽量减少依赖的情况下进行设计,所以决定自己构造一个解码函数(不使用 urllib
和正则表达式)。
根据 HTTP
标准,默认的编码字符集是 UTF-8
,所有的非ASCII
字符都会被编码成 %xx
的形式,所以问题的本质相当于在字符串中寻找以 %
开头的最大连续子串,并从相应的位置进行替换。
而再在这里只需要从左至右单向的遍历,因此可以用一个栈来保存已经解码完成/不需要解码的字符串,使用 (bytes.fromhex()).decode()
函数解码十六进制字符串。这里给出在 request.py
中的 unquote
函数:
1 | @staticmethod |
在这之后,我们可以将请求的首行 parse
成好几段,对于上面这个例子来说:
/user
{"name": "桂小方", "age": "13"}
GET
HTTP/1.1
将他们装载到我们的 Request
类中,就可以通过实例化的 request.args
等去查看关于请求的信息了。怎么样,是不是感觉和 Flask
越来越像了呢?
接下来是对HTTP
头信息的处理。其实头部是一个简单的键值对而已,如果你用 curl
请求本博客,就会看到请求的头部信息如下:
1 | HTTP/1.1 301 Moved Permanently |
所以在这里我们仅仅需要以 :
对头部信息进行分割,之后存入headers
字典即可:
1 | try: |
当请求的方法为 POST
UPDATE
等可以上传资源的方法时,在以上两者信息结束之后会产生一个空行(CR-LF
),并在空行之后给定请求的主体。对于服务器来说,请求主题仅仅是一段无意义的字符串,但是之后要做的事情是根据上文中获得的 Headers
信息对这段信息进行处理,例如:
当 Headers
中的 Content-Type
指定为 text/plain
时,不需要对请求主体做任何事情,但如果是 application/x-www-form-urlencoded
时,则需要进行解码(依照上文的 url_unquote
方法)。
在这里可以建立一个类似向量表的东西,存储在类的静态变量中,将给定的值与对应的操作函数进行绑定,这样就可以扩展性的实现请求主体的处理,例如:
1 | __body_handler = dict({ |
而对于使用者来说,可以轻松的添加一个针对 POST
信息的扩展:
1 | Request.register_body_handler( |
将上述处理过后的信息存入request.body
中,后面就可以通过这个接口访问请求的 body
信息了。
至此,我们将 HTTP
的请求头部信息处理完毕了,具体的全部实现代码大家可以去 Flaks 的 request.py 中查看,下面一篇文章我们将介绍 WSGI
与 CGI
,并使用已经解析过后的头部信息实现对这两个标准的支持。
因此我构建了这个系列文章,以及 Flaks 项目(没错就是 Flaks – 高射炮),它模仿了一些 Flask 框架的特性(路由、可配置、…)并添加了一个简单的 并行/异步 HTTP 服务器与 CGI 支持;在这个系列文章中会较为详细的讲解该框架的构建流程以及思路,希望大家喜欢。
这是这个系列的第一篇文章,本篇文章我们使用 Socket 模块搭建一个简易的 Web 服务器。
本人才疏学浅,如果在文章中有任何错误,还请大家不吝指正。
这个系列文章将会由以下几篇文章组成:
socket
到 selectors
选择器HTTP
请求解析WSGI
与 CGI
支持HTTP
响应asyncio
的协程异步 I/O从根本上讲,HTTP
协议只是一套指定消息格式的语言,通过这一套语言允许我们在两台计算机之间发送有意义的信息(也就像我在之前的文章中说过的 —— 协议就是计算机之间的语言)。
我们都知道 HTTP
协议是工作在 TCP
协议之上的(当然现在有更新的QUIC,在此我们不讨论它)。根据 OSI 的分层模型,TCP
作为工作在更底层的协议对上层 HTTP
的数据报进行进一步封装,于是在 HTTP
开始工作之前,我们先要确保两台计算机之间已经建立了 TCP
连接。
而数据仅仅交给计算机硬件是没有意义的,我们需要进程来对数据进行处理。这样就相当于在服务器-客户端的两个进程之间建立了连接,我们将这样的一组唯一的连接信息:(进程PID+端口号+地址)抽象成为 socket
,并对 TCP
进行了进一步封装,这样我们就可以在两台计算机的不同进程之间发送消息了。
所以很多朋友不用把 socket
想的过于神秘和特殊,它和其他的 IPC (Inter-Processing Communication) 方法的本质是相同的(其他的有类似管道、FIFO管道、Shared Memory 等等),都是提供了在不同进程之间交换信息的方法。
而在 Python 中有开箱即用的 socket
库可供我们使用,我们一般仅仅需要使用这样的代码就可以侦听一个端口上的连接请求并在有请求时建立与客户端的连接:
1 | import socket |
需要注意的是,因为 TCP
是基于数据流的协议,我们需要指定获取的数据报最大长度(也就是 BUFFER
),而超出这个大小的数据就会被忽视(当然我们也可以不断地获取直到一个包被完整的发完),这也就是很多包括 Nginx 之类的伺服器会要求指定一个包的最大长度的原因,超出则会直接返回 413 Too Large 之类的错误。
上面的代码已经可以工作了,在浏览器中访问:127.0.0.1:65432
,没有其他问题的话,你应该已经可以看到一个亲切的 Hello World
问候了。
正如你所见,HTTP
协议的工作并不复杂。而关于响应的内容,我们暂且留到下一篇文章再说。除此之外你可能发现了,我们的代码仅仅只能运行一次,第二次需要重新启动,而且有一定几率出现打开失败。
这是因为我们在这里仅仅监听了一次连接请求,要解决这个问题,我们只需要在连接外面加上一层 while True
即可。
但是这样还不够,我们的一个端口仅仅只能处理一个连接,如果同时有多个浏览器请求我们的“服务器”,则他们的请求就会被全部丢弃,这并不是我们想要看到的。
在 socket.listen
方法中有一个可选项 —— maxsize
,通过该选项,我们可以指定同时等待处理的连接个数,这样我们对上面的代码进行修改后就可以处理多个连接了:
1 | import socket |
我们不满足于此,因为这样毕竟还是一个一个请求被处理的,我们想要更高效的处理请求,基于以下思路:
而在操作系统内部其实已经有相关的组件帮我们实现了这样的功能(类似协程),这个实现在 Windows 平台是我们熟悉的 select,在 Linux 平台的默认实现是 epoll
. 而我们在应用过程中,可以使用 selectors
库,帮助我们实现对应的操作。下面我截取一段 server.py
中的代码进行说明:
1 | def start(self) -> NoReturn: |
其中,listener
是我们的侦听连接、_poll
是我们的事件连接池、_sock_accept
是当有可读事件发生时的回调函数。
我们可以看到,对于 selector
类型的实例,我们可以使用 register
将其绑定在事件管理器上,让它来提我们检测连接的相关事件。
而我们知道 socket
的本质也是一组文件描述符,所以 readble
的事件对它来说就代表在侦听端口上有新的消息到来(也许是新的连接请求,也许是以往的请求有新的数据发送…)
于是我们可以这样编写回调函数:
1 | def _sock_service(self, connection: socket.socket, mask: int) -> NoReturn: |
考虑到新的连接一旦被建立之后就不会再次建立了,我们将建立之后的连接的新可读事件绑定在 _sock_service
函数;而这个回调函数获取到信息之后将连接和客户端信息转发给 _handle
函数,它将会从连接缓冲区中读取消息,交予上层的应用层处理。而 _handle
函数的返回值则作为请求的响应值进行 sendall
操作返回给客户端,这样我们就完成了一个 HTTP
请求。
我们已经可以高效的异步处理来自客户端的请求了,现在我们假设 _handle
函数进行了如下操作:
1 | def _handle(self, connection, client): |
同样是返回 Hello, World 问候,我们在其中加入了一个长达10s的休眠,用它来模拟一些非常长时间的I/O、计算操作。这时如果我们在浏览器中打开上述的地址时,会发现需要等待10s+的时间才可以,但是意外的,在这10s之内,我们试图打开新的访问请求也被延后了…
我们需要注意:selectors
内部的事件是由操作系统替我们调度的,而当事件被交予处理之后的延迟,由于我们的Python程序仅有一个线程,当这个主线程被阻塞之后,任何的事件都会被搁置处理,直到程序回复响应。
所以思路很简单,我们将对于每一个客户端请求使用一个独立的线程去执行即可:
1 | import threading |
我们给 _sock_service
方法使用上述定义的 thread
装饰器之后,每个请求就可以单独的使用一个线程去处理了,这样也就实现了并行(当然由于GIL的问题,这个并行也是切分时间片的),这里我画了一个简单的流程图供大家理解架构:
当然现在有了更先进的 asyncio
为我们提供了原生协程,包括 handle
函数中的 I/O 调度,后期我们会尝试用它来改写这一版的服务器。
现在我们已经可以很好的获取到来自客户端的请求,下一篇文章将要实现对 HTTP
请求的解析。
但昨天,我在网络上看到鲍某涉嫌性侵14岁养女的新闻,当我点进南风窗的详细报道之后,我被我看到的触目惊心的事实所震惊了 —— 我感受到了出离的愤怒,我无法再继续保持沉默。
我向我身边的很多朋友都谈起了这件事情,表达了自己的愤怒。
但除了愤怒之外,我们还能做些什么呢?
如果你在Google上以 “Sexsual abusement againist children” 作为关键字进行搜索的话,你会发现大量的相关搜索结果,有非常多的公益组织和团体 —— 他们有政府组织,非政府组织、甚至个人都在以各种方式关注着针对儿童的性暴力行为,并以各种方式提供援助,帮助孩子们脱离困境。
在来自 NSVRC (National Sexual Violence Resource Center) 2015 年的 一篇调查报告 中指出:
One in four girls and one in six boys will be sexually abused before they turn 18 years old.
每四个未成年女孩和每六个未成年男孩中,就有一个遭受过不同形式的性暴力。
而如果以中文关键字 “中国儿童性暴力” 进行搜索,除了联合国儿童基金会 (UNICEF) 之外,很难找到相关无论政府/非政府组织的帮助/说明页面;而关于调查数据,也许是我能力有限,我只在 知乎共青团中央的一篇回答 中找到这样的数据:
2015年中国农业大学方向明教授在向世界卫生组织做的一次报告中,运用本土研究数字估计,中国9.5%的女孩和8%的男孩遭受某种形式的成人性侵,从猥亵到强奸。
另据最高人民法院的数据显示,2013年-2016年的4年间,仅全国法院审结的性侵害儿童案件量就达到10782起,换算下来,平均每天有超过7件;也就是说,至少每天有超过7名儿童被性侵害。
人性都有缺陷,人类都有欲望;可我实在无法想象,在这一个个数字的背后,有多少双罪恶的双手伸向了尚未成年的孩子们,而又有多少孩子因此在心中留下了一辈子的疤痕,甚至提前终结掉了自己幼小的生命。
由于文化原因,中国人向来不善于公开的讨论性话题。因此,在儿童以及未成年人方面的性教育就更是缺失。以我自己为例,我从小到大完全没有接受过哪怕一分钟的正式性教育,所有的性知识,都是在几乎踩雷之后才知道。
同样的,对于成年人的性权益,我们给予的关注也相对太少了,每次的新闻过后,有多少受害人能够免于二次伤害,又有多少恶魔重新返回人间了呢?
我们个人的力量是有限的,但是如果我们所有人都能够更多的关注儿童的性权益(也包括成年女性的性权益),也就能促使政府、乃至社会慢慢重视起来这个话题。长此以往,我们的社会终将有所改变。
图片来自联合国儿童基金会,此处非作盈利目的,如有侵权请告知,我将立刻删除
更令我震惊和不能接受的是,当昨天我和我身边一位我非常敬重的朋友提起这件事并表达了自己的愤怒时,她看完了那篇报道之后,竟然说出了这样的话:
事情也许没有报道的这么简单,说不定是姑娘自愿的呢!你还是把社会想的太善良了。
我当时情绪十分激动,完全不知道该用什么样的话来反驳她,因为这种观点真的让我产生了生理上的不适;换句话说,让我感觉恶心。
我无法知道,是什么样的动机让她做出了这种假设,但令我更加难过的是,这个人是我十分尊重的朋友。
假使她的这种假设是正确的(在事实清楚之前,我不相信这种假设),但是我依然无法理解这种假设的动机 —— 我们为什么要去怀疑一个陌生人,难道人类已经没有了最基本的正义感吗?
最近在YouTube上经常听罗翔老师的刑法课程,其中有一段关于正义客观性的辨析令我印象深刻,它大致是这样的:
我们每个人都会经验到一些不正义的事情,我们内心深处都渴望有正义概念的存在。而人类所有的思考其实都是建立在相信的基础上,我们相信存在正义,而正义一定是客观存在的。
而怀疑主义,则是人类发展的枷锁,因为你用来构造怀疑主义的大脑,本身就应被怀疑。
在这之后到今天的一段时间,我也在反思自己,我甚至开始认为自己的思考是错误的。但是,直到现在为止,我都被内心深处的那种正义感所驱使,每每我看到关于这个事件的报道时,我都确认我做的、我所思考的是正确的。
因为,它来自人类内心深处那对于善良的根本向往。
但是,是什么原因导致我们在成长过程中越来越麻木,以至于能见死不救、见到跌倒的老人不去搀扶呢?
我认为是人性之恶。
每当我们看到做出善举的人没有得到应有的回报时、每当我们看到搀扶老人的热心人被讹诈时,我们会感到害怕,我们会不知所措 —— 人性的恶,便从那一两个做了亏心事的被帮助的人身上传播开来。
你问我今天是否敢于去搀扶跌倒的老人时,我也许也同样会给出否定的答案。
所以不可否认的是,恶的传播远远比善的播撒要快得多。长此以往,我们便会在成长过程中慢慢丢失了对善的感知,而变得越来越麻木,甚至在很多情况下做出错误的判断。
那能不能说我们彻底丧失了善呢?答案是否定的,当大环境逐渐变得温暖起来时,心中的善便会向种子一样重新萌发,慢慢的战胜人性之恶。
截至我写这篇文章的时候,微博上关于这个事件的热搜已经消失了,我不清楚微博的热搜排名是什么机制,但是我希望这背后不要有资本的力量来过多操控。
只是讽刺的是,前一段时间的N号房间事件又重新站上了热搜的第一名。我们不能忘记,因为它们就像是悬挂在我们头上的警钟,时时刻刻提醒我们铭记着有多少人因为性暴力,因为人性之恶而遇害。
同样的,我也希望世界能给星星一个公道,让她不要在这个温暖的春天再次感受到来自人性之恶的彻骨寒冷。
以上仅是我的个人观点。
]]>终于,苹果用户可以在西安公交上使用 NFC 进行支付了,但这也是有代价的。
—— 我自己说的
4月9号,我偶然看到新闻说 Apple Pay 可以添加新的一种交通卡片 —— 京津冀一卡通,我本来想着和自己也没什么关系 —— 京津冀,这名字听起来和西安就没有什么关联。我又向下翻了翻,突然看到:
该卡片将能够在全国范围的公交内通用
当时心中不觉一惊,难道我那艰苦的刷二维码日子要结束了吗?
抱着试一试的心态,我在手机上开通了这张卡片,充值了10元进去,下午跑完步便高高兴兴的去坐公交车了。
上车,将手机缓缓地靠近读卡器,只听见“叮”的一声,我完成了人生中第一次用 Apple Pay 坐公交的操作。
可是还没等我高兴完,我一看扣费通知:2元
心瞬间就凉了,平常坐公交都是1块钱的。后来才知道,因为这张卡属于全国通用的卡,所以没有优惠,按原价扣费。
而在地铁和某些小中巴上,原本的长安通就没有优惠,所以按道理说是可以正常使用的,但这样就使得用户面临一个两难选择:
还有一个令我感到不很方便的点是:iPhone 和 Apple Watch 没办法共用同一张交通卡,要是试图在 Apple Watch 的 Apple Pay 中添加卡片时,你会得到这样的消息:
所以现在的解决办法只能暂时是:将卡片转移到 Apple Watch,在地铁和小中巴上使用手表支付,在其他公交上使用扫码支付。
希望能尽快的跟进优惠,让我们这样的普通老百姓也能拜托扫码的烦恼。
]]>Google Adsense
, 审核速度非常快,当天下午就可以正常显示广告了;但是我之前的那个主题总是被广告搞得布局混乱,加上那段时间确实没有手头很紧张,遂停用了广告代码;一段时间之后 Google 给我发来邮件说是网站广告代码不活跃,停用了我的账户,并询问我原因。我当时在 Questionnaire 中给出的原因时广告效果不好,选择停用。
前几天交完今年的托管费用+域名费用之后发现实在是难以为继,于是想重新启用 Google Adsense
,于是在前天提交了申请。
之后发生的事情就很奇怪了
昨天是第一次驳回,原因是我的站点有抄袭文章/低质量内容,我认真查了一下,把5年以前以前质量不高/水贴全部删除了,并重新提交。
今天又收到了驳回,一样的理由;朋友们,我想问问我这个站点哪里抄袭别人文章了,哪篇文章是质量低下?
说报复可能是有些过了,但是我觉得这个和我之前的账户停用多少是有关系的。
既然如此,那就不搞广告了 :)
]]>aircrack 和 reaver 是无线渗透中的两个常用的工具包:而因为需要对指定的信道进行侦听获取全部的数据报,我们需要一块支持 monitor mode
的网卡。所以经过选择,我购买了这块 TP-Link WN722N USB 无线网卡。
但是这块网卡太坑爹了:TP-Link WR722N 这块网卡一共有过三个版本:V1-V3,而在 Linux 上可以正常使用 monitor mode
的版本只有 V1,这三块网卡在购买时不仔细看时很难看出区别(仅仅在外包装右下角的一个小标签上写有版本号)
所以当你像我一样由于各种原因购买了 V3 版本的 WN722N 网卡之后,如果仍然希望正常使用 monitor mode
,你需要对内置的 driver+reaver
进行一系列的改造;而如果你在 Google 上搜索,你是没有办法看到任何的有效信息来让你手里的这块网卡正常工作的。但是经过我的一系列摸索,终于找到一条改造之路。
进行系统改造,你需要准备以下条件:
PCAP
组件和 aircrack
套件的 Linux 系统首先我们需要让手里的这块网卡有能够支持 monitor mode
版本的驱动,
git clone https://github.com/quickreflex/rtl8188eus
Linux kernel version >= 5.20
,那么请修改驱动的源文件 ./os_dep/linux/os_intfs.c
第 1099
行:1 | -#if (LINUX_VERSION_CODE>=KERNEL_VERSION(4,19,0)) |
make && make install
安装驱动至此,我们将修改后的驱动安装完毕,下来我们需要屏蔽系统内自带的驱动:
/etc/modprobe.d realtek_blacklist.conf
,并在其中写入:1 | blacklist r8188eu |
shutdown -r now
重启之后我们就可以正常的对这块网卡启用 monitor mode
具体的方法会在下面进行说明(这里不能够使用 airmon-ng start
进行网卡初始化)
aircrack 工具包官方推荐使用 airmon-ng start
命令来对网卡进行初始化,这里的初始化主要是进行了以下几个步骤:
monitor mode
power_save
选项关闭,防止网卡自动休眠但是出于_不知道为什么_的原因,我们使用 airmon-ng start
指令初始化网卡会失败,在这里经过测试我们需要使用 iw
指令来进行手动管理(也就是初始化网卡的指令):
1 | airmon-ng check kill |
这时候我们就可以正常的使用 aircrack 工具包,但是使用 reaver 还是有些许问题,我们需要进行一些修复。
在 GitHub 上的 reaver 的项目 issue 中看到这个版本的驱动和 reaver 的协作有些问题,会导致无法正常的获取到 pcap handle
,所以我们需要下载另一份 _reaver_,并且进行一些修改使得他们能够正常工作:
git clone https://github.com/t6x/reaver-wps-fork-t6x
./src/init.c
第 144
行:1 | -pcap_set_rfmon(handle, rfmon_active); |
src
文件夹内执行 ./configure
配置程序会检查依赖, 如有缺少请根据提示安装make && make install
shutdown -r now
之后你的 reaver 就可以正常工作了,你可以尝试 wash -i wlan0
命令来进行测试。
因为每次重新启动之后都需要重新配置一遍网卡来进入 monitor mode
模式,我建议将上面初始化网卡的指令保存为一个文件,之后每次使用时运行一下即可: bash start.sh
这样会方便很多。
祝大家使用愉快。
]]>在学习了新的 YAML
语法、看了官方文档之后发现这个东西用来做 CI/CD
是非常方便的,于是决定使用它配合 Pylint
对代码自动评分来控制每次 push
的代码质量。
GitHub Actions 会使用一个完全隔离的虚拟环境每次运行我们的代码,所以我们需要在工作流中设置 CI/CD
的工作环境,并且绑定一些 GitHub 的动作,这样每次动作发生的时候我们的工作流就会自动被调用了。
这在团队协作开发的时候非常有用,我们可以自定义测试/评分脚本,这样就能避免质量过低/测试无法通过的代码被提交了。
下面给出使用 GitHub Actions 结合 Pylint
给你的代码自动评分的脚本:
1 | name: Test and Pylint |
这段代码在 .github/workflows/main.yaml
中,代码中有几个点可能难以理解,我大概说明一下:
if: success()
- 这个是 Actions 的自带函数,当上一步运行成功的时候才会执行当前的 step
;这里的意思是:如果上面的测试都没有通过,就不用评分了steps.linting.outputs.score
- 在 id: linting
的那一个步骤中,我们调用了 python linting.py
它会在一个 actions
的 steps
环境变量中输出一个键值对,之后我们可以通过这个语句调用这个值我们现在看看 linting.py
中的内容:
1 | """Linting and return score as system code""" |
可以看到,我们评分获取到分数之后调用了系统命令:
1 | echo "::set-output name=score::your_score" |
这是 Github Actions
虚拟环境的 tricks
,这样就可以将分数值留给下面的 step 去使用了。关于更多的指令,可以参考 GitHub 的官方帮助。
有想法的朋友也可以使用测试的结果获取一个酷酷的 badge,每次可以根据自己的测试情况来动态的更改 badge 的图案。我这里因为单元测试还没有写完,暂时没有用到。
好了,我们现在可以给自己的代码评分了呦,每次测试通过的 commit 还会有一个漂亮的绿色对勾~
]]>首先我们在实际业务中不可能像是第一篇文章一样仅仅涉及到两个订单状态的切换,在不考虑退/换货流程的情况下,我设计了一套较为完整的订单状态流程,下面以一张状态图来表示:
这幅图中的绿色节点表示订单的起始状态:订单已经创建;儿两个红色节点表示订单可能的终止状态:订单完成/订单关闭
请注意,订单的完成与关闭是完全不同的两个概念,完成是指用户走完的所有的支付、发货、收货流程;而关闭则倾向于描述订单出现了意外的情况,例如:
而图中的圆角矩形用于表示状态;菱形用于表示判断;箭头用于表示事件及状态转移。
我们可以发现,在一张设计良好的状态图上,所有的状态之间都是可以使用事件来进行抽象的,而在某些情况下我们需要业务层进行一定的判断来指定发生事件,用以驱动状态转移。
我们有了以上的订单状态概念之后,可以开始使用上一篇文章的有限状态机模型构建状态/事件类了。
首先,我们考虑事件类该如何构造。
因为每个事件都有自己的发生时间和操作码,我们不能直接将事件实例化,而是应该在事件发生时对其实例化,在状态转移之间传递他们的实例。
我们可以重载 fsm.Event
类,并定义 action
接口,这样就可以实现定义一个事件了,下面以用户正在付款作为示例(在用户点击了付款按钮之后触发):
1 | class Paying(fsm.Event): |
我们在事件中保存了支付的详细信息,用以与支付平台对接以及后期给用户展示。
有了事件我们可以开始考虑状态类了。
之后,我们考虑状态类如何构建。
根据上面FSM
的模型,我们需要重载 fsm.State
模型来指定状态的进入/退出接口,之后实例化重载类,获得一个状态实例,例如订单正在运送这个状态:
1 | class _Shipping(fsm.State): |
在订单正在运送状态这个状态中,我们希望保存事件,也就是原因到 extra
中,并且我们将要初始化一个 ShipDetails
列表变量,用于保存物流的一系列信息通知。我们经常可以在电商后台看到这样的物流状态:
我们就可以保存在这样的列表中返回给用户。
但是有些状态可能并不关心状态转移的原因(也就是事件),他们也并不需要重载,直接实例化 `fsm.State` 就可以,例如订单已经创建这个状态:
1 | # 状态 - 订单已经创建 |
这样我们可以更简单的构建状态集合,下来我们需要一个状态机(也就是状态管理器)来进行状态管理了。
状态管理器可以直接重载 fsm.Machine
,最重要的是需要根据上面的状态图来进行状态转移表的设计,下面给出代码:
1 | class StatusManager(fsm.Machine): |
可以看到,我们重写了 start
函数,因为订单的状态只能以订单被创建开始;之后我们在类构造函数中将状态转移表初始化,这里我使用了二维数组来描述状态转移表 :
1 | # 状态转移表 |
以上工作都完成之后,在使用时,我们可以直接实例化管理器类,就可以获得一个订单状态管理器,使用 handle
接口将事件实例传入,我们就可以实现订单状态的管理了。
有的同学可能会觉得这样问题更复杂了。事实上,我才开始尝试过使用 if
条件分支的方式来管理状态,那样是完全行不通的,写一段时间之后就会被分支给搞的晕头转向。
并且我们这样做是更有扩展性的,之后如果要添加/改变状态仅仅需要增加状态、事件、改变转移表即可。我甚至不敢想象使用条件分支判断的代码该要怎么维护,如果没有注释,那估计就是传说中的屎山 吧~
以上就是FSM实现订单状态管理的全部内容了,希望大家能够喜欢。
]]>根据有限状态机的定义,我们的状态机将有三部分组成:
下面让我们开始建模。
我们需要的状态类应该有以下功能:
据此我们设计如下的状态类:
1 | class State: |
这里的私有静态变量__codes
表示的是一个状态码集合,用以检查新状态的状态码是否冲突;而accept
方法用以检查当前状态是否支持发生制定事件;extra
字典用于保存额外信息。
我们需要的事件类应该具有如下功能:
uuid
,并且保存发生时间根据以上的需求,我们设计这样的事件类:
1 | class Event: |
状态管理器则是以上两者的结合,我们可以写出他的工作流:
accept
方法exit
函数来退出当前状态enter
方法我们在调用状态的 enter/exit
方法时可以将事件传入,作为原因使得状态感知;同时我们可以在类内部维护一个记录器,记录历史发生的所有事件,这在订单状态管理中是非常有用的。
用一张流程图说明会更清晰一些:
下面开始编码:
1 | class Machine: |
下面一篇文章,我们将使用以上的状态机来管理系统订单状态。
]]>例如,假设我们的系统中仅涉及两个状态:订单被创建、订单完成,此时我们可以写下如下的代码:
1 | from collections import namedtuple |
可以看到,这个系统的管理仅仅涉及到两个状态的转移,就已经较为繁琐了,并且将字符串直接写死在程序中也是非常不好的实现;这样写出的程序也很难扩展。
那么当状态较多,问题较为复杂时,我们应该如何处理呢?
众所周知,FSM (Flying Spaghetti Monsterism) 是飞天面条神教的简称
FSM (Finite-State-Machine) 是有限状态机的简称,有限状态机定义为一个五元组:$(Q, \Omega, \delta, q_0, F)$,其中:
根据定义,在有限状态机模型里,我们需要确定初始状态和中止状态;而每一个状态在发生一种特定动作的情况下有且仅有一中目标状态可以转移;同时,在状态转移时会计算转移函数的值。
那么,如果我们将订单状态管理的问题抽象出来,可以发现订单的状态的管理需要以下几个步骤:
我们会发现,如果将订单状态类比为FSM中的状态集合,发生的事件类比为FSM中的输入符号集合,事件执行的转移函数类比为FSM中的转移函数,我们可以发现,这种问题如果引入有限状态机的模型去解决,会方便很多。
下面一篇文章,我们将实现一个简单的有限状态机。
]]>我们今天只对从 main
函数开始的部分进行分析,STM32 的启动过程在 startup_stm32f10x_md-vl.s
文件中以汇编指令进行编写,留作以后分析。
下面给出本次需要分析的 C
代码:
1 |
|
在 μVision
中新建工程,在调试中选择使用模拟器调试,之后对程序进行 ReBuild
,就可以开始我们的分析了。
我们从 main
函数的入口开始分析,所以将函数断点设置在 int main(void)
处,按下 Ctrl + F5
,程序自动为我们执行到了断点处,并给出了 main
函数入口处的反汇编代码:
1 | 57: int main(void) { |
首先第一行,我们看到程序执行了 PUSH
对 r4
至r11
以及lr
指令集进行了入栈,这里我们主要关注两个点:
r4 - r11
进行了入栈状态保存?lr
寄存器进行状态保存?我们首先来看第一个问题 - 为什么只对 r4 - r11
进行了状态保存
这个问题与 STM32
中对寄存器的分类有关:
r0 - r3
在 STM32
除了通用的寄存器作用之外,需要用作传入参数的状态保存;而在程序返回值时,他们则负责保存程序的返回值。而在子程序的调用之间,程序可以将 r0 - r3
用作任何用途;这也就说明在被调用函数返回之前不需要恢复他们的值。
而如果调用函数需要再次使用他们的内容,程序反而需要保留他们的内容(最常见的例子一般是递归调用)
而 r4 - r10
寄存器保存的内容根据 STM32
的规定,在函数返回之前必须要恢复这些寄存器的值。
第二个问题 - lr
寄存器的作用以及为什么需要保存它
首先对这个寄存器进行简单的介绍:lr
- Link Register
(链接寄存器)执行子程序调用指令(BL )时,会自动完成将当前的PC的值减去4的结果数据保存到LR寄存器。即将调用指令的下紧邻指令的地址保存到LR。
而如果我们此时观察寄存器中 lr
的值:
会发现它保存了一个地址 - 0x080001AB
;那么这个地址代表什么呢?
可以看到,我们的 main
函数实际上是由 0x080001A6
处的 BL.W
指令跳转执行到的,而紧接着下一条执行的便是位于0x080001AA
的跳转至 BL.W exit (0x080004C4)
指令。
也就是说:lr
寄存器在进入函数之前对函数返回后需要跳转的地址进行保存;当函数返回之后,lr
寄存器的值会被更新,指示流水线下面需要继续执行哪一步的指令。
我们继续,程序执行到了:SUB sp,sp,#0x14
这一行。
有基础编程经验的朋友应该都知道 sp
寄存器 - Stack Pointer
作为栈指针保存了当前栈顶的位置,当在程序中需要为在栈上分配内存时就会推动该指针为程序保留指定大小的栈内存;而在脱离了变量作用域之后则会回推该指针表示释放了指定大小的栈内存。
这里我们的 SUB
指令将栈指针推动了 0x14
- 也就是20字节,这也就意味着在下面程序中变量比较多,其中有20字节的栈内存被使用 - 这点我们下面会做印证。
下面我们的的程序进行到了变量初始化并赋值的阶段:
1 | 59: int a = 1, b = 2, c = 3, d = 4, e = 5, f = 6, g = 7, h = 8, i = 9, j = 10, k = 11, l = 12; |
这段看起来比较多,但大都是重复的,我们有这几个点需要注意:
MOVS
指令,另一部分是 MOV
指令?MOV + STR
- 栈内存的赋值操作第一个问题 - 为什么一部分移动使用 MOV
,另一部分使用 MOVS
这个问题同样与 STM32
的寄存器分配有关,根据官方的说明文档:
我们可以看到,官方对 r0 - r7
这8个通用寄存器称为 “低位寄存器”;而相对的 r8 - r12
则为 “高位寄存器”;而在程序中涉及到高位寄存器的四步操作,程序统一使用了 32 位的 MOV
指令(ARM
指令集);
这也就意味着,在低位寄存器进行立即数赋值操作时,程序总是需要使用 16 位的 MOV
指令(thumb
指令集),而如果表示的是符号数的话,则需要使用 MOVS
对其进行符号位扩展。
而在高位寄存器的立即数赋值中 STM32
默认使用了符号位扩展,所以只需要直接调用 MOV
指令即可,这一点在官方文档中也得到了印证:
第二部分 - 栈内存的赋值操作
因为寄存器数量不够,没有办法保存我们程序中要用到的所有变量,编译器向栈内存申请了一部分空间进行保存,这个保存的过程使用 MOVS + STR
两条指令完成:
首先将立即数保存至 r0
寄存器
之后使用 STR
指令将 r0
寄存器的内容保存至栈内存,这个栈内存的地址通过栈指针 sp
加上一个偏移量确认
请大家记住,我们已经使用了4个 int
类型长度的栈内存 - 也就是 16 字节
程序现在运行到了最关键的部分 - 子程序调用以及数组赋值;我们来看生成的反汇编代码:
1 | 63: uiMas[1] = Fun(a, b, c, d, 0xabcd); |
首先,程序将立即数 0xABCD
移动到了 r0
寄存器以备使用(MOVW
表示移动2个字节);而参数中按照顺序调用了 a, b, c, d
这四个参数,可以看到程序按照倒序依次将 d, c, b
三个变量的值移动至了 r3, r2, r1
这三个寄存器作为参数。
按照我们上面对参数的四个寄存器的描述,剩下需要将 a
变量移动至 r0
寄存器,可是现在 r0
寄存器被用来存放临时的立即数 0xABCD
,所以程序使用 STR
将这个立即数保存至栈内存 sp
指针的地址。请注意,这里我们再次使用了 4 个字节的栈内存空间 - 至此,我们在 main
函数开始时分配的 20 字节栈内存被全部使用。
解决了 r0
的冲突问题,程序将 a
变量移动至 r0
寄存器。
下面是这次需要关注的重点:BL.W
指令,我们的程序使用它来进行子程序调用;通过对寄存器状态的关注,我们来看看这条指令在执行时发生了什么:
lr
寄存器被更新至 0x8000024B
- 也正是我们刚刚执行函数跳转之后的指令地址 pc
寄存器被更新至 0x080001BC
- 也就是 Fun
函数的入口,指示流水线下面一步该执行的指令位置我们现在进入了 Fun
函数,关注生成的反汇编代码:
1 | 49: int Fun(unsigned int a, unsigned int b, unsigned int c, unsigned int d, unsigned int e) { |
和进入 main
函数时相同,程序对 r4 - r7
以及 lr
寄存器的值进行了入栈。
之后程序将 r0
寄存器分配给变量 iLoc
使用,为了保存函数参数 a
的值,程序将 r0
的数据移动至寄存器 r4
。
之后程序从数据地址 sp + 0x14
进行了 LDR
操作,可以看到这个地址存储着值 0x0000ABCD
- 也就是我们刚刚在栈内存存储的临时值,并把它移动到 r5
寄存器,以备使用。
由于 Fun
函数内部剩下的代码并不是很难理解,我们将几条语句放在一起分析:
1 | 51: iLoc = a; |
因为 iLoc
存储在 r0
,程序将刚刚备份的参数 a
从寄存器 r4
拷贝回寄存器 r0
。
之后进行了一些普通的算术运算,并将临时的结果存储在 r6
寄存器,当运算结果完成之后将结果重新放回了 r0
寄存器。
好了,现在 r0
寄存器保存着我们的 iLoc
变量 - 也正是 Fun
函数的返回值,函数开始进行返回的操作 - 使用 POP
指令从栈内存中调出函数执行前的程序状态,更新 pc
寄存器到需要执行的指令地址。至此,Fun
函数的调用过程全部完成。
我们现在来关注得到返回值之后,程序是如何对数组进行赋值的:
1 | 0x0800024A 490F LDR r1,[pc,#60] ; @0x08000288 |
程序首先将 0x08000288
处的值拷贝至 r1
寄存器,我们来看看这个地址存储着什么:
这个地址存储着一个地址,指向了 0x20000000
这个地址,而这个地址正是数组 uiMas
的首地址。
之后程序对该地址偏移量 4 的地址进行了 STR
操作,也就是数组的第2位被改变了。
现在程序需要对一串值进行累加操作,我们来看看这部分的反汇编代码:
1 | 65: a += b += c += d += e += g += f += h += i += k += l += j; |
这部分与上部分一样,需要注意的是,刚刚存储在栈上的变量现在仍然需要在栈上更新 - 也就是说,这部分变量的更新需要首先调用 LDR
将值保存至寄存器,之后使用 STR
保存回栈上。
现在进入程序的最后一部分:main
函数的返回:
1 | 67: return 0; |
首先我们看到,因为函数的返回值是 0 ,程序将 r0
寄存器更新至立即数 0。
之后将 sp
寄存器增加了 20 - 也就是推回20个字节,释放了刚刚在 main
函数内申请的栈内存。
在这之后将 r4 - r11
寄存器恢复至执行 main
函数之前的状态,而 lr
寄存器的状态被更新至 pc
寄存器,用于指示下一步执行的函数 - 也就是我们开始所说的 0x080001AA
,这里存储着跳转至 exit
函数的指令。
至此,我们的分析过程全部完成。
从这次的分析过程中我们可以了解到 STM32
指令流水线的一些基本特点;我们还学习了 B
指令的特点,了解了函数传参以及返回值的方式。在这个基础上我们对 STM32
的寄存器有了更加深入的了解。
这两天感冒挺严重的,还是很羡慕室友们一个个都有知冷知热的女朋友…
顺便祝大家身体健康。
]]>彼得堡的天气一直都是阴雨,就像甩不掉的项目、拉不出的屎,总给人一种很恶心的感觉 。
昨天我和曹博,哦也就是我的老板,正式提了“离职”。其实说离职也不是很正确,因为你无法想象整个项目除了股东和他自己之外只有两个人:我,还有另外一个做翻译的小姑娘。
这两个月大概是我从小到大以来过的最累的两个月:从底层的 C++ 驱动,到中间层的 Python-Flask 业务逻辑,到用户交互的 HTML+JS+CSS —— 嗯,都是我一个人赶出来的。
我虽然不是什么很有水平的程序员、写出的程序到现在上线了还是bug频发,但是我自认不是一个不负责任的人;去年曹博第一次找到我说要在彼得堡一起做共享充电宝,那时候我的初心是想好好做好一件事,不要给青春留下遗憾的。
但当一个完全没有任何管理能力+不愿意学习的人管理一个创业团队时,你就会发现:全栈工程师变成了全干工程师、你不但需要写全部的程序,操心服务器的运维,你还需要负责整个项目的备案流程,甚至你还需要兼职当客服。
上线之前我不知一次的问过他需要什么功能,回答就是你看着给咱做;上线之后问我:统计功能呢?分账系统呢?价格系统怎么是这样的?
一脸懵逼
当一天被催两回进度,还要不断地和上级解释怎么登录微信公众平台这种在网上一查就有的事情时,我觉得是时候退出了:
怎么办?
这半年让我明白了:只要团队的运营管理足够糟糕,什么样的人都会被干跑的。
大家一定以为你既然这么辛苦,一定拿了很多钱吧,我可以跟大家直截了当的说,从这个项目开始到现在我一共拿了曹博 13000.00 人民币。
听起来很多?卖给我们机器的厂家当时要卖给我们一套系统,当然他们系统都是批发的,大家猜猜多少钱给我们 —— 8万
我为什么问他要这么一点钱呢?我上面说过了,我当时是很想好好干一番事业的,所以我只问他要 2万,而且曹博当时吹得可好了,又是分红,又是股份:这让我感受到了他的诚意,我也就认真的加入了团队。
可是后来我慢慢发现,他一直在签空头支票。说要给买的一切东西除了及其必要的,剩下一分钱也不舍得多掏在团队身上。
哦,至于剩下没发的那7千块钱?当时说是项目上线之后就给发,我到现在连影子还没看到;那天问他要工资:
M:曹总,项目上线了工资得结一下吧,还有你招新人的工资得好好想想,程序员不能饿着肚子干活。
C:什么叫饿着肚子干活,我没给你钱吗?项目开始到现在我就给了你一个人钱!那天你从我这回家的打车费用还是我给你掏的,出去吃饭也都是我请客!
——曹总经典语录
真是很可惜这些对话是语音,我真想截屏出来给大家看看。
那天我7点多下课之后就坐地铁去他家上线前最后调试,他把我晾在门口半个小时之后才有人来给我开门;我之后一口气干活到1点,那阵子已经没有地铁了,可能曹总觉得我应该自己打车回去吧…
为什么只给我一个人发了工资呢?上面我也说过,剩下都是他的合伙人和股东,大家可以自己去网上查查他注册的公司股权信息,他到哪里跟人家发工资呢?人家投了钱怎么会不认认真真催你把活干好呢?
我到现在也不知道他把这七千块钱扣着是不是为了要挟让我把bug修完;但现在我也不想要了 —— 人没必要为了那7千块钱恶心自己。
其实说这个事情,我是有责任的:去年12月的时候他问我项目需要多久上线,我人生第一次接这么大的项目,对自己的技术能力和项目复杂度预料出了严重的错误:我当时给他说的是半年,实际上从3月我开始才开始写代码而到10月份才上线,之前那3个月曹总定不下来机器的厂家,我这边拿不到硬件的技术文档,工作暂时没法开始;导致项目延期比较严重。
这个是我的问题,我承认。
事实上在9月15号左右我就把代码写完了,剩下一直在等阿里云和甘肃管局的备案,我来给大家看看这段时间曹总是怎么和我说的:
可能备案真的是程序员的事情吧。
因为他之前给的deadline是9/15,在那之前我几乎是每天熬夜玩了命的写,项目质量确实有些问题;但我想也没人能在那种情况下写出高质量的代码。
而在那段时间我的课程落下了也有很多,以至于到现在都还没有补完,有些东西多的我甚至不知道该怎么补了。
现在已经脱离了这个项目,就希望自己能够把课程好好做好;我的 Leaf 这段时间已经实现了插件的热插拔,我又对事件系统进行了优化,希望能尽快的开源出来;这段时间我因为太累了,健身的频率也降低了 —— 压力又很大,肠胃系统的老毛病又出来了,希望能尽快好起来,这个真的太影响上课了。
就先写到这里吧,希望一切都能快些好起来。与各位共勉。
]]>我们逐个解决
Python 中并没有直接提供类似 “阻塞字典” 这样的概念,我们可以通过使用 threading 模块提供的 event 操作进行自己实现如下功能:
1 | """一个线程安全的阻塞字典""" |
很高兴 Wordpress 自带的代码区块终于有了高亮功能。
]]>通信协议(英语:Communications Protocol,也称传输协议)在电信中是指在任何物理介质中允许两个或多个在传输系统中的终端之间传播信息的系统标准,也是指计算机通信或网络设备的共同语言。 通信协议定义了通信中的语法学, 语义学和同步规则以及可能存在的错误检测与纠正。通信协议在硬件,软件或两者之间皆可实现 。
—— 维基百科
打开熟悉的 Visual Studio 选择你正在调试的工程,打一个 Breakpoint,运行时你不出意外会在 VS 的左下角看到这样的一个窗口:
很明显,这是一块以16进制展现数据的内存截图;我们都知道,内存用来在程序的运行时保存各种临时数据 - 作为计算机行业的爱好者,我们甚至可以脱口而出每个数据类型在各种平台上的默认长度等等…
但是,现在的问题是,谁能告诉我上面的一段内存区域表示了什么样的数据呢?
大家可能会觉得我这个问题问的十分的荒谬,因为事实上这个问题是无解的 - 在没有给定这片内存区域所表示数据的类型之前,这些数据是完全没有意义的。
那么,如果将我们作为信息的接收者,这个 Debug 的窗口作为发送者,这些内存数据作为被发送的信息,我们之所以无法理解这些信息,是因为在这个信息交换的过程中缺少了一个重要的组成部分:协议。
在上面的例子中,我们将自己作为了信息的接受者;然而现实中,我们不像 Google 的大佬那样人人都是人形编译器,程序最终还是要交给编译器去编译的;编译器在编译的过程中同样需要知道每一个变量的类型:
1 | sth = 1; // 不可以通过 |
即使在动态类型语言中(例如 Pyhon),解释器在解释过程中仍然会为我们尝试推导这个变量最合适的类型;而我们都知道,在 Python 这样语言中是不存在单独声明变量的这种行为的,需要将变量的声明和赋值放在一起:
1 | >>> a |
这就是因为 Python 需要为我们自动推导数据的类型进行存储,没有类型,解释器根本不知道需要为你申请多少内存,以及如何解读存下来的数据。
换个思路想想:在编程语言中,我们就可以将类型系统看作一种协议,它规定了程序 - 计算机内存之间的数据交换格式。
回到最开始我们的定义 - 网络通信协议;我们大家都很熟悉 OSI 的 7 层模型 - 我们的互联网络像一个洋葱一样的层层工作,其中涉及到的协议千千万,但是就像维基百科给出的定义那样,在不考虑容错以及将消息同步交给下层协议工作的情况下,任何规定了语义的共同语言都可以算成一种通讯协议。
我们不妨看几个例子即可明白:
这是工作在链接层的 ARP 协议报文格式,由 RFC 826 提出定义,它用来将网络地址转换成为 MAC 地址,算是 IPv4 协议簇中非常重要也是很基础的一个协议。
这是 RFC 7540 规定的 HTTP 帧格式,因为 HTTP 基于 TCP 协议实现,而 TCP 是一个以流方式工作的协议,所以 HTTP 以帧格式规定。
可以发现,即使是工作在应用层的 HTTP 协议,也和工作在链接层的 ARP 协议一样,核心都是规定了数据交换格式的一组语言,本质上并没有太大差别。
那么现在回头想想,我们学习的外语是不是也可以看作一种协议呢?
我们每个人学习外语的过程可以看作是在大脑这个层面安装一种协议,这种协议规定了音频信号、视觉信号、还有语法信息的处理;在学会一种语言之前我们听外国人(老毛子)说话肯定是一脸懵逼;但在这之后,我们便能够了解音频信号中的信息含义。
而在说话时,我们大脑内的信息被这种协议的语法规定所编码,并通过大脑的指令信息让气流通过、声带振动发出声音。
其实这些都是我瞎想的,但是认真思考一下,如果将协议的概念从互联网中剥离出来,并加以推广,我们的整个世界的信息交换过程都可以看作由各种各样的协议参与。
而人类之所以能够有如此的进步,其中的信息交换也离不开各种而样的协议参与。也许,人们正是被这种自然界中的“协议”潜移默化的影响着,才会将互联网协议设计成为今天的样子吧~
]]>我们在做题目的时候将方程组求解,使得原来复杂的形式变得简单;同时原来方程的信息并没有丢失,也就是说我们通过解方程组的过程降低了方程的熵。
那给题目的负熵是从哪里来的呢?
毫无疑问,我们的大脑在解方程的时候做了功,消耗了能量;也就是说我们为了使大脑内熵不剧烈增加的同时给出方程组的解,在这个过程中不可避免的消耗了能量。
再向上一步思考,我们的能量从哪里来呢?
进食。
也就是说,我们吃掉了其他的动/植物,在这个过程中获得了负熵(使用能量),来维持自身的低熵状态,去维持所有的正常生命活动(在这里看作解方程)。
如此递推,我们最终的负熵来源应该是太阳,它赋予了地球上几乎全部的生命活动所需能量;而太阳又是宇宙演变的产物…
如此类推,根据热力学第二定律,所有事物都在自发的向着混乱变化;而所有的生命,却能够通过主动的获取能量而降低自己的熵;从这个角度看,生命是不是也可以定义成能够主动维持低熵状态的物质体呢?
如果从这个定义出发,生命的诞生、终结都有了新的看法。
我们诞生在这个世界,由与外界无异的各种原子组成;而我们生命在获得最初的有序状态之后(母体赋予的)便开始不断地消耗能量来维持这种状态。
我们一生都在不断地消耗能量来维持底熵,从每一次心脏跳动到大脑内的每一个电信号传递;而因为所有的事物(包括我们人类本身)也会不断地自发性走向混乱,当某一天我们从外界获得的能量不能够在维持自身的有序状态时,我们便不可避免的走向崩溃 —— 这便是死亡。
从这个角度想,秦始皇和古代那些吃各种重金属仙丹企图长生不老的皇帝,可能都没有意识到:真正的永生,便是死亡。
]]>我在这里使用了 Boost::Python 来进行 C++ 代码的导出。
由于自己在 C++ 方面算是个新手,尤其是在对编译器的工作方面理解的不够深刻,于是在 Linux 中决定自己手动进行编译、链接的工作。果然,第一次编译就喜提了一大堆的 error 与 warning… 我在 MSVC 中赶紧把编译器警告级别提高到了 W4,根据相关的提示信息进行了修改,现在还剩下一个让我十分不解的问题。
1 | class Iterator { |
上面的这一段代码是用来在字节流中格式化出指定的变量,特化的模板函数用于针对 std::string 类型给出结果,在 Windows 平台它工作的很好,编译器没有给出 warning,但当我试图在 gcc 中编译时,却给出了这样的错误:
GCC error: explicit specialization in non-namespace scope
在阅读了上述的解答之后,我知道了,特化的模板函数在 gcc 中是不能直接在类的声明中给出的,需要在类外实现。
1 | template<> inline std::string Iterator::get<std::string>(bool endian, size_t size) { |
经过一番 Debug 之后成功的在 GCC 中编译、链接通过了。
可是在使用 Python 调用时,却出现了 Memory Error
因为工程不算小,我经过了蛮久才定位出了出错的位置:所有调用了这个特化的模板函数的操作都会引起 Memory Error ,而这段代码应该是没什么问题的,因为在 Windows 平台的工作一切正常。
可这又是为什么呢?
模板函数在编译之后和普通函数一样,存在于整个程序的代码区,这部分也是有地址的,代码在调用时其实是调用了函数的地址(指针),将参数入栈进行函数的调用,这部分如果有问题是在编译期间就能发现的;而 Python 这里的 Memory Error 除了内存的 malloc 错误应该就是只对应的函数地址不存在相应的代码。
针对这一点,我便有了一个想法:如果我将函数声明为 inline,在对应的调用地方就会将这段代码进行展开,于是便可能消除这个错误。
我是幸运的,当我将这个成员模板函数声明为 inline 之后,Python 中的调用便成功了。
所以建议大家,如果在 GCC 编译之后,成员模板函数的特化出现了各种奇怪的问题,不妨尝试将函数声明为 inline 来进行 debug;如果函数体较大,可以在 debug 之后删除 inline 标志。
]]>我不怎么了解游戏,也不是很清楚为什么游戏会有这么大吸引力;
但是我每天凌晨两点被吵醒,之后便难以入睡,十分影响正常生活。
跟他们沟通过好几次,可是都不起作用。
就在几天前,我实在受不了了,趁着51假期,我决定做点什么。
我的想法是只要他们继续半夜打游戏,我就让他们网络断开,早上在连上。这样既保证了人家能够正常上网,也能够让我们都睡一个安稳觉。
OK,我打算用无线渗透的方式进入他们的路由器,获取管理权限。
首先,用 Windows 肯定不是很方便的,从网上下载一个 Kali-Linux 的镜像,装了虚拟机,配置完各种更新。
其次,由于我们用的是 VMWare PlayStation 进行的虚拟化,所以虚拟机中的 Linux 需要一张外接的 USB 无线网卡,经过 Google 以及网上论坛的推荐,我选定了这款普联的 TP-LINK WR722N。
可是千不该万不该,就是不该信那老毛子的话;这块网卡仅有 V1 能够在 Linux 上即插即用,并且启用 monitor 模式。买的时候我没有看到标签旁的 V3 ,还特地问了毛子店员,它拍着胸脯说:“绝对可以用!”
既然买回来了,那就得折腾,通过查找,发现了在官网上有一份测试版本的驱动,说是支持 Ubuntu 的 monitor 模式,于是下载下来试着编译一下 - 根本不行。
我又以为是 Kali 的问题,于是按照安装手册上指定的 Ubuntu 和 GCC 版本重新安装了虚拟机 - 依然无效,他的这份驱动内部全都是错误。
怎么办呢?
经过多方查证,发现在 GitHub 上面有一份魔改版的驱动,貌似可以在 Kali 上编译通过,并且能够启用 monitor 模式,于是下载编译安装。
能够正常启用是没有问题,可是 aircrack-ng 和 reaver 都无法正常使用,后面又经历了我多次魔改(过程中还要修改 reaver 的源码),终于能够正常使用了。
后面有时间我会把 TP-LINK WR722N V3 在 Linux 上的使用过程专门发一篇帖子总结出来,大家有需要的可以看看。
总之,现在我们获得了一张可以在 Kali 上正常工作的无线网卡,这还只是万里长征第一步:下面我试图通过 airodump-ng 获取 WPA 的握手包。
ASSDD 和 TP_LINK**** 这两个的可能性最大
ASSDD 那个包抓的很轻松,这个普联的路由就比较困难,等了很久才有活动客户端,使用 aireplay-ng 发送伪造 deauth 包迫使重新连接。
我们很轻松的获得了 ASSDD 这个 AP 的密钥,可是连上去之后经过验证,发现这个并不是我们需要渗透的 AP;而那个普联的路由我本地没有解开。
于是我们现在可以集中注意力在那个 TP-LINK 的路由上,既然本地算不出,不如就交给网上专门干这个的卖家;于是我打开咸鱼,与一个卖家谈好日期和价格,就把包给她发了过去,然后便结束了一天的折腾,进入了梦乡。
可惜令人失望的是,卖家违约了,至今都没有给我消息,我在咸鱼上申请了退款,也没有回复我…
既然靠别人不成,便自己在网上继续搜寻关于无线渗透的资料;偶然发现,这个 AP 的 WPS 没有关闭。
但是很快便得到了令人失望的消息:对方路由在几次失败之后便将我网卡的 MAC 拉入了黑名单,我短时间内无法继续了,这个方法也不成。
在网上继续搜寻,发现有帖子说普联的路由默认密码和 pin 相同,可以通过生成8位数字字典,然后用 hashcat 进行穷举运算的方式猜解密码。
抱着试一试的态度,我借来了同学 1060ti 的主机进行测试(因为我只有一台笔记本,核显运算能力太差,只有 7000H/s,而同学的那台可以上到 250kH/s ),可惜还是失败了。
emm… 对方把密码改的还挺复杂的。
本来已经放弃的我,躺在床上翻着帖子,翻到一个毛子论坛,里面有篇帖子提到可以用 pixelwps 工具快速解开部分 TP-LINK 路由的 PIN。
我下了床,尝试用 pixelwps 获取 WPS 的 Authentication 信息,结果发现对方还是对我禁 PIN 的状态,这可怎么办呢?
我突然想,能不能让对方路由器重启,这样就可以重置禁 PIN 的状态。怎么让对方重启呢?
对,用 DOS 的方法向他的路由器发送大量的无效 PSK Auth 包,阻塞路由,就算路由器自己不重启,他们也会觉得网络很卡,去重启路由的。
果然,没过一会,我在用 wash 工具去检测的时候发现 WPS 的 Locked 状态已经解除了。
我们拿到了对方路由器的 PIN,之后通过 reaver 很快的就获取到了他们的路由器密码。
令人欣慰的是,他们没有更改管理员的默认密码,我使用 admin - admin 便成功登陆。
经过了三天的努力,我终于能够睡一个安稳觉了~
]]>在经历了与15舍吸血虫的斗智斗勇之后,我 (即将) 再次离开现在的窝;新的窝暂时还没有定下来。
原因很简单:租给我床位的老哥被开除了,不在这里继续上学了,这两天就去办退宿手续,之后我如果继续在这里住下去就算是白嫖了。如果宿舍管理处安排人来这里,我随时就得让出位置让人家住进来,不管白天还是黑夜。
为了避免像开学那段时间再次睡中厅板凳的尴尬,我现在正在寻找新的窝。
住外面暂时不考虑,因为房租确实无法承担。现在考虑看看还有没有人出租短期床位的。
问了一圈,但是暂时还没有找到,因为这个事情在开学那阵应该就已经定下来了,现在出租床位的人不是很多(尤其我只住三个月,很少有人有人愿意给租)
今天去了12舍洗衣服,因为那里的洗衣机是新的,不会像15舍一样把白色的衣服洗成黄色的 (●′ω`●) ,回来的时候把自己的衣服扔到那两个破纸箱子里,稍微收拾了一下。
我的两个小纸箱
是啊,他们就是我的全部家当了,不管我走到哪里,他们都跟着我。
当然,我还有两个空的行李箱和一床被子。
那天我王哥说的其实蛮对的:我一直把杰神当作弟弟看待,从开始想帮他好好学习、把Lab什么的都给他、帮他写LaTex作业、一起复习数学,到现在他决定要回国转经济系,我都一直蛮支持的。
他还小,不懂很多我给他说的东西,常常做出让人哭笑不得的事情。有时候甚至让我怀疑他的智商,觉得这个人已经无药可救。
但是小也有小的好处。
他就给我说过,他不想长大,因为长大就不会很开心了。他不刮胡子也是这个原因。
昨晚和他去健身,他还是像个傻子一样,踢着路上的石头。但是看着他开心的笑,我突然觉得:这样与世无争,知难而退,也挺好。
杰神
不知道从什么时候开始,我每天都会计算什么时候回家(很久了好吗 ←_←),每次想到把东西打包好,坐上飞机的感觉,能发自心底的笑出来。没错,我也是个傻子
那是种真诚的,发自内心的,温暖的幸福感。
就好像寒冷冬夜中的一杯热咖啡;孤独无助时的一个问候。
我想要那种稳稳的幸福。
是啊,只剩不知疲倦的肩膀,担负着简单的满足。
茕茕无依,像是一只小船,飘在各自的航线上。 (不要问我,我也是查的成语)
小的时候,父母像是领航者、避风港,为我们照亮未来,遮风挡雨。
当我们慢慢长大,父母也会慢慢跟不上我们的脚步,我们要自己迈出步子,探索未知的路。
幸运的人可能会在航路上遇到另一只船,陪伴我们走一程。
慢慢的,我们也会有自己的家庭,承担更多的责任。
最后也会像所有人一样,迈向人生的终点。
《漫长旅途》
让我想起之前玩的一个游戏,叫做《漫长旅途》(上图是从这个页面复制的,因为俄区下载不到这款游戏了)。当时玩的时候只觉得很烦躁,因为那个船走的确实太慢了,半天都按不动…
看到我的那两个小箱子,听到室友催促我离开的话,我心里升起一股暖意。好像就在那一瞬,我自己温暖了自己。
也许,流浪,才是人生真正的意义。
]]>这么说大家可能不明白,给大家举一个例子吧:
假设用户添加了一个产品类目:Apple MacBook Pro 2018,这个产品有很多属性,譬如可以调整处理器、内存大小、硬盘大小…
假设我们可以选择的部分写成一个字典,大概就是这样的:
1 | selections = { |
这样一来我们在数据库中一共就要生成 2*3*5*2 = 60 种不同的型号,他们大概长这个样子:
1 | 'skus': { |
显然,让用户手动去一个个添加不现实,这样的系统也是没有人愿意用的,那么问题来了我们该如何根据提供的信息生成这些商品呢?
我在这里利用树的生成与遍历算法解决这个问题。
我们想象将每一个商品信息都添加为一棵树上的节点,那么我们可以得到大概这样的一颗树:
这里由于树分支太多,我只对后面两级的第一支做了分支。
可以看到,当我们生成这样一颗产品树之后只需要沿着路径对产品信息树进行遍历所有的叶子节点既可以生成所有的详细产品信息:
我们这样就生成了第一条产品信息:
1 | ['13.3 英寸', '8GB', '128GB', 'Intel Core 第八代 i5'] |
下面我们来讨论如何使用给定的信息来生成这样一棵树。
我们首先定义一个节点类,类里面最好存储一下这个节点的度,方便我们后面遍历使用:
1 | # Author: guiqiqi187@gmail.com |
这里的 remove_child
方法不对 self.__children
进行 pop
操作的原因会在底下进行解释(当然我猜大家也都知道啦~)
类中有一个 children 方法会返回子节点的迭代器,因为我们在其他任务中不一定需要完全子节点列表(例如搜索任务),只需要对子节点进行遍历即可,这样可以节省一定的内存,而不失优雅性。
接下来我们观察树的结构:
由于第二条性质,我们考虑使用类似 BFS 的方法遍历生成树,这样在遍历过程中,在每一级中只需要取到该级别的信息,添加子节点即可。
由于是使用 BFS 方法生成树,我们需要一个队列(先进先出)来保存下来需要继续遍历节点的详细信息,我们这里使用 Python 自带的 queue
在这里我们不需要不断地查询节点需要使用哪一个级的数据,我们可以使用一个二元数组 tuple(level, node) 来直接保存需要继续遍历的节点与对应节点,而每次进入子节点遍历时,只需要将 level 值加一即可;而对从任务队列中取出的遍历任务,直接将 level 赋值成保存的值即可。
这样一来我们就可以写出代码:
1 | def make_tree(details): |
好了,我们已经构造好了这样一颗产品信息树。现在我们只需要对他进行遍历就可以获取详细的产品信息啦。
在遍历树的过程中,我们关注的重点是当前节点是否是叶子节点,如果是的话则将路径与节点拼接返回即可。所以采用 DFS 进行遍历可能是一个更好的选择,因为这样我们可以方便的获取到叶子节点的路径。
一般情况下我们需要使用一个列表去保存整棵树我们已经访问过的节点防止进入死循环,但是当树的节点非常多时,我们就需要大量的内存来保存这个列表。
还有一个问题,就是每次我们判断节点是否在列表中时都需要 in 操作符,这个操作是 O(n) 的,也会比较耗费性能。
所以我在上面定义节点的时候加入了一个 remove_child
方法,既然我们生成信息之后这棵树就没有存在的必要了,那我们就通过剪枝的方法来防止遍历死循环。
细心的朋友可能会发现我在定义 remove_child 方法时候并没有将子节点从子节点列表中 pop 出去,而只是将该节点的度减一,这是为什么呢?
我们在定义类的时候,将 __children
设置为了私有属性,外部是无法访问的,而作为一个栈,它肯定是有序的,我们在两个对外部访问子节点的接口中都只是根据当前节点的 degree 去进行返回,所以我们没有必要对 children 列表进行 pop 操作,这样一来,在每次剪枝的时候就可以又少一步操作。
由于 Python 的垃圾回收会帮我们做内存回收,所以这里不用担心内存泄漏的问题(最近 C++ 作业写的魔障了…)
OK,经过上面的分析我们可以写出如下的代码了:
1 | def traverse_tree(root, height, func): |
这里第二个参数 height 是需要遍历的高度,我们要取得所有叶子节点,就传入一共有多少级信息(也就是树的高度)。
这里的第三个参数 func 是你想对路径和当前节点操作的函数,记得返回值。
我这里使用了这样的函数:
1 | def splice(path, current): |
细心的朋友可能会发现,我这里对路径 remove 了 None,因为我们的根节点是没有 value 值的,所以所有 path 的第一个 value 值都是 None。其实完全没有必要这么做(在后期遍历的时候跳过第一个即可),但是为了结果的清楚,我这里使用了 remove 这个效率并不高的操作。
我们运行上面的代码(使用 selections 作为测试数据),就可以得到这样的结果:
可以看到,生成了 60 条结果,结果还是蛮不错的O(∩_∩)O~~
这是我的测试用代码,供大家参考:
1 | selections = { |
整个算法经过测试在日常数据集生成过程中暂时没有很卡顿的情况出现,效率应该在 O(b^m) ,在我的项目中暂时够用了。
其实这个需求我也是头一次接触,5min 想到了用树去解决,我觉得一定有效率更好的解决方案,只是我现在不知道而已,如果有大佬知道可以在评论区评论哈!
才疏学浅,上面地方如有疏漏错误,还请大家指出哈!
]]>思来想去最终还是选择了阿里云那个新推出的轻量应用服务器,一咬牙花了288买了一年,用起来确实比我国内那个用来听歌的1M小水管舒服许多。
由于服务器在香港,为了稍微提升一下体验,正好也不想在dnspod解析了,就干脆全套一换:
自己搭建的过程基本也就是参考网上的教程,这次选择的是debian系统,感觉用起来还挺舒服的,听群友说这个系统比较省内存,256M也能跑的很欢。
Cloudflare解析的功能即使是免费版的仍然很强大,但是对于新手来说并不很友好,我说一说我迁移过程遇到的几个坑吧:
由于懒得配置nginx的SSL证书,所以才开始就打算用cloudflare的直接代替,但是由于没有经验,听网上胡说选成了 Full 模式,导致网站直接打不开,宕机了将近2个小时。
这里说一下几个模式都是什么意思:
所以如果像我这样懒得配置 Nginx HTTPS 的同学可以直接选择 Flexible 模式,简单又好用。
这个问题才开始也是让我头大的不行。
问题的主要原因是:你的博客地址选择了 https 模式(就是后台-设置-常规的 Wordpress 地址),但是你的服务器没有开启 HTTPS(所以我们选择了Flexible),当你登录时就会出现这样的情况:
解决方法有这么几种:
坑就暂时发现这么多。自己搭确实蛮累的。
前天先把博客所有404的图片链接全部都手工去除了,现在老帖子基本已经没有图片了(wp的升级不兼容性/之前迁移时候图片的编码问题,详情:https://init.blog/archives/1508),花了一下午时间。
昨天早上问同学东拼西凑在支付鸨上搞到了足够的钱。
昨晚买了服务器之后先拔了阿里的看门狗系统,折腾了一阵,配置 iptables 折腾一阵,各种包都缺,相互依赖,装完 nginx mysql php-fpm 之后就很累了,睡觉打算早上起来之后再继续。
中午搭完之后写文章突然发现没法保存,以为是数据库用户权限问题,后来检查之后并不是,多方查找无果,只得重头再来。
下午博客又莫名其妙的无法登录(经典的reauth问题),折腾很久之后还是不行,最后实在没办法回滚了快照(幸好早上配置完成之后打了快照)。
希望明天别再出问题了,折腾。
马上就要过年了,这里考试还没结束,看到俄罗斯人就烦,天气也是一直阴天(中间晴的那天出去玩了,心情就很好)。
想家。
]]>想写写东西总结一下2018年自己的工作,但是想了半天也不知道该写些什么。
于是拿起了手机看看相册,翻了翻2018年全年的相册,发现基本没发生什么有意思的事情啊( ⊙ o ⊙ )!
以下是流水账时间…
年初我去了一趟摩尔曼斯克,后来是红帆节的照片,后来是预科毕业,最开心的就是我的预科结业成绩基本上是最优秀的。
确实,那段时间我是最开心的,因为我要回家了。再往后就是在国内的不到两个月,确实是很开心的两个月。那段时间也没去什么地方玩…
不,我突然想起来,我去了一趟连云港。
没什么多说的,我只能总结出一句话:中国旅游最差城市——连云港。
从到现在为止都还没有退出来的6块钱公交车费(在连云港市哪个公交APP里,一旦充值就不能退款,但是少于6元他又不让使用…)到那里“无可比拟”的景色(还不如莲湖公园的荷花好看),我去的时候不巧还正好把岛给封了,以至于我硬座了20个小时去了那里,却只在手机里存了一张靠近海边的照片…
强烈不推荐。
之后就很不舍的离开了家,又回到这个荒凉的小村子里面。开始那段时间我过得很艰难,没有地方可以住,以至于我来的前几天竟然是在学校中厅里面盖着衣服睡长椅的;当时没有反应过来我之后的合租伙伴会不愉快,硬是在学校里等到他们来才住进了新房。
于是后来就慢慢的有各种矛盾,直到后来搬了出来。
想看大戏的同学可以看 这个V2的帖子,基本说的很详细了,那之后一天早上那个女室友大声的骂我“傻逼”,说我不交房租,让别人擦屁股。
我从小没有被女生这样的骂过,忍受不了,搬了出来。付清了自己该付的房费,也一分钱不少他们。我们从此各走各的吧。
才来的时候因为学费的问题差点和中介打起来。那段时间也非常难过。
最后那个中介已经:“*你妈” 这样的骂我,我当时也没有骂回去,我说以后不用你来给我交学费了,我已经把所有钱给 Мария 转账过去了,他就像是一条狗一样气急败坏。第二天我才知道他们竟然是一个公司的,只能感叹这里水太深,刚出虎穴,又入狼窝。
好不容易安安稳稳上了学,班长一帮子毛子又极度讨厌外国人(民族主义),甚至在VK群里面公开的羞辱外国留学生和黑毛子,就在这样的环境下我坚持完了一学期。
还有一个上课不睁眼,一年不换衣服的老师,所到之处周围10m半径内全部都是浓烈的酸味,以至于令人无法呼吸;每天上课也不睁眼,就自言自语,有时候甚至一节课都不从座位上起来。
不过很幸运,我们的 Сесиия 过完了,现在 Зачёт 基本全部拿到手了,我还是以两个班里基础编程第一名的成绩拿到了计算机科目的 免考 Автомат,现在就希望Экзамен 能顺利的通过,这学期就算是结束了。
可以找找我在哪里啊~
今年还写了写项目,明年6月份,如果没有意外的话,给曹老板的全套微信商城系统功能(包括微信支付和线下实体对接)就能上线了,到时候彼得堡的同学们也能用到我写的软件了,现在完成度还不错,希望一切顺利,能拿到工资。
刚刚开始写文章的时候觉得自己什么都没有干,荒废了一年。正如胡66的歌:
我懵懵懂懂过了一年,这一年似乎没有拆迁
——《钱包空空如也》胡66
现在想想,克服了其实蛮多的困难,这样的环境里面能坚持活下来都不错了…
所以在同学们动辄2、30条的 2019 flag 面前,我只写了一句话:
希望自己能好好活着
确实,好好活着很不容易了,我身体不是很好。从年中的半月板损伤,去体检毛子医生说我心衰,到前一阵子严重鼻炎导致无法睡觉;昨天去健个身感觉还有点横纹肌溶解的症状…
虽然身体不很好,但是我想一直慢慢锻炼让它变好,希望上天能给我这个机会。
希望自己2019年,能每天坚持锻炼锻炼身体,然后能每天坚持写写代码(现在项目在github上,争取每天都能commit一下),然后扇贝打卡能坚持下去(现在半年了)。然后就是老妈和自己身体健康,没什么要求了。
好了,还有1分钟就要2019年了,我就写这些。
希望大家新年快乐,新年吼啊~
]]>我们先来分析一下以下代码能否正常运行:
1 | // 一个正常工作地 sum 函数, 两个参数都有 const 修饰符 |
首先,我们很明显的看到在typedef中对目标函数的签名和以上两个函数 sum 与 wrong_sub 都不符合,按理说构造这样的一个函数指针数组是会报错的。
可是我实验了一下,程序可以编译通过,上面的结果也都和对应的函数运行结果相同。
这就说明在这里:
为了检查到底是哪里出现了问题,我开始对这段代码进行调试,首先下一个断点在函数结束处,然后观察 operators 数组中的变量:
在这里我们发现了两个奇怪的现象:
从右边内存中可以看到,从地址 0x0034fd1c 开始有两个连续的指针在栈上,分别指向 0x008b119a 和 0x008b105a ,也就是左边监视列表中所对应的值。
那么这两个地址到底对应了什么呢?
我们进入反汇编查看:
很明显的,0x008b119a 这个地址的 sum 只有一条 jmp 的跳转,那么他跳转去了哪里呢?
0x008b2730 - 也就是我们真正 sum 函数的入口地址。
这是对应 0x008b2730 地址的 sum 函数,代码分配在代码常量区。
那么当我直接调用函数会发生什么呢?为了验证,我重新开始了程序,此处的sum函数地址如下:
这里 sum 函数地址在 0x013c3260
而在断点处所看到的汇编代码如下:
1 | 013C6E34 F2 0F 11 04 24 movsd mmword ptr [esp],xmm0 |
这里程序调用了 0x013c11ea 处的代码,那么这里又是什么呢?
由于不知道怎么用VS查看指定处的汇编代码,我用 OllyDbg 检查了以上地址的代码:
可以很明显的看到,在 0x013c11ea 处也仅仅是一条跳转指令,而目标地址就是我们的函数入口: 0x013c3260
基于以上的实验分析我们得出了两个结论:
在网上搜索到的很多帖子都说直接调用函数会直接 call 函数的入口地址,现在看来并不都是如此。
希望对大家有所帮助,我个人对汇编和C++也只是入门,有什么不对的地方还请大家指出,谢谢!
]]>凌晨三点半 睡意全无的夜晚
舍不得点燃 最后一支烟
不愿想起的事 不住在脑中盘旋
离开的人那 再没有归来的一天
已是凌晨3:30,睡意全无。 脑海中不断盘旋着离开家的场景。 无数次问自己这样做是否值得? 终究没有答案。
偶然间发现 失掉了所有感觉
也许成长 只是个谎言
走出很远 才发现绕了一个圈
丢失和得到的 只有时间
感觉很孤独,或许这就是成长的感觉吧。 亦或许,成长本身就是一个谎言。
转眼如隔世 已是很多年
前路遥无可期 后路渐远
看那物是人非 与时过境迁
任世界改变
无法熄灭 熄灭
时光飞逝,已经来这里快两年了。 梦想的灯塔却渐渐模糊。 越来越不知道自己想要什么,也不知道目标到底在哪里。 前路仍遥不可及,后路却已渐行渐远。 感觉变得颓废,像是在掉入深渊。 但是却没有抓住两侧藤蔓的信念。
下午三点半 阳光才照进房间
抽一支烟吃顿早餐 垃圾还没扔
躺在床上不想动 还没开始 就结束了一天
跟室友也因为一些事情闹掰,也许是我性格不好。 一天基本不会跟除了老妈的人说话。 每天只一顿果腹晚饭。 就像是夜赶走了光,使心充满黑暗。
琴瑟果腹 不知疾苦 逝如斯夫 问君何度
多想有个人能来拉我一把。 有的时候就会很想谈恋爱。 觉得两个人在一起能相互鼓励,也许就不会那么颓废。 可是我知道。 那个人,不会出现。
]]>任务很简单, 于是写下代码:
1 |
|
可是还没见运行, VS 的红色下划线就大大的画在了 M_PI
这个宏底下, 鼠标移上去一看是个undefined
, 当时我知道肯定是还要做点别的工作, 上手去Google一搜, 原来是使用 cmath
中定义的非标准常量需要定义_USE_MATH_DEFINES
宏, OK, 代码变成了如下模样:
1 |
|
以为 VS 日常卡顿得我慢慢的失去了信心 - 很奇怪, 还是有undefined错误
于是上StackOverflow查了查, 查到了这样的一个帖子:
M_PI works with math.h but not with cmath in Visual Studio
最后题主自己解决了问题, 说是把 #define _USE_MATH_DEFINES
移到首行就好了
当时我就觉得怎么有这么玄学的事情, 但是, 世间玄学的事情还真的多, 这次 VS 没有日常卡顿, 红标直接消失
虽说问题解决了, 但是我很好奇是什么原因导致的如此玄学的问题, 找了一遍 Google , 也没找到有详细说这个问题的
我当时想, 既然要移动到 #include iostream
之前, 那么肯定是 iostream
的引用导致的这个问题, 可是标准输入输出库会有什么问题呢?
我打开了 iostream
文件, 并没有发现关于数学常量的宏定义, 于是改变思路, 先去寻找定义这个 M_PI
宏的文件, 从 cmath
出发, 经过一番寻找终于找到了 M_PI
宏定义和条件编译 #define _USE_MATH_DEFINES
的位置,分别在:
1 | // cmath -> stdlib -> math.h, line 13: |
以及包含的 correcrt_math_defines.h
:
1 | // corecrt_math_defines.h, line 22: |
找到定义和条件编译选项的宏之后, 我开始分析预编译器的引用过程: 我首先打开了 iostream
, 在里面只有一个引用 istream
于是打开它接着逐级寻找所引用包含的文件, 终于在这样的包含之中发现了线索:iostream -> istream -> ostream -> ios -> xlocnum
在最后一级的 xlocnum
中包含了 cmath
, 这一刻我终于明白了为什么要把条件编译的开关定义在引用 iostream
之前:
如果将宏定义放在引用输入输出库之后, 预编译器大概是这样工作的:
iostream
头文件xlocnum
头文件cmath
头文件cmath
找到了 math.h
_USE_MATH_DEFINES
编译器便没有加载 corecrt_math_defines.h
中的符号iostream
加载完成之后, 预编译器定义了宏 _USE_MATH_DEFINES
cmath
但是发现其实自己已经加载过了, 于是跳过了于是最终的结果是条件编译选项并没有被执行, 即使我们定义了宏 _USE_MATH_DEFINES
为了验证我的想法, 我使用编译 /P
选项将预编译结果写到一个 .i
文件里:
1 | #include <iostream> |
查看预编译的结果, 在其中找到了这样的信息:
1 | #line 1368 "c:\\program files (x86)\\windows kits\\10\\include\\10.0.17134.0\\ucrt\\stdlib.h" |
可以看到, 预编译器在一路加载的过程中遇到了 stdlib.h
并从这里跳转到了 math.h
由于 math.h
的首行定义是引用 correc_math.h
预编译器开始加载其中的符号, 但是根据对整份文件的搜索(结果文件太大了, 有60000行之多)
从这里跳转出去之后并没有再回到 math.h
而是开始加载其余的内容和展开函数之类的工作, 因为整个 math.h
的内容只有一个引用和条件编译对于 corecrt_math_defines.h
的引用, 所以可以知道这个 _USE_MATH_DEFINES
的常量定义并没有生效.
而当我搜索所有关于源文件代码 source.cpp
的预编译结果时只有两个:
1 | #line 1 "c:\\users\\guiqiqi\\source\\repos\\testforprecompiler\\source.cpp" |
这里验证了, 第二行的 cmath
加载由于在加载 iostream
的过程中已经完成, 预编译器直接对这一行进行了忽略(甚至在源文件中定义 _USE_MATH_DEFINES
也一并被忽略了…)
不得不感叹一句, 现代的编译器是真的太智能了。
]]>彼得堡凌晨一点了,这天亮的跟国内5、6点一样,刚刚睡了两三个小时,现在又睡不着了。加上又觉得很烦,就来给各位看官老爷们诉诉苦(反正博客也没人看,自娱自乐一下就行)。
刚刚想查个单词,打开软件立刻闪退,我只看到桌面上写了一行字 This item is no longer available for you. If you want to continue use it, need to buy it in AppStore. 具体的话是什么我记不很清楚了,但是大概意思就是这样,说我的这个软件不能再用了,得去AppStore买。你还别说,这种情况我估计还真的没有多少人遇到过。
事情是这样的,我在AppStore发现一款非常不错的俄语词典,但是可惜太贵了(将近200块钱人民币),实在是买不起(我一个月生活费也就1200左右,这一本破词典就1/6了)。但是之前看到班里一个土豪在用,真的好用到不行(学英语的同学可能不是很理解为什么不用其他的软件代替,但是真的,俄语这种小语种的学习资料真的太少了,少的可怜)。
实在太眼馋了,就想了个这么个方法,求了求土豪,把我们加入一个家庭组里面,然后用家庭组分享的方式下载用。但是由于我之前在俄区,加入家庭组需要转区,说起来不麻烦,操作起来还是废了点劲。好在最后用上了,你都不知道,我当时多开心。
但是我在tandem上认识的几个外国朋友,有时候需要我下载一些别的聊天软件,在国区是没有的,我最后实在不厌其烦,离开了家庭组,重新转回了俄区,想着已经下载的软件起码不会不让我用吧!结果就出现了我开头说的情况….
这个软件要是能打个折,20/30块钱我也就咬咬牙买了,关键是太贵了。
就坐在这里,也不知道啥时候,吸血虫又给我腿上咬了两个包,实在痒的不行。
下午出去吃个饭,中餐馆的饭要想吃饱实在太贵,买个50卢布的面包还得咬咬牙…
其实说真的,我也是很矫情,彼得堡也有很多很多人,为了钱的事情发愁。我就知道一个老哥,没钱了,就买了一大堆燕麦片,好像喝了一个月,还有牛奶,其他啥都不吃,一个月才花了4000卢布(合人民币差不多400)。
但是真的不好意思,我做不到,我怕这样把自己吃死了。
有时候就在想,我要是有很多钱该多好啊,有钱我就自己在外面租个小房子,这样就没有吸血虫了。有冰箱了,我每天都能做想做的饭,去超市买东西也不用考虑很多,一次买够,放冰箱(其实要是有冰箱,能在吃上面省下不少钱的,这个我算过)。到时候在装个WiFi(居民小区的WiFi很便宜,学校里面的就很贵,上次去给狗子航他们租房子,6个月才一千多卢布,合人民币一个月20多块钱,网速还贼快),就再也不用用手机给电脑分享网络了,到时候有窗帘了,晚上睡觉窗帘一拉,没课了睡到大天亮。这日子过得是真的太幸福了。
也就只能想想了。
说到WiFi我才想起来,上次装WiFi的时候,被装机的人给坑了,那个毛子估计看我们是外国人,就问我们多要了2000卢布,但是没有把这部分钱的路由器给我们。
没钱怎么办呢?肯定得自己赚。
想去做代购?彼得堡的代购比柜姐都多,关键是我得要点本金进货,我也跟狗子航跑过一小段时间代购,我的俄语水平还是支持去提货买货的。在一个关键的就是得有客户;我记得曹博他代购做起来的主要原因是他们家认识的亲戚朋友都是LV的人,我和老妈认识的大部分都是像我这样吃刀削面还得纠结是8块钱还9块钱的人,狗子航也有一定的开始的买家;但是我还是打算有时间了试一试,看看有没有可能赚点钱。
做代购不好做?那去干导游或者中介?可以是可以,就是真的没时间;我这个系不像是狗子航还有杨队他们的国关和经济,一个礼拜4节课,不想去了还能翘,所以有大把的空闲时间。我们一个礼拜课基本是满的,一学期翘2、3节课可能就开除了。
那就写代码吧!靠劳动吃饭。但是每次打开sublime看着这么多代码,还有很多很多没有写,真的有一种无力感。我真的不知道我得什么时候才能把这个项目写完,关键是写完了还不知道有没有人用。但是还是有时间有精力就写点。
我也想不出其他的方法,记得上次有人让我去代考,一天200美金,我真的好心动,只可惜那天有课,我真的不好意思骗老师请假。
但是人不能矫情,日子还是总有办法过的。
没事就想想下个学期就出去和狗子航住了,想着还是挺幸福的,但是想想多出来一些钱要付房租,又头疼了。没事,假装自己项目马上就写完的样子,想想剩下不多了,再做做梦,能有很多人用,赚很多钱,也就很开心。
又给咬了个包。
这样的生活,让我时常有种无力感,这种无力感让我想家,想念以前在大学的快乐生活,也同时让我沉沦;我经常会有种破罐子破摔的想法:学习有什么用处呢?我现在在两个班成绩能排到前几,有免考科目,提前入系等等,可是有什么用呢?家里没钱,自己没时间赚钱,一点办法都没有。我真的不知道如何和生活继续刚:想想自己以后的梦想,那么遥不可及,感觉不到说的什么每天一小步有什么用。
我想起来马飞的《帮忙》中的一句话:这日子过得太恓惶。
但是也就是这样破罐破摔,也不能不会放弃对美好生活的渴望。
虽然知道做梦是假的没用,但是我还是会有时候梦想变得很厉害,很有钱,能过上好日子,给老妈买个房子。这样的想法还是能给我一点动力,让我不至于完全破罐子破摔。
我觉得生活就像是游泳,我不是一个激进的成功主义者,每天玩了命的游,累了也不歇,一定要拿第一;我只想过到让自己和自己爱的人满足即可;所以我更会这样游:奋力游一段时间,就会觉得很累,休息到快要沉下去的时候,在奋力游一段时间,如此往复。
虽然目标很远,但是还是要慢慢走的嘛,毕竟生活,就是在破罐破摔中奋力向前。
]]>我这片文章是用手机流量共享给电脑上打出来的。原因是因为我旧的小路由器坏掉了。那个迷你路由本来是我从国内带来准备备用的,当时装网线的时候想着凑或者用一下,到时候再买新的,结果就一直凑合到它寿终正寝。我跟寝室人提议去买一个新的我们平摊钱,没有人愿意掏钱。
好吧。
正文。
1:30 补完作业,回来打算睡觉了。那个时候室友们还在开心的玩,吃饭。我被吵得根本无法入睡。
2:00 寝室稍微有些安静,我开始进入模糊的梦境。这时候加工15宿舍的黑夜大使就出来招待我们了——吸血虫。
很多人也许根本就没有听说过这种东西,但是它给我造成了不少的影响。
我的脚还有小腿已经被咬的面目全非,每天都会有因为挠烂而产生的新出血口;旧的伤口也有时候会被揭开出血。下半身至少被咬了50个包,这些包在愈合之后会呈现暗红色的疤痕,于是我的脚现在已经全是疤痕了。
我去找过104宿管说过,他们没有管;包括之前寝室所有炉子都是坏的事情也说过,也没有了下文。
前一阵子去买了杀虫药,管用了两三天,这几天又出来了,我被咬的睡不着觉。
3:00 我起来用手机灯光开始找虫子却什么都找不到(那种虫子有的很小),对床的老哥还在开着外放看视频。
3:30 我再次躺下尝试入睡,却被外面的乌鸦叫声和汽车引擎声音吵醒。(最近彼得堡热的出奇,我们如果关上窗户就会很热)。
4:00 天已经基本上亮了,加工的宿舍没有给装窗帘/遮挡物,晨曦的阳光开始让我这个靠窗的人有点觉得刺眼。(彼得堡在过一段时间就会白夜,现在已经每天只有4-5个小时黑夜了)
4:30 我从床上下来,看着外面跟国内9点一样的天色,叹了口气;还有不到5个小时就要上课了,我放弃了睡觉的打算
5:00 打开电脑,写下来这篇文章。
清晨的彼得堡有种出了奇的宁静,可是我却感觉很烦躁,不知道是不是没有睡觉的缘故。
很烦。
]]>机型:华硕 VM590Z;配置:AMD P10 处理器 8G HDD 500G+SSD 120G 双显卡交火
问题现象以及初步推断:机子才送来的时候问题就是启动之后win10一直提示正在准备修复,但是会一直卡着转圈,没有办法进入修复以及系统。初步推测是win10系统问题/中毒/硬盘坏道。因为系统启动修复的原因太多了,才开始没有一个比较固定的方向。初步考虑重装系统。
在问过了是否有重要文件需要备份之后,决定启动一下PE先进去看一下两个硬盘是什么情况,确定系统装在那个硬盘上之后干掉分区,然后引导装个win
就在这个时候,奇怪的问题出现了,U盘PE系统进不去了。才开始我以为是PE的问题,重新装了一个还是进不去,后来尝试直接引导win7也会卡在进入安装界面的过程中(大致就会卡在win7走进度条那个地方)。这个问题很奇怪,在网上没有找到有帮助的答案。
我打了个电话问她,随口问了一句:最近有没有什么大的硬件改动?她说:你怎么看出来的好厉害,我找人装了个硬盘,之后不久就坏了。
硬件改动不告诉我,我表示。。。。
于是我猜想问题出在硬盘上,就把C面拆了下来(这个机子挺奇怪的,内部的USB竟然是走硬盘上面的排线连接),然后发现他的加装硬盘采用的是牺牲光驱,加一个光驱位硬盘托架的方式,加的是一个HDD。心想:应该没有人会把系统装在HDD上(结果证明上一个装机的人真的把系统装到了HDD上)。但是光驱位的固定螺丝只有一个很好拆,我就先把机械盘拿了下来。
可是依然没有进去系统。
很难受啊,正当没有思路的时候,我无意间尝试启动了很早之前的03PE系统,竟然进去了。尝试看了一下,发现没有硬盘!
这是个很关键的事情,我重新启动了系统,在过程中发现:03PE启动的时候硬盘灯就没亮过,而所有启动不了的系统卡着的时候硬盘灯都是常亮的!
难不成主板上的硬盘会影响U盘系统启动?我一拍脑袋:肯定会啊!加载SSD驱动的时候应该会对它进行检查,这部分是属于内核模式驱动的,如果驱动没有写好,SSD也出现了问题,系统不就会崩溃/卡住。
我为了最后定位问题出现在哪里。在启动win7的时候,我选择带命令提示符的启动,因为这样它会仅仅加载内核模式的驱动,并且整个加载过程对我们都是可见的。
于是最后定位出来了问题出现在这个驱动上,加载到这个驱动之后,系统启动就会卡住:
查了一下,ahcix86.sys 是SATA口的一个驱动,因为此时SATA口只有一个SSD,问题便得以确定。
经过艰难的拆卸工作之后(上一个装机的人把主硬盘位固定螺丝其中的一颗拧滑丝了,最后用我室友修脚的钳子拧下来的)。将托架放空,然后主硬盘切成HDD,启动win7安装,一路畅通无阻!
整个修机器花了两天,其中一天半都是在拧那颗滑丝的螺丝…..
]]>有个老头面向着我们走了过来,站在了地铁站对面,比地面稍微高一点的石头上。
我不知道他要干什么,但是老师告诉了我们他是一个很有名的人,在彼得堡很多地方都表演过 (Он известный человек, выступал много раз здесь.)
随后他把包里面的小号拿了出来(我不是很清楚那个乐器是不是小号,但是很像)吹了一首 КАТЮША ; 不得不承认,真的很动听:我、老师,还有同学们都等了一个灯时,听完了那个曲子,老师还说我们过一段时间的 День Русского языка 可以表演这个曲子。
很快,红灯变了绿灯,我把踹在口袋里面的手拿了出来,鼓了鼓掌,准备向马路对面走去。
他用中文说了声谢谢。我觉得还蛮惊讶的,就又鼓了鼓掌。
绿灯的时间剩下的不多了,我准备快点过去。
突然,一声响亮的号声响了起来,很久很久没有听到了,但是又无比熟悉。
我竟然一时间叫不出它的名字。
我转过了身,站在那里,身边的所有噪声好像都消失了,空气在那时无比安静。
那急促的声音我让愈发觉得亲切。对,是国歌!
我和另外一个中国同学,在那里站着听完了他的演奏,心头涌上了说不出的感受。
真的,那种在异国他乡听到响亮的国歌声的感觉我无法用言语形容。
那种感觉,让我想哭。
演奏完毕之后,我和他还站在那里,什么也没有说,什么也没有做。
那个老头拍着手,示意让我们鼓鼓掌,我们这才鼓了鼓掌,我走上前去,把我身上剩余不多的零钱全部给了他,握手示意。
老师和其他同学早已经过了马路,在那一边等我们。我告诉她:我们是中国人,这是我们的国歌。
]]>问题的起因是这样的:我的上一个主机上只能在Panel内选择压缩成为zip文件然后下载,当时我为了迁移站点,将所有文件全部压缩了;但是当时我没有意识到的问题是:zip格式压缩会导致文件名的编码被改变,于是重新上传解包之后,文件的实际编码与wp数据库中的编码变得不一致了,这导致所有的非英文字符命名的文件全部都无法正确的被访问。
这个锅….我不知道该让谁背;但在这告诫大家,对于wp尽量不要使用中文作为文件名,因为这一套东西对中文的支持并不很好。
产生这个问题之后,我去了各个地方寻求解决方案,最终都无果;但是我看到 WP大学上的这篇文章 中,作者遇到了跟我一样的问题,病提供了一个基于Framework2.0的小工具去解决;但很不幸的是,我实验了4台电脑(寝室里面的),没有一台能够正常的使用这个程序,在改文件名的时候始终会出现Script的执行错误。
但是我了解了它大概的执行原理:讲所有中文附件名改成拼音,同时将数据库中的文件名也替换掉;有了这个思路,我就可以重写一遍。
于是今天闲下来了,就花了2个小时,将这个小工具重写了一遍。现在站点中的大量图片404的问题已经解决了。
我已经把这个工具发到了git上,地址如下:WPChineseAttachFix
希望能帮助到大家,大家使用愉快。
]]>自己想到一种 Python 跨平台实现函数执行时间限制的思路。
网上的方法比较多的是利用信号量 signal 设置一个定时器,超时之后执行回调函数,引发 TimeoutError
,退出执行。
这种方法易于实现,并且占用资源少,只需要设置一个 signal.alarm
的闹钟即可。
但是缺点也显而易见:在 Windows 平台 并不能够使用。
分析 signal 的这种方式,我有了一种灵感:让触发退出的回调函数与任务函数 “抢占执行” —— 谁先执行完,就返回谁的值:在计时线程返回之后,不管任务子线程的执行结果,直接返回错误/返回指定值。
threading 库中有一个 Timer 函数,我们用它来计时;在整个过程中,需要同步线程内部的值到父线程/主进程中 —— 最好的选择个人认为还是队列。这里我们不单单将闭包中的队列变量用来同步 return 的值,还可以利用队列一个很好的特性 —— 锁,在任务线程和计时线程均开始运行之后,可以用队列的 get 锁把父线程/主进程阻塞掉,这样既能保证很低的系统占用,又可以实现回收线程返回值的目标(但我这里单独给了一个锁变量控制),一举两得。
下面贴代码:
1 | # author : guiqiqi187@gmail.com |
如果不想让超时之后引发 TimeoutError,请给装饰器传入 default参数,作为超时之后的默认返回值。
另外需要注意的一点时:任务函数并不会随着装饰器函数的退出而结束,因为在这里父线程是主进程,虽然设置了 Daemon ,但是装饰起函数退出后,主进程仍在执行,便不会退出任务线程(如果一定要退出任务线程,请考虑将整个过程单独出来成为一个线程,但是这样回更多的占用资源,并且需要再次同步父线程和主进程之间的返回值,比较麻烦。)
这样就可以实现跨平台的函数执行时间限制。
]]>2018年已经过去9天了,是时候回头看一下2017年了。
随便写一些,没有什么逻辑,想到哪里写那里吧!
维护博客真的很难
在从年前的几天到今天为止,我一次又一次的打开了后台的面板,可是一次又一次的退出,不知道能写什么。
虽然心里有很多话想说,但是真的看着这个白色的编辑面板时,却又一句话都说不出来。
今天收到主机商给我发的邮件,主机快要到期了;虽然很穷吧,但是我毫不犹豫的又给它续了一年的命,毫无疑问,明年我也会做相同的事情。
算一算啊,不知不觉这个博客我已经维护了3年半了,我想一直继续下去。
时间真的会给人带来很多改变,上午的时候没事,又把之前的帖子全部翻了一遍;如果非要用一个词来总结的话,我觉得我会选:成长
这毫无疑问的,从最早生成一份sitemap都会很开心的写一篇博客来记录;到今天,我自己在写一个全栈系统,遇到的技术问题也越来越难、越来越深;
个人觉得去年写的最好的一片技术帖子:Python for in..遍历中的问题与原理分析
记得那个时候我已经决定要出国了,可是放假的时候一点紧迫感都没有啊,我还是整天很浪,学学俄语,写写代码。
那天碰到这个问题的时候我也可以算是刨根问底了,之前写C的时候用的一直都是MinGW编译的,我当时改了一下Python源码的遍历临界条件,准备重新编译一下,看看是不是会在遍历的时候引发越界之类的,只可惜MinGW一直编译不出来,莫名其妙的很多错误啊,于是就放弃了。但是当时那份激动的心情,我到现在还能感受得到。
只要胆子大,天天都是寒暑假
我刚刚写上面内容的时候突然发现一个很有意思的事情,关于我性格的,我不知道这个小标题能不能准确概括,但是大概就是这个意思吧。
高三的时候啊,我所有的同学们都在每天努力的学习,我印象很深刻啊:坐在我前面的可欣姐,有一天做了一整天的数学题,那个题好象是关于圆锥曲线的,很难啊,我拿来看了半天,真的不会,好难,于是我就放弃了。
下午放学的时候,我看到她还在做题,我就去看了看,竟然还是那道题·····
当时我整个人都惊了,不论这样的学习方法是否正确,但当时我心里是很波澜的:一方面,我真的佩服她的毅力;另一方面,我觉得我自己得好好学习了·····
可是回到家里,我又打开了BiliBili,选择了熟悉的鬼畜区(这里手动@小萝莉同学,你不应该给我看那个挖掘机的视频的)·····
日子就这样过去,要知道当时离高考还有40天的样子,我竟然丝毫不觉得紧张。直到高考前一天晚上我才有了紧张的感觉,我很紧张,很紧张,我问老妈:“妈,我要是考不好怎么办啊!”,“没事,考不好就考不好,上不了大学我们还有其他路可以走。”
不知道为什么,当时我没有什么感觉,但是这句话现在想起来,就像是一块石头投进了我心里,让我感到五味杂陈,突然想起 Ed sheeran 《One》里面一句歌词:
All my senses come to life.
是啊,就是这感觉,我所有的感觉到都用了上来,甚至有一点想哭。
也许真的是我的性格使然,每当走到人生的重大转折点的时候,我总是在最后一刻才会感到紧张。
记得当时我和老妈拿着两个行李箱,从607路公交上下来,走向西安火车站,老妈哭了,我到现在都不很明白她为什么会在那一刻哭,我问她为什么,她给我的回答是:”和你姥姥一样,过一会儿就好了“。
和老妈坐在火车候车厅,她还是过一会儿就会哭,我安慰她:没事的,迟早有一天我会回来的。
看到背后的幕布上写着:“再见,乡党!”,当时我唯一的感觉却是一种即将一展宏图感觉。想一想觉得自己好没有良心啊!
可是现在我又想起来,那秦腔脸谱,那皮影戏的照片,那一句“再见,乡党”的道别,老妈当时的眼泪,我很难受。
直到在首都机场候机的那个晚上,我开始紧张了,我紧张到有点想吐,吃不下东西,也睡不着觉。这时候老妈却没有一点难受表现出来,她只是安慰道我:“”没事,如果呆不惯还可以回来啊!“
是啊,世间还有什么爱能比母爱无私伟大呢!
我走向国际检票口,走向下行的电梯,我不停的回头看,老妈就站在那里,我忍不住哭了出来。我看着周围一群人,带着要去旅游的喜悦心情;我站在摆渡车的角落,尽量让人不要看到我,尽量让眼泪不要掉下来。
去平复一下情绪
刚刚去平复了一下情绪,深夜写帖子,一盏台灯,一首曲子,真的很容易让悲伤情绪蔓延。
话说回来,我的性格可能真的是这样的,虽然平常比较谨小慎微,不会很张扬,但是真的到deadline才会感觉紧张吧。
本来想给大家分享些曲子的,但是网易云全部灰色了,我刚刚听的曲子:Five hundred miles & Liekkas.
Я люблю получить пять.
哈哈,如果能算得上今年(误,其实是去年年末)比较让我开心的事情,就算这个了,上半学期的Зачёт签过都是5分。
摩尔曼斯克之旅
一月2日,和一群坑爹的土豪队友,开始了一次摩尔曼斯克之行。
真的很坑爹,一个个都是20+的人了,做出来的事情却幼稚到可笑,让人觉得像是9、10岁小孩子的水平。
去之前,群里面除了谝闲传,少有人做正事的;从确定航班,到支付,回来火车票,接机服务,旅行住宿,市区打车,基本全都是我一个人考虑的。每次要征集大家意见的时候群里面却只有聊天,有时候竟然没有人理我。
我去,是我一个人去旅游吗?WTF
1月2日晚上8:50的飞机,普尔科沃机场到Академическая有1个半小时的车程,当然因为我没有带什么大行李,我准备走地铁和39路
下午4:30我开始问要不要走,没有人回复我,到5点多有人回复,但是都说是觉得早了。
也许吧,但是我东西都收拾好了,我也愿意在机场坐着候机,而不是着急到屁股着火的往过赶。
OK,晚点走吧,5:30我又问了一次,还是说早;我的门卡叫其中一个大哥拿着呢,我也出不去,我给他们两个人发消息,他们6点回复我就要到了,我提着箱子在一楼大厅站了半个小时,一个人都没有,消息也不回。
我给航哥打电话叫他给我刷了卡,OK,我自己走,快到阿卡的时候碰到了他们两个,才准备回来。
也是没谁了。
结果确实,你在怎么准备充分也架不住人家有钱,两个人回来拿了箱子,直接Taxi去了,到的比我还早;我因为从地铁站出来迷了路,耽误了20min,但还好,赶上了航班,等我到了登机口,却不见他们人,问他们在那里:”我们在麦当劳吃饭啊!“
我吃你妈的大头鬼!
直到最后一分钟,检票的空姐吧,一直都在给我说Будет окончен.她可能觉得我没有理解吧,我是很着急,一直在给他们发微信,但是没人回我,我又找不到他们,最后那个空姐直接给我说英语了:
Your friends just have 1 min.
我给你说,我当时真的有把这几个人砍了的冲动。
我进去了摆渡车,还好他们赶上了最后十几秒。
下了飞机,我已经见到了预约车司机,我真的是····却见不到这几个人了,一问在哪,厕所。
完了我给你人家这几个人说这是司机,也没人理我,三个人直接就走了,把我扔在最后?WTF?
到了酒店,那个大佬才发现入境卡给丢了······百般周折联系到彼得堡的酒店让给了一张扫描件,人家才勉强让入住。
尽管队友都是这样的人,俄罗斯的热情好客却也温暖了我的心。
入住的EvroHostel,这里算是个广告吧(估计也没人会看),酒店前台的小哥哥是个很好的人,我俄语不是很好,跟他交流起来比较费劲,但是也能感受到他的热情,他知道我没有拖鞋之后把他的借给我了。
和我一间屋的那个俄罗斯小哥哥也很好,我跟他用蹩脚的俄语说,我觉得俄罗斯有很多很好的人,没有他们的帮助,我将很难生活,他还给我说了谢谢,后面给前面的酒店报电话,我也是很奇怪,这些学了3年俄语的人电话号码都报不出来,这个小哥哥帮我们报了出来。
到第二天走的时候,这些人非得要去买化妆品,完了就是不在金拱门吃饭,非要去神TM上海餐馆,结果就是一个人花了1000卢布,还基本啥都没吃饱。也许真的是我没法理解有钱人的思维吧。
晚上叫车去基地,这里是全程我唯一没有提前考虑好的点,这些人叫完车,三个人全部回到了房子里,丢下我一个人在冰天雪地里看行李,摩尔曼斯克,当时零下20度。我艰难的把三个人的行李搬到了房子里,没有理他们,听着许巍的《蓝莲花》,努力让自己宽慰一些。
在基地游玩的三天,这三个人跟我说的话不超过20句(除了那天晚上玩真心话大冒险,问了些很无趣的个人隐私问题,真的好傻逼啊,我真的忍不住爆粗口了,一个个都是成年人了,思维幼稚的像小学生)。
OK,那把我一个人丢在那么黑的路上算是什么情况?我艹,还可以这样无视的吗?
我不想把文章变成吐槽的地方,但是这些人真的很过分。
不过比较好的就是我看到了极光,吃到了寡头爸爸给的礼物帝王蟹,味道真的很好吃。
下面贴几张图(有极光啊):
不得不说,极光真的好美,当你躺在雪地中的时候,抬头看到极光月光交织,那份宁静,真的是无法用语言描述的。
和那个司机聊了半天,真的我要是能很好的说俄语的话,我肯定和他们好好聊。我给他说,我的家乡很远,当人们看到月亮的时候就会想念家乡,想念父母和亲人,他拍了拍我给我说:“всё будет нормально”,然后打开手机给我看了他孩子的照片,那阵子他笑得很开心。
想起来《我是怎样成为俄罗斯人的》里面说:俄罗斯人遇到不好的事情总会告诉自己,一切都会好起来的!
之后他们三个人去一边玩,我躺在雪地里,司机也在雪地里,他竟然睡着了,俄罗斯人是真的很强,他起来我问他“усталость?”,他说不累,就是睡得很少,工作太多!哈哈,他最后还免费带我们绕着湖彪了一圈雪地摩托,
回去之后在基地见到了一对泰国情侣,泰国的那个小姐姐真的很漂亮,在美国待了7年,一口非常标准的美式口音,非常好听。
我们的大佬也是在美国待了3年的人了,啥都说不出来,口语比我还要差·····我要是它的父母,我就叫他回来板砖了,去States真的浪费钱。
最后回来的的火车票也算是白买了,只有我一个人,这三个人又去了捷里别尔卡,我问他们火车票怎么办,他们说:”那就Miss掉了啊~“
OK,有钱,很强。
于是我一个坐了一个包厢回了圣彼得堡,这是在路上外面雪后天晴拍的照片,真的很美:
至于那位大佬和那两个女生嘛,大佬的入境卡丢了也不着急,现在还在捷里别尔卡玩呢;到哪里一定是要给别人旅游的,饭上来可以不吃,一定要先拍照,上传到朋友圈,商量一下用什么语言发别人看不懂,能显得自己很高大上,再加个定位,第二天比一比谁的赞多嘛。你们接着发,我已经屏蔽了,回来钱一给你们,两清;愿之后再也不会有联系,谢谢。
项目进入瓶颈期
不知道是因为代码量上来了,还是自己架构做得不好,毕竟我是第一次试着写一个全栈的项目,可能对各位大佬来说不大,但是对我来说很大,我也很尽力了,但是不得不说,这段时间项目进入了瓶颈期:
新功能可以往上加,但是自己总是不愿意,因为总觉得有点如履薄冰;
觉得代码脏,要重构,看一天自己的代码却找不到什么地方能重构;
单元测试也写了。但是还是不能很好的覆盖到边角情况;
各个组件的解耦也都做了,日志系统也上了,遇到的BUG却越来越多,越来越不好定位;
希望能快一些结束瓶颈期,之前的代码也能尽快稳定下来,继续新功能开发,争取卖出去,能赚点小钱能自给自足哈~
新年愿望
至于新年愿望呢,就简单几个,如果дед мороз看到了,麻烦实现一下,谢谢啦!спасибо!
愿望毕竟还是要自己努力的!新的一年,加油!
]]>但是不能一个个改吧,于是我自作聪明,把所有文件都选中,然后单击右键,重命名:第1集,回车!
我发现其他文件名竟然一次性自己重命名好了,这可把我高兴坏了!我关闭了文件管理器。
等下!
好像文件名有点不对!
然后我惊喜的发现,Ctrl-Z好像也失去了作用·····最关键的是文件的的顺序都是乱的,也就是说第一集可能被重命名成了第1集(5)…
幸亏我在移动硬盘上有备份,要不然这次得累死。
于是我就在想有没有什么方法能批量重命名文件呢?
其实方法有很多,关键就是都比较麻烦,而且不人性化,于是我就决定自己写一个工具。
经过接近一个月在业余时间的工作,我完成了这个工具,并把它发布在了Github上,使用MIT协议。
大家如果有兴趣的话可以去看看,给个Star我也很开心哦~ 软件截图:
具体用法软件帮助中有比较详细的介绍
希望大家用的开心!
]]>