最近这段时间一直在研究 Web3 相关的技术,而迈向 Web3 数字世界的遇到的第一个门槛就是拥有一个数字钱包。数字钱包地址可以理解成现实世界的银行卡号,不少人会去追求号码有好的寓意(比如尾号尽可能多的 6 或 8,或者是对自己有独特含义的号),不同的是,银行卡号的靓号往往需要去银行花钱办理;钱包地址则只需要你花时间去碰撞生成即可。这种去不断碰撞得到的地址一般称为虚荣地址(Vanity Address),也就是满足自己的虚荣心而生成的,Solana 也形象地将这种行为称作 Grinding(磨),虚荣地址其本质以及功能性与其他的地址对普通用户来说没有太大区别。
在运行生成地址的算法时,显卡相比 CPU 有很大的优势:显卡的流处理器相比 CPU 有数量级别的优势,因此使用 GPU 来生成虚荣地址会更快速。网上找了一圈,虽然 Solana 也有 solanity 这款使用 CUDA 编写的工具,但是我使用 RTX 3080 运行的生成其实和我用 CPU 跑不出什么多大的区别(issue 也有人反馈根本达不到预期的性能),而我在使用 profanity2 这款 OpenCL 编写的 ETH 虚荣地址生成工具运行速度是相比 CPU 有数量级的差距。因此我便开始研究生成地址用到的加密算法,打算自己写一个出来。
对于不同的 Web3 钱包种类而言,其实生成地址的步骤都是大同小异的,最大的区别在于选择的加密算法不同:
对于 ETH 地址选择的椭圆曲线是 secp256k1,Sol 地址选择的椭圆曲线是 ed25519;找到对应坐标点后,ETH 会对公钥进行 keccak 哈希,然后取最后 20 个字节转 hex 后作为地址;而 Sol 则是直接对公钥进行 base58 编码作为地址。
也就是说,用 OpenCL 实现 ed25519 和 basee58 这两个算法就大功告成了。
因为 OpenCL 本身的语法是基于 C99 的扩展,因此从 0 开始实现加密算法并不是首选的方案。找一个 C 语言的实现,验证没问题后移植到 OpenCL 是更安全、方便的做法。
网上有很多选择,我选择的是: ed25519 和 base58 。
之前在不太了解 OpenCL 的时候,我在网上 copy 了一份使用 OpenCL 计算哈希的代码作为入手代码,但是在批量运行 10w 次时,甚至相比单纯 CPU 算慢了非常多。因此托朋友问了一个游戏渲染方面的大佬「显卡加速哈希运算应该怎么做,为什么我批量计算比 CPU 还慢」,然而得到的回答并没有解答我的疑惑,他认为哈希运算并不能分块计算,因此使用 GPU 并没有优势可言,而是像图像那样可以细粒度计算才能体现 GPU 的优势。他的回答某种程度上没问题,对于单次的哈希运算而言,GPU 不会比 CPU 更快,而我其实需要的是批量输入的并行计算,在后来我明白了,其实是我输入或者说调用方式有问题,我要运行 10w 次哈希,不应该是调用 10w 次 OpenCL 内核代码,而应该是调用 1 次 OpenCL 内核代码,然后设置 10w 个 worker,然后在内核代码里计算 10w 次。
worker 可以理解成 thread,其数量是通过调用内核时传入的 global worker size 参数来指定的。
具体到地址生成算法,我的做法是:随机生成 32 个字节作为 seed,然后将 global worker size 设置为 256 ** 4
,每一个 OpenCL 线程分别去获取当前 thread id,然后转化成字节的大端序格式,然后覆盖的末尾四个字节,每个线程计算一次。如果找到符合条件的地址,就记录在输出中。然后每一轮 OpenCL 调用,都会把 seed 倒数第五个字节 + 1(满则进位),这样来做迭代计算,直到找到满足条件的地址为止。
OpenCL 程序并不像 C 程序一样,代码编译后直接运行,而是分成了两部分:
其中 ed25519 算法以及 base58 算法都放在内核代码里,然后暴露一个入口函数,由主机端代码调用,并对结果进行后续处理。
我使用的 mac 系统自带的 OpenCL 版本是 1.2,在迁移的过程遇到的最大阻力就是入口函数的问题,OpenCL 相比标准 C 多了地址空间的概念,尤其是在内核函数的参数里,需要指定空间为 global、private、local 三者其一。
而我在把程序迁移到 Nvidia 设备的生活,遇到的问题就比较麻烦了,因为自 OpenCL 2.0 开始函数的参数未指定地址空间默认为 generic,如果调用这个函数传入的是 private 地址空间的变量就会编译错误,而默认声明的变量都是 private 地址空间的,因此需要逐个把调用函数的参数都改成 generic 的空间,一共一百多个地方(掀桌子…
profanity2 本身其实是修复了 profanity 私钥 seed 生成不够随机的漏洞,但是此外它还有一个很大的改进,就是它不生成私钥 seed,由用户提供公钥,然后利用对公钥的偏移量来计算不同的虚荣地址,就算公钥被泄漏,但因为椭圆曲线的特性,也无法从公钥逆推出私钥,最大化地确保私钥的安全性。
我初次了解这种设计时,感觉真的很酷,于是深入了解了一下这样做的技术原理,并且也想让 Solana 地址也采用这种方式生成:
k
,偏移量为 delta
,那么新的私钥可以表示为 k' = k + delta
。由于椭圆曲线的性质,这种加法操作对应于曲线上点的加法;k'
将生成一个新的公钥 P'
。由于公钥是私钥乘以 G 点,因此 P' = (k + delta) * G = k * G + delta * G = P + (delta * G)
,其中P
是原始公钥,P'
是新公钥,delta * G
是偏移量乘以 G 点,表示在椭圆曲线上移动的效果。因此,当使用 profanity2 找到满足目标的 delta 时,将 delta 加上原本的 seed 就是目标的私钥了。
不过 Solana 的地址并不能使用公钥 + 偏移量这种方式生产,因为 ed25519 的公钥生成略有不同:需要对原始私钥 seed 进行一次 SHA512 运算得到结果 H,然后用 H 前 32 字节转化成大数去乘以 G 点,才得到公钥。这也就意味着,即使我对公钥偏移了 delta 得到了目标的地址,但是得到的输入其实是 H + delta,我们并不知道对原始 seed 进行什么样的变化才能让 SHA512 之后会得到刚好偏移 delta 的值,SHA512 同样是不可逆的。
我使用的是 M1 版本的 MacBook Air,使用官方的 solana-keygen grind
命令计算的哈希速度是 0.3 Mh/s。
GPU | Memory Clock | TFLOPS | Hash Speed |
---|---|---|---|
Apple Silicon M1 | - | 2.3 TFLOPS | 1.2 MH/s |
RTX 3080 | 1188 MHz | 29.2 TFLOPS | 23.1 Mh/s |
RTX 4090 | 1313 MHz | 81.9 TFLOPS | 67.8 Mh/s |
另外说一点,这种 Vanity Address 的工具吃的是 GPU 的计算性能(而非图形性能、显存),预估不同 GPU 的速度,使用 TFLOPS 指标会更接近真实情况。
项目的代码 已开源,欢迎自行对比。
两年前的我,自认为本身的技术已经达到了一个比较高的水平,再加上对搜索引擎使用的也比较熟练,我想我对软件开发领域的大部分技术已经信手拈来,就算真的遇到了没有接触过的领域或者技术(比如:计算机图形学),上手也无非是需要一个熟悉的过程罢了,而在过去工作的几年时间,我也没有遇到什么无法解决的难题。而这段时间学习 OpenCL 的编程其实是给我上了一课。
虽然本文写的过程很轻松,但主要原因在于这不是我第一个接触的 OpenCL 的项目了,加上地址生成算法比较简单,只是涉及到椭圆曲线的点乘运算,完成这个项目也就花了十来个小时。回想起一个多月前,首次接触 OpenCL 项目时,遭遇的困难远比我想象的多得多,途中我也曾数次想过放弃——因为如果完全不了解一个领域时,你甚至都不知道如何下手,对 ChatGPT 提问也问不到点子上,那么自然也得不到想要的回答,但我终究还是坚持过来了。
只是经过这次之后,我不再会那么「狂妄」的认为我已经熟悉了大部分技术了,计算机终究还是一个非常大的领域,我所了解的技术大部分也只不过是限定在 Web 开发领域而已。
]]>最近因为 Clash for Windows 作者被请喝茶的缘故,Clash 从内核到各平台客户端的仓库大部分都删库或者归档了,这也不能怪开发者太过风声鹤唳,毕竟还身在国内,写开源而已,犯不上和自己的人身安全作对。之前我一直都是使用 Clash 作为主要的科学上网工具,不过经此一役后我也在考虑是否应该放弃 Clash,于是我仔细梳理了一下目前对科学上网的需求,最终决定将 Clash 切换成 Quantumult X。
其实我很早就觉得 Clash 在 macOS 上使用有一些不便:
对于第一点,其实 Quantumult X 就做得很好,分流的规则以及机场的节点是分开配置的,而且无论你有多少不同机场的节点,你都可以通过新建一份分流规则来在所有节点之中切换,而且新建的分流规则并不会被机场本身定时更新的订阅所覆盖。
对于第二点,其实是代理工具和 VPN 的区别,像是 Cisco Anyconnect 这种 VPN 工具,会新建一个虚拟网卡,并新增一条路由规则让所有应用的流量都流经此网卡,也就不再需要单独为不同的应用设置代理选项了。Clash X Pro 的增强模式也是使用这样的解决方案。
注意,这种强制所有应用走代理与 Clash 提供的全局代理是不一样的概念,Clash 的全局代理意思是所有通过 Clash 的流量不经分流直接转发到机场节点。
对于稍微大一些的机场,都会添加各种各样的审计规则,也就是在服务条款里写的禁止访问政治敏感或者新闻等类型的网站。大部分的机场并不会写明具体是哪些网站被禁止访问,甚至也有些正常的网站会被「误伤」。
对于机场的审计规则我在一开始时表示很不理解,毕竟科学上网就是为了突破 GFW 封锁,怎么机场还又给上了一道锁。后来我想明白了,这类机场一般都是在国内有中转入口的机场,而中转服务器架设在国内受到的监管会比家宽更加严格,本质上也只是机场主规避风险的一种手段。所以目前没有审计规则的机场,要么是直连境外的机场,要么是机场的规模不大,或者机场主愿意承担这种风险。对于前者,访问速度比不上中转机场,对于后者,我只能说:而你,我的朋友,你才是真正的英雄。
不过,机场的审计规则对用户来说,也确实也有隐私泄露的风险存在。有审计规则,那必然就有记录审计日志,也就意味着你访问的浏览记录都会被机场所记录,最坏情况下,机场因为某些不可抗力的因素或者是被黑了,流出了所有用户的浏览记录……
免责声明:本文并不是要教唆大家通过链式代理的方式去访问被机场封锁的网站,而是仅从技术的角度,探讨如何让自己的网络浏览更安全、隐私。
那么,何为链式代理?
其实和机场本身提供的中转类似,我们可以在机场线路外,再加上一层中转,也就是把机场的落地节点也当作是二次中转,而由我们自己的境外 VPS 提供真正的落地。网上有很多关于如何配置 Clash 的链式代理的教程,不过 Quantumult X 的却很少,所以我就来抛砖引玉了。
以日常使用的中转机场来举例:
先看不加上 VPS 的情况,我们的电脑到机场国内的中转服务器(Relay Proxy)是通过公网通信,中转服务器通常是国内的公有云(阿里云,华为云),然后再由中转服务器通过 IPLC/IEPL 等内网专线连接到海外落地服务器上,从而实现科学上网的功能。
需要注意的是,虽然 IPLC/IEPL 不会被墙,但是因为它仍然有一端是处于国内,因此从你的所在地到中转服务器的线路也直接影响到了科学上网的使用体验。而当你请求被机场封锁的域名时,连接在中转服务器时就会被丢掉,根本不会再往专线另一端发了,因为中转服务器才是运行 Shadowsocks 的服务端,它可以直接拿到访问请求的域名。
而在有 VPS 的情况下,我们就可以把机场线路的整体当成是中转服务器,由 VPS 来做落地(Final Proxy)。这样做的好处就是,流量会被加密两次,一次是中转机的加密,另一次是 VPS 的加密,而中转机拿到流量后,因此还存在一次加密,所以也根本看不到你具体的访问,只能看到是对 VPS 的访问,只有 VPS,才能看到你真正访问的域名。这样也能解决机场审计记录的隐私问题。
经过了两次加密和转发,性能可能会有些损耗,不过这在浏览网页时一般是感知不到的(跑测速可能有细微差异。
Quantumult X 配置链式代理不复杂,而且配置完成后,并不会被机场的定时更新的规则所覆盖:
VPS 的服务端并不需要开启什么流量伪装或者混淆等复杂的配置,因为 VPS 与机场节点之间的连接并不过墙,设置一个稍微复杂一点的密码,选一个安全的加密方法即可,因此 Shadowsocks 就够用了,你可以参考 我的配置 。
在 Quantumult X 点击设置 -> 节点 -> 添加,把以上配置填进去,标签可以随意,我填的是 hh-jp。
或者在 Quantumult X 的 设置 -> 节点 -> 节点资源,这里可以加上 fast-open 和 udp-relay 参数:
shadowsocks=xx.xx.xxx.xxx:xxxxx, method=aes-256-gcm, password=xxxxxxxxx, fast-open=true, udp-relay=true, tag=hh-jp
在 Quantumult X 设置 -> 配置文件 -> 编辑,会弹出一个编辑框,在分流规则部分的尾部加入以下规则:
ip-cidr, xx.xx.xxx.xxx/32, ♾️ Relay
# 有两种选择,直接给 final 加上中转
final, hh-jp, via-interface=%TUN%
# 或者针对特定域名加上中转,我比较推荐这种
host-suffix, xxx.xxx, hh-jp, via-interface=%TUN%
其中第一行 ip-cidr 的规则,是为了让所有流经 VPS 的流量,都通过机场的节点进行中转,♾️ Relay
这个策略组是我新建的,你可以把它重命名为你目前现有的规则策略或者新增一个策略组来专门做转发。
我比较推荐后者,选择新建一个策略组并把与 VPS 在同一个地区的节点都加进来,这样从中转节点到 VPS 的延迟就会更低。
如果你觉得一个域名一个域名加比较麻烦,也可以直接针对 final 添加中转,需要注意如果已经存在 final 的规则,把原有的删掉。不过我并不推荐直接给 final 规则加上中转,因为我觉得 final 规则应该设置成距离自己最近、延迟最低的节点。
我为此写了一个 简单的工具 ,可以更方便的管理需要链式代理的域名,可自行部署在 VPS 上。
有两种方式:
在 Quantumult X 的网络活动菜单栏,请求配置后的中转域名,应该会有两条流量记录产生:一条记录的目标服务器是 HH-JP,也就是我配置的 VPS 节点名称;另一条记录的目标服务器就是 JAPAN 17,也就是机场的节点。
在 VPS 运行命令:
$ lsof -i:[listen_port]
COMMAND PID USER FD TYPE SIZE/OFF NODE NAME
ssserver 21286 shadowsocks 11u IPv4 0t0 TCP [listen_ip]:[listen_port] (LISTEN)
ssserver 21286 shadowsocks 13u IPv4 0t0 UDP [listen_ip]:[listen_port]
ssserver 21286 shadowsocks 14u IPv4 0t0 TCP [listen_ip]:[listen_port]->[relay_ip]:[port] (ESTABLISHED)
你需要查看,与 VPS 建立连接的这个 relay_ip 是否是你选择的中转策略的 IP,或者确认它不是你目前宽带的 IP 就行。
最后,还是谈谈机场的选择,我并不建议把机场是否有审计规则作为评价机场好坏的标准,因为可能科学上网 99% 的情况下都不会碰到被审计规则封锁的网站。而我在过去一年自费购买了差不多 10 家机场,其中只有一家规模不是很大的机场没有审计规则,这家机场的线路比较少但是流量又比较贵,因此我并不会推荐这家。
如果你不知道机场有审计规则这回事,那你就继续安心用;如果你目前的机场用的挺满意但是带有审计规则有点膈应,所以想换一个没有审计规则的机场,请慎重考虑。
目前的科学上网工具大部分都是支持链式代理的,我也比较推荐你用链式代理 + VPS 来绕过审计规则,如果你用的科学上网工具不支持链式代理,那么我更推荐你换掉工具而不是机场。
]]>我其实之前一直对旅行提不起什么兴趣,一开始觉得这只是因为我的性格比较内向、不愿同陌生人发生不必要接触,后来往深了想这其实是来自于我内心追求安逸、拒绝踏出舒适圈的映射。那么为什么我会想到来突破这个舒适圈呢,这很大程度归咎于我开始健身了。从三个月前跑两公里就累得要死,到现在已经可以比较轻松地跑完五公里,我的确成长了不少。因此我觉得适当突破一下舒适圈也许不是什么坏事情,于是就有了这次的韩国旅行。
毕竟是第一次出国,选择目的地费了一些心思,不能太远,最好是个发达国家,加上女朋友之前一直都在青岛出差,离韩国最近,于是就选定了韩国。结果因为女朋友假期匆忙开始的原因,还是从广州出发了,也导致策划得有些不够充分,这些我都会在文章里注明,希望能给有需要的人带来帮助。
淘宝找旅行社办的 5 年多次签,价格九百多一个人,一周多顺利出签。出签后把电子版打印,出发时带上就行,出境值机时和入境边检都会查验。
网上看到说最好把行程单以及往返机票打印出来,出境时有人会询问,但我并没有遇到。
国内能订购的韩国电话卡大致分为 3 种:
如果是只在首尔等公众交通比较方便的地方玩耍且预算有限,我比较推荐第一种,而我们因为要去济州岛玩,济州岛没有地铁,基本上出行只能靠打车,打车软件 Kakao Taxi 需要韩国的号码才能注册,因此我们选择了购买两张 LG U+ 电话卡,语音通话额度可以在机场领取时充值,我们充了 40 分钟,不过完全没用到。
不用考虑国内的电话卡开漫游了,价格贵太多,我是联通卡,打电话问了客服,他说日韩的流量是按 MB 来收费(5 元 / MB),每天封顶 25 元,每天首 1GB 是高速流量,超过后变为 2g 网,也就是十天一共会花费 250 元。
建议提前在国内买个质量好一点的,办签证、电话卡送的插头质量很差,插上吹风机吹了几分钟就烧坏了…
一线城市大部分银行应该都有提供韩币兑换,不过如果你所在的城市银行恰好没有韩元,可以选择兑换成美元,然后带美元去首尔的换钱所兑换,换钱所美元换韩元的汇率优于人民币换韩元。
类似国内的交通卡,地铁、公交、便利店都可以用,可以在地铁站或者便利店用现金充值。因为我购买的电话卡自带 T-Money 的功能,所以就没有买,可以提前在网上买或者等到了机场便利店再买。
在写这篇文章的时候,发现了韩国有为国外旅客专门提供付款、换汇、T-Money 功能的储蓄卡。不过我们在出游时还不知道这个,因此没用上,下次去就打算办一张了,有这个就不用专门带信用卡了。
首尔的物价水平挺高的,比国内的一线城市还高不少。随便找个路边小店吃饭两个人基本上要花 16000 韩元以上,店的规模就是猪脚饭或者拉面店的级别。
以可乐来对比衡量物价的话,500ML 的可乐在便利店的价格是 2000 韩元。
韩国吃的种类很少,主食基本上就是烤肉、拌饭、面、酱汤等。餐馆一般比国内要早关门,大约 9 点就会关门,但是其实提前半小时就会停止接纳新客了。
我们刚去首尔的前两天因为时差没倒过来,早上起来就十点多了,煮点泡面吃就十一点了,下午两点多才饿,而两点多大部分餐馆都是休息时间,午饭吃得晚就又会导致晚餐吃得晚,所以我们在首尔就经常出现饿肚子找饭店…炸鸡店、大排档之类的虽然开到深夜,但也不能当主食吃。
济州岛的物价比首尔高(毕竟是离岛),不过可能是因为济州岛中国人很多,又或者是我们的作息调整过来了,在济州岛吃的反而觉得比首尔舒服一些。
我们在韩国一共住了 9 晚:首尔 6 晚,济州岛 3 晚。其中在选择首尔的住宿位置上犯了一个比较大的错误,一开始想的是住在明洞或者弘大附近,但是担心周围比较喧闹会影响睡眠,于是选择了住在了鹭梁津洞,它距离明洞、弘大,江南地铁都只需要 20 分钟左右的地铁路程。一开始我还因为选择了这个绝佳的地理位置沾沾自喜,但实际住了几天之后才发现完全不是这样。
首先是吃饭不方便,楼下的店铺很少,基本上就是炸鸡店 + 便利店 + 大排档,没有早餐店,所以早餐只能在家煮泡面。晚上逛完也只能选择在外面吃完再回家而不是在楼下吃,这点真的很要命。
其次,离三个热门商圈都很近其实也就是离任何一个商圈都不近,这对于逛街来说其实挺致命的,因为二十分钟地铁加上走路和等地铁的时间基本上就需要半个多小时了,而且我们去的那几天还正好赶上了韩国地铁的罢工,发车间隔十几分钟。
所以如果再去首尔的话,我们肯定会选择住在热门的地方,这样下楼就是商圈,餐厅多,也不需要专门乘坐交通工具去逛,早上出门或者晚上回家抽点时间逛一下就行了,一次两次逛不完,住个三五天,逛个十次八次就熟悉了,白天的黄金时间去逛远一点的地方,这样在首尔会逛的比较惬意。
济州岛的住宿是在 Sogil-ri,一个典型的济州乡村,有着很不错的自然风光,房东提供的早餐也很丰盛。
首尔的地铁还是挺方便的,热门商圈基本全覆盖,下载一个 Naver Map 或者 Kakao Map 可以查看地铁运行情况。Google Map 或者别的导航在韩国基本处于半残状态。
在首尔一般情况下是不用打车的,有需要的话可以试试 Uber 和 Kakao Taxi,其中 Uber 可以线上支付,不过车辆少一些,我在济州岛的各个地方都使用过 Uber 但是都叫不到车,首尔可能车辆会多一些。Kakao Taxi 本身是支持线上支付的,但是只支持 Kakao Pay,而且需要绑定韩国的银行卡,因此我们用不了,还是只能给司机现金。
无论是 Kakao Taxi 还是 Uber,账户注册都需要本地的手机号接收验证码。
第一次搭乘国外的航空——韩亚航空的航班从首尔飞到济州岛,价格很便宜,才两百出头(没有燃油基建费,这就是总价),类比国内大概就是广深飞海口,广深飞海口一天的最低价也是两百多,但是时间很阴间,不是大早上就是大半夜。下午时段最低也要五百多,加上燃油基建费,就要七百了。
另外如果大家也需要在首尔飞韩国国内的话,尽量选择韩亚和大韩航空,其他的都属于廉价航空,虽然价格更便宜(一百多),但是值机时人很多、柜台很少,飞行体验也会比较差。
去济州岛游玩的话,建议多备一些现金打车用,公交线路基本上只有环岛和从济州市到西归浦市的。
首尔大部分景点都玩了,景福宫、明洞、弘大、江南、爱宝乐园等等。不过我对城市景观并没有多大兴趣,而且根据女朋友的对比,其实大部分商品在首尔免税后的价格还不如中免日上的价格低。
这次在济州岛呆了两个完整的白天,第一天去了涯月海岸道路,沿着海边走了一会发现太阳太晒了,找了个风景很好、靠海的咖啡厅呆了一下午,傍晚在海边找了个皮划艇,价格是三万韩币,两个人可以划半小时。
第二天去爬了汉拿山,从御里牧入口上山,然后从灵室入口下山。整个路程共花费了 5 个小时,因为之前已经跑步了一段时间,所以倒没感觉吃力。不过爬汉拿山的时候需要做好防晒,我们防晒霜漏擦了鼻子,结果爬完山下来被晒了个红鼻子……
另外因为汉拿山的海拔接近两千米,气温会下降的比较厉害,山顶的紫外线也很强,所以最好穿长裤以及防晒服。我们就是穿短袖短裤爬山,上山的时候不冷,但是爬到山顶歇了一会突然起雾,风一吹就感觉好冷。
在韩国遇到的大部分人其实都挺友好的,不会因为你不会韩文就歧视你——除了我在涯月海岸公路的一家名为(Team Blow)咖啡厅,这家我感觉歧视中国人:我们点了两杯饮品在窗前坐着看风景拍照,隔壁坐的是三个韩国人,占了两个小桌子。因为我们拿了一个大袋子,所以放在了一个单独的椅子上。然后等隔壁三个人走了之后,店员马上跑过来问我们是不是两个人,如果是的话需要把袋子拿走,不能放在椅子上。
一开始我倒没有觉得这个事是歧视,就把袋子拿走放在小桌子上了,没过多久隔壁就又来了一对韩国情侣,他们喝完很快就走了,过了一会儿我们也准备走,走到门口的时候,店员跑过来拦着我们不让我们走,说什么 Return 之类的,我以为是问我们还回不回来,结果是让我们把咖啡盘送到回收台!可问题是,我明明看到之前走的一对韩国情侣走的时候也没有把餐具送到回收台,是店员收的!
走出来后,和女朋友讨论了一下,越想越觉得被歧视了,另外抛开这些不谈,价格也挺离谱的,两杯果汁 16500 韩元(差不多 90 元人民币),还不如蜜雪冰城好喝。建议大家也避开这家咖啡店。
除了咖啡厅的店员歧视,我们也遇到了超级好的公交车司机——从汉拿山灵室出口到济州市中心的线路(忘记是几路公交了)。当时我们只有一张 5 万面额的纸币,并且忘记携带 T-Money 卡了,于是我们上车就问司机这个 5 万的纸币能不能用,司机不会说英文,我们沟通起来很困难,但是司机就一直在车站等着和我们沟通,后来有另一个热心的大姐,先是问我们的目的地是否在公交车行驶路线上,然后得到了肯定答案后,司机就让我们先坐下。然后让大姐把司机的钱包拿出来,给我们换了 5 张 1 万面额的纸币,然后投了 1 万的纸币,因为投币机只能找出 500 面额的硬币,所以司机是按了 17 次找零的按钮,才找了 7500 的零钱给我,真的太感谢司机了!
花费的钱其实比我想象的多一些:
分类 | 首尔 | 济州 | 小计 |
---|---|---|---|
住宿 | ¥3433.78(6 晚) | ¥1704.58(3 晚) | ¥5138.36 |
机票 | ¥3256(飞首尔) | ¥429(飞济州岛)+ ¥1266(飞上海) | ¥4951 |
餐饮 | ¥1507 | ¥758.55 | ¥2265.55 |
购物 | ¥4788.54(衣服 + 化妆品 + 包) | ¥4788.54 | |
美妆 | ¥2320(色彩分析 + 骨骼诊断) | ¥2320 | |
旅游 | ¥850(拍照)+ ¥446(爱宝乐园门票)+ ¥376(手机卡 + AREX 票) | ¥1672 | |
杂项 | ¥1096.3(纪念品,灵室,等) | ¥871.7(皮划艇 + 家人礼物) | ¥1968 |
现金 | ¥1272(某一天的晚餐,交通卡充值,小商品) | ¥415(出租车) | ¥1687 |
二人共计花费:¥24790.45。
这次旅游现在回看其实是有不少缺点的,安排得匆忙了些、一开始打算在首尔住 4 晚,但是因为时间安排不过来又额外定了两天住宿,导致了一些额外支出,济州岛的东部以及南部没有去等等……不过,我对我的第一次出国旅行还挺满意的,见识到了不同的风土人情。让我意识到了之前想法的问题所在:性格内向和旅行并不是冲突的,主要是我不想迈出舒适圈。
本次旅行给我的感受还是挺特殊的,万般新奇,我亦新奇。后续我也会在博客上继续分享我的旅行记录。
]]>我曾在离开上家公司时 做过总结 ,离职的原因肯定是公司有些毛病你无法忍受却又无力改变,如今看来,这句话多少有些片面:因为我所在的部门同事和领导关系融洽、工作氛围还算轻松,可我依然决定了离开。这倒不是说公司没有毛病,只是说毛病还没有到让我无法忍受的地步,相比于有同事打小报告、工作氛围卷得要死的前公司来说,简直是小巫见大巫。
而今,促使我决定离开的最重要原因则是工作内容:我已经无法对目前的工作内容提起兴趣了、甚至还感到了一丝厌恶。我在之前的文章多少提到过一些,现在我既已选择离开,倒可以好好说一下了。我会按照时间的顺序讲述我入职的三年的经历以及我心态上的变化。避免隐私泄漏,我会在叙述时故意模糊一些关键细节。
简单介绍一下,我们产品的定位是对企业的流量进行检测和分析,结合云端的情报与本地的算法模型,定位企业网络内存在的各种威胁。
我在离开上家公司后思考自己的职业规划时,曾在博客的关于页面写下:现在的理想是做一些真正为人所需要的软件或者产品,让人们的生活可以更舒心、高效,也借此来满足我内心的满足感。因此当时在论坛上看到部门招聘的帖子时,我就动心了,面试时进一步了解到部门正是因为觉得当时的安全产品都同质化严重,想做出一款市场真正需要的安全产品,这正和我的未来职业规划不谋而合,于是我抱着很大的期待,加入了当前部门。
入职以来的 10 个月是我梦想的工作状态,也就是到 2021 年六月截止,我曾不止一次的怀念这段时光。我在这期间的工作很纯粹:只是做产品的研发,这也是我最享受的一段时光,我像海绵一样疯狂地吸收在这段时间里我接触到的知识,并将它们应用到产品里。包括学习 GraphQL,并对 Dataloader 异步改造、学习基于三重指数平滑方法,并设计异常流量监测系统等等。
这期间学到的知识大多数都是我之前没有听过的,不过我本身就是十分乐于研究学习的,何况这也能够拓宽我的知识面,让知识体系更加完整,本质上也是我自己受益。虽然我那段时间还是按时下班,不过回到家后我也会继续研究工作上的东西。虽然有些累、但是我非常满足。
这里也挺感谢当时领导的支持。
在产品本身的主体功能开发完成后,我接下了另一项重担——产品的部署工作:即如何把各个组件用自动化的方式在一台裸机上安装并运行。最终我花了两个月的时间敲定使用 Kickstart + Ansible 的方式来完成产品的部署工作。我将这两个月定义为理想到现实的过渡阶段。
这期间我遇到了很多问题:包括对 Ansible 的不熟悉、U 盘引导怎么都无法识别 Kickstart 文件、不同硬件的设备兼容性考虑等等。我在这期间的工作与其说是一名开发,到不如说是一名运维。当然我对运维工作倒没有什么偏见,我最终还是完美地完成了这项任务。
只是,我没想到产品部署这件事情所带来的影响,会超乎我的想象。
2021 年九月底,产品正式发布,我也步入了为期 1 年的平台期。由于产品的部署都是由我主导的,因此每一个申请试用产品的客户,都需要我来负责产品在客户提供的设备上的部署工作。这其实是一件很费力的事情,这也是我对工作内容感到厌恶的开始:
你可能注意到了,这三个问题都是由客户方带来的。这也是我认为目前 toB 的网络安全行业的困境:
在这个行业里,网络安全公司就是典型的吃力不讨好,企业客户就是又吃又拿:我们这边出人(开发、实施、销售、项目经理)又出力,客户那边提供个机器就行(有时甚至连机器都需要我们提供),使用几个月之后还不买,摆明了就是想白嫖,但是又不得不维系客户关系。所以你很难能见到赚钱的网络安全公司,基本都是营收越高,亏的越多。
中国的网络安全行业其实是一个「畸形」发展的行业,因为这个行业的需求并不是由市场决定的,而是很大程度上由那句「没有网络安全就没有国家安全」的话创造出来的。所以这个行业 99% 的客户都是国企或事业单位,为了要响应国家的号召,从而购买一些安全产品,至于有没有用——who cares。网络都物理隔离了,还有比这更安全的方法吗?
中国的非国企单位就没有网络安全的需求吗?当然有,而且这些公司大多是真正的有这方面的需求,但是这些公司不会选择用其他的公司开发的安全产品,因为使用了安全产品反而引入了不安定因素,不安定因素来自安全产品本身可能存在的漏洞,以及与现有架构集成后系统复杂度增加的风险,所以通常自己来做解决方案,自然也不是 toB 网络安全公司的目标客户了。
如今回想看来,自进入平台期开始,我的心态就已经出现了变化,只是这当时心态的变化并没有被我所察觉,却也潜移默化影响到了其他的地方。比如,我在 2021 年九月到 2022 年九月,只写了 2 篇博客,因为工作上的焦头烂额已经侵入了我的正常生活,下班后我只想着放松(甚至捡起了我许久没碰过的网络小说),对学习提不起兴趣了。
这一年里我也并非没有想过解决这个问题,我在 2022 年新年时就和领导提过,我不喜欢目前的工作内容,第一次提时,可能是因为我措辞比较委婉,领导没有太在乎以为我只是发牢骚;直到过了两三个月我再次提起时,领导才重视起来,开始让我把部署相关的事情给公司的安服团队以及另一位同事分担。一开始我确实以为这样能解决这个问题,后来发现是我想的简单了。
在平台期里,我们不到几个月的时间又发布了新产品。很奇怪对吧,上一个产品发布才不到半年,怎么又开始了发布新的产品,当时的我们也都很奇怪。领导是这么解释的:市场部的同事研究发现,现在的安全市场对某方面有很强的需求,于是我们做出一款产品专门抢占这部分市场。也就是说新产品完全是为了「迎合市场」来做出来的,考虑到埋头苦干两年多的产品因为对市场需求的错误判断导致销量惨淡,那么新产品肯定能一举挽回销量的颓势……吗?
不过在新产品开发的阶段,我也的确短暂地找回了刚入职时的感觉,不过没多久又回到了这种状态中,我才知道,有些事情是没办法的。
焦头烂额的事情还包括产品运行时 bug 的定位与排查,网络安全产品的核心流程其实差不多:无非是监听流量、解析流量并入库、匹配情报、产生告警。在客户环境里,表面现象往往都是无法产生告警,但是深层次的原因是因为情报还是解析流量的问题,还是需要由「我」来排查,因为我主导了产品的部署,最熟悉产品各个组件的作用。由我定位到问题后,再找组件具体的开发人员解决。
所以我说,有些事情是没办法的。
紧接着平台期,在 2022 年九月,公司的组织架构进行了剧烈的调整,字面意义上进入了动荡期。当时正在和我对接部署工作的安服团队,被一锅端了,不过还好当时的部署工作已经基本上都交给同事了,只有在问题排查的时候才需要我来处理。之后没过多久,我们小组就被裁了一个,当时剩下的我们都处于一种因为动荡衍生出的不安状态,做事肯定是没心思了,整天担心自己会不会被裁。
浑浑噩噩地过了两个月,到了年底时,领导居然还没有沟通绩效。于是脉脉上也开始有人爆料出今年不发年终奖了,可毕竟也只是爆料,这时,最应该出来稳定人心的时候,公司却选择了任风言风语愈演愈烈。
年后,部门裁员了一批后,相比去年打对折还要少的年终奖终于到手。紧接着公司又开始了一批批的裁员,而我,也在这时最终决定了离开。
在这之后,我开始慢慢理解了:
一个人的命运啊,当然要靠自我奋斗,但是也要考虑到历史的行程社会整体经济处于下行时,个人的力量是非常渺小的,公司的一轮又一轮裁员只是一个小的缩影,没有人能独善其身。
世事无常,难免无奈。我不愿再陷入这双重情绪的挤压之中了:一方面是公司动荡的不确定性;另一方面是对工作内容的不喜。因此,就算同事关系再融洽、氛围再轻松(现在其实也不存在什么氛围了,部门裁得只剩下一小半了),我也唯有选择离开了。
在公司的三年时光,给我的感觉和念书时很类似:第一年是雄心壮志,第二年是平淡无奇到乏味再到反感,第三年则是无奈、不安,快离开时则是怀念,我也的确从这份工作学到了很多东西。这次离职,没有不忿,只有不舍,毕竟我所不喜的工作内容领导已经尽力帮我摆脱了,只是无法全部摆脱,我想究其原因是身在 toB 的网络安全行业,下份工作我会考虑换个行业试试。
很幸运与部门同事一起度过三年的时光,我会永远怀念这段职业生涯。
前路漫漫亦灿烂,往事堪堪亦波澜,江湖相见。
]]>在去年年底时,我 重新设计 了一版博客的主题,不过此版主题主要是针对 UI 样式方面的调整,底层的架构没有变化:静态内容还是 Hugo 生成,动态内容如:搜索、消遣页面,暗色模式、评论等功能则是由 Svelte 提供支持。这个技术栈使用起来其实挺搭的:Svelte 打包后的结果很小,生成的 JavaScript 文件直接在 Hugo 模板中用一个 script 标签引入就行了。
那么,是什么原因让我仅仅时隔半年的时间就再次重构博客呢?让我先卖一个关子,先说一下 Hugo 的一些问题。
Hugo 其实本身的集成度很高了:模板生成、静态资源打包、自定义的输出格式等功能都有。但有两点我用着不是特别舒服。
Hugo 主题的开发体验太差了,差的体验其实都和它的生态相关。
VSCode 的插件本身提供的功能非常有限:文件跳转不行、语法高亮残废、代码补全仅限于一些基础的 snippet。而且因为 Hugo 的 template 是扩展的 HTML 语法,如果写了稍微复杂一点的 Hugo 表达式,在保存自动格式化时有时候会按照 HTML 的语法缩进,导致表达式的出现语法错误,这时候只能在右下角将语法切换为 Plain Text,然后再保存。
Hugo 本身本身虽然具备 JavaScript 和 Sass、SCSS 文件的打包功能,但其实支持都比较有限:
Hugo 在很努力地支持这些现代化前端构建工具,这些是开发组的努力,但只能说任重而道远啊。
Hugo 内部具备了非常多的功能,但是就像一个黑盒一样是集成在它内部的,我三年前从 Hexo 切换过来的时候,正是看中了它这一点,但直到我自己开发起来我才发现这其实并不是什么好事。因为有些功能你总会需要自己来定制实现的,比如博文加密、构建前后的 Hook、针对某些元素自定义渲染等,这种功能可能并不是每个人都会用到,开发组不做也很正常,但是我觉得 Hugo 的开发组对此的态度也有一点奇怪。
很早之前我在寻找 Hugo 博客加密的方案时,曾经想过一个方法就是在 Hugo 渲染的时候调用外部的可执行文件或者脚本(Shell 或者 .go 文件),于是我就搜了一下,发现早在 2015 年就有人提了 Issue,开发者一开始也认为这个想法很赞,不过还是出于安全(?)以及兼容性的考虑放弃了这个做法,并把这个 Issue locked 了。开发组对不同平台的兼容性有顾虑这一点还是有道理的,不过说出于安全的考虑我真的是没搞懂,开发组似乎是担心有人在主题文件里的可执行的脚本加入了恶意代码,执行起来就会导致设备中毒或者删除数据之类的,乍一看好像还挺有道理的,可仔细一想完全不通啊。哪个语言的包管理器都支持从网上下载代码啊,怎么难道他们都不担心下载的是恶意代码吗?
另一个让我不喜开发组的例子是 Markdown 元素的自定义渲染,目前仅支持有限的渲染:标题标签、超链接标签、图片标签、代码块标签(这个还是最近加上的),当询问到为什么不支持更多的标签自定义渲染的时候,开发组说因为担心渲染的性能问题,所以只开放了这几个,可 Hugo 的渲染速度真的很快很快了,我博客的近三百个页面,只需要 120 ms 的时间就可以全部生成,就算加上所有的标签的自定义渲染会牺牲 50% 的性能,我认为大多数的人都是可以接受的,甚至他们根本感受不到区别。
开发组的态度,加上 Hugo 本身像是一个黑盒,所以 Hugo 的生态其实很差,仅仅是包括一些主题而已,任何针对 Hugo 本身功能上的扩展都没有。
Anyway,吐槽归吐槽,用还是得继续用下去。(几年前迁移到 Hugo 时都说了 Hexo 再也不见了,难道还能删文自己打脸不成?
“什么是 Hugo 的本质?”
“内容生成和模板渲染。”
因此本次重构我只使用 Hugo 生成文章的内容,不包含任何样式、脚本等,输出格式我选择了 JSON,包含渲染后文章的主体内容,还包含了文章的元信息以及下一篇、上一篇等额外字段。
这些 JSON 文件,就是后续网站构建的内容核心。
扯了这么久 Hugo 的缺点,其实本次的重构的缘由并不是我完全无法忍受这些缺点了,只是单纯想吐槽一下,又觉得为了吐槽单独写一篇文章有点没必要,于是就在这里写了。本次重构的主要原因是我想稍微系统地学一下前端目前的主流框架,并且利用现代的 Web 开发技术来提升一下博客的用户体验。
我之前的博客就已经很注重用户体验了:CSS 和 JS 文件都只有一个,且 JS 还使用了 async defer 属性加载,完全不阻塞页面的渲染。不过虽然目前 CSS 和 JS 文件都只有一个,但是在页面切换浏览器重新渲染页面的时候,同样的 CSS 文件和 JS 文件都会再请求一次,对于用户来说,再次请求的 CSS 和 JS 文件其实没什么必要,因为它们俩每个页面都是一样的,不同的只是 HTML。因此本次重构的核心目的就是在切换页面时支持增量同步内容,而不用完整地重新渲染整个页面;同时在浏览器首次访问页面的时候,能够渲染出的是 HTML 内容,而不是要通过 JavaScript 来生成 DOM,虽然现在搜索引擎抓取时都支持了 JavaScript 的运行,但在 JavaScript 加载之前就能渲染出页面内容对用户的体验也会更好。
接下来对框架的选择,我便会以支持增量同步内容 + 预渲染 HTML 的功能作为最重要的考量标准。
因为我之前的博客就是使用的 Hugo + Svelte,Svelte 给我的开发体验还可以(比 Hugo 是要强不少的),因此本次重构我第一想法就是选择 SvelteKit,不过简单的尝试后我还是放弃了,相比于 Svelte 的模板语法我还是更喜欢 JSX。而之所以在两年半前迁移到 Hugo 时候选择和 Svelte 搭配使用,无非就是因为 Svelte 的性能以及打包后的脚本最小(因为没有 runtime);这对当时以静态内容为主、动态内容为辅的博客来说无疑是更合适的。
而现在我的重构目标则是要把博客变成以动态内容为主,这样的话选择 Svelte 的理由似乎不是那么充分了。
我的 表情包网站 正是基于的 Gatsby v2 写的,现在都迭代到 v5 了,可惜 Gatsby 的缺点是太重,依赖太多,而且不支持 pnpm。
连 React 的官方文档都从 Gatsby 跑路到 Next.js 了,你还有什么理由用它呢?
这个框架也是最近才冒出来的,我的个人主页在前段时间就用了这个框架重构,不过它只能当 SSG 来使用,不同页面的切换无法通过异步刷新来解决。因此还是放弃了。
Next.js 可以算是前端圈最大名鼎鼎的一个框架了,我简单使用了一会之后发现开发体验真的很顺畅:文档完善、社区繁荣、功能齐全,还有商业公司在背后支撑。而且 Next.js 还可以使用 getStaticProps 和 getStaticPaths 函数来可以生成纯静态的资源,不需要借助 Node.js 的运行时,似乎和我的 Hugo 方案完美契合。
于是我就愉快地使用 Next.js 重构起了我的博客。
你可能注意到了,如果 Next.js 真的这么美好,那么本文的标题就应该是「使用 Hugo + Next.js 重构博客」了。
一般来说,如果要使用 Next.js 的 SSG(Static Site Generator)模式,会在 getStaticPaths 函数返回所有静态页面,然后用 getStaticProps 来把静态页面的属性传递给组件。
对应到我的博客上,getStaticPaths 需要返回所有博客的列表,然后 getStaticProps 会去找寻每一篇博客对应的 JSON 文件,读取内容并把它通过 props 传递给组件。我一开始就是按照这样的思路来写的,没遇到什么问题。可就在编译完成后,我发现生成的结果除了包含了渲染后的 HTML,居然包含了传入的 props 内容!这也就意味着我的文章信息在渲染后的 HTML 中存了两份。我一开始以为是我的使用方式有问题,后来我发现 React 官方文档 也有这个问题。
这个问题其实让我有点膈应,因为它对于越长的文章,影响越大,尤其是我的博客放在 Cloudflare Pages 上运行,本来国内用户与 Cloudflare 之间的带宽就小,这样一来就更影响用户首次打开页面的体验了。
好在这个问题其实是有解决方案的。那就是写一个 Webpack 的插件,来自定义解析 JSONX 格式的文件(注意这里不能使用 JSON 格式了,需要把 JSON 格式改一个后缀,不然会和 Webpack 默认的 JSON 解析器冲突),把文件读取后返回一个定义了 React 组件的字符串,Next.js 会直接以这个包含着组件定义的字符串来作为正常页面渲染。而且这么做有一个好处,就是自定义的组件也可以正常渲染。
我为博客文章内部的图片都写了一个 LazyImg 的组件(图片出现在视窗里才加载)并通过 Hugo 的自定义渲染 Hook 来将 img 标签都替换成 LazyImg 的标签。如果是通过 dangerouslySetInnerHTML 来设置的话,LazyImg 组件的内容是不会被正常渲染的,只会当成纯文本来显示。
这个问题得到了妥善的解决,这也是我在本次重构博客耗费最多时间的地方。
是的,Next.js 的 URL 是不支持中文的,不光不支持中文,所有的非 Ascii 字符都不支持。而且几年前有人提到了这个问题,但是官方一直没有修复。
虽然通过 getStaticProps 是可以支持中文的,不过因为重复包含 props 的问题,我不是特别想用这个方式。不过也没有其他更好的解决方式,中文的标签几百个,改起来是在太费事了。
于是,我只好针对文章目录,使用 Webpack 的自定义插件载入页面;针对标签和分类目录,还是通过 getStaticPaths 和 getStaticProps 来生成页面。虽然这样标签和分类目录会包含两份 props,不过好在这两种页面的内容都比较少:只包含文章的元信息的内容,这样影响到也不是很大——我是这样安慰自己的。
问题基本得到了解决,以一种不完美的方式。
这个问题其实还挺奇怪的,因为经过我的测试,其他支持 file-based 路由的框架遇到文件夹是软链接时,文件夹内的文件也都是可以正常被路由的。这个问题我懒得去研究了,索性直接在构建完成后,把 Hugo 输出的 posts 目录拷贝到了 routes 内。
Next.js 本身打包生成的 JavaScript 文件挺多的,而且挺大的,这其实是很影响渲染性能的。
在我配置稍差一点的设备上运行 Lighthouse,使用 Next.js 重构后的移动页面性能评分是 85 分,其中 Total Blocking Time 是 300 ms,Largest Contentful Paint 是 3.4 s。
老实说,看到这个结果我还挺失望的……我能做到的优化已经做得足够了:图片全都懒加载,React 的组件也都是懒加载,文章内容也都是静态 HTML、没有借助 JavaScript 渲染,我只能把这个锅甩给 Next.js 和 React 本身了。
于是,我不得不忍痛放弃了 Next.js。
那么,有没有一个框架写法上是接近于 JSX,但是打包的结果比 Next.js 更小、性能又更强的呢?
其实还真有,而且还不只一个:
于是,我就决定使用 SolidStart 了。之前提到,Next.js 是通过写一个 Webpack 的插件来自定义 JSONX 文件的解析渲染和渲染,SolidStart 自然也是支持的,写一个类似功能的 Vite 的插件即可。而且 Vite 打包的速度比 Webpack 快太多了。
下面列一下我针对本次博客重构所做得一些优化和调整。
这次我给深色模式加上了一个点击弹出的菜单项,不再是之前点击一下直接切换了:
为此,需要有两个状态来分别表示用户的选择以及网站的实际模式。为什么需要两个状态呢?因为用户的选择(userSelected)和网站的实际模式(theme)并不是完全对应的:用户选择跟随系统选项的时候,网站的模式是不固定的,可能是深色也可能是浅色。
const ThemeMenu = ({ show, toggleShow }: ThemeMenuProps) => {
const [selected, setSelected] = createSignal(isBrowser ? window.lt() : "auto")
const handleClick = (e: MouseEvent, key: string) => {
toggleShow(e)
setSelected(key)
let systemMode = key;
if (key === "")
systemMode = window.mt();
setGlobal({ theme: systemMode })
localStorage.setItem("customer-theme", key)
};
onMount(() => {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (selected() !== 'auto') return
const newColorScheme = e.matches ? 'dark' : 'light';
setGlobal({ theme: newColorScheme })
})
})
...
}
梳理一下深色模式生效的流程:
当用户初次进入网站或点击刷新的时候,首先会从 localStorage 中载入用户的选择,如果不是 auto,那么就直接把 html 标签的 class 设置成对应的值;如果是 auto,就用当前系统的模式。这部分的逻辑,是我单独抽出来的放在 head 标签里的,并没有和 JSX 组件放在一起:因为组件是在 DOM 加载完成后才渲染,如果这部分也放在组件逻辑中,那么页面可能会在组件渲染时出现闪烁的情况。
等到 JSX 组件加载完成后,theme 相关的逻辑会首先执行,它会直接使用 html 的 className 来作为初始值。
接着是切换模式按钮的渲染,这时 userSelected 会读取 localStorage 中的值,作为初始值。同时,在模式切换按钮的内部,我加上了一个 onMount 的函数,为系统的模式切换加上了一个 EventListener,当检测到系统模式发生改变且用户选择的是跟随系统时,会根据系统的模式来设置 theme 的值。
最后,当用户点击了切换按钮的时候,userSelected 会直接修改成用户选项所对应的值,同时存入 localStorage;而 theme 会复杂一点,需要判断一下:
userSelected 我使用的是 createSignal 存放,毕竟生命周期只是在按钮组件里;而 theme 我使用了 createStore 存放,因为跨了多层,而且使用的地方是最外层的 HTML 标签,无法使用 ContextProvider 包裹了。
切换成现有架构后我发现了在页面切换时存在的一个问题,当用户点击页面时,会有几百毫秒的停顿时间,这期间是在等待加载渲染新页面所需的资源,而在当前页面也没有任何变化,这会让用户怀疑是不是没有点击或者网络出问题了。对比传统的静态页面,点击 a 标签时,左上角的刷新图标和标签 tab 会转圈给用户反馈表示新页面正在加载。
我首先考虑使用 Skeleton Screens,但是我发现它应用在全页面加载的时候会很怪,只适合在页面的部分区域加载时使用,于是我放弃了这个方案,转而使用了大部分的网站都用的顶部进度条来表示加载状态。顶部加载进度条其实就是两个状态,当页面切换的时候设置进度条开始,然后页面切换完成设置进度条结束;控制进度条结束比较简单,放在 onMount Hook 中即可。
控制进度条的开始则稍微复杂一点:SolidStart 有两个 Hook 可以设置成进度条的开始:分别是 useIsRouting 和 useBeforeLeave,经过实验我发现 useIsRouting 有一个 bug(feature?),当点击的是同页面的锚点时,useIsRouting 也会被触发,而点击同页面的锚点并不会造成 onMount Hook 的重复执行,所以如果使用了 useIsRouting 那么在点击锚点就会导致进度条开始了但一直不结束。
因此,我还是使用了 useBeforeLeave 这个 Hook 来设置成进度条的开始。不过 useBeforeLeave 也有一个 bug(feature?),虽然页面的锚点切换不会导致 useBeforeLeave 触发,但是页面的 SearchParams 的改变会导致 useBeforeLeave 的触发,我的博客在 搜索页面 会把搜索内容映射到 SearchParams 中,因此需要在 useBeforeLeave 中做一个判断:如果当前页面和目标页面的 pathname 一致,那么就不设置进度条开始:
const ContentLayout = (...) => {
useBeforeLeave(e => {
if (!(e.to.toString().startsWith(e.from.pathname) && e.from.pathname !== "/")) nProgress.start()
})
onMount(() => {
nProgress.done()
})
...
};
这里还需要额外判断一下源页面不是首页,否则首页跳转到任何页面都不会有进度条开始。
之前有提到过,我的页面是由 Loader 读取 JSONX 文件并将其内容包含在组件定义中返回,这样做的好处之前已经说了,可以让自定义的组件也可以正常渲染。但是也有坏处:渲染内容必须是合法的 JSX 组件。Hugo 生成的内容自然是标准的 HTML 组件,可却不一定是合法的 JSX 组件。比如<hr>
就是一个标准的 HTML 组件,但是它没有闭合,所以不能算是一个 JSX 组件,如果直接丢在组件定义中返回,那么页面渲染就会出错。
当然,代码块中不存在未闭合组件的问题,却有{}
的问题,在 JSX 组件中,花括号包含的内容会被认为是 JavaScript 表达式,而代码块中出现的大括号基本上是不可避免的。如果在 Hugo 的自定义 render-codeblock Hook 中对花括号进行转义,那么又会影响到 RSS 的输出。
所以我只能放弃了 Hugo 提供的代码高亮,render-codeblock Hook 我是这么写:
{{- $lang := .Attributes.lang | default .Type -}}
{{ .Inner }}
Pre 大写是为了标识它是一个自定义组件。接着在自定义 Loader 里,解析 Hugo 生成的字符串,并通过设置 xmlMode,这样 cheerio 会把所有 HTML 标签转化成自闭合的,然后不断调用 hljs 去渲染,然后把 base64 编码后的值返回,这里如果不使用 base64 的话,花括号仍然会导致 JSX 渲染错误。
import { load } from "cheerio";
import hljs from "highlight.js";
import { encode } from "js-base64";
const renderHighlight = (content: string) => {
const $ = load(content, { xmlMode: true, decodeEntities: false });
$('Pre').each((index, element) => {
const lang = $(element).attr('lang')
if (!lang || lang.includes("$lang")) return
const code = $(element).find("code").html() || ""
const highlightedCode = hljs.highlight(code, { language: lang }).value
$(element).html(encode(highlightedCode))
})
return $.html()
}
export default renderHighlight;
Pre 组件就比较简单了,把传入的 children 用 base64 decode 一下然后塞到 innerHTML 中,确保以文本方式渲染就行了:
import { decode } from "js-base64"
const Pre = ({ children, lang }) => {
const code = decode(children)
return (
)
}
export default Pre
之前的博客因为担心懒加载的图片到 RSS 输出中就看不到了,所以文章内的图片没有做懒加载。本次本次重构中,我用了一种比较巧妙的方式让文章内的图片懒加载的同时,又可以能让 RSS 输出能正常看到图片。首先是 Hugo 定义的 render-image Hook:
这里的 Img 同样是我定义的组件,我用它覆盖掉了原生的 img 组件,这是它的定义:
import { createEffect, createSignal } from "solid-js";
const LazyImg = ({ src, ...rest }) => {
const [visible, setVisible] = createSignal(false)
const SVGFallback = '...'
let self: HTMLImageElement;
createEffect(() => {
const observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
setVisible(true)
observer.unobserve(entries[0].target)
}
})
observer.observe(self as HTMLImageElement)
})
return (
)
}
export default LazyImg;
初始情况下,图片展示的是 SVGFallback 的内容(是一个 1x1 像素的图片的 base64 编码),当组件出现在视窗内的时候,将 src 设置成真实的 URL。
同时,因为我在 render-image Hook 中针对 Img 组件的写法和原生 img 组件的写法是一致的,只是名称大小写不同,所以它输出到 RSS 中的就是普通的 img 标签,能够被 RSS 阅读器展示。
之前提到,SolidStart 是一个刚起步的框架,因此存在一些小 bug,除了之前提到的 useIsRouting 外,还有不支持中文路由的问题。
我很确定这个是一个 bug 而不是 feature,bug 的原因在于包含了非 Ascii 字符的文件路径与 URL 的路径存在一层 URL 编码的转换,而 SolidStart 没有做这一层转换。知道了原因,修复起来就很简单了,在路由前给路径参数返回的值 Encode 一下,在 build 阶段的时候,根据 URL 路径写入文件前,再将路径 Decode 一次,it works like a charm。
我还就此问题给 SolidStart 提了一个 issue,不过他们还没回复我。
题外话,我试了好几个 file-based 路由的框架,只有 Svelte 是对中文的路由支持的。其他的框架都有这个 URL 编码的问题,不过 Next.js 貌似并不是因为这个问题。
以下是一些没那么重要的调整,放在一起说一下:
在本次重构中,我还在文章页面的侧栏新增了一个区域,用于列出与当前文章相关的其他文章。相关度是根据文章标签计算的,如果找不到同标签的其它文章,那么就展示同分类的其它文章。
之前移动端在文章页面是不展示侧边栏的,不过现在我感觉随着我的文章越写越长,为了移动端的阅读体验还是加上一个目录展示更合适一些。现在移动端文章页面会在右下角展示一个目录的按钮,点击就会弹出目录。
搜索页面现在会把用户的输入映射到 URL 中的 SearchParams 上面,反过来 SearchParams 中的输入也会映射到搜索框中。
本文算是我目前写的最长的一篇文章了,写长文的感觉还挺好的,能够把自己对一个项目完整地思考展现出来,写文的过程自己也能重新梳理自己的思路。
本次重构博客耗费的时间与精力远远超过了我的预估。我一开始预估也就一周内就可以完成,毕竟样式什么的都不用调整,直接复用就行了,没想到居然耗费了我将近一个月的时间,看来项目实际的开发时间起码是预估开发时间 x3,只多不少。
虽然过程稍微长了点,不过这次重构我也学到了不少东西,我挺满意的。
]]>几年前,我专门组过一台电脑折腾黑苹果与 Windows 双系统,之后便一直稳定用到现在(没有手贱乱升级)。最近发现硬盘已经快占满了,500G 的空间还要分一半给 Windows 用,也真是难为它了。正赶上固态价格大幅跳水,于是我买了一块 2T 的固态来升级硬件配置,正好心里也想顺便升级一下软件来体验最新版的 macOS Ventura,是以写下这篇文章作为记录。
我的黑苹果配置见 之前的文章 ,只有固态更新成了梵想 2T PCIe 4.0。
请按照 OpenCore 官方教程 推荐的设置来更新你的 BIOS,如果 BIOS 缺失上述某项,可以忽略。比如我的主板就缺失 CFG Lock 这一项,不过使用下来也没有遇到什么问题。
这一步可以直接从 App Store 下载最新版操作系统,比如我这里需要安装 Ventura 就直接打开搜索 macOS Ventura 即可。
如果你手头没有 macOS 设备,可以参考 OpenCore 官方文档: 如何使用 Windows 来刻录启动盘 。
不过有的驱动或者补丁与最新版系统可能会有兼容性问题,因此可以选择下载稍微早一点的系统。不过早一点的系统就不能从 App Store 下载来,只能用其他方式了,在终端运行:
>>> softwareupdate --list-full-installers
Finding available software
Software Update found the following full installers:
* Title: macOS Ventura, Version: 13.3, Size: 11776013236K
* Title: macOS Ventura, Version: 13.2.1, Size: 12555992911K
* Title: macOS Ventura, Version: 13.2, Size: 12555703258K
.
这里会列出所有可下载的macOS 版本,如果你不想使用 Ventura,可以选择 Monterey 或者 BigSur。
选定版本之后,比如我这里选择的是 13.1:
softwareupdate --fetch-full-installer --full-installer-version 13.1
命令执行完成之后,应用程序的文件夹里就能看到 Install macOS Ventura 安装程序了。
插入 U 盘,打开磁盘工具,定位到 U 盘的物理磁盘(而不是宗卷)选择抹除:名称随便设置(我设置的是 Ventura),一会刻录的时候安装程序会重新命名。格式必须选择 macOS 扩展格式(日志),方案选择 GUID 分区图。
注意,如果这里的抹除弹窗中没有出现分区方案的选择,请在磁盘工具顶部的
显示
菜单栏中选择显示所有设备。因为如果没有分区方案的选择,证明你格式化的是分区而不是整块设备!
随后将刚下载的操作系统刻录到此 U 盘中:
sudo /Applications/Install\ macOS\ Ventura.app/Contents/Resources/createinstallmedia --volume /Volumes/Ventura
这条命令有两个需要根据具体情况修改的地方:
这个过程会比用其他软件的刻录镜像慢一些,耐心等待完成。
刻录完成后,我们正式开始进行 OpenCore 相关的配置。
我们接下来的所有动作都是在 U 盘的 EFI 分区上进行,目的就是为了让刚刚刻录的系统能正确地从 EFI 分区引导。
使用 diskutil 命令来查看 EFI 分区和挂载,如果你觉得太麻烦,可以试下 MountEFI 这个更方便一点的方案:
#######################################################
# MountEFI #
#######################################################
1. EFI | 209.7 MB | EFI | disk0s1
2. Core | 249.3 GB | Microsoft basic data | disk0s3
3. macOS | 250 GB | APFS | disk1s5s1
4. Install macOS Ventura | 15.8 GB | Mac OS Extended (Journaled) | disk2s1
5. Shared Support | 12.2 GB | Apple HFS+ | disk3s2
S. Switch to Full Output
B. Mount the Boot Drive's EFI
L. Show diskutil list Output
D. Pick Default Disk (None Set)
M. After Mounting: None
R. Toggle Window Resizing (Currently Enabled)
Q. Quit
Pick the drive containing your EFI: 4
我这里选择 4,代表刚刚刻录了 macOS 的 U 盘,可以看到 U 盘名字被安装程序自动更改成了 Install macOS Ventura。随后打开 Finde 就能看到 EFI 分区被挂载上了。
然后下载 OpenCore 的 最新版 ,建议下载 DEBUG 版本。解压后,进入 X64 文件夹,然后把文件夹中的 EFI 目录,整个拷贝到刚刚挂载 EFI 分区内,注意这里的目录结构:EFI 分区里存在 EFI 目录:
>>> pwd
/Volumes/EFI
>>> tree -L 3
.
└── EFI
├── BOOT
│ └── BOOTx64.efi
└── OC
├── ACPI
├── Drivers
├── Kexts
├── OpenCore.efi
├── Resources
└── Tools
9 directories, 2 files
虽然 EFI 分区已经建立,但是还有许多东西需要针对我们自己的情况作出调整。
打开 EFI/OC/Drives 目录,可以看到 OpenCore 自带了很多 .efi 文件,这些大部分对于黑苹果友好的硬件都是多余的,只需要两个 .efi 文件就行;
其他的都删掉。
OpenCore 的 kexts 文件夹( EFI/OC/Drives)是空的,需要自己添加以下这些:
我在几年前安装黑苹果的时候没有配置无线网卡相关的 Kexts,因为当时 Intel 的网卡没办法在macOS 上驱动,现在发现居然有大佬移植了 Intel 网卡的驱动(终于不用去买不知道转了多少手性能还差的拆机网卡了)。这种造福大家的 kexts 值得单独列一下:
很多 OpenCore 的初接触者对 ACPI、SSDT、DSDT 等名词不太了解,简单来说:
在我们日常使用的 Windows,其实也是遵循 ACPI 标准的,在 Windows 安装的过程中,会自动向 BIOS 请求获取 ACPI 文件并加载。而因为黑苹果是在非苹果硬件设备下运行,BIOS 并不会向苹果的安装程序提供 ACPI 文件,因此需要自己手动来提取并在引导时加载。
对于我的硬件配置来说, EFI/OC/ACPI 中需要至少两个 SSDT 文件:
虽然 OpenCore 官方文档声称还需要 SSDT-AWAC(使用主板上的实时时钟)、SSDT-EC/USBX (虚拟嵌入控制器),不过我觉得这两个影响不大,加上我的 Windows 系统也是用的 OpenCore 引导,所以就没有加载这两个文件。
保留 OpenShell.efi 就行,在 OpenCore 引导菜单中添加命令行选项,在出现问题时调试能方便点(不过我从来没用过)。
OpenCore 图形化引导界面所需要的资源,如果你想以命令行方式引导,那就没啥用。
刚刚我们做的所有修改,都需要在 config.plist 文件中体现,它相当于启动引导的入口文件。
把 OpenCore 仓库里Docs文件夹下面的 Sample.plist 拷贝一份到 EFI 分区的 EFI/OC 文件夹并重命名为 config.plist。
用 ProperTree 加载这个 config.plist 文件,然后按 Cmd/Ctrl + Shift + R(OC Clean Snapshot),在弹窗选择 OC 文件夹,会自动把 ACPI 目录的文件以及 kexts 目录的文件在 config.plist 里对应位置填充好。
此外,针对我的 9700KF CPU(Coffee Lake),还需要修改如下项,如果你是其他的 CPU,请参考 这个文档 :
Quirks 子项中:
Add 子项:
如果你的 CPU有 iGPU,也就是 CPU 带有内置的 GPU 芯片,需要在 Add 子项中添加 PciRoot(0x0)/Pci(0x2,0x0) 子项,具体添加内容可以 参考这里 。
我的 9700KF 处理器不带 iGPU,所以这部分保持不变只需要有 PciRoot(0x0)/Pci(0x1b,0x0) 子项(默认)即可。
PciRoot(0x0)/Pci(0x2,0x0)
AAPL,ig-platform-id
Quirks 子项中:
Boot 子项中:
Debug 子项中:
Security 子项中:
Add - 7C436110-AB2A-4BBB-A880-FE41995C9F82 子项:
修改 MLB、SystemProductName、SystemSerialNumber、SystemUUID 这四项,可以用 这个工具 来生成。
Drivers 子项中:
分别添加 HfsPlus.efi 和 OpenRuntime.efi。
无论是升级黑苹果还是首次安装,重做 EFI 都是一件麻烦事,难免会犯一些错误。我们需要开启一些调试的功能,能更好地帮助我们解决问题。
在根据我们的具体需求修改完 config.plist 之后,如果直接重启大概率是无法引导的,我们可以用工具来验证一下。进入下载的 OpenCore 源码的 Utilities/ocvalidate 文件夹,我们使用它来验证我们的 config.plist 是否是正确的:
>>> ./ocvalidate /Volumes/EFI/EFI/OC/config.plist
然后按照给出的错误信息进行修改即可。
后续如果想要通过自带的软件更新来安装新版的 macOS,请一定一定要先更新 EFI 分区的内容:
不要担心 OpenCore 更新后,会不兼容旧的 macOS 引导,OpenCore 团队会做好向后兼容的。当你使用新版的 OpenCore 以及 kexts 也能正确引导当前的 macOS 系统之后,就可以开始使用系统自带的软件更新来升级了。
虽然我在 之前的文章 里已经比较详细地介绍过双系统,不过这次可能是因为升级的原因,操作起来的步骤略有不同。不过大体的操作逻辑是一样的。
macOS 下,确实没有一个像 Rufus 一样好用工具来制作镜像,像 Etcher 这种工具是无法用来制作 Windows 11 的安装镜像的。
这篇教程 提供了一个比较好的方法。总结下就是:给 U 盘格式化成 MS-DOS(FAT)格式带 GUID 分区表,然后把 Windows 11 镜像的 sources/install.wim 这个超大的文件用工具切割成两个小一点的文件,不然就超过了 FAT 最大文件 4G 的限制。
打开启动转换助理,接下来选择镜像以及配置 Windows 分区大小(如果你插着 U 盘,可能会提示让你拔掉,暂时拔掉即可),在等安装完成后会重启电脑,目前 Windows 11 的镜像没有自带可引导的分区,所以重启电脑后会发现找不到 Windows 的安装入口,这时选择进入 macOS 系统,如果这里出现了 Windows 安装分区,也不要进入!
再次打开启动转换助理,在左上角菜单操作菜单 -> 下载 Windows 支持软件,保存在 U 盘中。
打开 macOS 的磁盘工具,给现有的磁盘分出一块分区来,分区格式随便选,反正一会 Windows 安装的时候要把分区重建。如果上一步骤你已经通过启动转换助理做了分区,这里的分区步骤可以跳过。
分区完成后,重启电脑,关键的步骤来了:按 F12 进入 BIOS 的启动引导项,然后选择 U 盘,注意这里不要通过 OpenCore 的引导项选择 U 盘然后进入安装界面。
如果直接通过 OpenCore 的引导项选择 U 盘进入的安装界面,大概率会安装失败,原因猜测是 Windows 安装的时候从 BIOS 拿到的 ACPI 文件与 OpenCore 本身加载的 ACPI 文件冲突了。
为什么我会这么猜测呢,因为很明显从 OpenCore 进入 Windows 安装界面的话,分辨率是 4K 的,而如果从 BIOS 进入 Windows 安装界面的话,分辨率模糊的一塌糊涂。
Windows 11 的安装程序需要添加 BypassTPMCheck 和 BypassSecureBootCheck 这两项注册表才能运行,如果你有 Windows 设备的话,可以用 Rufus 来刻录 Windows 11 的镜像,他会自动帮你跳过这两个检测。随后在选择安装位置的时候,定位到刚刚的那块硬盘分区,把它删除掉,分区会显示成未分配,选择安装 Windows 到此分区即可。
在 Windows 安装程序拷贝文件完成之后,会自动重启,并修改 BIOS 的启动项顺序,把 Windows 的启动引导放在第一位,所以这里会继续进入到 Windows 的安装界面(伴随着模糊的分辨率),等待安装完成后,这时不要急着装驱动,也不要运行我们的 Windows 支持软件,直接运行的话会失败。
这里同样还是因为 ACPI 的原因,因为我们直接通过 Windows 启动引导进入的系统,没有使用 OpenCore 中的 ACPI 文件,而 Windows 支持程序会检测当前操作系统是否使用的苹果的 ACPI 文件启动。
设置完成后会再次重启,这时可以进入 BIOS,调整一下启动项顺序,把 Windows 的启动项放在后面(或者直接禁用了),OpenCore 的放在第一位。然后重启就会进入到 OpenCore 的引导菜单了,这时就可以看到 Windows 的菜单项了,选择 Windows,然后进入系统后,打开 U 盘下载的 Windows 支持软件,进入到 BootCamp 文件夹,点击 Setup.exe 安装。
完成后,双系统就成功了,可以从 Windows 启动到 macOS,也可以从 macOS 启动到 Windows 而不需要在引导菜单的时候手动选择。
当然。开启了 NVRAM 的支持是前提。
所有 OpenCore 的配置完成后,苹果系统已能正常工作时,一定要把你的 EFI 分区备份一下。另外可以再准备一个 U 盘,格式化成什么格式无所谓,只需要带有 GUID 分区表即可,然后将目前黑苹果的 EFI 分区给备份到 U 盘上的 EFI 分区。
这样万一引导分区出现什么问题导致无法启动,重启电脑按 F12 从引导菜单选择 U 盘(这里需要从 BIOS 引导菜单选择 U 盘,而不是从 OpenCore 引导菜单选择 U 盘)也能进入系统及时修复。
如果你只是用 OpenCore 来安装黑苹果,不用来引导多个系统的话,一般不会出现引导丢失的情况。
引导的丢失分两种,一种是 EFI 分区被覆盖或者不小心删除了,另一种是 EFI 分区没变,但是引导项没了。这里只讨论第二种情况(第一种情况自己做好备份即可)。
我遇到过两种情况,出现了 EFI 分区没有变化,但是引导项被改了的情况:
第二种情况需要进入 Windows 后,下载 EasyUEFI 或者 DiskGenius 重新给 EFI 分区的 boot 分区下的 bootx64.efi 添加回来。DiskGenius 可以 参考这个教程 。
OpenCore 新版里需要在 OC/Drivers 文件夹中保留 ResetNvramEntry.efi 文件并在 config.plist 中加载,在 OpenCore 引导菜单中才会出现这个选项。
OpenCore 的配置请一切从简:从零开始慢慢加,而不要从网上找了个同样配置的成品,开始在上面一点点调整。
比如我就没有使用 OpenCore 官方推荐的 USBX 和 AWAC 等 SSDT,只保留了 PLUG 和 PMC 这两个;USB 定制的 kext 也一直用的是几年前就定制好的,没有在用 USBToolBox 来重新定制;以及虽然我用的 Radeon Vega56 显卡,但是我发现没有 SMCRadeonGPU 等 kext 也是可以正常显示 GPU 温度,因此我也没有加载这个。
其实在本次更新 Ventura 之前,我的黑苹果就已经工作的比较完美了,除了无线网卡没有驱动外,其他的比如睡眠、NVRAM、USB 等功能都正常工作,这次有了大佬移植的驱动,蓝牙,Wi-Fi,隔空投送与接力,剪贴板等全都正常工作:
唯一可惜的是,iMessage 和 FaceTime 无法登陆。
写这篇文章的一个目的就是为了方便后续自己的查阅,避免以后每次安装黑苹果都要重新搜一下怎么动手(因为我这次就是这样又在网上搜了一圈),写了这篇文章后,自己要是遇到重装或者更新的时候也有个参考。
参考:
]]>其实早在去年底 ChatGPT 刚发布的时候,我就申请到了账号,不过当时的用户体验并不算好:ChatGPT 在说大段话的时候会突然断掉,然后还经常会出现请求量过多导致服务拒绝的情况,所以也就开始尝试了几天,新鲜感满足后就搁在一旁了。
谁知过了两个月之后,ChatGPT 在国内的概念突然又被炒了起来,它的更多用法也被挖掘了出来,同时它本身的用户体验也提升了不少:不会再出现大段话突然断掉的情况;拒绝服务的情况也越来越少了;还支持了历史对话等。于是这时候我开始在有意识地使用 ChatGPT 来处理一些日常的事物了:比如写 PPT、常识性的知识问答,以及重构代码等。虽然在重构代码这一块,错误还是不少,但也确实会给予我一些灵感。
当时就有一个想法,ChatGPT 什么都好,就是只有一点不好,只能通过打字交流(咳咳,要是能通过语音交流的话,面试啥的岂不是都可以用上了?)。可惜 ChatGPT 一直没有公开 API,想使用的话估计只能用无头浏览器来模拟了,不过这样一来还要破解 Cloudflare 的人机验证,工作量太大了…
终于,在三月的时候 OpenAI 发布了 ChatGPT 的 API 以及对应的语音转文字模型 Whisper 的 API,之前想与 ChatGPT 语音交流实现起来应该没多大难度了,不过我这人有点懒,还是缺一个契机或者动力来做这个事。直到前几天的时候,女朋友转给我她的一个学长在朋友圈发布的视频,视频内容正是通过语音与 ChatGPT 进行对话,她觉得很有趣,问我能不能给她做一个,我想这正巧和我的想法不谋而合,于是乎,我就开始研究了这件事起来。
很显然,要通过语音与 ChatGPT 对话,整体的功能可以分成 3 个模块完成,分别是:
以下是设计图:
下面我会分别阐述一下这三个模块的设计与实现。
这个模块是我耗费时间最久的,我一开始并没有打算使用 Whisper API(白嫖惯了的人,总是想找个免费的),而是打算使用离线的方案,主要是考虑到用这种 API 会涉及到网络层面的开销,拖慢语音识别的速度,用户的体验也会相应地打折扣。
我相继尝试了 PocketSphinx 和 DeepSpeech 这两个离线的语音识别方案,可效果都不太理想。PocketSphinx 的准确率实在差劲,DeepSpeech 的准确率倒是还可以,就是识别的速度太慢了,而且离线的方案还需要我自己来维护一套训练模型,我这半吊子的机器学习水平,实在懒得维护这些模型。
于是兜兜转转我还是回到了找云服务商提供的语音识别方案上,找了半天,终于找到了一款 Python 的开源库 SpeechRecognition ,虽然是个开源库,但它本身集成了市面上大部分服务商提供的语音识别方案,虽然大部分都是付费的(需要你自己去服务商购买然后输入 API KEY),但好在还是有 Google 家的方案还可以免费使用。
于是语音识别模块我基本上都是封装了 SpeechRecognition 这个库的一些操作,等后续如果发现有什么用得不爽的地方我再来自己定制。
这个模块是最短时间完成的,也没啥好说的,直接调用 OpenAI 的 API 即可,不过我稍微研究了下 API 的参数,相比较默认的 API 进行了两个调整:
我目前的调教方法是,普通聊天就保持默认配置不动;如果想要进行特殊的会话,比如让它当百科全书来回答问题,那么会话保留的条数可以更短,system 指令可以设置成:「尽可能详尽地回答」;如果想要练习英文口语,那么可以设置 system 指令为:「Play a English teacher, Point out grammatical errors and ask questions according to the context」;如果想要模拟面试,那么可以设置 system 指令为:「扮演一名 xx 岗位的面试官进行面试,简洁地回答和提问」。
这个模块也颇费了我一番功夫,一开始我也是想找离线的方案,同样也是因为想节省一些网络的开销,让对话进行地更顺畅。我也找到了对应的离线方案: pyttsx3 ,这个方案是调用操作系统本身的 TTS 引擎来朗读:Windows 是 SAPI5,MacOS 是 NSSpeechSynthesizer,其他平台使用 eSpeak。
这个方案虽然免费、省事,但是 pyttsx3 这个库有一个很严重的 bug:它不支持多线程,它如果放在非主线程之外运行,Speak 的时候不能阻塞住,五年前就有人在 Github 提了 这个 Bug ,反正目前是还没有解决。
因此我不得已只能放弃了这个离线方案,转而白嫖起了 Azure 和 Google 的 API)——分别是由 edge-tts 和 gTTS 开源库提供。
另外值得一提的是,我优化了语音合成播放的流程,将 ChatGPT 的交互模块与语音合成模块解耦成两个线程,之间用队列通信。如果是线性地执行,等 ChatGPT 的回答完全返回之后再进行朗读的话,在 ChatGPT 回答比较长的时候等待的时间太久了,因此每当 ChatGPT 的一句话说完的时候,就先把这一句话的内容发送到队列中,同时语音合成的模块会不断地从队列取数据开始朗读。
这样,用户体验会好不少。
演示视频:https://www.bilibili.com/video/BV1rY411z7tA/
代码仓库:https://github.com/WincerChan/talkgpt
高中的时候,我的数学老师挺有趣的,我在之前的文章里也有提到他。他并不是数学专业毕业,而是学机械的,后来因为找不到相关的工作就转行来当数学老师了。
在某天上课的时候,他给我们普及历史上的三次数学危机(忘记是什么话题引起的了):第一次是因为无理数、第二次是因为无穷小、第三次是因为罗素悖论,老师像讲故事一样讲完三次危机之后,问了我们一个问题:你们觉得还会不会有第四次数学危机的出现?当时一下班上都叽叽喳喳讨论起来了,无非都是说「应该不会有吧?」、「如果有的话那是什么呢?」之类的话,他在讲台上静静地看着我们讨论了一会,然后说:「看来你们都觉得再有数学危机发生了吧」
看着大家都没有反对,应该是默认了他的话。紧接着他说:「我不这么认为,我想接下来一定还会有第四次、第五次数学危机,我们研究数学的,不能总是按照常识来做判断,在前几次危机发生前,当时的大家也不认为还会有数学危机发生,可事实就是发生了,所以我断定只要人类文明还在发展,接下来一定还会有数学危机发生」(原话已经过了快 10 年了,我自然是无法复述,只能说个大概的意思)。当时的我虽然听不太懂,但是也给我弱小的心灵带来了很强的震撼,现在回过头来我想我有点能理解了。
我们是属于科技行业的人,在 ChatGPT 这波「浪潮」来临的时候,我们首先感受到了,至于是忽略这波浪潮还是利用这波浪潮,在写这个小工具的时候,我想我已经有了决断了。
花费了几天的空余时间,也算是把这个工具初步完成了。虽然使用起来还比较粗糙,但剩下的打磨可以慢慢进行了。
最近一段时间人有些浮躁,没怎么专心研究技术,做完这个项目下来,感觉还挺好的。我会慢慢拾起对技术的热情。
]]>感觉 2022 年时间过得飞快,快到仿佛在看一部电影,电影开头有点枯燥所以没仔细看,直到电影中段我才刚开始进入情节,思考电影主角的经历怎么和我这么类似的时候,电影已经结束了,后知后觉才发现原来主角正是自己。这种「主角」、「观众」身份的反差带来的荒诞感让我有些不知所措,却也是我当下内心最真实的感受。
2022年初的游玩经历比较单调,基本上就是逛商场和吃饭,从年中开始的印象还是比较深刻的。因为把女朋友家闲置在广州的车开到深圳来了,周末和节假日的出游多了很多选择,不过也确实发现了深圳是真的没啥玩的……海滩、爬山、公园、商场、游乐园这些地方玩个两次就玩腻了。
腻了之后,游玩路线就开始向深圳的周边城市发展。像是惠州、广州去了不少次,不得不说:惠州的海以及沙滩比深圳的看起来还是舒服不少,长隆的游玩体验也比深圳的欢乐谷要好,广州的美食更是可以把深圳吊起来打。去过了惠州以及广州之后,我和女朋友达成了一致的观点:深圳就是一个只适合打工人的城市。
回头来看今年的工作内容挺乏味的,从两年半前刚入职时,内心充满新鲜感到现如今的略显麻木,工作内容的变化应该是占据绝大部分原因。从 2021 年底开始,我工作的重心就已经不在开发上面了,不过由于 2021 年的年终终结鸽了,因此并没有机会介绍当时的情况。
当时产品的研发其实已经接近尾声了,接下来就是由我来主导由产品标准化部署、到生产成硬件盒子步骤,这期间整个流程的打通大约耗费了大约半年左右。这部分的工作其实都算是一些脏活累活,虽然能积累到一些经验,但相比来说,还是程序的开发过程会让我更有成就感。
在公司呆了两年半,眼睁睁看着当初刚入职时的半成品产品,变成如今比较成熟的商业产品,也让我发现了公司或者行业的一些问题,我开始从更高的层面来审视自己的未来发展。
我所在的是面向企业客户的网络安全行业,所研发的产品也是为了给客户提供威胁分析、基础的防护等功能,这些客户大多数是一些传统企业,如:国企、银行、政府单位等。它们都有一些共同点:网络基础建设很差,没有类似网络安全部门的存在,主要靠物理隔离来抵御威胁;同时又不具备基础的威胁分析的能力,因此对于我们来说,往往是出钱(由我们来提供服务器设备)又出力(帮客户进行威胁分析)。
而且我们的产品是部署在客户侧的网络中,偶尔出现一些程序的 bug,查看日志调试变成了一件异常困难的事。有些客户无法提供远程服务,便只能请安服的同事去现场排查,由我们这边告诉安服的同事运行的命令,再由他拍照等方式反馈,这种方式效率又低、人又累。而且由于我是负责产品标准部署的人,对产品的各个组件也比较熟悉,所以主要是我来排查发现问题,再由具体的人去解决问题。这部分工作内容也让我觉得工作变得乏味了起来。
年中的时候,我和领导提起了我的困扰,于是领导安排了另一位同事帮我分担。可没过几个月,公司层面开始了动荡,公司在前几个季度巨额的亏损,让公司开始了为期半年的裁员。我们的组织架构也是一变再变。人们对自己无法控制的动荡就会滋生不安情绪,尤其是今年过年公司竟首次没有在年前发年终奖,甚至都没有发一封邮件或是文章来说明一下此事、安抚一下员工,这无疑更是加剧了我们每个人的不安情绪,也引发了部分同事的不满(这其中自然也包括我)。
其实通过我之前对行业的描述中就是可以预见公司的亏损是必然的,又出钱又出力,销售恨不得把客户供起来,客户提的什么要求都接受反而要我们研发来擦屁股。在这种模式下盈利基本上是不可能的,这也是我认为这个行业最大的弊端。
之前一直有成为一名黑客的梦想,因此我毕业后选择的两家公司都是和网络安全相关的。可惜作为一名研发人员来说,没有机会体会到网络攻防带来的刺激感,反而受到了行业弊端所带来的一地鸡毛。下一份工作,我大概率不会再从事这个行业了。
生活中最大的改变是我终于在女朋友的陪伴下学会了自行车,家里到上班的路上骑车大约 4.5km 左右,其中有 3km 是沿着河骑行,风景很不错,空气也比较清新,还可以锻炼身体。
惭愧,翻了翻书单,去年一年有部分空闲时间消耗在了网文上,对于实体书并没有看什么,说来也是奇怪,在一个月内高强度看了四五本网文,之后半年就都没有看过了;影视的话貌似没有看到那种惊为天人的:爱情神话、甄嬛传、侠僧探案传奇、开端、无证之罪、圆桌派、黑社会、异物志、唐朝诡事录这些都算是还不错。
本文的大部分篇幅都是在描述我对工作的感受,说是年终终结到还不如说是我对工作、行业的吐槽文,不过这也的确是我在写这篇文章时最想要表达出来的:同质化严重、且内容不喜欢的工作所造成的后果就是每天盼着下班、工作日盼着周末;过了很久回味起这段时间时又回想不起来有什么值得纪念的事情,于是便会感觉到荒诞:这段时间到底是我亲身经历过的吗?终于发现,原来的确是我的生活,只是它怎么过得这么快啊,快到我都没有来得及好好体会。
2023,愿我们都有空闲能好好生活。
]]>最近不知怎么,忽然就有了消费的冲动,除开双十一买的实体商品外,我把之前订购的一些云服务也做了一次大升级。俗话说货比三家不吃亏,可有些软件服务并不像实体商品能通过货比三家来对比选择,尤其是和网络相关的服务,所以你只能自己买来尝试,也就难免花了些「冤枉钱」。为了能让这些「冤枉钱」显得不那么「冤枉」,我决定写一篇博客来整理一下这些服务,如果恰巧能给有选购需求的人带来帮助,那就更好了。
话说我从高中开始,还不知道编程是何物的时候,就开始学着翻墙了,那时候印象比较深的有两个软件:
后来上了大学,有了自己的服务器,开始自己搭建翻墙服务了。再后来,GFW 变得越来越智能,自己搭建的 SS / SSR 服务很快就会被 GFW 发现,然后限速,最后封锁。于是那时候风头开始转向了 V2ray 这类可以流量伪装的软件。不过 V2ray 配置起来实在是太麻烦了,要自己生成证书、配置 Nginx、配置域名解析、WebSocket 等,实在是对我有些劝退……而且这层层加密、伪装后,性能也会有所损耗。
后来不知道什么时候开始,翻墙服务商(以下简称机场)开始流行起来了,大大地减轻了用户配置起来的心智负担,我也在这时候开始使用起了机场,那时候大多数机场很不稳定,经常遇到用了没几天就跑路的,好不容易找到一个能用机场,在头几天速度很不错,然后在用户多了之后,就又很慢很慢了。
闲话扯的有点多,下面正文开始说我这段时间对几个机场的体验。
这是一家典型的使用协议伪装的服务商,大部分都是 V2ray 节点。也算是我使用时间最久的一个机场(快一年半)这个机场也是有之前提到的问题、在刚开始使用的大半年里,速度都还不错,然后每到重大日子或者晚高峰的时候总会有一些节点挂掉或者速度变慢,尤其是最近,大批量的节点不可用伴随着部分线路又被撤掉。
不过,这家机场有个很有趣的点就是每天签到可以续 1 天(所以理论上可以无限期的白嫖?),可惜线路实在一般,而且 IP 也并非是原生的 IP。
基于以上考虑、我决心开始寻觅新的机场。
这家是我在逛 V2EX 的时候看到有人推荐的,稍微搜了一下测评,发现是一家中转机场,并且只提供 SS 协议的节点。
所谓中转机场就是在国内架设一台中转服务器,用户都是先连接到这台服务器,再由中转服务器与目标服务器通信。这家中转选的是华为云和阿里云。
优点是线路好,国内访问阿里云和华为云基本不会受到高峰期的影响,尤其是在深圳地区,直接接入阿里云深港专线,香港节点延迟低得吓人(电信网 30ms);缺点就是成本比较高,所以比较贵。
这家只提供香港、日本、台湾、新加坡、美国这五个地区的节点,不过节点全部可用,且质量非常高,均为原生节点。
不过我在使用了一段时间后发现了这家的另一个问题:有些网站打不开,这些打不开的网站里有部分是属于政治敏感的网站,也有部分是比如日本雅虎等正常的网站。我看到在官方的 telegram 里也有人反馈这个问题说是因为落地的限制(这家大部分都是 Kirino 落地),不过官方并没有承认是什么原因。我在他们的服务条款里也没有翻到具体有哪些域名是不能访问的。这一点得夸一下 GlaDOS,购买套餐的时候明确表示他们运行了域名拦截器拦截部分政治敏感域名的访问,并给出了一个域名列表。
不过即使 AmyTelecom 运行了域名拦截器,但日本雅虎等网站打不开仍无法解释。
这家是我在搜 AmyTelecom 的评测的时候看到的,同样是一家中转机场,不过相比 AmyTelecom,在中转服务器与目标服务器使用的是 IEPL 线路,理论上会比单纯中转机场线路更好,同样是只提供 SS/SSR 协议。
这家南方的中转的就不是深港专线了,而是广港专线,所以我在深圳体验起来延迟会稍微高一些(60ms 左右),不过也是基本感受不到延迟。
这家地区的节点比 AmyTelecom 多不少,像是欧洲、北美、南美,西亚等全都有覆盖。也是基本上所有的节点都处于可用状态。不过这些冷门节点相信大部分人也都用不到。
这家同样运行域名拦截器,不过像日本雅虎等网站可以打开(不知道是不是因为这家落地比较多的缘故)。
我目前是 AmyTelecom + FlowerCloud 双持。由于这两家都是年付,待后续再使用几个月,明年续费时候可能就会只使用一家了。
题外话:这两家在我订购后没两天都宣布了双十二以及圣诞的优惠活动(85 折),原价订购的我像个大冤种。
这期间我也尝试了几家不同的 VPS 提供商。
这家在 lowendtalk 很出名,我在之前的博客里也有介绍过,也是我一直在使用的。服务挺不错的, 有啥问题基本上提工单都能得到回应。黑五的折扣也是实打实的,
我在黑五的时候入手了一款美西的服务器,线路嘛,就那样,没啥优化,但硬盘和内存给的比较实在。
这家 VPS 的Location 有美国中部、西部、东部和荷兰阿姆斯特丹可选,如果要购买建议选择美西地区(LA 或者 SJ),尤其注意不要选择荷兰,年费贵 6 刀,而且延迟超高。
是的,我在前文所说的冤枉钱就包括订购荷兰节点的 VPS。
这家应该是个国人商家,我在最近购买了他们家的日本 VPS,这家和 Racknerd 家正好互补,这家的内存、硬盘、流量给的比较少,但是线路很不错,IIJ 线路,沿海地区基本上 60 ~ 70ms 的延迟,带宽也可以。
不过我没打算自己搭建翻墙服务,两个机场怎么也够用了。没必要给自己的服务器惹上被封锁的风险。
这家的地区比 Racknerd 多了几个亚洲的地区:香港、日本、韩国,都比较适合国内的用户使用。
这家也是在 lowendtalk 发现的商家,我购买了日本地区的 VPS,价格与 VMISS 相比高了 50% 左右,配置差不多相当于翻倍,不过他们声称供货需要 2 周左右,因此等拿到货之后我再来写评价。
这家地区更多了,除了亚洲新增了新加坡地区,还包括澳洲、欧洲等地区。
2022-12-14 更新:
不出意外,这家的线路还是比 VMISS 差点,并不是 IIJ 线路,而是 NTT;电信网络的延迟有点高,移动网络的延迟倒是还行,带宽也比 VMISS 差不少。不过 CPU 是 AMD EPYC 7443,内存、硬盘 IO 等基础硬件配置比 VMISS 强了许多。这家我应该会继续用,毕竟电信的延迟再高,也比不上美西(LA、SJ)的延迟高。
之前我一直都是使用 Shadowrocket 来作为 iOS 端的代理工具,不过它在配合 GlaDOS 机场的节点使用的时候我经常会遇到一个很奇怪的问题:连接公司 Wi-Fi 的时候,开启了 VPN 导致手机无法上网的问题,必须要断开 VPN,开启飞行模式,再重新开启 VPN才能恢复上网。但是当我换成了 AmyTelecom + FlowerCloud 之后就没有出现这个问题了。
另外 Shadowrocket 的分流规则实在有点简陋,去广告的功能研究了半天也没成功。
在换了机场之后,我又入手了 Quantumult X。老实说,Quantumult X 这软件设计的交互逻辑真的有点奇葩……功能又复杂,如果不找教程的话完全无法通过自己的摸索来使用,而且稍微老一点的教程里的界面相比最新版 Quantumult X 界面都有不少改动,使用门槛比 Shadowrocket 真的高了很多。
优点在于功能比 Shadowrocket 强大了不少,除了更细致的分流、重写规则外,还可以查看网络流量统计、日志、DNS 查询等,折腾了几个小时配置之后用了几天倒也用得还算舒服。
不过去 Youtube 广告去得不是很明显,在首页的推荐还是会有广告,而且点进视频的时候,有时候广告会先弹出来然后一闪而过。有点影响体验,而且不知道是不是错觉,使用了 Quantumult X 之后,我发现手机掉电特别快。
对于大多数没有复杂翻墙需求的人来说,我还是会比较推荐 Shadowrocket,除开售价便宜不少之外,Youtube 的广告去除也稍显鸡肋:因为我发现 AmyTelecom 有些节点打开 Youtube 就是没有广告,使用 Shadowrocket 测试首页推荐和视频播放都没有广告。
总结一下,本文记录的所有花费大概在一千八百人民币左右,不过等明年会再取消掉部分订阅服务:
粗略估算了一下,明年的订阅服务应该会控制在 1000 元以内,其中机场订阅与 VPS 订阅的花费应该刚好五五开。
]]>两年前,我刚决定把博客迁移到 Hugo 时,因为找了许久都没有找到合适的主题,只好自己动手写了一个(项目地址: Cirrus );在随后的两年中博客便一直使用着这套主题,这期间倒是也陆续发现了其它一些还不错的,不过我却没有什么更换的念头,因为在自己动手写了一个主题之后明白了一个道理:只有自己设计的东西才能真正贴合自己的需求。两年后的现在,看着我写的第一个主题,心中虽感慨万千,却也愈看愈觉得它「稚嫩」与「不成熟」,不由得泛起重新设计一版主题的念头。
一番思考之后,我首先明确了本次重新设计最核心的理念:对现有的内容做一些裁剪。两年前因为不懂设计,所以总是想把博客的页面布局塞得很满,无论组件是否有用只要我想得到都塞进去,实际上部分组件根本没必要展示,还有部分组件可以换另一种更好的方式展示。
这次重新设计的所有页面我都通过 Figma 画了原型图,然后对着原型图开始实现;相比之前在脑子里想布局然后转化成 CSS 再根据浏览器的实际效果调整,效率方面实在提高了太多。Figma 上手也比较方便,花了两个晚上跟着 B 站的视频操作了一下就基本掌握了初级的用法,设计一个静态博客足够用了。
原来的样式可以通过 Cloudflare Pages 的快照 来对比查看。
博客的布局从原来的三栏变成了顶部导航 + 双栏:原先左侧的博客名称以及菜单信息挪到了顶部以减少空间占用,没啥用的博客副标题我也干脆直接去掉了。这样就有更多的空间来展示中间部分——也就是博客正文。
而菜单部分,我也略作了修改,首先删除了原先占位置的 ICON 图标,并且还删除了归档页面的链接,现在归档页面只有通过首页的查看更多文章的链接才能进入,这样在顶栏显示时会和谐一些。
随后我在谷歌分析查看了最近三个月每个页面的访问次数的统计,发现翻页的访问频率出乎我意料的低:第二页的访问量近三个月只有个位数,排名更在五十名开外,因此我决定把翻页的功能直接砍掉。
而将翻页砍掉之后,为了让首页展示更加和谐,我把文章占用的空间做了递进处理:最近一篇文章占用空间最大,并显示副标题以及摘要部分;剩余 4 篇近期的文章将摘要以及副标题隐藏,只展示标题以及日期(这或许能骗一部分浏览量?);而更早的文章则在首页直接隐藏,只留一个归档页面的入口。
而右侧的布局也做了一些小调整,加上了文章篇数、总字数的统计,文章数也由原来的按年份统计改成了按类别统计,组件标题旁的 ICON 也移除了。
之前的主题动画效果太多了,当时的我近乎炫技一般想给所有的交互都加上动画效果,并且同一类组件交互起来动画还不一样,使用起来就感觉很混乱。比如同为按钮:
本次更新我也会将同类组件的交互显示效果趋于一致,我将原来一些标签组件、分类统计组件的按钮样式改为了超链接样式,因为它们从功能其实更接近于超链接——都是链接到其他页面。
这样按钮组件在形式风格、功能上就更统一:
同时我也将之前的一部分按钮变成了超链接的形式:比如文章 Card 中的类别。
而超链接的样式我也分为了 2 类:
这样交互起来的动画效果就看起来比较和谐了。
另外还有些比较细碎的优化,不值得拿出来单独讲,在这里列举一下:
最后,博客主题虽然更新完成,不过今年只更新了 2 篇文章,希望后续我能将更多的兴趣放在博客的内容创作上来——我好像每次都这么说,也不知道能不能做到。
]]>又是大半年没更新,或许我不得不承认,我写博客的热忱相比两年多前的确是在逐渐退却,思考了一下,原因或许可以归咎于如下:
不过,最近在工作上遇到了一个有点意思的事,钻研了一下觉得值得写一篇文章来记录——于是屁颠屁颠地花了三个晚上时间完成了这 2022 年的第一篇博文(希望不要是唯一一篇博文)。
最近我们组要推出一个 DNS 防火墙产品——拦截黑名单域名并提供简单的查询分析功能。我主要是负责实现拦截功能,具体的工作是写一个插件让 CoreDNS 可以实现 DNS Sinkhole 的功能,也就是针对(特定的客户端)访问特定的域名返回错误的结果,同时将解析的结果输出到数据库保存下来以供后续分析。开发过程并不困难,反而是在开发完成后的测试阶段遇到了困难。
如果只是对拦截功能的测试,还比较容易解决——无非是多写几个单元测试用例,可要对整个 CoreDNS 的解析、拦截功能进行测试就会麻烦不少:完整的 DNS 请求涉及到网络、IO、转发请求等各个方面,不再是通过测试用例就可以覆盖的。因此我最初的想法是在另一台服务器上把默认的 DNS 服务器改为运行 CoreDNS 的服务器地址,再在这台服务器上批量运行 gethostbyname
函数来发出域名的解析请求,每一条的解析请求背后都意味着一套完整的 DNS 解析流程,因此可以较好地覆盖 CoreDNS 解析、拦截功能的测试。
不过这却引来了另一个问题,之前提到过,我们还需要把 CoreDNS 解析的 DNS 记录保存下来供后续分析使用,如果所有的 DNS 请求来源都是在另一台设备上,那么所有 DNS 记录的源 IP 都会是同一个,虽然这对功能测试以及性能测试没有影响,但在产品的实际展示效果以及产品体验上会大打折扣。
因此,搞来一批 DNS 流量并想办法让流量流经 CoreDNS 的服务器并保持源 IP 不变,问题就能得到解决了。
声明:文中出现的所有 IP 协议谨代表 IPv4 协议,暂未进行 IPv6 协议测试
思考了一阵,我冒出了一个有点不太靠谱想法:要是能把公司内网的 DNS 流量全都指向 CoreDNS 就好了——这对我们来说是最省事的,可显然是不符合实际的,先不说公司的其他部门会不会同意,我们是以测试 CoreDNS 为目的,而测试的过程是不稳定的:如果 CoreDNS 挂掉了那意味着全公司的员工都无法上网了…这损失我们显然承担不起。
于是我想到了另一个方法:既然不能改公司的原始 DNS 流量,那么我把原始流量通过 Tcpdump 导出成 Pcap 文件,然后把 Pcap 文件本身包含的 DNS 记录请求的地址改成 CoreDNS 服务器的地址,再通过类似 Tcpreplay 的手动重放不就可以了吗?
乍一看,这似乎是比较好的解决方法,然而在实际验证过程中发现此法也行不通:原因在于 Tcpreplay 只会把 Pcap 文件里的数据重放在对应的网卡上,而并不会实际在网卡上创建 TCP 连接。这也比较好理解,毕竟我在机器 A 上抓包与机器 B 通信数据生成的 Pcap 文件,再通过机器 C 上的 Tcpreplay 重放,这并不可能重新让机器 A 和机器 B 建立相同的连接。
虽然 Tcpreplay 的方式最终被验证是行不通的,但也算给了我一些启发:DNS 是建立在传输层之上的协议,我可以自行构造一份 DNS 协议的报文,再通过传输层协议(通常是 UDP)发送到 CoreDNS 的 53 端口,这样应当就能解决 Tcpreplay 无法产生真实连接的问题。
我这里是使用的 Scapy 工具来解析出 Pcap 文件中的 DNS 协议,Scapy 可以调用 iPython 解释器用于调试,比较方便:
>>> records = rdpcap("./dns.pcap")
>>> records[0][DNS]
an=None ns=None ar= |>
>>> records[0][DNS].qname
>>> UDPClientSocket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
>>> UDPClientSocket.sendto(raw(records[0][DNS]), ("127.0.0.1", 53))
38
>>> records[0][DNS].qd.qname = b'www.zhihu.com.' # 我们可以尝试修改 DNS 协议的 qname
>>> UDPClientSocket.sendto(raw(records[0][DNS]), ("127.0.0.1", 53))
42
这时,在运行 CoreDNS 的终端就能看到两条日志输出了:
[INFO] 127.0.0.1:61730 - 31895 "A IN baidu.com. udp 38 false 4096" NOERROR qr,rd,ra 88 0.041027042s
[INFO] 127.0.0.1:61730 - 31895 "A IN www.zhihu.com. udp 42 false 4096" NOERROR qr,rd,ra 256 0.069857333s
这意味着 CoreDNS 能成功接收到我们伪造并发送的 DNS 解析请求(Yes!终于向着解决问题迈出了一步)。不过观察到 CoreDNS 的地址还是显示请求是来自 127.0.0.1——因为我们是通过 127.0.0.1 发起的连接,但我们的目标正是想伪造这个发起连接的地址,让它不能显示成实际发送 UDP 数据包的机器。不过这并不是 DNS 协议能够做到的事了,注意看上一个代码块,DNS 协议本身是不具备目标 IP、端口等连接信息的,这是更底层的协议做的事情。
因此我们需要尝试伪造更底层的协议。
这里我再次恶补了一下计算机网络的知识,因为之前对计算机网络协议的使用都是到传输层便戛然而止,更低的 IP 层以及数据链路层则完全没有接触过了。而为了构造更底层的协议,就不能直接使用系统的套接字了——而需要使用 Raw Socket(原始套接字)。
扫盲时间:无论我们创建流套接字(通常是 TCP 传输)或是数据报套接字(UDP 传输),都需要指定目的地址和目的端口,因为前者是网络层所需要确定的,后者是传输层所需要确定的;而我们创建的数据报或是流套接字并没有让我们去构造传输层以及网络层的首部,因为这些都由操作系统帮我们完成了。而我们现在想要自己构造网络层以及传输层的首部,因此便不能使用流套接字或数据包套接字了。
>>> RawSock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_UDP)
>>> records[0][UDP]
# dport 是 domain 意味着 53
>>> records[0][UDP].sport=10000 # 将 sport 改成 10000
>>> del records[0][UDP].chksum # UDP 首部有改变,chksum 也会变,直接删掉此字段
>>> RawSock.sendto(raw(records[0][UDP]), ('127.0.0.1', 0)) # 虽然我们删掉了 chksum,但 raw 函数会重新生成 chksum
46
特别需要注意的是,因为我们构造的 UDP 首部本身是包含着 sport 和 dport 的,因此我们在发送的时候的目标端口直接填写 0 即可。
CoreDNS 成功打印出如下日志,这意味构建 UDP 的首部也搞定了,离成功更近了一步:
[INFO] 127.0.0.1:10000 - 31895 "A IN baidu.com. udp 38 false 4096" NOERROR qr,rd,ra 88 0.070468417s
虽然使用原始套接字能让我们成功伪造 UDP 首部,即更改源端口为任意数,但其实源端口的改动并不重要。我需要改动的是源地址,而源地址的改动则涉及到了 IP 协议的首部。
花费了一番功夫,发现了 IP_HDRINCL
这个原始套接字的选项,当创建的原始套接字是 IPPROTO_UDP
或者是 IPPROTO_TCP
时,此选项值默认填充为 0,此时待发送的数据包在流经 IP 层时会自动加上 IP 的首部,而当手动设置了此选项为 1 或者创建的原始套接字的类型是 IPPROTO_RAW
的时候,则不会自动加上 IP 的首部,也就是需要在发送的数据包上手动构造 IP 首部:
以下测试代码只在 Linux 下测试过,macOS 无法通过测试。
>>> records[0][IP]
>>> del records[0][IP].chksum # 因为 IP 的 payload 有所更改,因此 chksum 肯定有变化,可以直接删掉它
>>> records[0][IP].dst='10.6.3.33' # 更改 dst 为 CoreDNS 监听的地址, 但不能是 127.0.0.1
>>> RawIPSock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
>>> RawIPSock.sendto(raw(records[0][IP]), ('', 0)) # 因为包含了 IP 首部,因此目标 IP 地址也可不用填
之所以 IP 首部的 dst 字段不能填写 127.0.0.1 的地址,是因为 127.0.0.1 是本地回环的地址,本地回环的连接是不可能从其他的 IP 发起的,所以如果这里的 src 是 10.6.3.182 而 dst 是 127.0.0.1 的话,响应的数据包因为找不到发送方会直接被丢弃掉(可以用 tcpdump 来验证)!所以这里的 dst 需要填写本机的网络地址,如果是网络地址找不到发送方数据报会被发到网关,虽然到了网关也会被丢弃,但数据包已经从本机传输出去了(也能通过 tcpdump 来验证)
CoreDNS 成功打印出如下日志:
[INFO] 10.6.3.182:10000 - 31895 "A IN baidu.com. udp 38 false 4096" NOERROR qr,aa,rd 85 0.000099594s
IP 首部的构造也已经完成,无论通过 CoreDNS 的日志或是 tcpdump 均可以看到发送请求的 IP 的确是被我们篡改了。
问题已经基本得到解决了。
之所以还想着篡改 Ethernet 的首部,是因为只改 IP 首部还是有些限制:只能发送不经网卡的数据。这意味着我在解析 Pcap 之后只能发送到本机,也就是说我只能在运行 CoreDNS 的设备上进行解析 Pcap 测试,大多数情况下也不是什么大问题,可当进行压力或是性能测试的时候,解析 Pcap 也是会占用不少资源的,这样一来 CoreDNS 的性能测试就会有失偏颇。
很自然地,我想到了可以通过篡改 Ethernet 首部,把发送方挪到另一台机器上:
>>> EtherSock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW) # 注意第一个参数,是 AF_PACKET
>>> EtherSock.bind(('eth0', 0)) # 这表示我们通过哪个网卡发送数据包
>>> records[0].dst = 'xx:xx:xx:xx:xx:xx' # 目的机器的网卡的 mac 地址
>>> records[0].src = 'xx:xx:xx:xx:xx:xx' # 刚刚绑定的网卡的 mac 地址
>>> EtherSock.send(raw(records[0]))
不过此方法同样有限制:需要保证 Ethernet 的 src 和 dst 处于用一个局域网内,这两者的通信不能通过网关。
一旦需要通过网关,那么 Ethernet 的 dst 字段就不能是目的机器的 mac 地址了,而应该是网关的 mac 地址。而为了安全考虑,绝大部分的网关都会选择开启入口过滤, 即网关在收到 Ethernet 数据包的时候,会拆开 Ethernet 首部,去查看 IP 首部的 src 和 dst,如果查看到 src 不属于本网段,那么这个 DNS 请求会被直接丢弃。
在通过 Python 验证了此法的可行之后,我选择了使用 Golang 来实际写解析的程序,因为 Python 的性能实在太差——用 Scapy 打开一个一百多兆的 Pcap 文件结果把我电脑的内存占用完毕了还没有加载成功……
另外,出于兼容性考虑,但我在解析程序的发送端上没有选择使用原始套接字,而是使用了 gopacket——反正解析 Pcap 也是需要用到 gopacket,发送端也干脆用它,这样 macOS 上也可以运行了。
至此,本次 CoreDNS 测试所遇到的问题,总算都圆满解决了。
问题已经解决了,不过在研究过程中还发现了一些可以值得说道的东西:既然可以通过构建 IP 首部来伪造请求的发送方,那么就意味着攻击方可以伪造成不属于本机的请求,而被攻击方也没办法通过把发送方 IP 加入黑名单来抵御——因为发送方 IP 可以任意变化(这种手法经常用在 DoS 攻击里)。
这听起来好像有点无赖,不过刚刚也提到了 IP 欺骗的两个弱点:
这两者都极大的限制了 IP 欺骗的使用场景,思来想去,DNS 协议似乎是最适合 IP 欺骗的攻击了……
最后,列出一些在本次研究过程中,遇到的一些好用的工具:
我一直是一个有些特立独行的人,因此对一些小众、非主流的事物有着偏好:通常不使用 Windows 作为电脑的操作系统,通常使用 Linux 或着 MacOS;使用 Markdown 来写文档,如果有精细的排版需求我会选择 LaTex,不使用 Word;Shell 的解释器我会使用 Fish 而不是 Zsh 或 Bash;浏览器也是 Firefox 的忠实用户;编程语言我也尽量往冷门的学,比如 Elixir……因此,在知道服务器的操作系统除了 Linux 还有 BSD 之后,使用 FreeBSD 作为服务器操作系统的念头便在心里埋下了,这埋下的念头之所以一直没有发芽,除了 FreeBSD 的软件相比 Linux 要少很多之外,更多的原因在于 FreeBSD 一直不支持 BBR 拥塞控制算法,索性,在最近的 FreeBSD 13 版本中,已经可以支持 BBR 了,于是我立刻订购了一台服务器开始了 FreeBSD 的折腾之路。
大多数的 VPS 服务提供商虽然在安装操作系统时提供了多种选择,但是大多都是 Linux 的不同发行版,换汤不换药。因此我费了不少力气才找到一家宣称支持 FreeBSD 的提供商。
我开始选择的是 San Jose 的机房(他们提供有 Dallas、Los Angeles、),因为实测 San Jose 机房的带宽我跑得最满,于是我就发了一个工单和客服说我需要安装 FreeBSD,请他们帮忙挂载一下 FreeBSD 13 的镜像。十几分钟后他们回复说成功挂载了,需要我登陆 VNC 继续后续的操作,当我登陆 VNC 后虽然进入了 FreeBSD 的引导界面,但是报错了:
于是我和客服反应这个情况,他们怀疑是我的镜像的问题,于是我换了一个 bootonly 版本的镜像,让他们重新挂载,问题依旧。来回试了好几个不同版本的镜像,始终都存在这个问题,我都快没有耐心了,终于他们说会标记这个问题让高级管理员来查看。
过了几个小时,高级管理员回复了我,说他们正在探讨这个问题,并且怀疑是 San Jose 机房的内核、libvirt 版本不一致导致的,查验这个问题的修复结果需要重启,但由于机房存在大量的用户,所以这并不是一个很快就能解决的问题。于是他们提出可以为我免费迁移到他们确认可以安装 FreeBSD 的机房,一番测试后,我选择了 Seattle 的机房。
虽然这番操作耗费了我数天的时间,但我的体验并不算差,因为客服回复的速度很快、也并没有推卸责任,也展现出了专业性。
这里就不得不踩一下 WebHosting24 这家服务提供商了,这家虽然在重装系统列表里可以直接选择 FreeBSD 13 进行安装,但是在安装完成之后我发现 FreeBSD 系统没有 IPv6 的地址,于是我发工单询问客服,客服居然说他们的技术支持仅限于 Linux 系统,不针对 FreeBSD,所以需要我自行处理这个问题,如果需要他们提供帮助的话,我需要每十五分钟额外支付二十欧元的咨询费!
BBR 拥塞控制算法能够极大程度上地改善网络状况比较差时的吞吐量(尤其是在远距离传输时),因此它特别适合身在中国大陆地区但是 VPS 却在太平洋另一边的用户使用。
BBR 是一种拥塞控制算法,但是在 Freebsd 的实现却并没有包含在系统拥塞控制模块里,而是另一个 TCP stack,因此我们不能像 Linux 那样直接用 sysctl
指定一下 tcp_congestion_control=bbr
就行,而是需要自行载入 BBR 模块并重新编译内核来实现。
我也不清楚为啥 Freebsd 开发组不默认将其集成在内核中,可能美帝的网络连接质量已经很高了,大多数用户已经不太需要 BBR 了吧~、
我安装 Freebsd 13 时执行的是最小安装,没有包括内核的源码,所以需要自行下载:
>>> wget ftp://ftp.freebsd.org/pub/FreeBSD/releases/amd64/13.0-RELEASE/src.txz
内核里面的文件已经包含了目录结构,所以直接将其解压到根目录即可:
>>> tar -C / -zxf src.txz
>>> cd /usr/src/sys/amd64/conf # 进入内核目录
>>> cp GENERIC GENERIC-BBR # 创建 BBR 内核
使用编辑器打开 GENERIC-BBR
文件,然后将 ident
参数的值由默认的 GENERIC
改成 GENERIC-BBR
,然后在下面加上 options
和 makeoptions
参数,见下:
ident GENERIC-BBR
options TCPHPTS
makeoptions WITH_EXTRA_TCP_STACKS=1
保存后退出。然后新建 /etc/src.conf
文件并填入以下内容:
KERNCONF=GENERIC-BBR
MALLOC_PRODUCTION=yes
>>> /usr/sbin/config GENERIC-BBR # 注意此时目录还应该在 /usr/src/sys/amd64/conf
Kernel build directory is ../compile/GENERIC-BBR # 执行完后,就应该切换到对应的目录了
Don't forget to do ``make cleandepend && make depend''
看,还贴心的告诉你内核的 build 目录在哪,以及告诉你怎么编译依赖~编译依赖完成之后,我们使用 make -j2
来正式开始编译内核。这个编译的过程可能持续十几分钟,具体取决于 VPS 的 CPU 性能。我的 CPU 是 2 核心的 AMD 3900X,这个过程耗时了十来分钟。
编译完成之后,使用 make install
来安装新内核,然后重启。重新登录之后,在欢迎信息里应该就能看到内核变成了 GENERIC-BBR
了。
使用 kldload tcp_bbr
来载入 BBR 模块。 不过直接这样载入在重启后会失效,需要将 tcp_bbr
加入 kld list:
>>> sysrc kld_list+="tcp_bbr" # 或者直接在 /etc/rc.conf 文件末尾加上 `kld_list="tcp_bbr"`
随后就可以开启 BBR 了:将 net.inet.tcp.functions_default=bbr
写入 /etc/sysctl.conf
文件,然后重启即可。重启之后,sysctl net.inet.tcp.functions_default
输出的是 bbr 就表示已经开启成功。
贴一个对比测试吧,下面的 OUTPUT
文件是我在 Freebsd 开启 BBR 之后的速度;下面的 1000MB.text
文件是服务提供商在 Seattle 机房提供的测速文件(默认是没有开启 BBR 的)~
Jails 是 Freebsd 中提供的容器化技术,可以类比成 Linux 里的 Docker,但是会比 Docker 使用起来麻烦一点,因为 Jails 有许多配置项都需要手动配置(网络配置、磁盘划分等),就像重新安装一个 Freebsd 系统一样。
由于 IPv4 地址的匮乏,大多数的服务器提供商都不会提供一个 IPv4 的子网段供我们使用(IPv6 地址倒是一般都会分配一个 /64 的网段),因此我这一步会将 Jails 的网络配置成 NAT 模式:所有的 Jails 出口共用一个公网的 IP(即提供商分配的 IP)。
首先创建一个虚拟网卡分配给 Jails:在 /etc/rc.conf
里面加上两行:
cloned_interfaces="lo1"
ifconfig_lo1_alias1="inet 10.6.0.1/26" # /26 的子网足够用了
然后使用以下的命令让配置生效:
service netif restart
注意这里填写的 IPv4 地址段要写当前没有被占用的,否则在运行命令之后你可能就连接不上你的服务器了,不过重启之后可以恢复。
我们开启 pf
组件用来映射主机流量到 Jails 里。在 /etc/rc.conf 里写入 pf_enable="YES"
,然后我们创建 /etc/pf.conf 文件并填入:
ext_if="em0"
IP_PUB="xx.xx.xx.xx"
nat on $ext_if from lo1:network to any -> ($ext_if)
# 下面配置可以将主机对应的端口直接转发到 jails 里
TROJAN_PORT="{443}"
rdr on $ext_if proto tcp from any to $IP_PUB port $TROJAN_PORT -> "10.6.0.3"
随后启动 pf:service pf start
。
网络相关的配置已经完成,接下来可以正式开始配置 Jails 了。
刚刚提到,jail 的配置很多都需要手动管理,qjail 可以让 jail 的创建和配置更方便一些,它内置了包括 jails 的创建、删除、重启、备份等基本的子命令。使用 pkg install qjail
来安装。
安装完 qjail 之后,我们用 qjail install
来创建我们 jail 的模板,其实也就是下载 FreeBSD 的 base.txz 组件,它是一个可运行的最小 FreeBSD 系统。
接下来我们就可以创建 jails 了:qjail create -4 10.6.0.2 -n lo1 test
1,-4 表示的是 IPv4 地址,可以在
配置网卡
这一步填写的范围里面随便挑一个2,记得要挑没有被占用的。
接下来我们使用 qjail start test
来启动这个 jail,并使用 qjail console test
进入这个 jail。
进入 jail 之后,可以使用 telent
或者 host
3命令来测试网络连通性。
我目前有一些网站没有使用 Cloudflare 代理,比如自己搭建的 Trojan 翻墙服务,用 Cloudflare 会大大拖慢访问速度,而 Trojan 又是需要 TLS 证书伪装的,故而我专门创建了一个 jail 用来创建并更新 TLS 证书。
curl https://get.acme.sh
acmes.sh 默认会安装在用户根目录的 .acme.sh 文件夹内。
申请 TLS 证书的时候,有两种常见的方式验证域名的所有权:
setenv CF_Token="xxxxxx"
setenv CF_Account_ID="xxxxxxx"
setenv CF_Zone_ID="xxxxxx"
~/.acme.sh/acme.sh --issue --dns dns_cf --server letsencrypt -d example.com # 生成证书
~/.acme.sh/acme.sh --install-cert -d example.com --key-file /cert-path/key.pem --fullchain-file /cert-path/cert.pem # 安装证书
这部分完成之后,acme.sh 就会创建并自动更新证书了。
此时还剩下一个最后一个问题:因为不同的 jail 的文件系统是相互隔离的,那么在这个 jail 里生成的证书如何让其他的 jail 能使用呢?
不过还好,在主机端可以访问所有的 Jails 的文件系统(在 /usr/jails 里),所以我们可以把运行 acme.sh 的 jail 里的证书目录直接挂载到使用证书的 jail 的文件系统里:
mount -t nullfs -o ro /usr/jails/acme-sh/cert-path/ /usr/jails/trojan/root/certs
第一个地址是源目录(运行 acme.sh 的 jail 的文件系统),第二个地址是目的目录(使用证书的文件系统)。
这样我们就可以在对应的 jail 里的 /root/certs
目录看到证书的文件了。
参考:
这里的 -n lo1
参数非常重要,因为 qjail 帮我们简化了很多 jails 的配置,这也导致某些配置可能并不符合我们的期望,比如如果这里不加 -n 参数的话,这个地址就会被添加到默认网卡上,而不是 lo1,所以我们这里需要指定为 lo1 网卡添加地址 ↩︎
我们在前一步配置的时候只标注了子网范围,而在我们创建 jail 的时候实际上是给这个字网内加了一台机器,而 pf 的规则并不会主动去应用于新加入的字网的机器,因此我们创建完新的 jail 之后需要重启 pf 进程 ↩︎
Jails 相比主机有诸多限制,比如默认 jails 内部不支持 raw_sockets,也就是说你无法使用 ping 命令来测试网络连通性,当然:qjail config -k jail_name
命令可以开启 jail 对 raw_sockets 的支持 ↩︎
In the previous post, I introduced how to use Elixir to write a rate limit tool. After that, I was planning to integrate it with the DIEM-API . In this way, my API System doesn’t need to depend on Redis. However, DIEM-API is written in Golang, and I have not decided to re-write it in Elixir in the short term. Besides, I wrote a search api for my blog using Rust recently. Therefore, I need a cross-platform solution to address communication issues between Golang, Elixir and Rust.
Currently, there have been some ways to communicate between different processes (e.g. Pipe, Signal, Message Queue). But in fact, they have many limitations. Pipe and Signal require communication processes to be on the same machine. Message Queue and GRPC may be better solutions, but introducing new dependencies will complicate the system further.
TCP is another potential solution, but it is only able to transfer binary data. In other words, there is no simple way to decode data into typed parameters after receiving it from sender. REdis Serialization Protocol (RESP) is a request/response protocol over TCP, with various types of response (e.g. status reply, error reply, integer reply, bulk reply) but only one parameter type (string). However, function calls have only one response type but various types of parameters, which is the opposite to RESP. Therefore, RESP can not be used for inter-process calls.
Another way I tried is using separators between multiple parameters, which also introduces two new problems:
Although these ideas don’t work, they gave me some inspiration. I decided to transfer the length of parameters and the typed parameter together.
This is Type-Length-Value (hereinafter referred to as TLV).
TLV is an encoding scheme. It must be based on one communication protocol, like: TCP, UDP or Unix Domain Socket.
A TLV element should consist of three parts (Figure 1):
0x1
for String, 0x2
for Integer, 0x3
for Float, etc.. We use tens-digits to represent compound types, such as 0x1[any digit]
for List, 0x2[any digit]
for Hash. The enumeration can also contain types which are unique to programming language, such as Atom type in Elixir.In this section, I will introduce how I implemented the encoding and decoding of TLV elements.
This part should occupy 1 byte / 8 bits. Lower 4 bits represent the basic type, which can cover 16 basic types. Higher 4 bits represent compound type, which can also cover 16 compound types:
This part should occupy 4 bytes / 32 bits. It can represent a value whose length up to 2 ** 32 bytes(about 4 GB).
For Integer and Float, higher 3 bytes are 0s, lowest byte is 8, which means Integer and Float can be represented by 8 bytes.
For String and compound type, we need to calculate the length of value first, and then convert the length to bytes.
Besides, we can use
binary.BigEndian.PutUint32
function for Golang, use<<int_value::integer-32>>
for Elixir, use.to_be_bytes()
for Rust.
This part should occupy at least 8 bytes / 64 bit (except Bool).
For Integer, we can use the above method to convert it to bytes; For Float, we can use math.Float64bits to convert it to int64, then convert int64 to bytes. For Golang, Rust and Elixir, String is a byte array (char array), which can be converted to bytes directly.
If there are nested elements, they must be encoded first, which can be processed recursively (in Elixir) or iteratively (in Rust and Golang). Same as decoding. And we should consider the case where a List element contains different types, which is allowed for dynamically typed languages (Python, Elixir) and some weakly typed languages (Golang can use []interface{}), although using reflection will cause more overhead.
I wrote some bench tests for TLV (encoding, decoding) and Separator + strings (mentioned in the previous section ):
goos: darwin
goarch: amd64
pkg: DIEM-API/rpcserver
BenchmarkToString-8 1000000 298 ns/op 32 B/op 3 allocs/op
BenchmarkTLVEncode-8 1000000 130 ns/op 96 B/op 6 allocs/op
BenchmarkStringTo-8 1000000 115 ns/op 48 B/op 1 allocs/op
BenchmarkTLVDecode-8 1000000 14.9 ns/op 0 B/op 0 allocs/op
PASS
You can find the source test codes in this repo .
I used Elixir, Golang and Rust to implement TLV Encode Scheme, you can check the following repositories to view the source code:
不知出于什么原因,前几个月对写博客这件事情始终提不起太多兴趣和动力,连一年一度的年终总结都鸽了三四个月。其实早在去年十二月底时,我就开始写年终总结了,可似乎总找不到写作的「感觉」——对着屏幕脑子一篇空白,好不容易琢磨了一个开头出来也不甚满意,后来便不了了之了。
就当我以为这篇文章要一「鸽」到底时,最近居然找回了之前恨不得三天写一篇博客的状态,可眼看 2021 年都过了四分之一了,还写 2020 年的年终总结似乎有点说不过去…好在女朋友说:「没关系,上市公司的年度财报一般也都是次年的 3 月份发~」,我想了一下,觉得很有道理——于是便有了这篇姗姗来迟的年度总结。
由于最近的几篇博客都是偏向技术方面的,所以本文则会更偏向生活方面的记录,技术可能仅会选择性的提一下。
虽说我对奇安信有诸多不忿,但抛开工作上的事情不谈,那段时光我还挺怀念的——主要是怀念和同事们相处,一起在人均三十多的小餐馆里面吃饭、喝酒,苦中作乐。
在 2019 年的最后一天,我照例和小伙伴们在公司附近的学校食堂里吃饭,我点了一份牛肉小火锅,在吃的时候我打开手机刷微博,刷到了「武汉出现不明原因肺炎」,当时也没当回事,只是和同事说了一下,提醒他注意点(因为他租的房子正好在江汉路),随后另一名同事说晚上想去江滩跨年并问我们去不去,我本身也不是一个喜欢热闹的人,加上刚刚看到的新闻,摇了摇头拒绝了。
之后两周,疫情逐步「发酵」,新闻上隐有愈演愈烈之势,但也没有对现实生活产生多少影响,该上班还是得上。且由于当时我已有离职之心,想着年后回武汉便准备辞职,因此在离开武汉的前一天晚上还去江汉路找朋友吃饭,当时地铁上和商场里很少有人戴口罩,不过我因为比较惜命还是戴上了公司发的口罩(之后想想还真挺后怕)。之前文章里提过,我在积攒了不少调休,因此我在过年前一周就回家了,离开武汉的当天是工作日,一个在公司上班的同事偷溜出来送了我一下,我还挺感动的,可惜之后我们也没再见过了(不过现在也经常联系)。
早在月初时,便和几个初中同学约好了一场聚会。等到聚会前夕时,疫情已经变得有些严重了,我爸妈都劝我别去,但我还是去了——所幸我去了,遇到了我初中时代暗恋的对象,现在的女朋友。
再见她时,觉得她依旧那么有气质,而我的心境仿佛过了快十年都没什么长进——与她不经意见间的眼神交流都会让我紧张。饭局开始,自然而然地聊起了各自的近况,聊到她时,她似乎有些局促,说她毕业后就一直在家待着申请国外的研究生,是从大三开始就有的想法,因此放弃了保研的资格。顺着聊到选择研究生的方向时,话题不可避免的转向了我——她想从事数据分析方面的工作,最近在上 Coursera 上的机器学习课程,而我恰好对这方面比较擅长,于是我说我之前正好上过这门课,「喔~~」饭桌上响起大家心照不宣的声音——就好像初中时那样。
是的,即使我对初中时期的大多数事情已经淡忘,但和她有关的事情却记得很清晰。当时,我喜欢她这件事不知怎么就传遍了班上,以至于同学们总喜欢起哄——提到她时,总会有人小声说一句我的名字,接着便是哄堂大笑,我总会红着脸不说话,内心却有些欣喜。好在她是一个不怎么在乎这些「流言」的人,反而会经常问我数学题,而其他人看到她问我数学题时,便会说一句:「哦~又来问数学题了呀」,而她置若罔闻,待我解释完之后才面不红心不跳的回到座位上。当时的我挺希望有更多同学起哄,就好像起哄的人多了就能帮我说出我心里不敢对她说的话一样。
之后的事情我在 高中篇回忆里 有提到一些,中考结束时,我没有考上省重点,于是爸妈便想着参加并通过暑假夏令营考核的方式来进入这所中学,巧的是她与我被分到了同一个班里面,似乎她也是因为中考发挥失常。当时的我情绪有些低落,即使因为数学比较好能进入夏令营,在结营考试时却因为英语拖了后腿,无缘进入这个班级。在那之后,我在普通班里看着她在快班里成绩越来越好,而我只能在一本线外徘徊,虽然很不想承认,但我和她之间的应当是越来越远了。
高考结束时,她是她们班上的第一名,登上了我们学校的报纸,我犹豫再三,仍是点开了她的对话框,将她上报纸的版面拍了一张照片发给了她,想象中的热烈情绪并没有出现——毕竟我和她已经三年没有说过话了,心里默叹一口气,还是把这份感情埋在心底吧。
饭局结束后,我主动加上了她的微信,而几年前压抑在心中的感情突然有些蠢蠢欲动了,但我还是忍住了没有说出口,给她发送了一个课程的链接并告知有问题我可以随时解答后,我们结束了这场对话。
在随后的三四天里我过得很煎熬,很想问她是否有在看我发给她的课程链接,如果看了的话是否遇到了什么问题?可又怕她觉得唐突。就这样到了第五天,她终于发过来了一个问题,有关逻辑回归的,具体问题我已经记不太清楚了,借此机会我和她聊了一下午,尽兴收场。随后几天,我们每天都会聊,虽然话题总是离不开学习,但能明显感觉和她之间的距离拉近了不少。之后我们的关系一直稳中有升,直到我准备提离职的那一天晚上。
前文有提到过,我原本计划是在年后就直接辞职的(毕竟过年 7 天不上班还有工资拿),但因为疫情影响,以及公司随后宣布进行为期最少两周的在家办公让我决定暂时观望一下,想来不用去公司应该会轻松一些。后来的事实证明我真是太 Naive 了,也把我 Leader 想得太善良了——节后工作的第一天我们就开会开到了晚上 11 点,「办公室工作只是 996,远程工作就变成了 007」这话一点不假。
随后几天公司各种奇葩规定仿佛在秀下限,日报还不算什么,甚至要求「小时报」,我对公司积攒的怒气值也终于在那天达到了顶峰。那天上午,Leader 忽然对我说,下午有一个会(是和我之前去上海出差的项目相关的)由我来主持,于是我从下午五点开始开会,一直开到了晚上八点,而且我作为负责人一直在讲话自然是没有机会吃饭的。会议结束后,我走出了房门,和家人坦白了我今天就要辞职的决定,~~耶稣也拦不住我,~~他们见我态度坚决,也没有多说什么。那晚由于我在写辞职信,所以顾不上和她聊天,写完辞职信时,已经是十点半了,我迫不及待的点开了她的聊天框,给她看了看我的辞职信——后来才知道那天她一直在等我找她聊天。
在发给她看之后,我心里突然有些小忐忑:我担心她会像我其他的朋友一样不认同我的决定,而是希望我多忍一会 blabla 的,等到时机成熟一点、或者找到了下家之后再辞职;所幸刚说完我的决定之后,她就称赞我在这件事情上的果断,当时我特别开心能得到喜欢的人的理解和支持。
于是乎,3 月 6 号那天晚上,借着徐佳莹的歌词,我表白成功了——那天晚上我激动得没怎么睡着。
离职之后的那段日子,我在 这篇博客 里写的比较详尽,感兴趣的小伙伴可以移步观看,我就紧接着离职的两个月之后讲吧。
辞职后的两个月里没少被家里人数落:哎呀,你怎么还不去找工作啊;总在家待着怎么行啊,你不知道空闲时间越久就越找不到工作啊(还好有女朋友一直在身边鼓励我)……总之,无论是处于父母的唠叨还是我自己本身的计划,六月初的时候,我开始步入找工作的阶段了。首先是在 BOSS 直聘找了两家中型公司投了一下,结果都是已读未回,在这之后我就干脆放弃了招聘软件这条路,开始转向官网投递。
官网投递的公司包括了之前对工作氛围我很认可并向往的豆瓣;以及工作理念我很看好的 LeanCloud。可能由于许久没面试,又是特别重视的公司导致有些紧张:豆瓣一面的几个问题答得都不是很好(稍复杂一点的数据库设计我真是一团糟),而 LeanCloud 则是因为技术栈的问题,投递简历后虽然得到了回复说我潜力很大(大概类似「好人卡」之类,拒绝你总要找个好一点的理由),但没有后续面试的机会。
于是我又开始在 V2EX 上的招聘板块寻找帖子投递简历,先开始找的是一个比较小的初创公司(墨刀),一下午的时间就通过了两面,面试其实也就是和前后端的负责人分别聊聊天,过程还是挺愉快的,但是聊到福利环节的时候,因为公积金等缴纳基数只有 5000,所以即使技术栈我很感兴趣(Ruby)、交流也很愉快,在考虑良久之后还是选择拒绝了。
找工作时,我特意避开了知名的大公司,因为来自上一家公司的阴影——我不想加班了,而据我在脉脉上的观察,大公司加班都很严重。而在找工作开始过去了快两周时间,两周时间说短不短说长也不长,但给我造成的压力已经有一些大了,心里的信念也有一些动摇。原来在遭到挫折之后,自我认知真的会不断的降低:会怀疑是否错估了自己的能力、最初的理念是否有失偏颇等等。而如果一个人在每次遭遇了挫折之后就开始不断的怀疑、调整自己的信念,我也不清楚最后他会变成什么样子,可我不想这样——「你尽可以消灭他,可就是打不败他」。
就这样,我抱着十分矛盾的心理,讲我的遭遇告知了一位在博客圈「结识」的长者,并试图从他这里得到一些人生经验。其实发完之后我有些忐忑,毕竟我和他素未谋面,说是「结识」,其实仅仅是博客加了友链、Github 互 Follow 了而已。因此在仅几个小时,就收到了他长达三千多字的回复时,我感动得不成样子。而我也意识到了之前想法有些 Naive,事情并没有我想象的这么严重,都是我太钻牛角尖罢了。
在这之后就是现在的公司了,也是在 V2EX 上翻到的招聘贴,不过录取的经历却颇有些一波三折:一面发挥不是特别好,有几个偏底层的问题没有回答上来,其中有的是因为没有 GET 到面试官的点,有的是因为确实不会;所以之后几天得到的回复是没有通过面试,谁知过了两周之后面试官又说没有找到更合适的了,想问问我愿不愿意继续后面的流程……于是,在八月初时,终于结束了在家待业四个多月的日子,开始了一段新的旅程。
之前几个章节都是在描写生活方面,本节就稍稍往技术上扯回一点吧~
还是按照时间顺序聊吧,首先说说 GraphQL。我认为 GraphQL 就像是 ElasticSearch 一样,公司级别的产品会选择深入使用或者集成,因为它对于越大、越复杂的数据结构就越有优势,但是对于个人开发者而言,甚至都不会想到拓展这方面的技能;GraphQL 最大的优点就在于它对于复杂的数据结构非常具有表现力优势,比如图系统(从名字也可以看出),因为它可以非常方便的处理嵌套多层的数据,只需要编写对应的 Resolver(或者干脆配合 DataLoader 使用),各种嵌套字段的会由 GraphQL 自动寻找对应的 Resolver,然后解析数据再塞到 JSON 里。
在熟悉了 GraphQL 之后,紧接着就是试图将图系统的同步 API 改写成异步 API(本文有
详细的描述
),这一过程极大加深了我对异步以及 Python 的历史包袱的认识。以及在异步 HTTP 客户端调用同步 API 服务器时,大概率是会出现问题的——异步 IO 里一个进程同时监听了多个 fd,每个 fd 都对应了一个发送的请求,而同步服务器只能一个一个的处理这些请求,所以请求多了,要么把客户端本机的 fd 都用完了,报 too many open files
的错误,要么是服务器处理不过来了,只能选择舍弃掉一些连接,这时就会报连接失败的错误,所以及时关闭连接很重要。
随后就是设计了一套用户登录的系统,主要是认证的设计,使用了 Refresh + JWT 双 Token 的设计,让 Refresh Token 弥补了 JWT 无状态的缺点( 这篇文章 真的写的非常好)。
按道理来说,今年在家休息了四个多月,电影和书籍应该看了不少才对,可恰恰相反,可能是因为在家时有着找工作的压力,虽然有着完整的时间,但却没有什么观影的心情;而在找到工作之后又正好反过来了,有观影的心情却只剩下了碎片化的时间。好在我也已经看开了,不再去追求什么一周看一本书、两部电影之类的目标了。
不过也简单总结一下吧,剧集方面:之前看过《黑袍纠察队 1》,很喜欢,所以对《黑袍纠察队 2》的喜爱自然是水到渠成的(只要续集不是太烂,我都会很喜欢看);《想见你》则是我第一次与女朋友一起看的爱情剧,虽然是爱情剧,但是里面的情节引人入胜程度丝毫不逊色于悬疑片、《王国 2》同样是包含了一部分《王国 1》的偏好,加上僵尸、宫斗题材我非常喜欢;动漫最喜爱的是《异度侵入》,我和女朋友从下午开始一直看到了凌晨,欲罢不能~;而电影方面,因为我想寻求感官上的刺激,所以恐怖电影看得比较多,反而没什么特别印象深刻的。
书籍方面,虽然只看了寥寥 4 本书,还都是技术方面的(在家待业时真的很难静下心来阅读文学类书籍),但是有一点我很满意的是看完了两本英文的书籍——《Elixir in Actions》(让我入门了 Elixir 语言)、《Kubernetes in Actions》(对 K8s 的理解加深了),总算完成了很久以来的梦想,不过可能是因为《in actions》系列的书写的比较通俗易懂,像《Algorithm, 4th Edition》我尝试了数次还是一页都看不下去。。
如果让用一个词来总结我的 2020 的话,我想应该是「幸运」。很幸运在从奇安信离职前交到了几个可以成为好朋友的人;很幸运重逢了少年时期的暗恋对象并成为了我的女朋友;很幸运在自我怀疑时有「长者」传授了人生经验;很幸运找到了一份非常喜欢的工作……还有许多,就不一一列举了。
很幸运,很幸运让我遇到了你们。
]]>博客建站以来,我使用过 Hexo 和 Hugo 两个框架,它们生成的博客在本质上都属于静态博客,对于「搜索」这个与数据库关系紧密的需求,显得有些力不从心——不过也并非没有办法:比如主流的解决方案(这里不考虑使用 Algolia、Swifttype 等第三方服务)就是预先生成一个文档(包括所有的博客数据),然后在浏览器端加载此文档再通过编写 JavaScript 代码进行搜索匹配,最后再输出结果。
这种方案有几个缺点:
当然也有优点:搜索不同的关键词不需要额外发送请求了,因此搜索的响应速度会更快……
但这总归算不上一个优雅的解决方案。
因此我很早(大约两三年前)就想为博客构建一个真正的搜索引擎,当时也研究过一些方案:使用 PostgreSQL 加上一些插件(因为当时已经在使用它作为一言的数据库了),后来觉得这些要是跑在我 512M 小内存的主机上实在是有点太为难它了,于是便搁置了;最近在新购置了一个大内存的 VPS 之后,也终于可以将这个想法实现了。
之前使用 PostgreSQL 作为解决方案,如今看来不是很满意:并非是 PostgreSQL 不好用,而是我想将数据库依赖从 API 系统 中删掉——因为数据库只是存放了一言(我还专门为随机读取一言写了一个 存储过程 ),而查询也只是简单的 SELECT 操作,嵌入式数据库也足够用了,减少了依赖的同时性能还更好;同理我将 Redis 的依赖也删掉了,只是限流操作,我完全可以使用 别的方案 来代替。究其原因,其实是我想在系统设计的复杂度上做减法,尽可能谨慎的为系统引入新的依赖——如果已经引入了,那就尽可能的去掉。
因此我将解决方案框定在了基于编程语言构建的搜索引擎上,这一次我没有选择自己造轮子:一是搜索引擎涉及的技术太复杂,我没有这么多业余的时间;二是业界已经有不少完善的解决方案,没必要自己造轮子了——这并非是我对搜索引擎背后的技术不感兴趣,等以后有空的话,我仍然会研究相关的技术。
我对博客的搜索引擎有以下几方面的要求:
综上,我选择了 Tantivy ——支持复杂的查询以及对搜索结果的高亮;虽然没有提供开箱即用的配置,需要集成到程序里使用,但这反而更方便我自定义 API 接口;Rust 的内存管理不依赖于运行时,少了 GC 的存在,会比 Golang 之流要更省内存,非常适合编写底层性能敏感的程序。
其实符合要求的搜索引擎并不只有 Tantivy,至少 Go 编写的 Bleve 和 C++ 编写的 Typesense 也算符合要求,不过出于私心,我仍然选择了 Tantivy——我想通过它来学习 Rust。
忘记之前在哪看到一句话:学习第 N+1 门编程语言的难度,会比学习第 N 门时要低一半。之前由于接触的编程语言太少了,所以也没什么感觉。如今在断断续续接触了 Python、JavaScript、Golang、Elixir(其他的诸如 SML、Lisp、Ruby 等虽然也用来写过作业,但没有完整的项目支撑,经验也不够,就不提了)之后,觉得这句话得加上一个前提条件:必须是具备相同编程范式或者类似语言特性得语言。
因为在学习不同范式的编程语言时,之前所具备的思维定势反而会误导你:比如当你在习惯了使用动态类型带来的便捷之后,在接触这类静态类型语言的时候发现居然变量、函数还需要声明类型才能使用;当你在习惯了命令式编程的顺序分支循环等语句之后,突然发现函数式编程里面的变量是不可更改的,以及最简单的迭代过程居然还需要使用函数递归的形式来实现……
说这么多,无非是想掩盖一下「我在学习 Rust 的过程中遇到了麻烦」的尴尬😅。
说两个我在使用 Rust 过程中觉得不爽的点吧。
在使用 Golang 要初始化数据库连接的时候,一般都会先声明一个全局变量 DB
,在读取配置文件的时候初始化这个变量,这样就可以在其他的模块里导入使用了;Python(Flask)则一般是在初始化 app 的时候,提前配置好数据库的信息,再调用函数完成初始化,再通过导入 Model 层定义的 ORM 类进行查询操作。
无论是 Golang 或是 Python 所采用的方法,其实关键点都在于全局变量,而 Rust 默认(防杠)不支持全局的变量(即在堆上直接分配),虽然有第三方的 Macro 可以使用,或者使用 unsafe
包一下,也能勉强为全局的变量赋值,可用着第三方的库不感觉别扭么?还有在一片整洁的代码上冒出一个 unsafe
,就好像时时刻刻提醒你「不要这么做,这么做不好」一样,可也没有更好的方法了。
Rust 为了内存安全性,在编译期间就已经决定好了变量何时回收,因此需要避免在堆上声明变量,因为这样变量的生命周期变得迷惑了起来,道理我都懂,不能这么做的原因我也明白、我也没有更好的方法,可用着不爽该吐槽还是得吐槽。
Rust 同样也不支持空指针,而是使用独有的 Option
类型来避免空指针的存在。可在我看来,这个解决方法并算不上好——或者说 Rust 本身做的还不够好。比如,某类型被 Option
包了一层之后,就会失去原类型所派生的属性:原类型派生了 Clone,但是套上了 Option
之后就没有派生 Clone 了。而在 Rust 里经常会遇到 .clone()
某一个变量的情况,因为变量的生命周期只能属于一个 Scope,所以在调用函数的时候往往会将变量 .clone()
一份之后传入参数,而由于 Option
没有派生 Clone,还得自己写一个 Option
类型的 Clone 实现。
虽然自己写一个 Clone 实现也不到十行代码,但这种编译器明显可以优化的着实没必要自己写。
把时间戳转化为时间类型都得借助第三方库,懂得都懂……
在花了大约两周的业余时间之后,我完成了博客搜索引擎的搭建( 源码 ),虽然吐槽归吐槽,但真上线运行的时候还是「真香」了,尤其是性能方面——一个 Tantivy 程序居然只占用了几兆字节的内存(是的,你没有看错),本机测试 QPS 也达到了 3000/S。
吐槽得再厉害,可就冲着 Rust 的性能、稳定和内存占用……也只能捏着鼻子忍了 : )
在之前提到,不采用分词的话,虽然召回率(Recall)一定能达到都是 100%,但精确率(Precision)却会大幅下降。比如搜索「新鲜」,如果不采用分词算法的话,一般会直接将它按 Unicode 分成单个字符,也就是会查出所有匹配到「新」和「鲜」的文档,其中虽然也包括了作为词语一起出现的文档,但还会包含有这两个字分别与其他字组成词语的文档。
为了解决精确率的问题,我使用了 Tantivy 提供的 PhraseQuery——它相比普通的 TermQuery 多了一个匹配单词序列的步骤。在「新鲜」这个词的搜索时,它只会搜索「鲜」这个字紧跟在「新」后面的文档,这自然就很大程度提升了精确率。虽然 PhraseQuery 可以解决在搜索词语时的精确率问题,但是如果只使用 PhraseQuery,针对包含多个词语的搜索又会显著降低召回率,因此关键词的搜索我采用了 TermQuery + PhraseQuery 组合实现。其中单个词语内部之间使用 PhraseQuery,而不同的词语之间使用 TermQuery。
关键词搜索的长度被限制在 38 个字符以内,超过的会被忽略;
如果关键词以 -
符号开始,则意味着搜索不包含该关键词的文档。
Tantivy 的范围检索暂时不支持 Date 类型,因此我将博客的发布时间存为了 int64 类型的时间戳,由于不过对于接口调用,还是使用 ISO8601 的日历日期表示法来作为查询参数会更易理解一些。
如果想查询在某一时间范围的博客,使用:range: startDate~endDate
来查询(其中开始与截止均为 yyyy-mm-dd
的格式),如果只提供了 startDate,则 endDate 会被默认填充为当前时间戳;如果只提供了 endDate,则 startDate 会被填充为 0,二者之间使用 ~
(半角符号的波浪线)连接。
如果指定了多个时间范围,则只会选定首次出现的作为范围,其余的会被忽略。
标签以及分类的组合过滤我使用 TermQuery 来实现,其中标签以及分类的查询最多允许 5 个。
如果要查询某标签下的文章,可以使用 tags:标签名 来查询,如果想查询某分类下的文章,可以使用 category:分类名 来查询,注意使用的是英文的引号。
以上三种查询可以自由组合,但组合需要其中至少一个,否则被视为非法的查询。
快去 搜索页面 耍耍吧~
]]>时间倒回到两年前,当时的我刚刚结束实习返校:闲着没事又开始折腾起博客,那时博客才更换成 SPA 没多久,由于 Bug 太多,主题各地方的细节我也不是太满意,体验了一段时间满足了新鲜感之后,心里忽然升起了一种对折腾博客的倦怠感。于是我还是换回了原来的 Material 主题,这一用就用了两年。
直到前段时间,Github 提示博客仓库有好几个 CVE 的漏洞,作为一个强迫症,自然是想修复这些漏洞,然而我发现这些漏洞是博客使用的一些 Hexo 插件的依赖造成的,有些插件作者都不再维护了,于是我便想弃用掉这些插件,找寻新的代替品;可又转念一想,就算找到了代替品,这种情况也还是会发生——毕竟我不能指望每一个插件的作者都不弃坑。权衡利弊之后,我决定从根源处解决问题——如果没有 Hexo,自然也就不会有插件依赖的问题了,因此我弃用掉了从建站伊始就使用的 Hexo 框架,转而投向 Hugo 的怀抱。
Hugo 并不像 Hexo 提供各种插件用来增强或修改框架本身的功能,不过好在 Hugo 框架本身的功能已经足够的多了,我只需要在上面做一些修改即可——这也正好符合我的需求,根据我自己的想法定制、维护,也就解决了让我弃用 Hexo 最根本的问题。
其实早在两年多前我就有尝试过将博客换成 Hugo,只是当时逛遍了 Hugo 的主题,发现没一个能打的,原因也很简单:Hugo 是基于 Golang 的,主题创作者大多是后端;Hexo 则是基于 Node.js,主题创作者大多数是前端,而主题样式则完全考验的是前端能力,因此 Hugo 主题的整体水平自然落后于 Hexo。
如今,既然我已决定使用 Hugo,那么不妨自己写一个主题。
本次主题我选择使用 Tailwind 作为 CSS 框架,它与其他 CSS 框架的区别在于默认不提供各种元素的样式,而是将各种 CSS 的属性预先定义成 class 的形式,这样如果我想控制某个元素的样式的话,只需要将对应的 class 名称加入元素的 classList 就好,颇有点声明式编程的意思。同时,由于 Hugo 集成了 PostCSS 框架,几乎可以让 Tailwind 开箱即用了。
在 JavaScript 方面,由于之前 Hexo 年久失修的插件给我留下的阴影,一开始让我使用它时我是拒绝的,可转念一想有些 JavaScript 的功能的确可以提升博客的体验(Lazyload,Workbox 等),于是开始秉持着「主体功能上不能依赖于 JavaScript」的原则来使用 JavaScript;由于 Hugo 已然内置了模块用于 CSS 的预处理和 HTML 的压缩工作,在发布时我只需要打包 JavaScript 就好,因此在模块打包工具上我选择了 Rollup.js——一个非常轻量的工具,只支持 JavaScript 文件的打包。
一般来说,博客评论的加载有以下几种方式(以 Disqus 为例):
博客的实践是在第三种方式的基础上做了一点优化:当用户进入文章页面时,不请求配置文件,只有当页面滚动到文章底部的时候才发送请求配置文件来测试 Disqus 的连通性,这样当用户首次进入文章页面时,页面的加载速度不会有什么影响。
博客是否需要加密文章,以及需要加密文章的原因不在本文的讨论范围。本文仅讨论实现手法
之前 Hexo 博客的加密是通过插件完成的,如今更换成 Hugo 之后,自然要自己造轮子了。其实加密博客的思路很简单,在编译成 HTML 的时候将文章原本内容使用 AES(或者其他的什么算法都行)加密后替换掉,在前端展示时,再让用户输入密码通过 JavaScript 解密就好。
这里我踩了两个坑:
也由于第二个坑的存在,在本地使用 Hugo Server 调试时是没办法测试加密功能的,不过好在每次博客部署都是通过 Github Actions,配置文件写好之后倒也没有特别麻烦,只是心里总是觉得有根刺儿一样不舒服。
除此之外还有一些小改进,比如:
所有的动画都是使用 CSS 实现的,不依赖于 JavaScript;
所有的 JavaScript 都是延迟加载的,等到页面所有资源加载完成才触发执行;
所有图片都是 Lazyload 的,考虑到 RSS 订阅的用户,文章内部的插图没有使用 Lazyload;
……
一开始设计博客主题的时候,并没有打算发布,只想自娱自乐,因此将许多个人偏好耦合进了样式中,我也不打算改了,直接把博客 开源 吧。
博客的一些个人配置文件我使用了 Hardcode 的方式嵌入(比如 Google Analytics 的 ID),请注意修改后使用。
还有,因为包含加密的文章,所以我将 content 文件夹注册成了一个 Hugo Module(这个 Module 其他人没有访问权限),如果你想直接运行,最简单的方法是删除在配置文件和 go.mod 里的 Module 相关内容,然后将你的 content 文件夹拷贝到根目录,就可以运行了。
促使我从 Hexo 迁移至 Hugo 的原因或许大多数人的眼里看起来甚至有点扯淡,但我当时的确已经无法忍受了。后来冷静下来想了一想,Hexo 本身并没什么不好的,反而其衍生出来的生态、主题美观度、模板语言、API 自由度比 Hugo 强了不止一点。
而我终究还是换成了 Hugo,究其原因应该是我已经对 JavaScript 的编译、打包、构建等前端工具链累觉不爱了吧——咦,那么我为啥迁移到 Hugo 之后还要使用 Rollup.js 打包工具呢,人呐,果然是矛盾的动物。
]]>算算日子,我又有两个多月没有写新文章了,是时候给博客除除草了——拖更的原因是( 懒 )换了一份新的工作,在适应新的工作与生活环境。在入职奇安信(上一份工作)的时候,有半年的时间没有更新博客,当时的我把原因归咎于公司 push 员工太厉害,导致员工没有属于自己的空间;可现在的工作明明给予了我足够的空间,我却还是两个多月没有新文章产出,看来个人方面也有一定的原因。
废话好像有点太多了,本篇文章准备分享一下最近工作上遇到的问题以及解决办法。
起因是部门产品的图相关部分的 API 查询(使用 GraphQL 自己实现的)太慢,一次普通的查询往往在后端会解析成十几个不同的子查询,子查询的内容上没有相关性;涉及的数据源也大相径庭,包括:MongoDB、ClickHouse,以及其他产品的 Web API,因此也无法通过关联查询减少查询次数。由于子查询太多,即使每一条子查询耗时都能控制在 300 毫秒内,那一条完整的查询也会耗费将近 5 秒的时间(来自十几个子查询耗时的叠加),这显然不正常。如果能将十几个子查询的方式从「顺序」改成「并发」就好了。
因此我很自然的想到了使用 asyncio 来实现并发的查询,而 GraphQL 也提供了 AsyncioExecutor 的方式来异步执行查询语句,但是正当我沾沾自喜以为这个问题就这么从理论上解决了的时候——开发负责人告知只有图相关的 API 需要改成异步方式,其他的 API 保持不变,并说出了基于两个方面的考量:
但直觉上,我认为如果只是将图相关的 API 改成异步,在后续可能会出问题,但具体为什么会出问题、以及出什么问题我还不知道,因此就先着手试试。迁移的过程比我想象中要轻松很多——motor、aiochclient、aiohttp 等库已经很成熟了,在这些异步库上封装一层与现使用的同步库的兼容层就行了。
花费了一周时间,在本地测试没什么问题之后就放到了线上先让系统运行着测试。头几天没出什么问题,在我以为这个问题就这么完美的解决了并准备做其他事的时候,前端突然找到我说查询请求会报一个很奇怪的错误,复现的概率不高,但有时候就会出现,我心里咯噔一下,感觉是之前异步改造不完全造成的问题。在前端发给我看具体报错之后,我确认了的确是和异步相关的问题。错误信息相信大多数接触过 asyncio 的都看过,这差不多是 asyncio 里最常见的错误之一了:RuntimeError: This event loop is already running
。
和前端沟通后,在我本地的开发环境上却没有复现此 bug,因此我认为应该是生产环境上部署的工具与 asyncio 冲突了。
部署的方式与其他 Python Web 端程序并没有什么区别,WSGI 服务器使用的是 Gunicorn,worker_class 设置为 sync(一共开启 6 个),每个 worker 开启了 20 个 threads,一开始我怀疑是 worker_class 的问题,可将其设置为 gevent 之后,仍然会出现这个错误,我开始觉得有点棘手了。
在出现这个问题的时候,我就和朋友吐槽过,Python 这门语言真的是太割裂了(Python 2 和 3 版本间的割裂就不用说了),公司产品和开源社区完全就是两个极端:开源社区一个新 Feature 接一个发布,可大多数公司仍然守着 Python 2 不肯升级,原因也很简单:Python 3 对比 2 最大的更新就是 asyncio,可这玩意所解决的痛点相比 Python 2 升级 3 耗费的精力实在太微不足道了——Python 2 使用 monkey patch 一下再使用 gevent,也比用 asyncio 慢不了多少吧,既然如此,那我还费心思升级干啥?
往远了说,我认为这仍然是 Python 的历史包袱仍然没有完全甩掉(即使 Python 3 在发布的时候宣称想甩掉一些历史包袱从而决定不兼容 Python 2),也是我认为 Python 最大的痛点——并发的问题。既然 Python 之禅宣称做「最好是只有一种方法来做一件事」,此话也被社区奉为圭臬,那为什么 thread,process,greenlet,asyncio 都可以用来实现并发这件事呢?要我说就应该在 Python 3 问世的时候甩包袱甩得更彻底一些,干脆就像 Node.JS、Go 一样,想用并发?可以,只提供一种方式,想直接调用系统原生的线程?不好意思,没这种操作。让用户只用一种方法做一件事的最好办法就是不提供其他的方法。
在 Linux kernel 2.6 版本正式引入 IO 多路复用的时候,Python 2.4 已经发布,各种功能已经很完善了,不太可能抛弃掉现有的多线程模型转而投向异步 IO 模型。如果将此时作为分水岭的话,在这之前就诞生的语言(Python,Java,C/C++)大多都是使用内核提供的多线程实现的并发模型,而在这之后诞生的语言(Node.JS、Go 等)大多是自己实现的并发模型。而 Python 则因为 GIL(Global Interpreter Lock)的存在,即使提供的是系统内核级的多线程也无法像 Java 一样实现并行处理,所以 Python 的并发一直都为人诟病。而同为解释型语言的 Node.JS,人家压根就没多线程这玩意,所有 IO 事件都放在一个事件循环里跑,和 GIL 河水不犯井水。这也是为什么我说 Python 仍然具有一定的历史包袱的原因。
在确定了是 Gunicorn 与 asyncio 的冲突之后,我开启了 asyncio 的 debug 模式,希望能从中获取一些有用的信息,果然,一个新的错误出现了:RuntimeError: Non-thread-safe operation invoked on an event loop other than the current one
,报错文件是:asycnio/base_event.py
中的 BaseEventLoop.call_soon()
,此方法是一个非线程安全的方法,因此在开启了 debug 的情况下,会 _check_thread()
。报错原因则是 _check_thread()
函数会检测当前线程是否是 event loop(事件循环)运行中的线程。
那么,event loop(事件循环)可能会在其他的线程调用吗?——一般情况是不会的,对于大多数程序而言,有异步 IO 处理并发就够了,不需要再使用线程了。但是,基于上面的两点考虑,我们不得不选择在使用 asyncio 的情况下再额外使用线程。
这里的线程其实是广义的概念,并非指的是系统级别的线程,gevent patch 之后的 gthread 也算在内。
在我们的项目里,event loop 是作为 module 级别的变量声明的,按照 Python 的内存管理,是存放在私有堆上的,因此从同一个 Gunicorn worker 里衍生的线程自然会共享这个变量。也就是说出现这个错误的原因在于当前的线程操作了由另一个线程(通常是主线程)创建的 event loop。
可以更具体的解释为:AsyncioExecutor.wait_until_finished()
调用了 loop.run_until_complete()
方法,此方法会对 loop 进行_check_closed()
和 _check_running()
检测。而最先开始的 this event loop is already running
报错就是 _check_running()
时抛出的。
问题定位到之后,解决就很简单了。既然多线程一定要使用,那么将 call_soon 换成线程安全的 call_soon_threadsafe
就好了。
AsyncioExecutor.wait_until_finished()
里的 loop.run_until_complete()
也需要换成 asyncio.run_coroutine_threadsafe()
,该函数可以向指定的事件循环提交 coroutine。
这两处需要修改 GraphQL 相关库的源代码的地方可以使用 monkey patch 的方式修改。
除此之外,event loop 的创建也需要改一下,不能直接通过 asyncio.new_event_loop()
来获取了,需要把它专门放在一个线程里 run_forever
(因为 call_soon_threadsafe
会使用 _check_closed
检测 loop 的状态,所以需要保证 loop 一直处于 running 状态,而 run_forever
这个操作是阻塞的,所以需要另起一个线程),然后由其他的线程向它提交:
class ThreadEventLoop:
"""
Run Event Loop in different thread.
"""
@property
def loop(self):
return self._loop
def __init__(self):
self._loop = new_event_loop()
Thread(target=self._start_background, daemon=True).start()
def _start_background(self):
set_event_loop(self.loop)
self._loop.run_forever()
从接触 Python 到现在,已经有近四年的时间了。最初觉得它语法简洁、标准库功能齐全,工作了一段时候后又觉得动态类型的有些不便以及写法过于开放难以维护,再到最近感受到协程与线程的并发之痛,不知是否是因为我的水平逐渐变高,越来越能发现 Python 的不足。对于把编程语言当做是一种工具的人来说,发现工具的不足或者不好的地方之后,只需要换一种工具就好了,只是我始终没有把编程语只当做是工具,更喜欢把它当成一门「手艺」,所以我自然希望这门「手艺」使用起来越顺心越好。
吐完槽冷静下来后,我也尝试站在 Python 开发者的角度想,如果真的完全不顾历史包袱,不管和老版本的兼容性,那似乎就相当于创建了一门新的语言了。而假如未来真的有某一版本的 Python 完全没有历史包袱,又拿什么去吸引用户做迁移呢?我想这也是最初 Python 3 发布的新特性也会向 2.6 和 2.7 版本添加的原因吧。
所以如果要问我为什么喜欢 Python 反而还大力的吐槽,我想是因为我真的希望它变得更好吧。
]]>ETS(Erlang Term Storage),是一种运行在 Erlang 虚拟机上基于内存的项式存储系统,在功能上类似于「简化版」的 Redis,但由于集成在 OTP 内部,相比 Redis 来说有两个优点:
在稍稍查阅了 ETS 的相关文档之后,我便决定将目前 API 项目中的漏斗限流模块使用 Elixir + ETS 重写(稍后会介绍一下我为什么这么做),当然只重写限流部分并不能替换现有的基于 Redis 的限流,在日后我会将整个的 API 系统都用 Elixir 重写一遍。
漏斗限流是我之前在阅读《 Redis 深度历险 》了解到的一种限流方法,相比于传统的 Nginx 请求限制(ngx_http_limit_req_module)会更加的灵活。比如:漏斗限流可以接受短期内的多次访问,只需要不超过漏斗的总容量即可,在暂停访问则会一点一点恢复容量——这才应该是比较符合常理的限流方式,毕竟某接口的访问间隔不可能总是恒定的。
漏斗限流的初始化参数包含如下四个:
其中前两项参数相同类型的漏斗都会保持一致,后两项则是每一个独立的漏斗都不一样。因此我在设计目前的限流模块时,只使用了剩余容量、上一次时间这两个参数,总容量与速率设置成了恒定不变的。
继使用 Elixir 重写完 豆瓣的爬虫 之后,总感觉有写不舒服:Elixir 擅长领域不应该是在爬虫,而应该是在服务端应用上。因此我决定继续深入研究 Elixir。只是手中暂时也没有什么新坑,于是就想着使用 Elixir 把 API 系统重构一下,虽然重构完成后我也不一定会将现有的 Golang 版本的 API 替换(毕竟 Golang 的部署实在太香了),但重构应该是会加深我的 Elixir 的理解。
那么为什么我会选择使用 ETS 呢,其实有一个很重要的原因就是 Elixir 的数据是不可变的,因此当使用另外的数据结构(比如:Map)修改或新增键值对的时候,会涉及到比较大的内存和时间开销(复制旧的 Map 数据到新的 Map 上),于是我便把目光转向了 ETS。
其实我最早的打算是使用 Heap 这个数据结构,只是虽然 Heap 在新增键值对的性能很高(O(1)),删除的时候也不错(O(lgn)),但是在更新的操作很麻烦,需要查找出旧的删除,再插入新的。更关键的是 Elixir Heap 的实现方式是配对堆,与二插堆提供的上浮下沉操作不一样,自己实现配对堆的更新操作的话不知道会踩多少坑。。
简单写了一段代码来测试 ETS 的存取性能:
defmodule TestETS do
defp set(0), do: nil
defp set(n), do: :ets.insert(:ets_test, {:key}) && set(n - 1)
defp get(0), do: nil
defp get(n), do: :ets.lookup(:ets_test, :key) && get(n - 1)
end
简直出乎我的意料,一千万次的插入和查找操作均在 1 秒内完成(硬件水平:i7-9700K,16G 3000MHZ)。
在设计好了插入和更新(更新同样可以使用 insert
函数完成)后,还有一个非常重要的功能:「删除」。如果不定期将存储在 ETS 的键值对删除的话,内存的占用就会越来越多,所以得需要实现一个定期删除的策略。我考虑的策略是如果某 IP 五分钟之内没有请求,那么就将 Key 为 IP 的键值对从内存中删除。
为此需要记录每一个 IP 访问的最后时间,且最好按照时间来严格排序(从功能上来说 Redis 的 sorted set 其实是完美契合的,只是这样一来的话又得用 Redis 了,那目前为止的工作就没有意义了),如果不能严格排序的话,用 Heap 也是一个不错的选择。只是同样会因为 Elixir 数据不可变的原因,成为性能的瓶颈。
在 Google 了好一段时间之后,终于在 Stack Overflow 上找到了我想要的方案:使用另一个类型为 ordered-set(类似于 Redis 的 sorted set)的 ETS Process 来存储 IP 访问时间顺序的数据,为了避免数据的冗余,只需要保存 Key(即 IP)和时间即可,在需要删除的时候,先获得 ordered-set 的第一个元素,然后取出时间判断这个时间是不是已经过去五分钟了,如果是的话,就把这个数据删除,同时也需要将另一个 ETS Process 里对应的键值对删除。
对于这个「在需要删除的时候」的检测,我将其设置为每一次访问都会触发。
完成了删除的功能开发之后,限流系统已经可以正常使用,但是为了提高系统的可用性,还需要将当前系统的主要进程使用 Supervisor 监管。目前功能实现分为三个模块:
:public
或将整个 Ral.CMD 注册成 GenServer,但前者有些危险:任何进程都可以写入 ETS 的数据,理想情况应该是最多允许其他进程读取数据而不允许写入;后者与用 Message Queue 传递消息相比将显著的降低(大约 50% 的)性能。三个模块中,需要被 Supervisor 监管的有 Ral.CMD:需要保证 Message Queue 消息接收方始终可用和 Ral.ETS:需要保证 ETS 的服务始终可用——至于 Ral.Cell,只包含对 ETS 的查询操作和对 Ral.CMD 的调用操作,因此无需对其使用高可用。
功能 完善后 ,简单跑了一下 benchmark,每秒处理数可以达到 27w,而之前使用 Redis 的限流模块QPS 只有 2w,这就将系统的性能瓶颈从限流模块转向了 Web Server,算是一个比较成功的轮子吧~
]]>三月下旬的离职,已经是两个月前的事了。在那之后我没有立刻开始寻求一份新工作,客观原因是考虑到我正处于疫情的中心地带——湖北;主观原因则是在调整自己的状态:想让自己完全从上一份工作状态里抽离出来——时隔两月,我总算可以比较客观、冷静地谈论起离职这件事了。
我的 leader 并非是技术出身,对因产品迭代而产生的技术问题往往视而不见,因此划定上线(提测)的日期往往非常紧迫,加班加点也只能堪堪完成功能上的开发,也就让员工不得不忽略代码质量的好坏与软件设计的正确与否。当然,由于我们组的外包人员非常多(占 80%),就算给他们更多的时间,他们也不一定有能力写出更好质量的代码或做出正确的设计,不过这并不是可以破罐子破摔的理由。
目前产品使用的 Web 框架是部门 VP 在五年前根据 Flask 定制的,把开发流程全部封装得严严实实:只需要继承一个 class 并将 URL 参数放在重写的 get、post 等方法的参数列表里,然后将 URL 加入某 YAML 文件里面,就可以开始编写业务逻辑了。你不用明白框架是如何将 class 注册为对应 URL 的视图函数,也不用明白如何从 URL 参数里取出和函数参数对应的名称的值并校验值的范围、类型,只需要写业务代码就行了,像极了流水线上的工人。
代码质量的低下以及框架本身偏向业务的定制加速了架构问题的产生:目前的系统甚至无法支撑十个用户的同时访问。原因在于框架一开始就没有考虑到并发,部署时也是使用的 FastCGI(单线程) + Nginx,Python 2.7 又无法使用 asyncio。同时,代码的规模越来越大、历史包袱愈堆愈多,而架构却无甚改进,导致目前的系统已经完全不能在本地运行起来了,这给调试带来了非常大的困难——只能把代码替换到服务器上运行(还不能打包替换,因为环境并不是你一个人在用),然后看日志。
离职的最主要原因是 leader 的管理理念与我个人的职业发展理念不符:他过分强调业务(KPI)的重要性,只在乎可量化的指标,例如:何时提测、功能是否完备;而对:架构的改进、代码的质量、用户的体验等非量化指标完全不在乎。
具体总结后的原因如下:
这些原因既是我离职的原因,也是我认为目前产品的症结所在。我曾向 leader 指出这些弊端,并给出了解决方案,但 leader 想都没想就以当前项目太紧为由驳回了。看,领导根本不在乎这些,只要保证提测时产品本身是差不多可用的就行了,代码质量?那是什么,KPI 里有写吗?但我仍然相信,这些弊病迟早会拖垮这个产品,只是我没有必要等着这一天的来临罢了。
这样的管理理念磨灭了我钻研技术的热情,也违背了我做技术的初衷(从入职后的四五个月里 Github 的 contributiosn 基本为 0 可以看出),因此我选择离开。
在下定决心辞职之前,我也想过在论坛发一个帖子,询问我这样的状态是否应该辞职,后来想明白了:为什么我的人生需要其它素不相识的人来决定,或者难道大部分人劝我忍一忍我就真的能安心干下去了吗?答案是否定的。想明白了这一点,我便直接裸辞了。
离职之后我在技术方面做了这些事:
这些内容对我的提升比我在公司八个月所获得的更多,因此我也并没有因为辞职而后悔。
离职就算不是一件光明正大的事,至少也并不需要讳莫如深:我对公司并没有什么怨念,同事之间也相处得很愉快,只是这些并不在本文的讨论范畴。毕竟离职的原因肯定不是因为公司对你太好了,而是因为公司有些毛病你无法忍受却又无力改变,因此只能选择离开。
毕业前因为想离家近,所以我选择了武汉;而接下来,我想的下一份工作能让我的价值得到比较好的展现。更进一步的讲,我想做一些更贴近用户的产品、一些用户真正需要的产品,能让用户的生活更加舒心,也可以满足我个人的成就感。
]]>从大一暑假开始,我便一直使用 Linux 作为日常所用的系统,这三年要说我用 Linux 如何顺心,那绝对是鬼话;可要说我使用得特别难受,那也不至于。因为它的确提升了我的开发体验和编程技能。它的优点我很喜欢、它的缺点我一开始就知道并能接受——这便是我能坚持使用 Linux 三年的原因。
虽然坚持使用了这么久,可我还是要说一句 Linux 作为日常使用的系统真的很不方便,而不方便就会想要去折腾,折腾来折腾去发现时间都浪费了,它还是老样子;可使用 Windows 吧,看着丑哭的字体渲染和残废的命令行工具,感觉自己写代码都没什么动力了,所以当我第一次见到 macOS 时,我在心里就默默地种草了。
种草容易拔草难啊,这一拔就拔了三年。
说起来是有些奇怪,明明我这么想用 macOS,为什么不直接买一台 Mac 呢,其实是因为我对 Windows 还有需要:闲暇时我也会打打游戏,而购置一台用于打游戏顺畅的 Mac,恐怕就只能从 (i)Mac Pro 起步了——我肯定是不会买的。于是,我选择了黑苹果。
按照 Tonymacx86 的说法,黑苹果最好选用八代或九代的 Intel CPU ➕ AMD 的显卡配合 Z390 芯片的主板。
本节的撰写时间是 2019 年 9 月,以下是当时的价格,仅供参考:
配件 | 型号 | 价格(¥) |
---|---|---|
CPU | Intel 9700KF | 2499 |
显卡 | AMD Vega 56 | 1600 |
固态 | 西部数据黑盘 512G | 614 |
散热 | ID-COOLING CHROMAFLOW 240 | 359 |
电源 | 全汗 MS600 | 599 |
主板 | 技嘉 Z390 I AORUS PRO WIFI | 1319 |
内存 | 海盗船 DDR4 3000 | 718 |
机箱 | Sunmilo T03(定制) | 939 |
合计 | 8647 |
在本文发布时候,我已经使用基于 Clover 引导的黑苹果半年时间了,最近突然手贱升级了 Catalina 导致系统挂了,所以才想干脆放弃 Clover,转向 OpenCore,于是便有了这篇文章。
⚠️:如果你是 OpenCore 纯新手,强烈建议先读读我新写的这篇 这篇博客 ,是一篇更详细的黑苹果安装教程,同时包含了一些术语的解释,否则可能无法理解本文提到的一些术语。
ACPI 的补丁会被 patched 到 OpenCore 引导的所有系统,因此某些非必须的补丁(比如 SSDT-USBX.aml)不建议在 ACPI 中加入,不然可能会造成其他系统无法启动等情况。
目前比较推崇的提取 DSDT 的方式是采用 Clover 来提取,因为这种方法能提取出原生的未曾被 patched 过的 ACPI,但这对从未安装过 Clover 引导的人有些麻烦:需要在 U 盘上创建一个 Clover 的 EFI 分区。
由于 ACPI 的补丁范围并非针对单一的操作系统,因此在使用 OpenCore 引导的系统中提取到的 DSDT 可能并非原生的,不过如果你未曾修补过你的 ACPI,也没使用过黑苹果,可以考虑使用 SSDTTime 工具来提取 DSDT(支持 Windows 和 Linux,不支持 macOS):双击 SSDTTime.bat(需要 Python 3 环境),选 4 来提取 DSDT:
#######################################################
# SSDT Time #
#######################################################
Current DSDT: None
1. FixHPET - Patch out IRQ Conflicts
2. FakeEC - OS-aware Fake EC
3. PluginType - Sets plugin-type = 1 on CPU0/PR00
4. Dump DSDT - Automatically dump the system DSDT
D. Select DSDT or origin folder
Q. Quit
Please make a selection: 4
DSDT.aml 会被提取到脚本目录的 Results 目录下。
SSDT-PLUG.aml(名字并无限制) 补丁是用于支持原生的 CPU 电源管理。使用 SSDTTime 进行生成:
运行 ./SSDTTime.command(SSDTTime.bat)
#####################################################
# SSDT Time #
#####################################################
Current DSDT: None
1. FixHPET - Patch out IRQ Conflicts
2. FakeEC - OS-aware Fake EC
3. PluginType - Sets plugin-type = 1 on CPU0/PR00
D. Select DSDT or origin folder
Q. Quit
Please make a selection:
输入 「3」后,将 DSDT.aml 拖入当前终端窗口,并 Enter :
#####################################################
# Select DSDT #
#####################################################
M. Main
Q. Quit
Please drag and drop a DSDT.aml or origin folder here:
SSDT-PLUG.aml 会自动生成在 Results 文件夹:
#####################################################
# Plugin Type #
#####################################################
Determining CPU name scheme...
- Found PR00
Determining CPU parent name scheme...
- Found _SB_
Creating SSDT-PLUG...
Compiling...
Done.
Press [enter] to return...
SSDT-PMC.aml(名字并无限制)补丁是用于开启主板原生的 NVRAM 支持。
使用方法见 xjn 这篇文章 的 3.12 节。如果你的主板和我的一样,可以直接使用 我的 SSDT-PMC.aml 文件 。
加载了 SSDT-PMC.aml 之后,「系统偏好设置-节能」里面能看到五个选项——未加载 PMC.aml 之前只有 4 项;未加载 PLUG.aml 之前只有两项。
如果没有开启(模拟或者原生)的 NVRAM,系统可能会出现以下问题:
由于 OpenCore 的配置比较追求轻量化,因此一些人在将 Clover 换成 OpenCore 的时候可能会出现一些 USB 设备失灵的问题:鼠标和键盘的指示灯都没有亮,显示没有通电。
这里可以使用 SSDT-USBX.aml 来解决这个问题,但是并不推荐,原因之前也说过了,ACPI 的补丁对 Windows 也是适用的,而 Windows 压根不需要这个 USBX 的补丁,并且这个补丁有一个 bug(feature?):它会将所有的 USB 设备识别成内置的接口,即使你插入的是 U 盘。一旦系统将 U 盘识别成内置的硬盘,就无法使用 Mac 自带的「启动转换助理」来刻录 Windwos 系统盘了(这一点下一节会详细讲)。
除了使用 ACPI 补丁之外,还可以使用 macOS 内核扩展(kext)来解决这个问题:首先使用 USBInjectAll.kext 来设置所有 USB 接口为外置 USB,确保系统能正常使用;然后打开 Hackintool 的 USB 选项卡,随后将主板的所有 USB 逐个插一遍,将非活动端口删掉,并使用连接器定制每个端口的类型。
随后点击导出图标,会导出 USBPorts.kext 和几个 ACPI 补丁到桌面,在 config.plist 里面加载 USBPorts.kext,并停掉 USBInjectAll.kext 就可以了。
戳 这里 观看双系统使用 Bootcamp 引导的视频。
在使用 Clover 引导的时候,需要将 CLOVERX64.efi 设置为默认的引导,在使用 OpenCore 的时候稍稍有点不一样,不需要且不能在 Windows 直接使用 bcdedit 命令更改 Path 为 OpenCore 自带 BOOTx64.efi 的值。
如果你电脑目前已经安装了 Windows,直接看下一节。
下载 Windows 10 ISO 镜像,插入 U 盘,打开「启动转换助理」,将三个勾全部选上,点击继续。
如果启动转换助理没有报错的话,会让你选择分给 Windows 的硬盘空间。选择完了之后会重启。
如果在下载 Windows 支持软件的时候,提示无法连接到服务器的话,依次选择左上角的 操作菜单 -> 下载 Windows 支持软件,路径选择 U 盘。此时也需手动使用磁盘工具分区出一块 Windows 所用的硬盘。
重启后,一定要继续使用 OpenCore 引导,而不要按 F12 直接进入 U 盘的引导。
在 OpenCore 引导选择界面应该会看到名为「 U 盘的名称(external)」的引导项。选择它,就会进入系统安装的界面了。
安装完 Windows 之后,系统会自动重启,重新进入引导选择界面的时候,会看到多了一个名为 Windows 的引导项(排名应该只在 U 盘名称的后面一位),选择它。
如果在选择 Windows 启动时提示「ocb-startimage-failed-already-started」,别慌,这是因为 OpenCore 不知道从哪引导 Windows,你需要手动指定一下:进入 macOS,在 config.plist 的 Misc -> BlessOverride 添加一项:「\EFI\Microsoft\Boot\bootmgfw.efi」。这时再重启,应该就可以正常进入 Windows 了。
正常进入系统之后,将 U 盘里刚刚下载的 Windows 支持软件安装(文件夹名称应该是 WindowsSupport,文件是 setup.exe,双击安装),安装后会要求重启。
重启时在 OpenCore 引导菜单时选择进入 Windows,进入系统后,右下角应该会显示 Bootcamp 的运行图标,选择重启到 macOS,应该就能正常重启到 macOS 了。
需要注意的是:如果没有开启 NVRAM,是没办法做到这一点的:Windows 使用 Bootcamp 重启到 Mac,Mac 使用启动磁盘重启到 Windows,因为无法使用 NVRAM 来指定开机时的启动项。
参考:
]]>之前对 Kubernetes(K8s)一直抱有一种很「暧昧」的态度:我想了解它的特性,并且也尝试根据别人总结的经验去接触它,但尝试接触后却总像雾里看花,好像懂了一些又好像什么都不懂。
我对新技术技术又总是充满渴望的,渴望来自于对旧技术的不满,这种不满在最近辞职后达到了顶峰:我不想通过别人总结的「二手知识」来接触 K8s 了,而是希望全面地了解 K8s 如何解决了现有软件架构的缺点从而火起来的。正好借着本文,写一下我近期关于架构方面的一些思考(第一篇架构相关的文章,可能会有些稚嫩)。
几年前,软件的架构都是单体式(Monolithic)的:即使一个 Web 系统的大多数模块在业务上并不是紧密相连的,它们仍然会运行在一个操作系统级别的进程当中(如今大把的软件仍然是这样的架构),这也就意味着单独改动某一个模块,在部署时必须重新将整个系统都打包一遍。并且由于大部分单体式的软件在架构层面缺少优化,在打包部署时简直就像是一场灾难,而作为开发人员最难受的是明明有办法阻止灾难发生但却无能为力。
比如我的前公司:先把 Web 应用打一个包(包含各种 pip 的库),再把底层依赖打一个包(Elasticsearch、PostrgeSQL、Nginx,以及各种 rpm 包),这两步下来,包的大小已经直逼 3 个 G 了,每次打包部署的流程差不多都要花费两三个小时,而且有时候还安装失败。
近几年,随着 Docker 的问世,微服务架构(Microservices)火了起来。它将单体式的软件拆分了微小且可独立运行的组件,这些组件之间是相互解耦的,因此它们可以独立地开发、部署、升级。而在微服务架构中修改了某模块之后,因为模块组件之间的运行环境是相互隔离的,也不用将系统整体打包了,只需要单独重新部署这个模块就可以了。
而随着系统的复杂性逐渐提升,微服务架构中的组件必然会越来越多,配置、管理并让组件们一直保持顺畅地运行(即使是系统在升级中)也成为了一个问题。由人工来保证组件自动化配置、监督组件的运行、故障时进行处理,显然是一件吃力不讨好的事,于是 K8s 这类容器编排工具出现了。
高可用的系统设计至少需要满足以下几点;容灾性(Fault-tolerance)、扩展性(Scalability)、分布式(Distribution)、快反应(Responsiveness)、热升级(Live Update)。
K8s 的确是满足了这几点,但这些并非是 K8s 的「专利」 ,实际上,在二十多年前爱立信所创造的 Erlang 编程语言便已经满足了这几点要求:即使 Erlang 诞生的时候并没有微服务这个概念(没错,我就是 Erlang 吹)。
甚至可以说,K8s 在相当程度上借鉴了 Erlang 的思想:Erlang 虚拟机(BEAM)上运行的所有进程(这里的进程类似 Golang 的协程,并非是操作系统级别的进程)都是相互孤立的,进程之间的通信只能通过 message box 来传递,甚至没有共享内存,这简直就是天然的 Docker 啊!同样是因为进程孤立,跨虚拟机进程通信和虚拟机内的进程通信就只是网络开销不同,也因此 Erlang 天生就支持分布式。至于容灾性,Erlang 同样自带了进程级别的 Supervisor,当进程崩溃的时候,会自动重启。
那么,既然 Erlang 设计的这么厉害,为啥现在火起来的是「借鉴」 Erlang 思想的 K8s 而不是 Erlang 本身呢?因为 Erlang 的运行模型实在太特殊了,比如:Erlang 的数据是绝对不可变的,进程之间传递数据只能复制不能传引用,才导致进程可以完全相互独立,进而保证进程崩溃的时候不会影响到其他进程。
而 K8s,则是将这一系列的思想从特定的平台抽离了出来,不必再拘泥于 Erlang 这么特殊的模型了,让其他任何语言编写的程序均可以做到高可用。而对于如今的公司来说,将服务拆分然后用 K8s 进行部署的难度显然远远低于将现有代码改成用 Erlang 或者 Elixir 实现。
在前公司,我曾尝试在架构需要调整的项目里推过 K8s,但架构评审的时候,被领导以同事都不会 K8s 为理由给驳回了,这一点倒是在我意料之中,毕竟领导担心承担决策失败的风险,以及他大概认为给开发一两周的时间熟悉 K8s 给他带来的收益比写一两周代码要少吧 :)
既然目前在工作中用不到,我就只能在自己的项目中用了。
于是决定将 DIEM-API 项目改为使用 K8s 部署,并将整个项目解耦为三个部分:
单独说明一下第三点:虽然使用了 Redis 作为缓冲,但是其中存的数据仅是 IP 的访问频次,而访问频次对于功能来说并不重要,故这里还是将缓冲层的组件定义为无状态型服务。
无状态型服务(服务层、缓冲层)包含两方面的设置:
至于有状态服务(数据持久层)要复杂一些,除了包含无状态型服务需要的两方面设置外,还需要配置 PersistentVolume 用来生成存储的资源,以及 PersistentVolumeClaim 用来请求 PersistentVolume 的资源。
有关具体的配置文件信息,请查阅 项目内 的几个 Yaml 文件。
我在读完《Kubernetes in action》的前十章之后才对 K8s 有了一个比较清晰的认识,也总算明白了之前对着别人的教程创建了 Pods,然后怎么删都删不掉的原因:因为创建的是 ReplicationController,就算删除关联的 Pods 也会被重新创建。
也知道了容器是基于 Linux Kernel 提供的 namespace 和 cgroups 技术来实现的,因此在容器内运行程序几乎没有额外的开销。
本文并没有解释 Pods、ReplicationSet、Services、Deployment 等 K8s 的基础概念,一方面是我担心因为了解不够深入而难以解释清楚(毕竟我也就接触了两三周时间),更重要的是,希望读者能通过阅读书籍和官方文档来获取一手的知识。
]]>最近忽然有些念旧,想着距离写完 我的学生时代(高中篇) 也有大半年时间了,趁着前几天的创作欲还未散去,本文就好好聊聊我的大学时光吧(再不聊我都怕忘记了)。
温馨提示:本文会有些长,会尽量按照发生时间的先后叙述,不过我无法保证本文的叙述具有 100% 的准确性,毕竟人的的记忆本来就不甚可靠。
现在回想起来,应该是从高二开始吧,便对计算机(的某一分支)有兴趣了,当时沉迷于安卓系统的优化,比如:刷各种 XDA 的魔改内核、ROM 等,不过受限于条件,只是停留在使用别人修改好的软件包层次,并没有机会自己手动修改。
应该还是在高二吧,上微机课的时候,老师会控制全班同学的电脑,不让我们自己玩,但唯独只有一个同学不会被控制。有一堂课(好像是在讲顺序分支循环那些,记不太清了),老师向我们解释到为什么不控制那个同学:「因为我上课讲的内容他都懂了,所以我让他自己玩」。当时就觉得这个同学好厉害啊,可能这时开始,一颗种子便埋在心底了。
填志愿时,这颗种子经过了一年多的萌芽,已经慢慢开始生长了。于是乎,即使所有的亲戚朋友都反对,我仍是选择了计算机专业。
九月入学,校园各大社团(包括学生会)都开启了招新活动,不过我一个都没参加,大概是因为性格散漫惯了吧,不想受到什么约束。
大一上学期学了第一门与计算机相关的课:HTML 与 CSS3 基础,由于刚入学,课听得都很认真,在期末时交的静态网页作业拿到了全班最高分(直到之后很长一段时间,我都在因为这个事纠结我是从事前端还是后端),高等数学也考了满分,一时间有些膨胀了。
这人嘛,膨胀了之后,会有一段时间觉得自己很牛逼,我在这段时间里(大概从大一下学期开始吧),开始上课不听讲了,甚至还伙同两三个同学逃了高数课跑去看校园十佳歌手,晚自习也不怎么去上了,学习态度的散漫,导致我这学期的成绩比上学期退步了许多(当然这是后话了)。
也是在这段时间里,学院开设了第二门与计算机有关的课——C 语言,同时学院的 ACS 协会(别问我全称,我也不记得了~是一个与 ACM 竞赛相关的组织)开始招新了,招新的学长在台上说了一大堆加入协会的好处:找工作时各种大公司的内推、参加 ACM 竞赛可以获得加分(保研时有用)等等,虽然这些好处没怎么打动我,不过仍是稀里糊涂的就加入了协会(可能看着身边的人都加入了吧),开始了短暂的刷题生涯。在杭电上刷了五六十道题之后,我发现我对算法没什么兴趣,期间也曾尝试转到安卓组,发现还是没什么兴趣,于是便退了协会。
在五月份,某大牛同学(初中开始编程)组了个队参加「互联网 +」创业创新大赛,我也加入了。目标是做一个考研的交友平台,可以根据个人信息推荐相似的研友,他负责推荐算法,我负责用爬虫采集数据,并清洗。当时第一次接触网络爬虫,也没有经验,只能在网上随便找个爬虫改改凑活着用,虽然最后这个项目只拿了校级的奖项,但是在这个过程中我发现我喜欢上爬虫了,于是我开始觉得要自学编程了。
所谓「工欲善其事,必先利其器」,在学习之前,首先得要把环境搭好,上网看了一圈之后,决定装一个 Linux 系统(与 Windows 共存)专门用于编程,当时我对操作系统完全是一窍不通,导致在装完 Linux 系统之后重启直接回到了 Windows,百度后尝试了许多方法(比如:把 UEFI 模式改成 Legacy啊、用什么 Boot 编辑器啊),但都没什么卵用,逼得我甚至冒出了将 Windows 整个格式化掉的想法。
最后还是在暑假期间解决了,还在简书写了一篇教程(一年之后居然有两万多浏览量),成功安装好双系统之后,我又开始像高中折腾安卓一样开始折腾 Linux 发行版的各种美化(真是死性不改😅):Terminal 的美化,状态栏、主题图标的美化等等,原定好的学习编程计划也搁置了。如果时光可以倒流的话我一定会告诉当时的自己:「不要再花时间在这些没卵用的美化上了!快点学习吧!」,不过当时的我也肯定听不进去,毕竟颜值才是第一生产力~
对大二上学期这段时间的记忆比较模糊,应该也并没有发生什么值得记录的大事吧:唯独对数据结构这门课印象很深,老师是我们院的副院长,可以看出他很懂数据结构,但是讲课不是特别好,不是很能吸引人,这门课我们全班的战绩都挺惨的,好像没有人超过 80 分。
在大二的寒假期间,似乎终于想起了自己半年前好像就说要学习编程了,于是开始比较系统地学习 Python 了(大概是因为之前接触过爬虫,我对 Python 印象很好),在知乎上听说 MIT 6.0001 这门课用于入门 Python 不错,于是便开始看跟着视频一起学习,授课老师(MIT 计算机学院院长)讲得非常好,而我遇到理解不了的就用谷歌查,花了一个多月时间才差不多把这门课的内容学完。
不过这一切都是值得的:本课程在我对计算机的理解还是一张白纸的时候并没有急着在上面写写画画,而是耐心地教了我怎么才画才算是正确的,换句话来说,本课会花很多的时间来培养你的计算思维,而一旦养成了计算思维,编程便不再是什么难事了。
即使是现在,我依然会向所有想学习计算机的人推荐此课入门。
到了大二下,印象比较深的课就是操作系统了,先前提到过,我在大一暑假时便开始接触 Linux,老师在得知了这个消息之后,某堂课上走到我身边悄悄对我说:给你两周时间,两周后的这节课,有半节课时间给你表演👿,题材不限,风格不限,和 Linux 相关就行。我花了两天时间好好准备了一下,还抄了一个 PPT,至于结果嘛,emmmm 反正挺尴尬了,因为下面的学生似乎都没在听,从这一点看,当老师还是考验人的~
四月份(也有可能是五月份),学院组织了一个院级的算法比赛,闲着没事就参加了一下,看看许久没写算法是不是退化了许多(题目我是一道都不记得了),最后拿了一个二等奖,奖品是一个 32G 的 U 盘,感觉还不错~(只是我也并没有因此对算法提起多大兴趣。。
五月初,一朋友说她(好吧,其实是当时的女朋友)正在尝试用 Hexo 搭博客,我搜了一下,之后,便有了这个博客~
提问:搭好博客后最重要的一件事是什么呢?
回答:当然还是美化啊~
当时基本上把所有好看的 Hexo 主题都尝试了一遍(NexT、Yilia),最终还是决定了使用 NexT,选定好博客主题后的那两个月内非常高产地写了差不多十篇文章,应该是新鲜感作祟吧,很多文章都没什么营养,没多久便把大部分都删掉了。
得益于博客的搭建,我的知识面被扩充了不少,也了解到了 SICP(Structure and Interpretation of Computer Programs)这本神作,在暑假前,我在图书馆借阅了这本书,花了一整个暑假的时间,却仍只读完了半本(读完了前两章,做完了 习题 ),该怎么形容我那两个月的感受呢,感觉自己的编程世界观受到了冲击(也是在阅读第一章后,我彻底明白了递归与迭代的区别),感觉自己的脑回路好像都被重构了一样。
一般来说,当我们在编写软件的时候(尤其是大型的软件),核心便是控制复杂度,而本书的核心——抽象,便是控制复杂度重要的手段。我在工作后接手了祖传的项目代码,才理解控制复杂度的重要性(当然这也是后话了)。
在看完了 MIT 6.0001 后,如果你想成为一名更好的程序员,并打算继续深入,那么看看 SICP 这本书吧,不会错的。
大三时,逃课开始变成了家常便饭(尤其是离散数学,但是我一学期没怎么去上过,期末反而还考了满分),不过数据库这门课我倒是每次都去上了(只是差不多每堂课都会迟到😅),因为授课老师就是上学期的操作系统的老师,对她印象还蛮好的(不过我现在还很奇怪的一点是为什么老师要教 SQL Server 而不是 MySQL 或者 PostgreSQL)。数据库课程分为理论课和上机课,由于上机课和理论课不在同一所教学楼,于是在理论课下课时便会和几个同学绕一大圈,逛逛校园,再慢慢地抵达上机课的教室,好在老师也不会说些什么。
当时还有一门课是软件工程,遗憾的是软件工程的重要性是我在工作之后才明白,因此当时这门课我并没有去几次,而且老师一开始就说了这门课的考核标准:交一个系统上来就行了。于是断断续续花了两个月时间,读完了狗书(Flask Web开发:基于 Python 的 Web 应用开发实战),照着书上面的代码自己敲了一遍,也算清楚了 Python 的开发流程。
除了了解 Python 开发之外,当时还针对 NexT 主题做了很多定制化操作,也借此学习了一下 JavaScript 中是如何使用 Template 的。
大三的寒假正巧碰到搬瓦工上架了 19.9 刀的 CN2 套餐,于是毫不犹豫就下手了,从此我拥有了人生第一台服务器~有了服务器之后,最先做的事就是把博客挪到了服务器上,顺便折腾了一下 CI/CD;其次 Flask + SQLite 写的一言 API 也总是崩溃,于是就花了两天时间用 Golang 重写了一遍 ,数据库也换成了 MySQL,也部署到了服务器上(虽然服务器只有区区 512M 的内存,但是Go 这货还是真的省内存,满载时也不过 10M 的占用。
到了大三下学期,更加的放飞自我了,专业课程只剩下编译原理了,不过真的好难啊(而且我当时还是已经通过 Lisp 了解了 AST),为了学好编译原理我还兴冲冲的去图书馆借了虎书准备好好研究一下,结果拿回寝室就吃灰了。。
四月份,和高中那个微机课不被监控的同学交流时,得知他拿到了腾讯的暑期实习 Offer,给我羡慕得不要不要的。谈到职业生涯时,我也挺慌的,因为我甚至还没有想好是从事前端开发还是后端开发(因为当时正在学习 React,写了个 项目练手 ,反响还挺好),更别说是具体的岗位了。
大一的暑假是玩过去的,没必要担心,毕竟才第一年;大二的暑假是学过去的,一直在学 Lisp,感受着 Lisp 给我脑子带来的冲击。而大三暑假,则是最迷茫的一个暑假,一方面是在纠结自己的职业规划:前端还是后端?后端的话,我比较熟悉的语言只有 Python,但 Python 的岗位实在太少(且我不想转 Java);前端的话我又没什么能拿得出手的项目。另一方面则是有些陷入了 自我怀疑 中:两年来,我将学习的重心放在自己感兴趣的方面这一决定是否正确,又或者我是不是应该好好完成学校的课程。
大四上发生了一件特别有意思的事,一同学也在找工作,他说他暑假学了两个月 Spring,但现在对 Spring 基本还是一无所知,于是乎我和另一朋友「苦口婆心」劝了他一个晚上,终于劝他转 PHP 了,然后过了两个星期,他就找到了 PHP 的工作(没有任何黑 PHP 的意思😆)。
随后,便是去杭州实习了,去杭的两个月可以看看 杭州见闻 这篇文章,这里不赘述了。从杭州回来之后,在学校呆了一个月,那一个月是我大学四年过得最放松的一个月:逃课也不用战战兢兢地担心被老师发现(当时还有一门 Python 的选修课,因为学分还没修满)、实习证明也顺利弄到手,更重要的是,这几个月的实习,驱散了我几个月前的自我怀疑,也明确了自己未来的职业规划。
还有一个小插曲就是这门 Python 课了,由于我提前和老师打过招呼说我要去杭州实习,所以平时课去不了,老师说没关系,最后用 Python 写个东西交上来就行了。于是在最后一堂课,我把大三暑假复习计算机网络时写的一个 静态 HTTP 服务器 交了上去,并阐述了分别使用 Asyncio 和 Thread 实现的并发,结果老师说这个程序太复杂了,不相信这个程序是我写的(大概是觉得他一个教 Python 的都写不出来吧),费了好大力气甚至把 Github 页面都亮了出来他才相信。。
大四寒假,主要是搞论文的事,选题时恰巧写业务写得有点烦(现在的工作对比来看,能安稳写业务真的很难得了),于是选了个文本情感分类方面的课题,当时还在 Coursera 上学习机器学习的课程,上到第五周的时候,往后翻了翻课表,发现并没有介绍文本分类之类的算法😒。
于是便弃了这门课,改为自己找论文看着谷歌学,顺便学习了一下 Latex~顺便说一句,Latex 太适合我这种强迫症了~
毕业前夕还发生了一件事:我发现学校的校园网络不登录居然也是有 IPv6 的地址的,且具有互联网的访问权限。嘿嘿,略加思考,便想到了一个可以 薅学校羊毛 的方法,原理很简单:需要保证有一台支持 IPv6 和 IPv4 的服务器就可以了,用这台服务器在本机和互联网之间中转一下,那么即使即使只有 IPv6 的互联网权限,也可访问 IPv4 的网站了。
毕业时,我罕见地有些不舍:不知是不舍这所学校、还是不舍同过窗的同学、抑或是不舍可能是最后一段学生时光。但应该还是不后悔的吧,虽然投入开源项目我付出了很多时间,但的确也扩充了我的知识和眼界,不过也不是说程序员就都要做开源,只是我个人确实通过做开源获得了很多提升:如果你能坚持在两年内,每天平均编程四个小时,我想你也会获得很多提升,而开源只是我坚持的动力罢了。
大学四年里,我放弃了一些欲望,只想活得轻松一点。
这欲望是指对保研的渴望(努力上课,好好听讲,参加各种竞赛)、对组织能力的锻炼(加入各种学生会、班委)等等,转而将精力投向了我所感兴趣的方面。当然我意思也不是说我现在多么成功,只是想说明,当你决定要跟从内心做自己喜欢的事时,接下来只需要坚持下去就好了。
本系列正式完结(不排除有续作的可能,嘿嘿)~
]]>趁着最近肺炎,在家修养生息:看看剧,看看电影,偶尔也会学习一下。最近在学习 Elixir 这门语言,由于其实在太小众,只能抱着一本英文书啃(第一次看英文书籍,也没想象中那么难,就是看得比较慢),在看到 Elixir 实现累加器(就是《黑客与画家》里介绍的累加器)的时候,突发兴趣研究了一下,便有了本文。
Paul Graham 在《黑客与画家》中是这么描述累加器(函数)的:
我们需要写一个函数,它能够生成累加器,即这个函数接受一个参数 n,然后返回另一个函数,后者接受参数 i,然后返回 n 增加(increment)了 i 之后的值。「这里说的是增加,而不是 n 和 i 的相加(plus)。累加器就是应该完成 n 的累加。」
比如说,这个函数是 foo,那么它应该具备以下行为:
acc = foo(2)
acc(6) // result = 8
acc(7) // result = 15
想了一下,如果某语言可以比较舒服的实现这个函数,则该语言需要具备两个特性:
Common Lisp 的实现:
(defun foo(n)
(lambda (i) (incf n i)))
虽然根据图灵等价来说,所有的语言在功能上都是相同的,但这没有意义:因为题目要求的并不只是实现一个(累加器)功能,还对具体的实现有额外的要求(必须使用函数来实现),因此你没有办法使用 Java 来实现(因为 Java 无法把函数作为另一个函数的返回值)。
举一个更一般的例子,比如有个问题:要求计算 167564386575724718662 的平方,但不得自行编写处理整数溢出的函数。在这个问题中:计算一个数的平方是功能,而不得自行编写处理整数溢出函数则是额外的要求。
如果只是针对功能来说,确实所有图灵完备的语言都可以实现,只需要处理一下溢出的整数就好,但这类问题狡猾在对具体的实现还有额外的要求,因此,整数会溢出的语言便无法解决这个问题。
在书中,Paul Graham 给出了 Python 的一种(累加器)实现:
def foo(n):
s = [n]
def bar(i):
s[0] += i
return s[0]
return bar
之所以需要这么写,而不能直接使用 lambda 返回的原因有两点:
有关第一点,《流畅的 Python》一书中提到,因为 Guido 不想让 Python 变得太函数化,因此极大地限制了 lambda 的使用。
而第二点,则是因为 Python 不支持对词法变量「重新赋值」的缘故:
def foo(n):
def bar(i):
n = n + i
return n
return bar
在 foo 内部的 bar 函数中,n = n + i
语句会在当前词法作用域新建一个变量 n(当前作用域不存在 n 而且有「=」符号),因此这个写法是错误的,运行会得到 UnboundLocalError 的错误,除非在 bar 函数中显式声明:nonlocal n
,表示 n 使用上一层词法作用域的值。
那么为什么书中的实现没有声明 nonlocal
也可以呢?注意我刚刚提到的,Python 虽然不支持对词法变量的「重新赋值」,但是支持对已存在的词法变量「修改」:对于 s 来说,s[0] += i
这个操作,并没有把 s 重新赋值,而只是把 s 的其中一个元素修改了,换句话说 s 本身的地址是没有变的:
>>> a = [0]
>>> id(a)
4438808928
>>> a[0] = 1
>>> id(a)
4438808928
同理,其他可变的数据类型(class、dict)也都可以实现这个功能:
以下是使用 dict + lambda 实现的:
def foo(n):
d = dict(r=n)
return lambda i: d.update(r=i+d['r']) or d['r']
看起来虽然简洁了不少,但我觉得这远不如使用 nonlocal
来得优雅,而且这种方式也降低了可读性。
我学习 Elixir 也有二十来天了(从 这一次提交 开始),虽然早就预见数据不可变会给我的编程习惯带来一定影响,但是没想到影响会这么大。
还是拿累加器举例子。更近一步地说,要实现这个累加器(函数),只需要保证闭包内部的词法作用域能修改外部作用域的变量就可以了。
iex(1)> outside_var = 5
5
iex(2)> lambda = fn -> IO.puts(outside_var) end
iex(3)> lambda.()
5
iex(4)> outside_var = 6
iex(5)> lambda.()
5
但由于 Elixir 的数据是不可变的,定义 lambda 时,内部词法作用域保存的 outside_var 的引用地址的值是 5,定义完毕后,lambda 内部引用地址的值便无法被修改了。
这里虽然对 outside_var 绑定了两次值(5 和 6),但第二次绑定并不是修改内存地址的值,而是重新申请一块内存赋值为 6,再将其绑定给 outside_var。
也就是说,虽然 Elixir 对词法变量完全支持(不会像 Python 一样报错):
iex(6)> foo = fn n ->
fn i ->
n = n + i
end
end
warning: variable "n" is unused
iex(7)> f = foo.(7)
iex(8)> f.(8)
15 # 首次调用,符合预期
iex(9)> f.(8)
15 # 这里我们期待值为 23
但这种写法得到了不符合我们预期的行为,它同样会在内部匿名函数的词法作用域中添加 n 变量(由于 n 没有使用,所以解释器报 warning 了),并不会对外部作用域的 n 变量进行修改。且 Elixir 并没有 Python 那样的 trick(使用 list、dict 等可变类型),毕竟 Elixir 里的数据是不可变的(无论是什么数据类型)。
那么问题来了,Elixir 该怎么修改并保存变量呢,更一般地说,Elixir 如何保存进程的状态呢?
由于 Elixir 里的进程(这里的进程,有别于操作系统的进程,更类似于 Go 或者 Python 里的「协程」)都是完全孤立的,进程间无法通过共享内存来通信,因此 Elixir 采用消息传递(message passing)的方式进行进程之间的通信,也是通过它,我们可以构建出保存状态的进程。
比如这里的累加器函数,在每次调用累加器时,需要做两件事:
foo = fn n -> send(self(), n)
fn i -> result =
receive do
num -> num
end
send(self(), result + i)
end
end
值得注意的是,虽然 receive 语句是阻塞的,但是能保证每次开始调用累加器时,消息信箱中总是有数据的(来自于上一次累加发送的消息),因此进程并不会阻塞住。
iex(1)> f = foo.(2)
iex(2)> f.(2)
4
iex(3)> f.(2)
6
不过,由于这种方法涉及进程之间的通信,因此耗费的时间远比原生支持修改外部作用域变量的语言要多(大约是 Python 的实现方式的 10 倍左右)。
其实有些纠结是否应该发这类文章,主要是纠结其内容是否有价值,后来想了想,当然是有价值的,价值的名字叫做「独立思考」。
参考:
]]>翻了翻存档,我这个之前平均一月发一篇博客的人居然有半年没有新文章产出了,在这里给挂念着这个博客的各位小伙伴说声抱歉(前几天逛 V2 的时候还被催更了)。工作以后确实经历了许多事,也成长了不少,本文作为 2019 的告别文,就稍微记录这一年发生的事吧。
WARNING:本文负面情绪有些重,有些流水账,有些吐槽向。
在毕业论文选题时,我特地挑了一个完全陌生的领域——机器学习,本想着能跟着导师学点新东西,结果导师太不靠谱——压根不管事(啥资料什么的都不给,开题报告的要求都是研究生代写),只好自己摸索着学了。
花了差不多俩月时间(中间包含着春节),捣鼓了一篇万余字的论文出来(论文是用 Latex 写的 ,为了让排版符合学校规定,调整了好久,如果不是因为这点,应该一个月就差不多了),满心欢喜地发给老师,结果老师压根就没看:「明天打印一份出来我再看,电子版与打印版的格式会不一样」,我盯着屏幕上的 PDF 文件陷入了沉思,难道 PDF 电子版打印出来会不一样嘛?
第二天把论文带去了教室,老师把其他人的论文都看完了才看我的,所幸给出了比较正面的评价:「从论文看,你这两个月确实是做了点研究的」,心里顿时长舒了一口气。随后两周,果然比较顺利地通过了答辩,老师也把我的论文推上了优秀毕业论文。
回首这一年看来,捣鼓机器学习的那两个月应该是我这一年最快乐的时光了(我对学习新知识还是有挺大的渴望)。
七月份,我正式入职了某家公司(公司名就不透露了,因为接下来要说它坏话 :)
一开始听说公司要求去北京培训两个星期(我 Base 在武汉),我就不太乐意,把 Base 在全国各地的人都拉去北京培训,培训的目的肯定不会是培训工作技能,极大可能是「洗脑」——利用从众心理,当周围的人都认同一件事的时候,你很大可能也会被同化。
培训第一天,给全体人员了十二个组,每个组大约十来人,选定了组长、组长秘书,从此之后的每天早上都会进行组内分享,分享自己认为昨天成长了什么(无非是围绕着奋斗、客户、产品的扯淡),虚伪到了极点。每人都分享完毕后,组内投票选出成长达人,在全体人的面前分享「获奖感言」。
我并非不喜分享,只是这种明明言之无物、却又不得不说的分享,我很反感。
两周后,漫长培训如期结束。到岗的第一天,直属 Leader 就把我和另一个也是校招进来的人叫到办公室,客套了几句,随后说:「你们是校招生,前三个月对公司的产出肯定是比较少的,所以我要求你们前三个月每天都不得早于九点半下班」。
看着另一个校招生没有任何犹豫就点了头,无奈之下,我也只好妥协,答应的同时,心理仍然侥幸地想着,没关系,三个月嘛,很快就过去了。
只是我没想到,妥协一旦有了第一次,便会成了无数次。
九月初,我开始正式开发一个新的定制项目,没有需求、没有产品、没有原型、没有测试,只有一个 PPT,让我们对着 PPT 把产品做出来。九月下旬,要求我们出差上海,驻场开发,为期一个月。
去上海的一个月里深刻体会到了什么叫「朝令夕改」,每隔两天客户开会都会提出新的需求,没有产品,所以只能让开发去和客户对接需求,呵,我真想骂人。‘
在上海的最后一个星期,我们组有一个北京的校招生离职了,对外的理由是家里出了一些事。另一个在武汉的校招生说 Leader 已经宣布正式进入 996 了(还从其他组抽调了人),目的是为了完成部门产品的一年一次的迭代开发。
操他妈的」,告诉我消息的那个校招生在和我通电话的时候骂了出来,「我 es 压根就没学过,就直接让我着手开发」、「你能在上海多呆一会就呆一会,回来就苦逼多了」……我心里想着,没关系啊,已经妥协两次了,再妥协一次又有什么关系呢?
毕业前,我一直在思索寻求一份什么样的工作(自然是计算机行业,这里是指更加具体的方面),恰逢 996.icu 项目最火的时候,我告诉自己,既然不知道想找一份什么样的工作,那就反其道而行之——不找一份 996 的工作。大约是造化弄人吧,公司并没有履行约定好的 1075 工作制(没记错的话,入职半年只有两次准点下班),996 还是降临到了我的身上。
从上海出差回来的第三天,那天我八点半下的班,八点四十 Leader 打了个电话给我,问我为什么这么早下班,事做完了没有,我说做完了。
「这么早就做完了?看来是工作量不饱和啊,明天给你多加一点」。
「操他妈的」,我也爆粗口了,只不过是在心里骂的。
第二天一去公司,询问了一下提前走的三个人(其中有两个人都是其他组调来的),都同样被打电话询问了。有趣的是,Leader 昨天并不在武汉,而是在北京出差。
两天之后,被打电话的一个人直接提了离职(他是其他组借来的,已经在公司呆了一年半了),裸辞,只不过部门大领导没有同意。
经历了两个月的 996,我深深明白了 996 真正摧残人的地方不是身体,而是心灵。它会慢慢磨损掉一个人心气,当你知道即使在规定时间做完了事之后,你也无法到点下班时,效率对于你来说便无所谓了。
跟着领导混吧,他喜欢看着我加班,那就加吧,只要听领导的,技术什么的反正领导也不在乎,能做出来就行,管他用什么方式呢。
我不想,真的不想自己变成这样的人。
周一的上午,来到公司后,给自己冲了一杯奶茶,慢慢地饮下去,享受着胃里因为温暖传来的舒适感,惬意极了。
慢慢走到了 Leader 的附近,「XX,有个事我还是决定应该提前告诉你一下,我准备离职了」,说这句话的时候我一直在仔细观察他的面部表情。可惜似乎没看到什么变化,「你稍等一会,我一会去找你」。
两分钟后,他把我叫到了一个会议室。
「坐」,他指了指椅子,「啥情况啊,为啥突然想到要离职」。
「感觉刚毕业还是应该学点技术,业务写多了实在没什么意思」,我说得很委婉,毕竟我不知道该怎么和他说和我合作的一个同事连 URI 是什么都不知道,没准说出来之后发现他也不知道 URI 是什么(还是有这个可能性的,毕竟他认为把函数调用的结果赋值给一个变量之后在内存中的占用会多一倍)。
「那你找好下家了吗?」,他随口问到。
「没有,裸辞」,我没打算说谎。
「那如果可以给你换组,你可以留下来吗?」,他问道。
我愣住了,我没想到他会这么说。
「你想做什么方面的呢,安全?机器学习?渗透?这些我都可以帮你安排」,谈话的天平似乎慢慢向他的方向倾斜了。
……一场非常「诡异」的谈话开始了。谈话的双方分别是一个刚入社会不久的本科毕业生、另一方是整个部门最赚钱的组的 Leader。
他给出了留我的条件:我本周为当前的项目的内存稳定性进行优化,今后的工作他会把我平时工作的业务量控制在 30% 以内;并且下周开始,我负责另一个即将产品化的项目进行架构调整,我对该项目的架构、技术选型有着完全的控制权。那是一个与 Hadoop、Hive、HBase、Spark 有关的大数据项目,仔细想了想,似乎我的技能树里就差了大数据这一块了。
最终,我选择留了下来。不是因为别的,而是因为一月份上三周班能拿一个月的工资~
996 的人,还有生活吗?这是我一直在思考的问题,每天下班之后随便看看视频就已经十一点多,洗个澡之后躺在床上就十二点了,甚至每天八小时睡眠都无法保证。
这便是我工作之后博客便停更的最根本原因,也是我今年一年观影量锐减的主要原因。好在上半年还有不少存货,还是简单总结一下吧。
读书方面,今年一共读了 11 本书,推荐两本书:《乌合之众:大众心理研究》和《蜘蛛男孩》,前者讲的是心理学,有些观点有些偏激,不可全信;后者是一部奇幻小说,很温暖、很跳跃、很好看。
电影方面,今年共看了 62 部电影,似乎没有明显高出其他电影一筹的(像去年的我不是药神),觉得还不错的有:寄生兽、哪吒、升级(Upgrade)、海王、调音师。
剧集方面,今年看过的都不错,长剧强烈推荐:六龙飞天,是一部讲高丽如何被推翻的剧,虽然有 50 集,每集 1 个小时,但真的非常吸引人,一看就上瘾的那种;短剧推荐:致命女人、半泽直树、花甲男孩转大人。以及高中非常喜欢的一本小说——《庆余年》,终于改编成电视剧了,一开始担心会毁原著,但是却出奇精彩!
动漫方面,似乎今年只看过五部,最好看的定然是灵能百分百 II:温情、热血、打斗、治愈,太完美了。
写本文时,总觉得心中有股气,不写出来觉得委屈得难受,写完之后静下心思考了一下,我应当是把对 Leader 的厌恶都加在了公司的身上了,是有些不妥;可又一想,一个敢把公共会议室当个人办公室的人在这个公司居然混的这么好,这大概能说明公司本身管理就出了问题吧。
Anyway,2019,总归是结束了。至于 2020 嘛,简单一些,开心就好~
]]>最近和朋友聊天时,他说发现了一个云计算服务商:「 ZEIT 」,可以为程序免费提供托管,想问问我有没有什么好的想法。我查看了一下发现这货在全球提供的线路还真不少(亚洲大部分地区都有),用作代理软件想必体验会比较不错。
于是我首先想到搭建 ShadowSocks、V2Ray 这类代理软件,不过很可惜 ZEIT 在部署上有限制:不支持 WebSocket(当然更不支持 SOCKS5 了),且虽然线路的延迟比较低,但在带宽上却有限制,于是我暂时放弃了正向代理,把注意打到了反向代理的头上。
反向代理相比正向代理的限制不少,最大限制的在于一次只能代理一个网站。想了想还是决定代理一个搜索引擎—— Startpage ,用于手机等不那么方便翻墙的设备搜索。至于为什么选择它嘛,有两点原因:
至于为什么不选择 Google,答案也很简单,Google 会检测计算机的异常流量,一旦检测到异常,则必须通过「reCAPTCHA」检测才能继续使用。尤其是在使用反代的时候,出现检测的机率非常高,这对想迅速得到搜索引擎反馈的用户来说,无疑是一种灾难。
时隔一年,我真香了,在把 本站 的代理切换成 Google 一段时间之后访问时也并没有出现「reCAPTCHA」检测,因此本站会继续保持代理 Google。
⚠️:目前,我的 ZEIT 账户因为搭建的代理被太多人次访问(每个月差不多使用了 100g 的流量),已经遭到官方永久性冻结账户了。所以大家还是尽量自己搭建自己使用吧。
反向代理的核心思路或者说原理其实很简单:中转服务器把来自客户端的请求发送给服务端,再将服务端的应答返还给客户端。单纯地实现这一功能也非常简单,使用 Golang,你甚至不需要借助第三方库便可搭建一个很简单的反向代理。
func Proxy(w http.ResponseWriter, r *http.Request) {
reverseURL, err := url.Parse(protocal + host)
proxy := httputil.NewSingleHostReverseProxy(reverseURL)
r.Host = host
proxy.ServeHTTP(w, r)
}
这几行代码就足以搞定 Google 的反向代理了,也能让你愉快地使用 DuckDuckGo 了,然而却无法使用 Startpage。
是的,可能因为「Startpage」本身就相当于对 Google 的反代,所以它对反向代理极其不友好,具体见下。
当我运行刚刚的程序,打开浏览器并输入地址满心欢喜地看着 Startpage 的首页一点点出现时,我几乎以为已经成功了,可是当我输入关键字搜索时,打开 Firefox 的调试工具却发现它的请求资源都是从「Startpage」域名返回的。
是的,Startpage 很「聪明地」将静态文件的引用使用了绝对路径,而不是大多数网站都使用的相对路径,这意味着我还需要修改 Response Body,将原域名都替换成自己的域名,这一点倒是没什么难度(当时我是这么想的),正好 Golang 也提供了 ModifyResponse 用于 Response 的修改。
可当我代码写好了然后发现运行结果仍和原来一样时,我开始觉得有点难办了。
造成这个问题的原因其实很明显,但我却花了半天的时间才找到:Startpage 在网页传输时启用了 GZIP 的压缩编码,因此直接替换 www.startpage.com
是行不通的,需要将 Response Body 解码之后再替换。
完成解码替换之后,终于如愿看到请求资源都是从本域名返回的,我又一次以为自己要成功了。可是当我点击搜索结果的下一页时,网页却久久处于加载之中,我开始觉得或许不应该选择代理「Startpage」了。
打开 Firefox 的调试工具之后,发现它居然把第二页的域名给换成了与首页完全不同的二级域名——「www」会变成类似「s2-us8」、「s3-us6」这的前缀,而具体变成什么样是由首次搜索的时候随机返回的。
这个问题其实应该是无解的——除非你把所有出现的二级域名都进行代理(类似 YouTube 其实也是把每个视频的源文件放在不同域名的服务器上),不过很可惜,Startpage 只是单纯把域名换了,实测之后直接输入域名前缀也是可以正常使用的,因此只需要把代理的 URL 从 www.startpage.com
换成 s3-us6.startpage.com
就可以加载后几页的内容了。
在修复了以上三个 Bug 之后,搜索功能已经很完善了,不过还有一个小问题,就是用户的偏好设置无法保存,比如自定义背景、偏好语言等,点击保存按钮会 301 重定向至 www.startpage.com
页面,是的,这又是「Startpage」为反向代理设置的一道关卡。(没办法,自己选的路,哭着也要走完 : )
在修改完 301 的重定向地址后,点击保存发现虽然不会跳转到「Startpage」域名了,但是设置依然没有保存下来,一番 Debug 后发现是 Cookie 的问题,Cookie 设置的 Domain 不是同样使用的是绝对路径,「Startpage」为了不让别人反代真是煞费苦心呐!
在直接把 Cookie 的 Domian 字段干掉之后,使用起来终于和原网站无异了。
虽然一开始只想着反代一个搜索引擎,但中途想着还是把普适性做得更广一些,让它能代理任意的网站,因此考虑的方面也比较多,但最终的成就感还是挺爽的,自己也对 HTTP 各字段的理解更深刻了。
目前这个反向代理工具支持文本替换、重定向替换、Cookie 替换等,源码 已开源 在 GitHub,部署在 ZEIT 上,如果你想部署在自己的服务器上,建议使用 master 分支。
参考:
]]>在临毕业的这段时间,生活似乎短暂地失去了目标,并不是丧,我也没有认为这是一件坏事——至少我可以有更多的时间思考并记录我的想法,当然更多时间我在好好地、不带任何负担地放松自己。
直到前几天 WakaTime 发邮件过来说已经两周时间没收到我的 Code Activity 了,问是不是插件出了什么问题:「Please reinstall the plugin to continue using the WakaTime dashboard」。其实并不是插件出了什么问题,而是我真的两周没有编程了(笑。
休养够了,也终于意识到应该做或写点什么了:于是我开始了本文的创作。本文决定聊聊我的高中以及大学生活(其实我一直都有些畏惧谈论这个话题,原因之后会提到)。
从进初中开始,快班里的我们便只有一个目标——进入省重点高中,为学校赚来更好的名誉(当然也为自己的前途)。中考结束,很遗憾,我离省重点的分数线还有几分的差距,当时班上有几个和我分数差不多的人选择了市重点:学杂费全免,直接去最好的班,还有奖学金。当时的我,对于省重点和市重点没有什么明确的概念,所以还是听从家人的想法:交「择校费」,进入了省重点。
我仍然记得从初中班主任(我和他关系很不错,他曾不止一次地鼓励我,甚至在我说因为回家太晚而不想上晚自习时,提出可以每天晚上开车送我回家;在中考前,让我不用做数学卷子,把心用在其它学科上)手里接过录取通知书时,他脸上那种复杂的表情——惋惜与错愕。前者在于我最终还是没能过省重点的分数线,后者在于我居然还是上了这所学校。
高一上学期,还未分班,想学文的和想学理的混在一起上课,我所在的班级不巧在以后会成为文科班,一想到反正以后也会转班,那就转班后再好好学习吧!恰巧在朋友的安利下,又接触了网络小说(玄幻啊,都市啊~~,言情啊~~),于是一发不可收拾彻底沉迷于小说,而「转班后再好好学习」也成为了我心安理得的借口。
当时的化学老师非常照顾我(因为我第一次摸底考试化学考了并列第一名,当然是吃初中的老本),他也是最早发现我堕落的老师,因此找我谈过几次话,虽然我并没有因谈话而上进,但我仍然感激他在我陷入黑暗的时候愿意拉我一把。
在未分班时,考试排名差我也有理由搪塞:文科我不会。可到了高一下学期,便进行了预分班,我被分到了一个新的班级,预想中的「重新做人」并没有出现在我身上,反而是更加的堕落——因为我同桌也看小说。与此同时,「文科我不会」的谎言也不攻自破(虽然是预分班,但学校会针对文理科的学生单独出一份文理科目的排名)。家人以为是我沉迷手机(说实话当时也确实沉迷手机,毕竟是智能机刚兴起的时代,但不是沉迷于网络,而是沉迷于折腾手机 ROOT 之类的),于是把我的手机没收。仅留下一个 MP4,但 MP4 仍然可以看小说,于是我仍沉迷于小说。
最终,堕落的高一以我期末考试 1062 的名次(年级不到一千两百人)结尾,而我也没有和家人扯什么「其它人中考分比我高,我学不过他们」这种诛心的理由。
高二上学期,上课看小说的情况似乎并没有得到改观,反而是我在与老师斗志斗勇的过程中成长了——「发明」了一种能上课看小说而不被抓到的方法:用书在课桌的前面和右侧各摆一摞,形成一个角落,一有风吹草动就把「作案工具」塞到书下面专门预留的缝隙里。得益此「发明」,我上课看小说没有被老师抓到过一次。可惜之后效仿的同学越来越多,班主任就禁止桌面上的书摆放成这种形状了(话说我这也算是迫使别人改变规则的人了 233333)。
似乎很难想起当初选择从堕落的深渊里爬出来的理由(或许不是想不起,而是我自己也不知道),但终究还是选择往上爬了——我开始严格控制看小说的频率,并开始学习了。
没过多久,正巧班主任把我调换了座位,我的同桌变成了班长,前桌坐的是班上第一名,我们三人「一见如故」:感兴趣的课(数理化)会一起认真听讲,一起听数学老师讲人生道理;在另外的某些课上则会小范围地互相逗乐、互相扯淡,那段时间不仅是我高二过得最快乐的时光,也是进步最大的时候,我很感谢他俩,也开始明白为什么家人一定要送我来这所学校了。
除了他们,我还想说说老师们,尤其是数学老师。在他的课上,我学会了数学这种严谨的思考方式,以及他那近乎到自恋的自信(名言:答案和我不一样就是答案错了),兴趣来了,成绩也就水到渠成了。高二下,我数学考了全班第一——145 分,数学老师似乎开始注意到我这个默默无闻的学生了,从那之后,我像是想证明什么似的,每次数学都很尽力地考,可却再也没考上过 130,可也没低于过 110。
语文课,永远都是睡觉的课——语文老师是学佛的,也非常佛系:发现你睡觉时,便会很温柔地抚摸你的背让你不要睡觉,大部分人在被叫醒后仍然继续倒头就睡,他也不恼,继续上他的课。英语老师也非常有趣:经常在课上和我们这些成绩不太好的学生「互动」,还经常放电影给我们看,得益于她,我拾起了对英语的兴趣。
好景不长,高二下学期过了才两个月,我们三人组的欢乐时光被班主任强行结束(原因是某人告密说我们三人上课讲话影响课堂纪律),也因此,高二的我对班主任怨念颇深。
上了高三,学校开始要求学生要上两节晚自习(七点到八点半以及八点四十到十点),我们的班主任又要求早上六点五十就要到校,而且我还要搭公交往返,这样一来的话一天根本无法保证八个小时睡眠,于是我就让父亲和老师说我不上第二节晚自习,好在当时我的成绩稳定下来了,也有了底气提出这个要求。
虽然每天晚上比同学早一个多小时回家,但我早上仍旧无法那么早起床(尤其是冬天),迟到成为了我的家常便饭,基本高三大部分的早自习我都是被罚站在教室后面背书,最多的一次罚站了近二十人,这也算我们班独特的风景线了。
在临近高考的几个月里,我过得比之前更放松了:会在老师要求我们自习时和坐最后一排的朋友一起靠在墙上看《奔跑吧兄弟》,却又不敢笑出声;会在早自习下课的五分钟时间内和同学跑去食堂吃碗面条,然后理所应当地迟到十几分钟才进教室,并打赌班主任不会守在门口……多么美好的时光啊,美好而短暂。
高考前夕,我玩得特别好的一朋友(他成绩比我好,高三的摸底考试基本都比预估的一本分数线高四十分,而我一般高十分左右)问我:
「你要是没过一本线,会不会复读?」
「不会」,我不假思索地回答到。
「我也不会」,他透过黑框眼镜,深沉地看着我说:「除非我二本都没考上」。那是我们第一次比较正经地谈论我们的未来。
那年是湖北省高考自主命题的最后一年,出卷老师似乎拼了命想让这届高考的学生记住他一样,数学卷异常地难。导致湖北当年的一本分数线是十几年来最低,而我,也倒在了我最擅长的数学上(没考到 80 分,我现在还觉得有些对不起数学老师)。
去学校领分数条的那天,阴霾天空,我遇到了那朋友:
「考多少分啊?」,他问我。
「不好意思说」,我摇了摇头,眼睛望向地面。
「我不信你还能有我低,我四百五都没到」,我猛地抬起了头望向他,却只看到他嘴角的苦涩。那是我们第二次,无比正经地谈论我们的未来。
高中啊,以「我没过一本线,他没过二本线」这样荒诞地结束了。
高中部分已完,由于大学部分需要写的东西比较多,我会另起一篇文章,敬请期待。
]]>现在说起来我自己都不信,之前我居然一直以为秋招是为当年毕业的学生准备的,直到我们班有人签了百度,我才知道秋招原来是为次年的应届生准备的😅,不过当时已经十月,秋招已经基本结束,于是只好准备来年的春招了。
话说回来,本次春招我准备的也不算特别充分,很大一部分原因是毕业设计选题选了一个自己陌生的领域,并且还准备评优秀毕业论文,所以年后一直在准备毕设,空闲时间才会找公司投递。我对公司还是挺挑的(钱多事少离家近,起码要满足两点吧),而且还要招 Python 岗,可供选择的公司就更少了,找来找去也只投递了一家公司——奇安信(原 360 企业安全),所幸最后也拿到了 Offer。
本文是对这次招聘流程的一个总结。
我是三月底投递的简历,四月中旬发来的笔试通知。有两道编程题,一道非递减数列(AC 67%,这题 Python3 的输入格式有问题),一道实现哈希表(AC 91%,同上,Python3 输入仍然有问题),这两题难度均介于 Leetcode 的 Easy 和 Medium 之间。
由于两道编程题都没 100% AC,我以为凉了,结果在 4 月 23 日晚上十一点发来面试通知,通知我 25 号下午面试,当时就有点慌,面试时间太近,只有一天时间准备(24 号上午还要去看复联 4 首映,本来想不去了,后来想想首映一辈子就这一次😅),于是看完电影赶紧把数据库和操作系统还有计算机网络复习了一下。
等了小半个钟,面试官才姗姗来迟(可能是因为面试的太多了),面试官是一个中年微胖的大叔:
与我想象中的面试还是有很大的区别,计算机网络一点没问,操作系统一点没问,数据库一点没问(让我一天的复习付诸流水😅),总体来说都是按照简历来发问,很 Nice 的体验。
几分钟后收到二面的短信。
也等了小半个钟,二面面试官应该是小组或者部门的 Leader 了,特别温和,居然用了「您」来称呼我:
原本我以为一面没问数据库、网络,二面怎么也该问了吧,可是还是没有(看来是真的不按套路出牌啊😅),面试官超级 Nice。
几分钟后收到三面的短信。
这一面 HR 问的问题实在是太多,跟查户口一样,又没有录音,只能回忆起一部分了:
我这俩提问都是比较迫切的,问的并不算好,不过和 HR 聊天还是比较愉快的。
等待 Offer 的过程不可谓不煎熬,5 月 16 号在群里看到有人说接到 Offer Call 了,当时心里就凉了半截,17 号晚上九点看到有人已经收到 Offer 了,一看我的邮箱,心另半截也凉了。结果十点一看发现我也收到了😅,当天激动得一晚上没睡好。
祝各位都能拿到心仪的 Offer~
]]>最近一直忙于毕业的相关事项,所以也没有新文章产出——并非是找不到写作素材,实在是没写作时间。虽然这几天依旧很忙,但总算也抽出了一点时间完成了本文,希望能给广大高校生在办理宽带时带来一些帮助。
目前大部分高校的校园宽带应该都对使用者作出了诸多限制,比如:一号一机,禁止使用路由器(破解后才可以共享);与校方合作垄断,导致价钱比家用宽带贵一大截等。在校生也只能被迫接受——毕竟,你总不能真的不用电脑上网吧?
好在目前越来越多的高校里校园网已经开始支持 IPv6 了,而一般校园网只针对 IPv4 的流量计费,对 IPv6 产生的流量是不计费的,至于原因,我猜测有两方面原因:一是 IPv6 相关技术还不是特别完善,IPv4 计费系统可能需要修改;二是目前国内 99% 的网站都不支持 IPv6,而纯 IPv6 环境下是无法访问 IPv4 网站的,所以干脆就没做这一限制。
连接上校园网后,不要认证,戳 这里 来测试是否支持 IPv6,当然也可以直接打开 Google,目前 Google 可以通过 IPv6 直连。
但,谁让我是学计算机的呢,这并不能难倒我。既然无法通过 IPv6 直接连接 IPv4 的网站,那利用一个同时支持 IPv4 和 IPv6 的 VPS 做一层代理不就可以绕过这一限制了吗?原理见下拓扑图:
这就意味着,只要你具备 IPv6 网络,便可以通过此方法绕过诸多限制,从而免费上网。
目前比较出名的 VPS 服务商除搬瓦工外,大部分都原生支持 IPv6 连接,包括:Vultr、Linode、DigitalOcean。而搬瓦工的 VPS 中 OpenVZ 架构自带 IPv6,KVM 架构则需要利用 Tunnel Broker 技术来提供 IPv6 隧道给只支持 IPv4 的用户(我的搬瓦工 CN2 主机便是通过 Tunnel Broker 来获取 IPv6 支持的,这也是搬瓦工的客服推荐的方案),它定义在 RFC 3053 。
如果你的 VPS 原生支持 IPv6 连接的话,便可以跳过这一步。
目前 Hurricane Electric 免费提供 Tunnel Broker 服务(我 TM 吹爆!),该公司运营了世界上以对等数目计算的最大 IPv6 网络,所以服务方面是不用担心的。戳 这里 注册。
随后点击左侧的 Create Regular Tunnel
,再在框内输入 VPS 的 IP 地址,再选择一个地区服务器来作为隧道的一端,这里建议根据服务器的地区来就近选择,我这里选择的是 Los Angeles。
创建成功后,在以下页面选择你的系统,如果是 Debian 系就选择 Debian/Ubuntu,其余就选择 Linux-net-tools。
框中会出现几行命令,登陆 VPS,依次运行这几行命令就行了。
第四行被我抹去的地址便是公网 IPv6 的地址。
如果对 Tunnel 的速度需要更换的话,可以删除该 Tunnel 后在 VPS 运行
modprobe -r sit
命令或者直接重启,再重新创建一个 Tunnel。
不出意外,这时 VPS 已经可以使用 IPv6 连接了:
需要注意的是,如果选择非北美地区的服务器,会绕道美国,所以这里的 PING 值会略高。
代理可以选择 Shadowsocks,但本次要介绍的不是它,而是另一款代理软件:V2Ray。该代理软件比 Shadowsocks 多了许多种伪装流量的方法,且占用内存更低(毕竟是 Go 写的),这对于小内存的 VPS 来说,非常重要。只不过其配置文件比 Shadowsocks 要劝退小白一些。
输入以下一行代码进行安装,系统需支持 Systemd:
bash <(curl -L -s https://install.direct/go.sh)
有关更详细的安装教程见 官方文档 。
如果是通过以上命令安装的话,配置文件在 /etc/v2ray/config.json
目录,以下是我的配置文件,没有流量伪装等进阶配置:
{
"log": {
"loglevel": "warning",
"access": "/var/log/v2ray/access.log",
"error": "/var/log/v2ray/error.log"
},
"inbounds": [{
"port": 10086,
"protocol": "vmess",
"settings": {
"clients": [
{
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"alterId": 4
}
]
}
}],
"outbounds": [{
"protocol": "freedom",
"settings": {}
}]
}
inbounds
:入站配置,是一个数组。
注意协议这里填的是:VMess
,由 V2Ray 原创的一份加密传输协议。
clients
是一个 Object 数组,每一个元素里的 id 必须满足 UUID 格式,且服务端客户端需保持一直,作用类似于 Shaowsocks 中的密码。
outbounds
:出站配置,也是一个数组。
Linux 客户端的安装与服务端一致。
Windows 建议使用
V2RayN
,带有图形化界面,下载 V2RayN-Core.zip
解压,下载 V2RayN.zip
解压出的 .exe 文件放入刚刚的目录下。
目录应该与以下类似:
.
├── config.json
├── geoip.dat
├── geosite.dat
├── guiLogs
│ ├── 20190421.txt
│ ├── 20190422.txt
│ └── 20190423.txt
├── guiNConfig.json
├── pac.txt
├── readme.md
├── user-wininet.json
├── v2ctl.exe
├── v2ctl.exe.sig
├── v2ray.exe
├── v2ray.exe.sig
├── v2rayN.exe
├── wv2ray.exe
└── wv2ray.exe.sig
格式与服务端一致,你需要修改的仅有 address 和 id 部分:address 填写服务端的 IPv6 地址;id 需与服务端一致。
{
"log": {
"loglevel": "warning",
"access": "/var/log/v2ray/access.log",
"error": "/var/log/v2ray/error.log"
},
"inbounds": [{
"port": 1081,
"listen": "127.0.0.1",
"protocol": "socks",
"settings": {
"netword": "udp"
}
}],
"outbounds": [{
"protocol": "vmess",
"settings": {
"vnext": [{
"address": "xxxx:xxxx:xxxx::xxxx",
"port": 10086,
"users": [
{
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"alterId": 4
}
]
}]
}
},{
"protocol": "freedom",
"tag": "direct",
"settings": {}
}]
}
这时,打开网络代理,填入:
注意,这里一定要选择手动代理模式,不能使用 PAC/自动模式,因为我们的目的是要本地的所有流量都走代理。而 PAC 仅会当遇到被墙的 IP 时才会走代理。
双击 V2RayN.exe 后,点击右上角的添加 VMess 服务器
:
填入地址,端口,id 即可:
再将右下角的系统代理模式改为全局模式,道理同 Linux 类似:
这样,不出意外的话,就已经成功了,你的仅支持 IPv6 的电脑已经该可以通过代理来访问非 IPv6 的网站了。
正好手头最近入了一个原生支持 IPv6 的 VPS,贴一下与搬瓦工的对比,以下均在同一时段做的测试:
原生支持 IPv6 的机器可以直接观看 2K 视频并且不会出现卡顿现象(可以看到已经缓冲了一分钟了),而使用 Tunnel Broker 的就没这么好了,不仅连接速度只有三分之一,而且无法较为流畅的观看 2K 视频,时不时会出现卡顿。考虑到 YouTube 的线路优化已经很强了,国内的视频或者直播应当只能观看 720p(码率最好不要超过 3000)甚至更低了,而前者直播时蓝光 8M 无压力。
首先是搬瓦工的,经过了 15 个节点,教育网的入口和出口丢包率很高:
这是原生 IPv6 的,经过了 19 个节点,同样教育网的入口和出口丢包率较高(但还不是最高的):
可以看到其原生自带 IPv6 的主机其实也用的是 HE 的 IPv6 网络(前 13 个节点都一样),那么看来是返程的时候出问题了:
果然,问题出在返程上面,原生的 IPv6 并没有走 HE 的线路,丢包率为 0。而相比于浏览网页,在看视频时返程的网络状况会直接影响观看体验,这一点也确实在之前的网速测试中体现了。
所以,购买建议是:
如果你还没有购买 VPS 的话,建议购买原生自带 IPv6 连接的 VPS,使用体验会好很多,不过由于电脑是全局的代理,所以要注意 VPS 流量的使用哦~
最后附赠一个国内的 IPv6 电视网站: 清华大学 IPTV 。
参考:
]]>按照惯例,还是在每篇文章的开头扯几句不相关的:元旦前就从学校回家了,在家的十几天过得很是舒坦:闲的时候,上午玩两小时游戏或者看看直播,下午就看会 Coursera,晚上有时间就看部电影,没整段的空余时间就找朋友聊聊天、刷刷 V2EX;当然在不闲的时候也是做了一些事的,就比如本文将要说到的——让移动平台和桌面平台同步剪贴板的方案。
我为什么想到做这个呢:两周前,我把用了一年半的 Manjaro 格式化了(忍受不了 KDE 巨多的 Bug,还有硬盘都被我用完了),装上了 Ubuntu,桌面环境选择了 Budgie——一个新出的 DE。比 GNOME 漂亮,比 KDE 稳定,稍加配置即可满足我这个强迫症的审美需求:
从 KDE 转成 Budgie 之后,最让我不习惯的就是手机和电脑再也不能愉快地共享剪贴板和文件了,当然我也在网上找了一些现成的解决方案,但体验都不佳,于是我便考虑自己造一个轮子。
由于移动端(客户端)、桌面端(服务端)二者需要进行数据的双向传输,HTTP 协议肯定是无法做到了:于是选用了 WebSocket 作为底层的传输协议。同时,为了让适用性更广,桌面端开发选择了 Golang;移动端则使用 React Native 开发。但这两门高级的语言(Golang、JavaScript)都无法为剪贴板添加监听事件,于是我只好自己用轮询的方式对比当前剪贴板的内容与上一次内容的差异,再考虑是否发送数据。
桌面端的开发语言选择 Golang 其实我是有些不情愿的:
其它语言我也找了个遍:Python 虽可监听剪贴板的变化(通过 gi 这个库),但这个库并没有办法跨平台,且我 Windows 没有安装编程环境,我也不想安装。
总之,能监听剪贴板的无法跨平台,能跨平台的无法监听剪贴板。
So . . . 哪怕 Go 有万般不是,但在跨平台这一点的易用性上也足以让我抛弃其它的所有(纯静态链接库 + 交叉编译)。
于是乎,
「真香」。
其实我本想用 Flutter 来开发的,但遇到了一些问题:
于是我选择了 React Native。有了 React 的基础,上手确实很快,并且 JavaScript 写起来感觉还是挺爽的。
之所以会有刚刚提到的(重新绑定 WebSocket 的)需求,是因为在不同的网络环境中,电脑的 IP 可能有所改变,而我暂时想不到一个方法让手机自动识别同一网络的哪一台电脑使用了共享剪贴板的工具(逐一扫描 IP?那也太丑陋了)。
所以在初始化连接时需要手动输入一次电脑的 IP 地址,随后地址信息会被保存,之后就不用再输入了。
只需保持这两个软件在后台,会自动监控剪贴板的内容并发送。
由于代码写的有点不满意,功能也不太完善,等以后有空重构之后加上文件共享功能,会开源的。这里先放出各平台的可执行程序: 戳我👈 。
愿能有所帮助。
]]>实习回到学校十几天后,便开始思考年终总结怎么写,之所以这么早就开始构思,或许是我笃定在这 2018 最后的半个月内也不会发生什么值得记录的大事。当然我也期待着能发生什么事来冲击一下现有的生活——在校的时光实在是太安逸了,想来是应当正处于心理空窗期了。
本次总结将会就学习、生活、工作(实习)三个方面来描绘一下今年的生活轨迹。
我应该算是「兴趣驱动学习」的典范了:每当我抱着很强的目的性或功利性去学习的时候,总是坚持不了多久。Princeton 的算法课从两年前就开始学,学到现在也才把排序看完。
不过今年在学习方面还是有不少收获的:借助 Coursera 这一平台,观看了 Node.js 的开发,Golang 的入门等网课。我在学习一门新语言的时候总会选择看视频,如果从一开始就看书的话,我会觉得有些乏味和枯燥,尤其是动辄上千页的技术类书籍,看厚度就有一种劝退感。而在入门后,想要深入了解一门的语言的底层,我才会选择翻阅一些书籍。
除了学习新东西之外,对 Functional Programing(函数式编程,以下简称 FP)也有了更深刻地理解。比如,为什么 FP 中多以递归来代替 Imperative programming(命令式编程)中的循环语句。原因在于数学家和逻辑学家们验证递归的正确性比验证循环的正确性要容易得多。
一个很简单的例子,分别用 Java 和 Haskell 实现快速排序:
Java:
public class Sorter {
public static > void quicksort(T[] list) {
quicksort(list, 0, list.length - 1);
}
private static > void quicksort(T[] list, int low, int high) {
if (low >= high)
return;
int i = low - 1, j = high + 1;
T pivot = list[low];
for (;;) {
do {
++i;
} while (list[i].compareTo(pivot) < 0);
do {
--j;
} while (list[j].compareTo(pivot) > 0);
if (i >= j)
break;
T tmp = list[i];
list[i] = list[j];
list[j] = tmp;
}
quicksort(list, low, j);
quicksort(list, j + 1, high);
}
}
Haskell:
quicksort :: (Ord a) => [a] -> [a]
quicksort[] = []
quicksort(pivot : rest) =
quicksort[x| x <- rest, x < pivot]
++ [pivot]
++ quicksort[x| x <- rest, x >= pivot]
以上两段代码,哪一段的正确性更容易验证?答案不言自明。
对于前者,指定了计算的详细过程,而后者,仅指定了计算的规则(原则)。这也是 FP 的特点之一:不关心如何计算,更关心计算的结果(的正确性)。
「正确」是 FP 设计的重中之重。究其根本在于 FP 的鼻祖 Lisp 与 λ 演算那密不可分的联系。
生活方面似乎并没有什么改变,相比去年——好吧,并不是。我的体重告诉我比去年重了 ×× 斤。去杭实习不仅没瘦,反而重了。而且似乎睡得还更晚了,不过似乎「互联网依赖症」减轻了不少,比如本文初稿就是纯手写的。
在读书方面,算是有了一些进步,今年在豆瓣读书上为 15 本书贴上了「已读」的标签(不过仍未达到两周一本的目标)。文学类和技术类都有,技术类对我影响最大的毫无疑问就是《流畅的 Python》这本书了,断断续续地看了近三个月,做了万余字的笔记。
至于文学类嘛,《哲学家们都干了些什么》是让我眼前一亮:枯燥的哲学似乎在作者笔下都「皮」了起来。但印象最深地却是伊坂幸太郎的《金色梦乡》,等下,《白夜行》好像印象也很深刻,《解忧杂货店》构思也很巧妙(这么说来我果然很喜欢日系推理),钱锺书的《围城》也不错…好吧,文无第一,今年看的文学类书籍都非常不错。
接下来就该到电影方面了:今年在豆瓣电影为 72 部电影(包括剧集)贴上了「已看」标签。比较喜欢的剧集似乎都是日剧(非自然死亡,胜者即是正义);电影的话,《我不是药神》一枝独秀,在电影院里哭得还朝旁边的妹子借纸;动漫也看了不少:来自深渊(新番),怪化猫(旧番)都不错,属于能给灵魂带来冲击的番。
谈话类节目仍然强推《圆桌派》,可惜第三季之后窦文涛似乎就跑路了。纪录片这块则必须让陈晓卿的《风味人间》安排上,对于一个吃货来说,本节目的美食引起了强烈不适。不过还是作为吃货,即使吃不着,退而求其次,能过过眼瘾也是一种享受。此外,本节目还会介绍与美食相关的风土,人情,文化,品味等,美食之所以被冠以「美」字,绝不仅仅在于它的味道,更在于人们赋予它的某种意义,或者说是其背后所蕴含着的丰富内涵,这才是其绵延不绝的生命力所在。
如果说搭建这个博客是去年我做的最有意义的事,那么实习应该就是今年我所做的最有意义的事了。
九月份时我并没有走校招,原因有两方面:一方面我并没有发现有什么企业校招 Python 岗(大部分都是前端和 Java);另一方面是估计实习时间也不会太久,本学期期末学校肯定一大堆事,于是也没怎么考虑公司,面了一家,就进去了。
我面的是 Python 岗,本以为进来之后也是写 Python。谁知阴差阳错上了 Node.js 的贼船,当然这有一部分的原因在于我:Leader 问我能不能用 Node.js,我说之前接触过一些,然后他就给了我几天时间让我熟悉一下。不过好在是从零开始写的,不用「接盘」前人的代码,倒是省了不少事。在公司待的两个半月时间内,也帮同事解决了不少 Python 问题,不过多是业务方面的逻辑。除此之外,收获最大的就是 Elasticsearch(以下简称 Elastic)的相关组件(我编写的 API 是直接与 Elastic 交互的)的部署和维护了,因为个人的项目很难直接接触到 Elastic 这一庞大的生态系统。
不过我是有些认为面试的时候公司没有把我的水平面出来(颇有些怀才不遇的感觉),简历上写的大部分细节都没问。只是问了问笔试题,感觉有点憋屈。能力未充分得到挖掘带来的问题就是资源的浪费。一方面是职员能力的浪费(能力得不到体现),一方面是公司资源的浪费(项目进度跟不上,业绩无法按时达标),于是就会导致某些人上班只需要花费很少的时间即可完成公司的任务,剩下一堆可自由支配的时间,而另一些人不仅上班时间不够用,还需要加班才能勉强完成。在这个公司时,我应当时属于前者,虽然有些不好意思,但我在公司的空余时间还是会学些自己的东西。
同时我也感觉写业务写久了,脑子里也只剩下业务了。对技术反而没什么提升,而对于当下的我来说,技术仍远比业务重要。正在这时候,学校要求我们回去准备毕设的事。于是,我辞职了,开始享受这为数不多的大学时光。
如果说之前的我还对未来的道路存疑的话,那么这两个半月的实习生活让我开始明确了未来的道路——Python 开发相关。同时也明白:校园的时光虽然让人十分享受,但也总有毕业的那一天。如果说大学的象牙塔是我最后的天堂,那就让我从天堂里一步一步地走出来。
似乎该结尾了,我不想再像去年一样为我的 2019 立什么 Flag 了(去年的 Flag 一大半都没实现),不过我想毕业的年份应当过得比较有趣吧?
]]>Elasticsearch 官方对其的定义是一种搜索引擎,但我更喜欢把它当作一种非关系型数据库来看待,而作为数据库来看待的话,保障其中数据的安全性和可靠性自然是重中之重了。
首先声明,本文对 Elasticsearch 数据的备份是基于 官方提供的 API ,其它的诸如 elasticsearch-dump 等第三方工具暂且不谈。官方的提供的备份方式是一种 Snapshot,其中备份路径可以选择云端或本地,本文想就备份集群数据到本地写一份指南,并对遇到的问题做一些解答。
为了创建一个 Elasticsearch 集群的 Snapshot,首先必须先搭建一个集群文件系统,用以确保集群的所有节点对同一个目录具有操作权限——这里推荐使用 NFS(Network File System)。
NAS 需要指定集群的某一个节点的文件夹作为服务器来提供硬盘存储,也是 NAS 存储文件的实际位置,集群的其他节点作为客户端挂载服务端的共享文件夹。
sudo apt install nfs-kernel-server # 服务端
sudo apt install nfs-common # 客户端
这里是我踩的第一个坑。服务端的共享文件夹所有者必须也是运行 Elasticsearch 程序的用户,不然 Elasticsearch 是无法将数据备份到该文件夹的,比如运行 Elasticsearch 的用户是 els 的话,可以用如下命令来创建共享文件夹:
sudo -u els mkdir /path/to/backups -p
在服务端的 /etc/exports
中添加以下内容:
/path/to/backups 192.168.1.1(insecure,rw,sync,no_subtree_check,no_root_squash,no_acl)
其中 192.168.1.1
就是服务器本机的 IP。括号内的是配置参数,稍稍解释一下:
insecure
:允许客户端从大于 1024 的端口号连接。rw
:所有连接者都具有读写权限。sync
:将更改都提交到稳定存储之后再回复请求。no_subtree_check
:禁用子树检查。no_root_squash
:关闭 root 压缩。no_acl
:不要向客户端显示 ACLs。更多的参数可以参考 这里 。
exportfs -r
这时你可以使用 showmount -e localhost
来查看本机的挂载情况:
/path/to/backups 192.168.1.1
注意,客户端也一定要使用运行 Elasticsearch 的用户的权限来创建相同的文件夹,这一点非常重要。然后把服务端的文件夹挂在到各个节点:
mount -t nfs 192.168.1.1:/path/to/backups /path/to/backups
没有报错的话,就挂载成功了。
需在每一个节点的 elasticsearch.yml
中加上一行:
path.repo: ["/path/to/backups"]
然后重启 Elasticsearch。
这时就可以尝试创建一个 Snapshot 的仓库了:
PUT /_snapshot/my_backup
{
"type": "fs",
"setting": {
"location": "/path/to/backups"
}
}
如果没有报了权限错误的话,就 OK 了。
如果报了权限错误,请检查 Elasticsearch 对该文件夹是否具有写权限。如果确认具有写权限,那么就需要把集群各节点运行 Elasticsearch 的用户的 UID 和 GID 统一起来,具体做法见附录。这里是我踩的第二个坑。
你可以以如下命令来创建一个备份:
PUT /_snapshot/my_backup/snap1?wait_for_completion=true
{
"indices": "index_1,index_2"
}
其中 wait_for_completion
参数并不会马上返回结果,而是等备份完成之后再返回结果,如果备份的索引很多的话,可能会花费很多时间才返回,所以并不建议加上这个参数。可以为 PUT
加上请求体,指定索引,如上指定 index_1
、index_2
两个索引备份,如果不加请求体的话会默认备份全部索引。
当 Snapshot 正在生成中的时候,可以使用如下命令来获取备份的进度:
GET /_snapshot/my_backup/snap1
其返回的 state
项的值即是备份进度条。
Elasticsearch 是采取增量备份的形式,但需注意,Snapshot 不可重复创建,也就是说 Snapshot 的名字不能相同。
可采取如下命令从 Snapshot 中恢复:
POST /_snapshot/my_backup/snap1/_restore
默认所有的索引都会恢复,当然你也可以指定索引:
POST /_snapshot/my_backup/snap1._restore
{
"indices": "index_1,index_2"
}
而有关恢复的进度,Elasticsearch 并不提供查询像备份进度那样的 API,所以只能使用 indices recovery 和 cat recovery 来查询恢复进度。
与备份和恢复更详细的讲解请参照 官方文档 。
首先假设各节点运行 Elasticsearch 的用户名为 els
。先假设:他的旧 UID 为 1005,旧 GID 为 2000,要把他改成 UID 为 2005,GID 为 3000 的新用户。
首先为 els
更改新的 UID 和 GID:
sudo usermod -u 2005 els
sudo groupmod -g 3000 els
这就算完成了,可以用 id els
命令来查看用户的 UID 和 GID 是否被成功更改。
当然,这只是把用户名的 UID 和 GID 做了简单的更改,还需要把原用户的所有文件、目录的所有者都改成新的,不然是无法成功运行 Elasticsearch 的,会报权限错误。
以下命令来更改:
sudo find / -group 2000 -exec chgrp -h els {} \;
sudo find / -user 1005 -exec chown -h els {} \;
其中 -exec
参数对每个文件执行 chgrp
和 chown
命令。-h
参数是对符号链接起作用,而不是应用文件。
参考:
]]>好像又有一段日子没写技术类博文,翻了翻归档,发现最近一篇技术类文章已经是俩月多之前的事了,吓得我赶紧水写一篇的技术文章不然怕是要被人当成生活博主了。思来想去写些什么好,还是继续上次开的新坑,来聊聊 Python 中的继承。
Python 作为一种具备多种编程范式的语言,面向对象自然也是它所具备的范式之一;而继承,作为面向对象程序设计的三大特性之一,其重要性也是不容忽视的。尤其这一特性在支持面向对象范式的语言里还有着不同的规则,如:C++ 同时支持普通继承和虚继承;Java 则是将其它语言中的 class 细分为 class 和 interface;还有 Python,「无痛地」支持多重继承。
当一个语言支持多重继承时,至少需要解决这两个问题,以下图为例逐个分析 C++、Java、Python 对多重继承的支持情况:
A
/ \
B C
\ /
D
首先看看 C++ 是怎么解决第一个问题的:C++ 在遇到这种情况时,最顶层的基类(A)会被创建两次,虽然可以通过将 A 设置为 B、C 的虚基类来解决,但这种虚继承是有副作用的:不只是在获取成员会更慢、占用内存更多,在和虚函数一起出现时会更难以理解:当 A、B、C、D 中都有一个虚函数 f 时, D::f
内部调用了 B::f
和 C::f
;B::f
和C::f
内部都调用了A::f
,于是 A::f
就被调用了两次。当然也不是没有办法解决,
Template Parameters as Base Classes
就是 C++ 之父专门用于解决这个问题所开发的技术。
第二个问题:如果这里的 B 和 C 同时实现了 hello
方法,同时 D 中没有实现 hello
方法,那么在调用 d.hello()
(d 为 D 的 instance) 的时候会调用哪一个?编译器对于这种 Ambiguous base classes 的情况会直接报错,解决方法也简单粗暴,你必须显式指定 d.B::hello()
来调用 B 中的 hello
。虽然以上两个问题在 C++ 中都解决的并不好,但也总归是解决了。下面来说说 Java。
Java 之父觉得 C++ 太难用了,于是他决定创造一门语言来取代 C++,这门语言需要保留 C++ 的优点,但又需要把 C++ 中较为混乱、复杂、危险的部分剔除(其中就包括了多重继承),于是,Java 就在这样的理念里诞生了。在 Java 诞生之初,对多重继承的支持少的可怜:一个 class 仅可继承(实现)多个 interface。本来是挺好的,但是在 Java 8 中为 interface 中的方法引入了 default 关键字,这就让 interface 里定义的方法可以有方法体了。
那么 inteface 有了方法体之后,对这解决这两个问题有什么影响呢?将上例中的 A、B、C、D 换成四个 interface:
第一个问题:Java 遇到了和 C++ 中的虚函数一样的问题,即 D 中如果想同时调用 B 和 C 中的某方法,且 B、C 中也调用了 A 的方法,那么 A 的该方法会重复运行两次。并且 Java 无法像 C++ 中使用模板技术来解决这个问题。
第二个问题:Java 8 之前一个 class 可以实现多个 interface,即使 interface 有同名方法也没关系,毕竟 inteface 里定义的方法没有方法体,所以不会导致二义性。但有了 default method 之后,Java 反而无法处理这个问题了:D 不允许同时继承两个实现了 default 方法的接口(一个实现了,另一个没有实现也不行)。
C++ 解决的不好,Java 压根就没解决。所以我认为 Java 在多重继承这一方面比 C++ 处理的更加不好。
终于到 Python 了,那么先来看看 Python 是如何解决第一个问题的:如果 D 中需要同时调用 B 和 C 中的 hello
方法,且 B、C 中也需要调用 A 的 hello
方法,那么仅需在 B、C、D 的该方法中写下 super().hello()
即可。因为继承图的缘故,Python 中的 super()
会沿着继承图顺序依次找寻 D 的所有超类。
第二个问题:还是得益于继承图,当 D 中没有实现 hello
方法时,Python 会依据继承图顺序来寻找 D 的所有超类中是否有该方法,直到找到为止,而这个顺序也就是方法解析顺序。
Python 完美的解决了这两个问题,这也是为什么我说 Python「无痛地」支持多重继承。
既然这两个问题都是靠方法解析顺序解决的,那么它到底是个什么东西?看官先别急,下面会着重阐述。
方法解析顺序(Method Resolution Order,简写为 MRO),是描述该类自继承顶层超类的一种顺序,它在类中以 __mro__
属性存放,值是一个元组(.mro()
返回的则是一个列表,然而这列表并不可变),上例中的 D 的 MRO 为:
>>> D.mro()
[__main__.D, __main__.B, __main__.C, __main__.A, object]
这里的意思是:D 的最顶层超类为 object
(新式类中所有类都继承自 object
),其次是 A、C、B。
下面来具体看看 Python 的代码是如何解决上文提到的第一个问题的:
class A:
def hello(self):
print("Hello from A")
class B(A):
def hello(self):
print("Hello from B")
super().hello()
class C(A):
def hello(self):
print("Hello from C")
super().hello()
class D(B, C):
def hello(self):
print("Hello from D")
super().hello()
>>> d = D()
>>> d.hello()
Hello from D
Hello from B
Hello from C
Hello from A
在 D 中遇到的 super()
会沿着 D 的 MRO 依次向上寻找超类中的 hello
方法并执行,即依次执行 D -> B -> C -> A 这个顺序。
那么为什么 Java 或 C++ 无法通过这种写法解决呢,原因在于 Java 的 super
在多重继承中必须指定父类是哪一个,因为编译器是无法获知你想要运行的是哪一个父类的方法。而一旦指定了父类(B),那么与这个父类同时继承的另一个父类(C)你也必须要指定,而这两个父类又具有相同的更高层次的父类(A),所以就导致了最顶层的父类(A)中的方法被调用了两次。
下面来具体说说 Python 中的 super
类。
在 Python 中某 class 使用了 super()
后,super
即会沿着最初调用 super()
的那个 class 的 MRO 向上寻找超类:
>>> B.mro()
[__main__.B, __main__.A, object]
>>> C.mro()
[__main__.C, __main__.A, object]
也就是说:在 B 中调用的 super
不会顺着 B 的 MRO 来向上寻找,而是从最初调用的 D 的 MRO 来向上寻找。那么 super
是怎么知道最初的 class 是哪一个呢?
嘿嘿,你可能已经猜到了,没错,就是通过 self
这个参数来指定的:当我们调用 d.hello()
时 ,实际上调用的是:D.hello(d)
,而我们调用的 super()
也只是 Python 3 对 super(a_class, a_instance)
(其实这里的 instance
也可以换成 class
)的简写,所以其实 B、C、D 中的 super()
中的 a_instance
其实都是 d。
当我们以 super(a_class, a_instance)
调用时,这里的 MRO 即为 a_instance.__class__
的 MRO,而 a_class
必须为该 MRO 中的某一项,也就是说 isinstance(a_instance, a_class) == True
。
简单来说,super()
做的事就是:你提供给它一个 class 以及一个 instance,它返回从该 instance 的 MRO 中排在 class 之后的类里,查找方法的对象。
说了这么多,那么这个 MRO 到底是怎么产生的?
在 Python 2.3 之后,MRO 是由 C3 算法来计算得出的,而在 2.3 之前是按照如下规则计算:新式类是广度优先,经典类是深度优先。这里仅讨论 Python 2.3 版本之后的 MRO 计算方法,也就是 C3 算法:
为表述方便,先做出如下规定:
$$ \begin{aligned} head([C_1,C_2,…,C_N]) &= C_1 \\ tail([C_1,C_2,…,C_N]) &= [C_2, C_3,…,C_N]\\ L[C(C_1,…,C_N)]&=[C]+merge(L[C_1],…L[C_N],[C_1,…,C_N])\\ \end{aligned} $$
则用 C3 算法计算这个列表的线性化可以用公式 (3) 表示,其中的 C 是继承自 C1,C2,… CN 的类。
对其中的 merge 操作可以解释为:
good head
),则把 head(K) 加入 C 的线性化列表中,并将 head(K) 从 merge 的所有列表中删除,重复 2;good head
,重复 2;good head
为止;如果找不到 good head
那么就抛出异常,否则创建成功。来看个例子吧:
A = object
class B(A): ...
class C(A): ...
class D(C, B): ...
class E(C, D): ...
首先 L[A] = [A],然后: $$ L[B]=[B]+merge(L[A],[A])=[B, A] L[C]=[C]+merge(L[A],[A])=[C,A] $$
这两个等式很简单,没什么好说的,来看个稍微复杂一点的:
$$ \begin{aligned} L[D]&=[D]+merge(L[C],L[B],[C,B])\\ &=[D]+merge([C,A],[B,A],[C, B])\\ &=[D,C]+merge([A],[B,A],[B])\\ &=[D,C,B]+merge([A], [A])\\ &= [D,C,B]\\ \end{aligned} $$
稍微解释一下:
第二行,设置 K 为 [C, A],其中 C,也就是 head(K) 是一个 good head
,那么就把 C 加入 D 的列表,并把 C 删去;
第三行,这时设置 K 为 [A],A 此时并不是一个 good head
,因为他在 tail([B, A]) 中出现了,所以要设置下一项 [B, A] 为 K,此时 B 是一个 good head
,那么就把 B 加入列表,并删除 B;
第四行,这时 K 为 [A],A 此时是一个 good head
,加入列表,并删除 A,此时所有 class 都已经被移除,算法结束。
来个错误的例子:
$$ \begin{aligned} L[E]&=E+merge(L[C],L[D],[C,D])\\ &=E+merge([C,A],[D,C,B,A],[C,D]) \end{aligned} $$
这时算法好像没有办法继续往下走了:因为设置 K 为 [C, A],head(K) 并不是一个 good head
,那么就把 K 设置为 [D, C, B, A],这时还是不行,因为 D 也在后面列表中的 tail 出现了。
所以是无法选择 (C, D) 为基类来创建 E 的,如果你在解释器中执行一下代码,你就会发现,它报错了:
Traceback (most recent call last):
File "", line 1, in
TypeError: Cannot create a consistent method resolution
order (MRO) for bases C, D
以上,
愿对你有所帮助。
参考:
]]>似乎很难说出为什么我对杭州这座城市如此「情有独钟」,以至于暑假就和同学计划着来杭州实习,虽然还是拖到了开学。出发之前也没有准备许多东西,只是简单收拾了一下衣服。所幸到达杭州后遇到的问题(比如租房、工作等)都比较顺利的解决了:遇到了很棒的宾馆老板、主动给我指路的老奶奶、很 Nice 的面试官(随后顺利入职)……这一切都让我对杭州这座城市更有好感,也让如今的我庆幸没有来错杭州。
来杭州前,我和同学都约了面试,不同的是,他约了 5 场,我只约了 1 场。 在学校临走前的几天他都挺慌的,说 5 场万一都不合适怎么办,非要约 10 场差不多心里才有底,我就安慰他说没事,我才 1 场,我都不慌,你慌什么。他反而对我说,对啊,我要是你估计都慌得不敢去了,不知道你怎么这么有信心。
当时的我也不知道为什么这么有信心,似乎根本没有想过万一在杭州找不到工作怎么办。就这样,我抱着三分紧张,两分忐忑,五分兴奋的心情,进行了我人生中第一次面试。
面试中技术问题主要针对的是之前做的笔试题,而笔试题中大部分都是算法的,所幸虽然我虽好久没接触算法了,但也没有完全忘记。不过让我感到奇怪的是我简历中的项目反而问的不是很仔细,似乎公司更看重的是学习能力和思考方式,而说到学习能力我自然是不会虚谁了,毕竟现在掌握的技能大部分都是自学来的 :)
技术问题过后就是较为老套的提问流程了,比如你为什么要来杭州啊,你对我们公司想要了解什么啊,你对自己未来的职业规划是怎么样的啊等等。与面试官的沟通交流较为顺畅,面试官也说我是来面试中很优秀的了,两天后,顺利的拿到了 Offer。公司名字就不透露了,在和瑞科技园。
随后在等面试结果的一天里,让朋友领着逛了逛杭州。
那天早上朋友说带我去吃蟹黄汤包(青芝坞那家),当时已经早上十点了,朋友准备早午饭一起在那里吃,而我当时已经吃过早饭了,就准备到美院的食堂去吃午饭(我十分庆幸没有去美院的食堂吃,走了一圈愣是没找到食堂在哪),但禁不住朋友一直说这里的蟹黄汤包多么好吃多么好吃,于是我也点了一份。
等了差不多十几分钟,它才姗姗来迟。
外表乍一看和普通的汤包没什么区别,细看还是可以看出馅儿是偏黄一些的。我马上夹起一个就塞到了嘴里,差点没烫哭出来。朋友笑了,然后告诉我说汤包要先从褶子里把汤吸掉,要不然会很烫的。
想我以前在学校的时候,哪吃过现蒸的汤包啊,食堂大妈早就蒸好了,然后拿布盖着保温,虽然算不上冷,但也绝对算不上烫。
味道嘛,还算好吃。
话说其实一开始我不是很想去美院的(但没办法,谁让相机在我朋友手上呢),因为我觉得学校嘛,再好看还能比旅游景点好看?直到我见到美院的大门那一刻我才知道我错了。
前方多图预警(这个预警并没有什么卵用,因为当你点进这个页面的时候图片已经加载完毕了 :)
(不知是因为杭州的美女多还是因为学艺术的女生都比较有气质,真的不愧是中国美女学院中国美术学院。)
这次好像只逛了一半的校园,因为校园里面的建筑真的和迷宫一样,从一个建筑进去绕了一圈就不知道原先从哪出来的了,导致进校园和出校园都在同一侧,下次有机会还要去逛逛。
我目前所在的公司是早上九点上班,下午六点下班,中午午休一小时,一周五天。除了中午休息时间有一点短之外,几乎完美了,而我除了第一、第二天在下午会稍稍犯困之外,后面几天都挺精神的(当然,代价是晚上十点半就要睡觉,这对我反而是好事,正好能调整到健康的作息时间 :)
公司的同事都很年轻,办公氛围也比较轻松,不会像部分其它的公司那么压抑。那个 Nice 的面试官也成为了我目前所在项目组的 Leader。
我现在的日常生活就是每天早上睡到八点起,洗漱一下,买个早饭走十几分钟去公司,中午和同事一起去食堂吃午饭,晚上回来之后就写写博客或者看部电影。呐,惬意的生活啊。
今天也是第一周的最后一天班,早上去公司的时候发现桌上摆着一盒「蛋黄酥」,应该是中秋的礼物了。
说到中秋,这应该是我第一个不在家过的中秋了吧,有些伤感。
最后,还是祝大家中秋节快乐。
]]>人类似乎总是对自己无法掌控的事物有些畏惧,比如时间。我就有些害怕时间的流逝,总觉得去年那篇 写给 20 岁的自己 的文章还历历在目,如今又到了该写这系列文章的时候了。我很早就开始构思这篇文章该写什么,却直到最近才了有些明确的思路。
去年,我曾对自己说,希望可以「不在意别人的眼光,不为了生存而活,为了自己的热爱」那样真实的活着。所幸我应当是在大部分时候都做到了,因此这一年过得很自在。但怎么说呢,近来关于这个问题我却有些困惑了。
前几天一朋友在群里说他辞职了。
问:为啥啊?这工作不好吗?
答:工作挺好的,划水都能拿 12k。
更奇怪了,又问:划水都能拿这么高,还有啥不满意的?
答:感觉生活没有目的,突然想辞职冷静一下。
当时还不太能理解他的想法,因为在我看来这简直是梦寐以求的工作啊:划水意味着我就有更多的时间投入到我所喜爱的事情上,但我没有说出口,只是默默听着。他继续说辞职亲戚朋友都反对,但他还是辞了——「迷茫了,每天这么混,不知道该干什么」,他说。听他说完这句话时我就有点理解了,是啊,没有以自我认可为目的的生活,又有什么可以阻止它慢慢滑向悬崖的另一侧呢。
说来有些不好意思,我的自我认可很大一部分来自两部电影——《搏击俱乐部》和《三傻大闹宝莱坞》:做自己想做的事,不要做自己应该做的事情。具体解释就是我不想也没有必要活成别人眼中的自己。像别人一样告诉自己先买车、后买房、再结婚?这都是自己作,一辈子忙忙碌碌都不知道为什么而活,生命只有一次,为什么就不能为自己而活呢?做自己想做的事,不是很开心吗?
而现在我的想法好像有了些许的改变:在社会福利好的发达国家,也许公民是真的可以做到我梦想的这样:看起来不求上进,赚到钱就花,遇到热爱的事情就去做,一辈子都过得很开心、很舒服。但是在中国不行。
想成为自己、为自己而活当然没错。这也是电影想告诉我们的,但电影没有告诉我们的是,今天的自己是自己,明天的也是。十年后,三五十年后的都是自己。对于「自己」来说目前所有的选择都是自由的,可以选择自己所热爱的事,也可以按部就班地选择自己应该做的事。仅需记住目前做出的选择所造成的千差万别也是未来的自己所必须接受的。在中国如果按照我之前的那种想法去选择,可能并不会让三十年后的自己开心。当然,这种选择不能说是错的,更不应该被批判,只是说我们在自己没想清楚的时候可能会选择了以后的自己不想要的未来。
写到这忽然有点明白卢梭的那句「人生而自由,却又无往不在枷锁之中」中的意味了。
关于我 21 岁的谈人生瞎扯淡就在这里告一段落了。确实,我没有像去年那样在全文中都很明确的表达出一个观点并以此作为我接下来一年生活的座右铭。反而是给了一个模棱两可的答案。但我想,相比去年,这个答案确实更好了——因为得出这个答案我思考得更全面了。或许我需要像阿甘一样,连续奔跑三年才知道自己为什么要奔跑。谁知道呢,也许我明天就想明白了,Life was like a box of chocolates. You never know what you’re gonna get(译文:人呐,就都不知道(命运),自己就不可以预料)。
用杨绛答复别人回信作为结尾吧:
你们这些年轻人啊! too young too simple, sometimes naive!你们啊,还是要提高自己的知识水平。
啊不好意思,皮了一下,是下面这句:
「你的问题主要在于读书不多而想得太多」——杨绛。
最后,二十一岁快乐。
送给自己。
]]>八个月前,我把建站之初就使用的 NexT 主题换成了 Material 主题,依稀还记得当时告诉自己:以后就好好写文章,绝对不再耗费时间在这没啥价值的事情上(让你立 Flag!后悔了吧?)。时隔半年多,如今发现对我来说好像不折腾比折腾还要难一些,原因嘛,自然是新鲜感与强迫症在作祟,这次折腾的起因就在于新鲜感——我看上了一个主题。
这次更换的主题是
inside
,该主题相较于其它 hexo 主题的特殊之处就在于它的本质是采用 Angular 编写的 SPA(single page web application,单页应用程序)。优点就在于每次点击不同的链接只产生一个 HTTP 请求,返回的是一个 .json
文件,包含该页面的内容(内容已在 hexo g
时已经转成 HTML 格式了),而一旦接收到 .json
文件后,就会将文件的内容通过 innerHTML
属性嵌入页面。
而缺点在于相较于普通的页面,可能对 SEO 不那么友好。因为阻止页面呈现的 JavaScript 可能会对用户体验造成不好的影响,而我为此在额外方面做了补足:添加了 27 个 <meta>
标签,用 Google Chrome 测试 SEO 那一项拿了 89 分,应该还算不错了(拿不到满分是因为部分字体小于 16px)。
采用 Angular 编写的 SPA 有一个 BugFeature,即在路由时会自动去掉 URL 结尾的斜线(slash),这就很尴尬了,因为我的博客 URL 是默认在结尾都有一个斜线的,比如:posts/xxxxxx/
,我对此的解决办法是:
canonical
标记,其中标记的链接末尾都加上一个斜线;我在 Stack Overflow 上找到了更好的 解决方法 ,原理是根据请求 URL 的每一个路由地址在末尾都加上斜线。
但若想完美的解决这个问题,还需同时为每个匹配的路由路径在末尾都补足一个斜线(否则带斜线的 URL 一刷新就会出现路由无法匹配的情况)。
...
{ path: 'page/:page', component: VPostListComponent. resolve: { postList: PostListResolver }, data: { id: 'posts' } },
// =>
{ path: 'page/:page/', component: VPostListComponent. resolve: { postList: PostListResolver }, data: { id: 'posts' } },
...
这样一来的话,斜线的问题算是解决了。(这部分也是我折腾耗时最多的部分。)
我针对用户体验方面做了如下改进:
字体
原主题的默认字体大小是 14px,我将正文修改成了 16px,代码字体修改为了 15px,这应该会比原主题看起来舒服一点。并且我移除了主题额外加载的字体文件,而纯粹改用 font-family
来呈现。参考了
fonts.css
。我能吞下玻璃而不伤身体。The quick brown fox jumps over the lazy dog.
Service Worker
原主题不带 Service Worker 功能,但我还是为我的博客注册了 Service Worker 功能。
启用 盘古之白
主要是为了解决中英文混排时的问题。因为研究显示,打字的时候不喜欢在中文和英文之间加空格的人,感情路都走得很辛苦,有七成的比例会在 34 岁的时候跟自己不爱的人结婚,而其余三成的人最后只能把遗产留给自己的猫。毕竟爱情跟书写都需要适时地留白。终于找到了我之前过得如此不得意的原因了,看来从此之后我可以走上人生巅峰了。
Disqus
原主题对 Disqus 的 identifier 和 url 识别有误,自己新增的页面会在在首部添加 posts/
字段,比如我的 life/
页面会变成 posts/life/
,这并不是我希望的。我针对当前 URL 简单的做了一个判断,根据 URL 的不同来生成指定的 identifier。
API
我将原主题的 API 请求前缀改了一下,不从源站请求。因为我的网站是使用 Cloudflare 来减加速访问,但国内的速度却不佳,而我博客大部分用户还是国内的。将 API 请求源放在了国内的 CDN 后,会更大的提升页面的访问速度——访问页面几乎感觉不到页面的加载时间。
相较之前的 material 主题,我去除了 localStorage
功能,因为现在的博客内容需要用 JavaScript 来呈现,而放入 localStorage
的资源取出的速度是不如直接从 Service Worker 请求的速度来的快。
同时我也移除了 lazyload-image 的功能,因为之前朋友告诉我说她从 RSS 阅读器阅读文章的时候图片没有显示,想了想应该是 lazyload 的 JS 没有加载所导致的,于是乎干脆去掉了,等以后有空的时候再试试 Angular 的 lazyload 是否也有这个问题。
由于该主题文章的内容是以 innerHTML
呈现的,故 <script>
标签里的内容是不会运行的,导致我的音乐插件 Aplayer 无法加载,于是我另写了一个页面专门呈现单独的音乐插件,再以 iframe
嵌入当前页面,算是比较完美的一个解决方案了。
这篇文章一发布,预示着我博客栈的目录收录的文章也已到达两位数(10 篇)1,强迫症终于满足了。
最后我想说:
近来准备写几篇文章用于介绍 Python 较高级一些的特性,归为一个系列。本文是这个系列的第一篇文章,主要介绍一下内置的一些数据结构。
对 Pythoner 而言,元组(tuple)、列表(list)、字典(dict)这三个应该最熟悉的数据结构了,恰当使用这三个数据结构的话的确可以应对大部分的使用场合了,但有时因为其它方面的问题(内存占用、插入效率、删除效率等),我们仍有必要学习其它不那么常见的数据结构。
初学者可能会认为在 Python 里,列表(list) 就是数组(array),其实不然。数组应当是一系列类型相同的变量的集合,而 Python 中的列表却可以存放任何不同类型的数据。Python 里也是有数组这个概念的,与列表有所不同的是数组里数据的存放方式(类型)并不是 Python 的基础类型(int、float、char)等,而是数字的机器标识(说白了就是和在 C 语言中存储方式一样),也因此,在将数据存入和读取文件时效率会更高一些。
array.array
支持的数据类型包括整数、浮点数、字符三种,其中创建每个数组需要一个类型码(Type code),用以标识在 C 语言中存放怎样的数据类型,比如 array('l')
这样创建的就是存放四个字节大小的整数,范围从 - 2^31 到 2^31 - 1(更多的类型码使用 help(array) 查看)。
from array import array
from reprlib import repr
ints = array('l', (i for i in range(10**7)))
>>> repr(ints)
"array('l', [0, 1, 2, 3, 4, ...])"
# write to file
with open('ints.bin', 'wb') as fp:
ints.tofile(fp)
ints2 = array('l')
# read from file
with open('ints.bin', 'wb') as fp:
ints2.fromfile(fp, 10**7)
数组支持列表的大部分操作(准确地说是支持所有和可变序列的有关的操作),包括 pop()
、insert()
、extend()
等。
>>> len(ints)
10000000
>>> ints[-1]
9999999
>>> ints.pop()
9999999
在排序这里与列表有一点小区别,列表支持 list.sort()
这种就地排序的方法,但数组不支持,所以想对数组进行排序的话,得用 sorted
新建一个数组:
tmp = array(ints.typecode, sorted(ints))
队列的特性是先进先出,虽然我们可以把列表当作队列来使用:append()
来模拟进队列,pop(0)
来模拟出队列:
class Queue:
def __init__(self, queue):
self.queue = list(queue)
def pop(self):
return self.queue.pop(0)
def push(self, num):
self.queue.append(num)
def __repr__(self):
return 'Queue<%s>' % self.queue
q = Queue(range(5))
>>> q
Queue<[0, 1, 2, 3, 4]>
>>> q.push(5)
>>> q
Queue<[1, 2, 3, 4, 5]>
>>> q.pop()
1
似乎看上去很完美,但是这种方法的弹出操作是很耗时的,因为删除列表的第一个元素会牵扯到移动列表里的所有元素。
这里介绍标准库中两种不同的队列:
collections.deque
类提供了一个双向队列,也就是说 deque
也完全可以栈来使用。它的 append()
、appendleft()
和 pop()
、popleft()
都是原子操作,这也意味这 deque
是线程安全的。deque
同样实现了所有和可变序列相关的操作。deque
可以接受一个可选参数(maxlen
)表示队列可容纳元素的数量:
from collections import deque
dq = deque(range(5), maxlen=5)
>>> dq
deque([0, 1, 2, 3, 4])
>>> dq.append(5)
>>> dq
deque([1, 2, 3, 4, 5])
>>> dq.popleft()
1
>>> dq
deque([2, 3, 4, 5])
>>> dq.appendleft(1)
deque([1, 2, 3, 4, 5])
需注意,一旦添加了 maxlen
属性,这个属性就无法修改了。当对一个已满的队列进行添加操作时(第 5 行),另一头的元素会被挤掉。
(原谅我想不出一个好名字了,只能用单向队列来和刚刚介绍的双向队列做区分了)。
queue.Queue
类提供的是一个单向的队列,它与上面双向队列最大不同除了它是单向的之外,还有对于队列已经满了或空了的情况下,还要对队列进行添加或删除操作的结果不同:在满员(为空)时,如果还向 Queue
中插入(取出)元素的话,它不会扔掉旧的元素来腾出位置,反而是会锁住——直到另外的线程移除了某个位置,这一特性很适合用做生产者——消费者的模型,尤其是当生产者的生产时间与消费者的消费时间不匹配的情况,比如:生产者的生产时间快于消费者的消费时间,如果采用 deque
的话,就会丢失生产者最早时候生产的数据(反之就会造成消费者从空队列中取出数据的情况)。
from queue import Queue
from threading import Thread
import logging
import time
logging.basicConfig(level=logging.INFO,
format=('%(asctime)s %(message)s'))
queue = Queue(1)
def consumer():
time.sleep(.1)
queue.get()
logging.info('Consumer got 1')
queue.get()
logging.info('Consumer got 2')
def producer():
queue.put(1)
logging.info('Producer put 1')
queue.put(2)
logging.info('Producer put 2')
thread.join()
logging.info('Producer done')
thread = Thread(target=consumer)
thread.start()
producer()
>>>
2018-08-07 19:56:42,234 Producer put 1
2018-08-07 19:56:42,335 Consumer got 1
2018-08-07 19:56:42,335 Producer put 2
2018-08-07 19:56:42,335 Consumer got 2
2018-08-07 19:56:42,335 Producer done
消费者线程先等待了片刻是为了给生产线程留部分时间,使其在消费者从队列获取之前先将两个对象放入队列。然而,这里的缓冲区容量为 1,这就意味着生产线程在放入第一个数据后,会卡在第二个 put
方法那里,必须等待消费线程通过 get
方法把第一个数据消费之后,才能放入第二个对象。
堆的性质是父节点(下标为 k)的值,总是小于(大于)等于其左(下标为 2k + 1)右(下标为 2k + 2)两个子节点的值。堆这种数据结构实际中多用于实现优先级队列(在 queue
模块中也有优先级队列的实现:PriorityQueue
):在队列中,优先级较高的元素总排在前面。
heapq
模块可以在标准的列表之中创建堆结构:
from heapq import heappush, heapify, heappop
heap = []
heappush(heap, 5)
heappush(heap, 3)
heappush(heap, 7)
heappush(heap, 4)
>>> assert heap[0] == min(heap)
>>> heappush(heap, 2)
>>> heap
[2, 3, 7, 5, 4]
>>> heappop(heap), heappop(heap), heappop(heap)
(2, 3, 4)
heap2 = [9, 8, 7, 6, 5, 4]
heapify(heap2)
>>> heap2
[4, 5, 7, 6, 8, 9]
heappush
和 heappop
方法总会保持堆的性质,将数据插入或弹出,heappop
总会将堆中优先级最高的元素弹出。其中 heapify
可在线性时间内将列表转化为堆。
内存视图(memoryview)可以在不需要复制内容的前提下,在不同的数据结构之间共享内存。当你需要在内存中处理大量二进制数据时,或者需要反复修改内存中某块数据的内容,内存视图可能会对你有很大帮助:因为在 Python 中,对字符串(str)和字节数组(bytesarray)进行切片都是会造成内存的复制,尤其是当需要对较大的数据进行切片的时候,所耗费的代价将会非常昂贵。
from time import time
def mv_vs_bytes(factory, name):
for n in (100000, 200000, 300000, 400000):
data = b'x' * n
t0 = time()
b = factory(data)
while b:
b = b[1:]
print(name, n, time() - t0)
mv_vs_bytes(lambda x: x, 'bytes')
mv_vs_bytes(lambda x: memoryview(x), 'memoryview')
"""
output:
bytes 100000 0.15474176406860352
bytes 200000 0.6353733539581299
bytes 300000 1.5503427982330322
bytes 400000 2.8593809604644775
memoryview 100000 0.008246183395385742
memoryview 200000 0.017104148864746094
memoryview 300000 0.025980710983276367
memoryview 400000 0.03431963920593262
"""
可以看出,对 bytes
切片的时间复杂度是 O(n^2),而对 memoryview
切片总能在线性时间内完成。
当然,由于 bytes
本身是不可变(immutable)的字节序列,如果想对 memoriview
中的数据进行修改的话,就需要用 bytearray
的方式构造 memoryview
对象:
# readonly
b = b'Hello'
mv_b = memoryview(b)
assert mv_b.readonly == True
>>> mv_b[0] = 73
TypeError: cannot modify read-only memory
# can write
ba = bytearray(b)
mv_ba = memoryview(ba)
assert mv_ba.readonly == False
>>> mv_ba[0] = 73
>>> mv_ba.tobytes()
b'Iello'
由于 memoryview
使用了缓冲区协议(协议提供的是 C 语言级别的 API),导致 memoryview
只有在 CPython 中才能发挥它最大的作用。
本系列全部文章可访问「 知多少 」标签查看。
参考:
]]>在大二上《计算机网络》这门课的时候,由于并不是很喜欢这门课的老师,导致我在上课的大部分时间都在摸鱼~~(啊喂,学校教的哪门课你没在摸鱼啊?)~~。最近看了《 图解HTTP 》这本书,借这本书正好也复习了一下应用层和传输层协议,毕竟现在的 Web 应用几乎都是在应用层的 HTTP 协议运行的,而 HTTP 又是基于传输层的 TCP 协议来实现的。
我一直认为检验学习新知识是否牢靠最好的方法就是写一个小的实例,于是乎,借助于 Socket 模块(仅对 BSD Sockets API 进行封装),我也实现了一个静态的 HTTP 服务器,当然,比标准库提供的 SimpleHTTP 要强一点,因为我编写的支持并发。源码见 这里 。
HTTP 协议是基于 TCP 协议来实现的,也就是说要实现 HTTP 服务器首先就需要先创建一个 TCP 连接,而一个完整的 TCP 连接是同时需要客户端和服务端的,而客户端和服务端的创建,就需要借助 Socket(套接字)了。
通常创建一个 Socket 需要为其指定地址族(包括本机、IPV4、IPV6)、套接字类型(流式、数据报式,分别对应 TCP 和 UDP)
from socket import AF_INET, SOCK_STREAM, socket
# create a tcp socket
sock = socket(AF_INET,SOCK_STREAM)
# equal to
sock = socket()
随后需要为该 socket
绑定一个 IP 地址和端口,并开始监听该地址(listen 可传入参数,表示排队连接的数量):
sock.bind(('0.0.0.0', 8888))
sock.listen()
随后,就可以等待客户端发起连接请求了:
conn, addr = socket.accept()
随后该连接会阻塞,直到 accept 到客户端的连接之后(客户端可使用 telnet 0.0.0.0 8888
来连接),随后程序就会继续运行,这时就可以通过 socket 连接来传输数据了,在 telnet 输入任何字符,随后在客户端接收,再响应请求:
# accept 10 bytes data
conn.recv(10)
# send response
conn.send(b'hello world')
我们后续编写 HTTP 服务器仍是基于这一套流程,只是在客户端请求和服务端应答的内容不一样,故而封装成一个类,方便继承,以下为一个回显服务端,从客户端接收到的任何消息都会将其返回:
from socket import AF_INET, SOCK_STREAM, socket
class EchoServer:
def __init__(self, port=8888, addr='0.0.0.0', family=AF_INET,
type_=SOCK_STREAM, backlog=0, init=True):
self.addr = addr
self.port = port
self.family = family
self.type_ = type_
self.backlog = backlog
def _echo(self, sock: socket):
while True:
try:
req_head = sock.recv(1)
except BrokenPipeError:
break
else:
if not req_head:
break
sock.send(req_head)
def _run(self):
self.sock.listen(self.backlog)
while True:
sock, addr = self.sock.accept()
print('Connect by {} Port {}'.format(*addr))
self._echo(sock)
def __call__(self):
self.sock = socket(self.family, self.type_)
self.sock.bind((self.addr, self.port))
print('Listen in %s port.' % self.port)
self._run()
>>> serve = EchoServer()
>>> serve()
"""
telnet 0.0.0.0 8888
Trying 0.0.0.0...
Connected to 0.0.0.0.
Escape character is '^]'.
Hello
Hello
I'm Wincer.
I'm Wincer.
"""
Listen in 8888 port.
Connect by 127.0.0.1 Port 45186
...
那么有了 TCP 连接,该怎么实现 HTTP 协议呢,其实很简单,HTTP 协议只是在传输的内容上做了规定:满足「报文首部」、「空行」、「报文主体」,这样通过服务器发出去就算是一个 HTTP 报文了,不信?试试就知道了。
将上面的 EchoServer 中的 _echo
方法修改一下,让其返回以下数据:
data = """HTTP/1.1 200 OK
Content-Length: 11
Hello World
"""
class EchoServer:
def _echo(self, sock):
try:
req_head = sock.recv(1)
except BrokenPiperError:
return
else:
if not req_head:
return
# 注意这里必须要将字符串编码成 bytes 才能发送。
sock.send(data.encode('utf-8'))
sock.close()
>>> serve = EchoServer(8888)
>>> serve()
Listen in 8888 port.
咳咳,准备好了吗,打开浏览器,输入 http://0.0.0.0:8888
,如无意外,你就可以在屏幕上看见 Hello World
了。
这就算最「小」的 HTTP 服务器了,不管向它发送什么请求,不管请求的是什么,它都会返回 Hello World
:
curl http://0.0.0.0:8888
Hello World
curl http://0.0.0.0:8889/\?test
Hello World
curl -X OPTIONS http://0.0.0.0:8889/\?test
Hello World
curl -X POST http://0.0.0.0:8889/\?test\¶m\=block
Hello World
这是因为我们还没有对请求报文首部进行分析,从而根据请求路径的不同或者请求方式的不同来返回相应的数据。
既然要做一个静态的服务器,最少也应该分析 GET
请求,根据请求的 URL 作出响应,那么就需要增加额外的函数了:
class HttpServer(EchoServer):
def _echo(self, sock: socket):
try:
req_head = sock.recv(1024)
except BrokenPipeError:
return
else:
if not req_head:
return
head = self._get_head(req_head)
sock.send(head.encode('utf-8'))
self._send_body(sock)
logging.info('HTTP/1.1 %s GET %s' % (self.status, Signal.path))
sock.close()
我这里(在 _echo
中)增加了两个函数:_get_head
和 _send_body
。作用分别是根据客户端的请求报文的首部来生成相应的服务端响应报文首部和根据客户端的请求 URL 发送响应的报文主体内容,比如,请求首部:
GET /index.html HTTP/1.1
HOST: 0.0.0.0:8888
USER-AGENT: curl/7.61.0
Accept: */*
响应首部(可将请求的资源以 rb
模式打开,并读入内存,再作为响应报文主体发送):
HTTP/1.1 200
Content-Length: 11
Content-Date: Thu, 02 Aug 2018 03:58:09 GMT
Server: TinyHttp
Hello World
有关这两个函数的具体实现,可以参考我这部分的 源码 。
我们的服务器现在已经可以根据 GET 请求的 URL 来返回相应的报文了,很好,但现在的服务器不支持并发请求,也就是说必须先对前一个请求作出完整的响应,并将响应发送出去之后,才能处理下一个请求,造成这种后果最重要的一点原因就是:socket.recv()
和 socket.send()
都是阻塞型 I/O 函数,也就是说,CPU 会一直等待这两个函数执行完成才继续执行后面的代码。
虽然在本地局域网内,作出大部分响应的时间都很快(毫秒级别),但我们仍有必要对阻塞型 I/O 函数进行优化,优化方法有两种:
其中第一个方法很简单,借助 threading
模块即可实现,重写一下 _run
方法:
class ThreadHttpServer(HttpServer):
def _run(self):
self.sock.listen(self.backlog)
while True:
sock, addr = self.sock.accept()
Thread(target=self._echo, args=(sock,)).start()
而第二个方法就需要借助 Asyncio 这个库了(由于借助了 Asyncio 这个库,要求 Python 版本为 3.5+),该库重写了标准库 socket 中的阻塞 I/O 函数,将其改为了非阻塞形式的异步调用,由于该方法改动的地方太大,就不贴完整的代码了,可移步至这部分的 源码 。
同我在 之前一篇博文 提到的类似,这次同样遇上了一些薛定谔的 BUG:
当以 open
函数打开某一个文件时,会把这个文件的内容读入到内存中,如果只是普通的文本或者图片倒是不会出现什么问题,但是一旦读入的文件过大(比如我就喜欢在电脑开启静态 HTTP 服务,然后在局域网内其它的设备打开共享的视频来播放),就会出现两个情况:
于是乎,大名鼎鼎的「generator(生成器)」终于派上了用场。将 _get_body
函数(请求的文件内容)中的 open
函数作为一个生成器,每读取一行(readline()
)就 yield 一次,在 _send_body
函数中不断对 _get_body()
返回的数据进行迭代发送,这样既不会一次性全部读入内存,造成内存空间不足、又不会花费过多的时间在 I/O 上,一举两得,当然,为此你需要加上一个 Content-Lenght
的首部,用以告诉客户端什么时候接收完毕。
当请求的是目录时,URL 最尾端应当为 /
,这时返回的应该是该目录下的 index.html
文件,如没有的话就返回该目录下的文件列表(同样的,列表中的目录应当以 /
在末尾标识),如果点击了该目录下的子目录,则应递归的显示子目录。
但当以 os.listdir
列出文件列表时,并不会显式的将目录以 /
标识,而仍需我们手动判断,当请求同名目录但末尾没有 /
时,应当将状态码设置为 301,并在响应头部加上 Location: https://localhost:8888/xxx/
用以显式的指向目录。
其实这个服务器在结构上并不复杂,甚至可以说简单,就是依据 Socket 建立 TCP 连接,再分析请求首部得到的 URL,用 rb
模式加载并作为响应主体返回,但也确实让我学习到了不少:比如说「面向对象」范式的好处,即在构建以 TCP->HTTP->ThreadHTTP、AsyncHTTP
这样自顶向下的结构时,继承(ThreadHTTP
继承于 HTTP
,而 HTTP
又继承于 TCP
)可以大大的减少代码量和提高可重用性;再比如说生成器,即惰性求值的好处(节省内存),这好像还是我第一次正式在代码中用到生成器。
而这两点,想来只有自己在生产代码中遇到过,才能切实体会到好处。
]]>去年夏天的时候,用 Flask 开发了一个 简易版的一言 ,算是最初的 beta 版,部署在了 Heroku 上面(那时我还没购买服务器),由于 Heroku 免费版有时间池的限制,在我购置了服务器后就重新用 Go 重写了一下 部署在自己的服务器上 ,算是 1.0 版,这两天又重新拾坑,开发出了 2.0 版本。
在 1.0 版本使用了较长的时间后,基于以下考量,我还是重构了部分代码:
于是乎,本着「生命不死,折腾不止」的态度,2.0 版本诞生了。
本 API 的 源码 已开源至 GitHub,如有需要的可自行搭建。
以下是 2.0 版本的更新日志:
爬取数据时采用了异步爬虫,解决了 1.0 版本爬取时效率低下的问题,同时选取了 xxhash 作为散列函数,将一言主体 hash 后,得到的 64bit 的无符号整数作为主键,这样如果爬取到了重复的一言也不会插入数据库中。
得益于异步爬虫的高效率,在很短的时间内,爬取到了足够的一言数。目前,数据库内共有 15371
条一言。以后数量还会不断地增加。
爬虫程序
已托管至 GitHub。
数据库更换成了 MySQL,以承受高并发访问,以下为建表语句:
+----------+---------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------+---------------------+------+-----+---------+-------+
| id | bigint(20) unsigned | NO | PRI | NULL | |
| hitokoto | varchar(300) | NO | | NULL | |
| source | varchar(64) | NO | | NULL | |
| origin | varchar(12) | NO | | NULL | |
+----------+---------------------+------+-----+---------+-------+
2.0 版本共包含以下请求参数:
格式为 encode=
,包含以下四个参数值:
class = 'hitokoto'
的标签中×××××——「×××」
,即主体 + 出处格式为 charset=
,包含以下两个参数值:
content-type
字段添加 charset=utf-8
格式为 length=
,会随机返回一条不超过这个查询长度的语句。
格式为 callback=
,会根据回调参数的值返回对应的函数调用,其中函数的参数为一个字典,key 分别为 hitokoto
和 source
。
注意:callback 参数会覆盖掉 encode 参数
调用地址:https://api.itswincer.com/hitokoto/v2/
例如,我想请求一个长度不超过 10 的一言,并以 JSON 格式返回:
curl 'https://api.itswincer.com/hitokoto/v2/?encode=json&length=10'
{"hitokoto":"(눈_눈)","source":"进击的巨人"}
如果想在自己的网页使用的话,可以采取以下两种方法:
只需要在想要展示的标签加上 class='hitokoto'
属性,随后在任何地方加上:
插入页面的显示结果是:××××××× ——「×××」形式。
展示结果见侧栏。
如果对 encode=js
返回的格式不满意,可自行定义页面展示的格式:比如以下代码仅展示一言的主体部分:
定义标签和函数:
随后将请求地址加上参数 callback=showHitokoto
:
以上示例将会在 HTML 标签首个包含 class='hitokoto'
的标签内部插入仅包含一言主体的部分。
你看到某句熟悉的一言从屏幕上显示的时候,勾起了之前第一次看到这句话时或感动、或开心、或难过的回忆,而某个陌生人也会因此和你一样陷入属于他的短暂回忆——想到这些不是很快乐吗?而我想那个陌生人一定也正想着同样的事情。我一直这样觉得。
而这,应当就是文字赋予一言的最大作用了。
]]>我的博客 建站至今 也一年有余了,本想着在一周年(今年 5 月初)之际写一篇文章纪念一下,顺便公布一下本博客在这一年的访问情况,可当时发现统计记录还没有满一年(我是在 2017 年 6 月底才开始使用的 Google 分析),于是就想干脆等到 6 月底再写。而前段时间又忙于准备期末考试,直到昨天放假回家,似乎才有时间写这一篇文章。
首先我并不确定 Google 分析的准确性有多高,因为当我查看 Cloudflare 自带的分析功能时,得到的数据与 Google 分析的有很大很大的差别。以近一周的数据做对比:
虽说 Cloudflare 统计了所有的 HTTP 请求,但我博客实则只有 html 页面才会走 Cloudflare 的线路,其它的静态资源我都放在 CDN 了。而 Cloudflare 对于近一周访问 html 页面给出的数据是占总请求数的 48%——约 3411 次,这应该是与 Google 的页面浏览量(540)作为对比(加上其余两个子网站的浏览量分别为 121、4),可以看到仍然有将近 7 倍的差距,用户数也有近 5 倍差距——我确实想不出一个合理的解释(根据 Cloudflare 给出的解释,可能是由于某些网络爬虫,故与基于 Javascript 的统计工具来说有较大出入)。
但 Cloudflare 无法给出像 Google 分析那样包括平均会话、每次会话浏览数、跳出率等等指标,故本篇博客还是选取 Google 分析的数据进行分析。
在过去的一年零两周内,本博客的基本访问情况如下图:
这期间,本博客一共迎来了 5,446 位用户,他们一共产生了 10,508 次会话以及 19,989 次浏览。平均每天 14 位用户、28 次会话、53 次浏览。
图表中有一个较为凸出的高峰(5 月 29 日),原因是我在 V2EX 发了帖介绍自己写的一个 表情包生成工具 ,这个工具中算是间接性的把用户引导至本博客了。
其中流量获取的来源主要是三部分:Referral(引荐)、Organic Search(搜索引擎)、Direct(直连)。
其中 Google 的流量占了大多数:36.80%,其次是本博客自身的引荐;本博客并没有添加百度站长的信息,并且主动屏蔽了百度蜘蛛的爬取,故并没有来自百度的流量。
现在的 Google 分析为了保护用户隐私,已经无法显示用户查询的关键词了。
毫无疑问,本博客的主要流量都来自于中国大陆,不过令我感到奇怪的是第二名是美国。
在操作系统语言中:
简体中文(zh-CN + zh-cn)占了 72.27%,较地理为中国大陆的 68.93% 多出了 3.34 个百分点;
英语(en-US + en-us)占了 16.91%,较地理为美国的 11.06% 也多出了 5.85 个百分点;
也就是说,并非只有美国地区的人才会使用英文,也并非中国地区的人才会使用中文。
首先看看浏览器的占用,由于本博客的类型更偏技术一些,故 Chrome、Safari、Firefox 的使用占据前三甲,其中 IE 的份额不足 0.4%,这意味着我并不需要照顾 IE 的用户,可以尽情使用各种新技术。
第四的 Android Webview 应该是指 App(QQ、微信) 内置的浏览器。
操作系统毫无疑问是以 Windows 独占鳌头,其次是 Linux,我一直使用 Manjaro Linux 作为日常开发,比 Windows 方便许多,也没有 Windows 那么多 Bug,由于娱乐方式的缺乏,在 Linux 下开发也会更加专注。
在本站可访问页面中(仅统计文章页面),浏览量的前六名分别是:
(唉,最满意的几篇文章浏览量反倒是挺低的,心情复杂.jpg。)
自本博客运营至今共发布了 53 篇文章,其中自 17 年 9 月以来,发文的频率明显降低:首 4 个月发了 33 篇文章,17 年 9 月至今却只发了 20 篇。一方面是刚建站的时候事比较多;另一方面是相较于博客数量来说,更开始注重博客质量了。
最初我选择运营独立博客,并非想从中得到什么实质性的好处,只是作为一种兴趣。而如今在快餐时代坚持写独立文章的人越来越少,这也无可厚非,毕竟短期内看不到结果的话,有些人就无法坚持了。我周围的人似乎对此(我写博客这件事)也表示不太理解,但我还是会一直做下去。我一直认为,只要能长期的投入一件事中,最终一定能从中获取到乐趣和满足感。
我就是如此。
]]>我的博客在建站后不久就使用了 Travis CI 自动部署服务,即我只需要将修改的源码推送至 GitHub,Travis CI 会自动将我提交的代码拉取,在 Travis CI 端生成静态文件后,同步至我的服务器,这样可以减少一些麻烦的步骤:可以直接在 GitHub 端修改代码;不用等待生成静态文件、压缩静态文件的时间。
虽然使用 Travis CI 是能简化部分开发流程,但这货和 GitHub 是一对一的,只支持在 GitHub 托管的项目,并不支持 Bitbucket 和 GitLab,而 GitHub 免费版在私人仓库这一方面是比不上 Bitbucket 和 GitLab 的(虽然我是学生,可以使用 GitHub 私人仓库,可我也不一直是学生呀),同时支持 Bitbucket 的和 GitHub 私人仓库的 CI 工具(自建的除外)好像真的也就 CircleCI 了,这里之所以没有考虑 GitLab 是因为 GitLab 自带有 CI/CD,而且这家公司给我的印象实在不太好(包括之前的删库事件,以及莫名奇妙的 Bug)。
在了解 CircleCI 后发现比 Travis CI 真是强不少(CircleCI 是基于 Docker 和 Workflows 设定模式的),不过在网上并没有很完善的中文教程~~(虽然官方英文文档已经很完善了)~~。所以如果你懒得翻官方文档的话,继续往下看我这篇文章就好了🤓。
CircleCI 支持 GitHub 和 Bitbucket 帐号的登录,授权登录完成后,就可以添加 Projects 了,支持 GitHub 和 Bitbucket 的公有及私有仓库。这里以我的 Meme-generator 仓库为例。
选完仓库后,就可以开始配置 CircleCI 了。
Meme-generator 仓库用到 SSH 密钥的地方有两处:
如果你是用来推送至 GitHub 的话,可以直接用 GitHub 提供为该仓库提供的 Token 密钥,第一点也可以使用 HTTPS 方式克隆,就可以省去添加 SSH 密钥这个步骤。
点击 CircleCI 个人主页的 JOBS 菜单项,随后点击仓库名称右边的齿轮按钮 -> 点击 SSH Permissions
-> 点击蓝色的 Add SSH Key
按钮,将私钥(看清楚了,是私钥)粘贴进去(超级良心有木有啊,比 Travis CI 将私钥加密上传这种土办法不知道高到哪里去了)。
添加 SSH 密钥后,还需要将服务器的 IP 添加至 known_hosts 列表,否则每次部署的时候都会让你确认以下消息:
The authenticity of host '××.×××.×××.×××' can't be established.
ECDSA key fingerprint is SHA256:7hkfahfla8VeiuyF/TLHKfhakgcJ0sHjaLxDyIKlfhak9fuaofoa.
Are you sure you want to continue connecting (yes/no)?
同 Travis CI 类似,CircleCI 在运行的过程中也是不接受命令行输入的(当然运行完成后就更不行了),所以我们需要提前将 IP 写入 known_hosts(在 CircleCI 中如何做?继续往后看):
ssh-keyscan $SSH_IP >> ~/.ssh/known_hosts
在该仓库的管理页面中的 Environment Variables
选项卡中添加 SSH_IP 的环境变量。
由于我的配置文件太过长了,先以一个简化版为例:
version: 2
jobs:
build:
docker:
- image: circleci/node:10.4.0
steps:
- checkout
- run:
name: Install Dependence
command: |
yarn install && yarn build
- run:
name: Deploy
command: |
echo "Denpendence installed."
首先指明 CircleCI 的版本号——2.0(1.0 在 18 年 9 月之后就停止支持了)。
其次,为 Docker 指定 image( 这是 官方已经构建完成的镜像列表),可以指定多个 image。先前提到过,CircleCI 并不默认像 Travis CI 那样提供 Linux 虚拟机镜像,推荐使用的是 Docker(当然你也可以指定工作方式为 Machine),这是官方针对 Docker 和 Machine 的 对比报告 。
随后在 steps
里面是需要运行的指令:
checkout
是一个用于检查配置路径的源代码的特殊步骤,并可以通过 SSH 来 clone 远程仓库的代码(如果你已经添加了 SSH 私钥的话,不然就只好手动 clone 了),详解见
官方文档
run:
后面接的是 bash 命令,name
该任务的名称,command
为具体 bash 的指令需要注意的是,如果你需要将生成的静态文件同步至服务器所用的 rsync
命令是没有被安装的,只有
这些命令
是被安装在所有镜像中的。
docker 镜像预装的系统是 Ubuntu,可采取 apt-get
命令来安装需要的软件包:
run:
name: Update System
command: |
sudo apt-get update && sudo apt-get install rsync
CircleCI 建议的 Workflows 中建议将整个工作流分割成不同的子作业,比如说以 Yarn 项目为例,可以分成 build
和 deploy
两个流程。其中 build
用以安装依赖和生成待部署的静态文件;deploy
用以将生成的静态文件部署至服务器。
可以看出,静态文件是横跨两个作业的,所以我们需要将包含静态文件的文件夹缓存下来(当然你也可以选择不使用 Workflows,这样就只需创建一个工作就好了),在 build
工作中缓存采取如下命令:
steps:
- restore_cache:
keys:
- build-v1-{{ checksum "package.json" }}
paths:
- "build"
以上命令是将 build
文件夹以 key-value
形式缓存,其中 key
选择的是 package.json
的哈希值。这里的文件名最好选择仓库自带的文件。更多 key
的形式可以参考
这里
。
在 deploy
工作中恢复缓存采取以下命令:
steps:
- checkout
- restore_cache:
keys:
- meme-v1-{{ checksum "package.json" }}
注意在 restore_cache
之前一定要有 checkout
命令。
直接放 Meme-generator 项目的配置代码了: 点我 。
每次构建完成后,commits 列表的画风就变成这样了:
点击 Details 就会显示每次构建的详细过程。
虽然本文名为「使用持续集成(CI)开发项目」,但实际却好像只介绍了 CircleCI,当然我的意思不是钦定 CircleCI 作为最好的持续集成系统,我没有说 CircleCI 是最好的持续集成系统,没有任何这个意思。但你一定要问我为什么选 CircleCI,它现在对 Bitbucket 和 GitHub 的私人仓库支持最完善,我怎么能不支持它呢?
参考:
]]>前一段时间「这个仇我先记下了」的表情包突然火了,导致我也萌生了自己写一个表情包生成工具的想法,毕竟我是重度表情包玩家😌。其实之前我就很喜欢做表情包,不过是用的 PS 等软件,有些麻烦,而且改 GIF 也不太方便。
于是乎,我决定也蹭一波热度,也写了一个,最初是只有「记仇」这个静态表情包的,现在加上了王境泽、为所欲为、打工是不可能打工的等等动图,模板后续还会添加,如果有好的素材可以私我。
当然网上也有一些表情包生成器,比如「
sorry
」,但界面我不太喜欢,而且我觉得这类较为简单的处理没必要借助服务器端渲染合成,直接在浏览器端渲染就好了,毕竟 JavaScript 算是一门「万能的语言」。
核心思路是采用 omggif 对 GIF 进行解码,再用 Canvas 将文字绘制在每一帧上,最后再用 gif.js 将每一帧合成,再渲染后输出成 Blob 文件对象( 现在不支持 Blob 的浏览器应该没有了吧? ),传递给 IMG 标签进行显示。
这是解码过程:
// 解码
let gifReader = new omggif.GifReader(buffer);
// 获取帧
let frameZero = girReader.frameInfo(0)
// 获取帧的宽高,绘制 Canvas 的时候会用到
let [width, height] = [frameZero.width, frameZero.height]
let imageBuffer = new Uint8ClampedArray(width * height * 4)
gifReader.decodeAndBlitFrameRGBA(frameNum, imageBuffer);
// 生成图像数据,供 Canvas 使用
let imageData = new window.ImageData(imageBuffer, width, height)
这是绘制过程:
ctx.putImageData(imageData, 0, 0)
// 这是字幕的白边
ctx.strokeText(caption, width / 2, height - 5, width);
// 这是字幕的主体
ctx.fillText(caption, width / 2, height - 5, width)
这是编码(渲染)过程:
let gif = new GIF({
workers: 3,
quality: 10,
width: imageWidth,
height: imageHeight,
})
// Canvas 的数据加入帧
gif.addFrame(ctx, {
copy: true,
delay: frameInfo.delay,
dispose: -1
})
// 开始渲染
gif.render()
// 渲染完成
gif.on('finished', Blob => {
gifUrl = window.URL.createObjectURL(Blob);
img.src = gifUrl;
})
以上是动图的设计思路,静态图就显得简单多了,采用 dom-to-img 绘制就行了,但是在 Edge 上似乎是无法使用的,作者提到似乎是因为添加了 foreignObject 标签,导致 toDataUrl() 在 Edge 上无法工作,所以 Edge 用户只能使用动图部分了。
其实核心思路很简单,gif.js 和 omggif 提供的 API 也不复杂,但我还是花了将近一周的时间,因为这是我首次使用 React 开发应用,所以有大半时间都花在了学习 React 上,然而写出来的结果还是偏「Pure JavaScript」一些。
本项目 采用 create-react-app 构建,CSS 框架采用了 bulma ,部分动图模板来自 sorry 。
刚刚有提到,我在设计该工具的时候大部分时间都没有花在核心思路部分,而是花在了——我称为「薛定谔的 Bug」上,即:你在设计该工具的蓝图的时候,没有设想到会出现这些 Bug,而实际编程中,也不一定会遇到,只有你亲自编写了,才知道这 Bug 是否会出现。
我在这次编程中就遇到了四个「薛定谔的 Bug」:
关于静态图部分,我设计了两个按钮:「戳我预览」和「戳我下载」,其实本应该只需要一个下载按钮就够了,因为我使用 contenteditable 属性以编辑 p 标签。和生成的预览图几乎没什么差别,那么为什么要设计两个呢?就是因为 Blob 对象(后续思考了一下,虽然可以先行判断浏览器是否支持 Blob 下载,但针对动态图还是需要预览修正的,故为了设计上的统一性,还是将预览按钮保留了)。
其实大部分人应该是没有听说过这个名词的(包括我),但它还真的不是一个新玩意,甚至都不是 HTML5 新增的 API,相比于 HTML5 在 2014 年才完成标准制定,在 MDN 上查到 Blob 对象在 2010 年就被主流浏览器支持了(Chrome 5、Firefox 4、Opera 11.1),但,如今大部分手机浏览器却仍不支持 Blob 文件下载协议。
所以只好提供一个预览按钮来供不支持 Blob 文件下载协议的浏览器长按进行保存。
由于我的服务器是在国外,而且还套了一层 Cloudflare,故而在某些情况下,加载动图会非常慢,尤其是在晚上(大约花费 1min,而且居然还没断,我真是很佩服 Cloudflare 的稳定性)。
当然图片的加载问题还不算大,可以放在
支持跨域的图床
上,由 fetch
调用,问题最大的是 Web Worker(合成 GIF 的时候需要使用),但这个 Web Worker 的地址在 Chrome 下只允许同域名下的脚本,即使是公共 CDN 上允许跨域都不行。
这里采用还是借助 Blob 对象,巧妙的规避这一限制:
let tmpWorker = await fetch(url),
workerSrcBlob = new Blob([await tmpWorker.text()], { workerBlobURL = window.URL.createObjectURL(workerSrcBlob);
在将代码生成「production build」时,遇到了一个 Bug,有时访问二级路由会出现 404,多次复现后,终于确定了:
在访问二级路由时,如果是正常从一级页面点击跳转的,则会正常访问;
但如果是直接访问二级路由或者是在二级路由刷新页面,则会出现 404;
但是这个 Bug 在「development build」中是没有的,原因在于当你点击路由时,并不是直接向服务器发起请求,而是由 react-router 路由库给出路由网址,故而刷新二级路由页面或者直接访问二级路由页面服务器是无法正确响应的。
以下是解决办法,在 Nginx 中添加 try_files
语句:
server {
location / {
try_files $uri /index.html
}
}
当我解决了以上问题的时候,我发给室友首先试用,看到了「戳我预览」这个按钮,他就以「单身十八年」的手速猛戳了四五下,随后标志着渲染进度条就「鬼畜」了起来。因为他猛戳的那几下相当于在后台启动了好几个渲染程序,不仅会让进度条「鬼畜」起来,如果你以更快的手速戳的话(单身八十年?)还会让 CPU 负担加重,甚至会卡死,当然我是没有试过。其实这 Bug 算是无伤大雅的,本不太需要修复,因为不像其它生成器拿服务器做后端,可能会造成服务器宕机,我的纯前端写的。但我本着人道主义情怀、不让我的 Bug 陪我过夜的心理,以及最重要的强迫症,还是决定修复这个 Bug。
其实很简单,设置一个全局变量 finished
,在渲染的过程中,该变量为 false
,渲染完毕后设置成 true
,再将渲染过程放置在 if(finished)
内就解决了。
见 本项目的 Wiki 。
本工具还有很多需要改进的地方,比如 React 的写法不够规范、没有完全实现静态动态资源分离、用户自定义添加模板等等,这些我在空闲时间里都会一点点的改进。
目前在实用的角度来说,该工具已经可以投入使用了,剩下的细节就需要慢慢雕琢了。:)
参考:
]]>算算时间有段时间没写技术类的文章了,部分原因是最近过得确实比较忙。当然,也并没有忙到完全抽不出时间写博客,根本原因还是没有找到啥好的写作素材,随随便便糊弄一篇我又有点不好意思发上来,于是乎,就一直搁置到现在。
对于字典这一基础的数据结构来说,其对 Python 的程序重要性是无可替代的,在《代码之美》一书中,作者是这么描述的:
字典这个数据结构活跃在所有 Python 程序的背后,即便你的源码里并没有直接用到它。——A.M.Kuchling
在 Python 程序里,无论是模块、函数、还是对象,均有自己的「命名空间」,而这命名空间即为一个字典(dict),key 就是变量名,value 就是变量值,除去「命名空间外」,对象的函数(方法)关键字也是存放在字典中,此时的 key 就是函数(方法)名,value 就是该函数(方法)的引用。可以采用 __builtins__.__dict__ 来查看这些函数(方法)。
Python 的字典是依据 散列表 (也叫哈希表)来实现的,首先简单介绍一下散列表的原理。
散列表中的每一个单元称为表元。在 dict 的实现里,每个 key-value 均占用一个表元,其中 key 为键的引用(这里是键的引用,而不是键本身,因为 key 可以为任意可散列对象),value 为值的引用。因为是引用:表元大小均一致,所以可通过偏移量来读取某个表元。
在 Python 中,散列函数由 hash() 方法出任,当我们查询 my_dict[search_key] 时,Python 会调用 hash(search_key) 来计算 search_key 的散列值,并将这个值的低几位数字当作偏移量,在散列表中查找表元,具体是几位,需要根据散列表的大小来决定。若表元为空,则说明 search_key 不存在,抛出 KeyError 异常。若非空,则表元会有一对 found_key:found_value,这时若 search_key == found_key 为真,那么就返回 found_value。
如果 search_key 和 found_key 不相等,这种情况成为散列冲突,发生这种情况是因为散列表只把该元素映射到了只有几位数字上。为了解决散列冲突,算法会在散列值中另外取几位,用新得到的数字做偏移量再次寻找。
创建一个字典有许多方式:
a = dict(one=1, two=2, three=3)
b = {'one': 1, 'two': 2, 'three': 3}
c = dict(zip(['one', 'two', 'three'], [1, 2, 3]))
d = dict([('two', 2), ('one', 1), ('three', 3)])
e = dict({'three': 3, 'one': 1, 'two': 2})
>>> a == b == c == d == e
True
在刚刚的原理中说到,由于字典的索引是根据 hash() 函数来获得的,所以 dict 其实是无序的,这也解释了为什么上面代码中的等式会成立。
没错,在 Python3+ 里,推导式不再是列表的特性了。
numbers = range(5)
numbers_square = {number: number ** 2 for number in numbers}
最简单的方法是采用下标方式来查询。即:my_dict[key],这也是推荐的方法,但这是 key 存在的情况,而现实中,一定会遇到 key 不存在的时候,这时就会 raise 一个 KeyError。以下有几种解决办法:
my_id = {'name': 'wincer'}
>>> my_id.get('name')
'wincer'
>>> my_id.get('age', 'default')
'default'
若 key 存在,则返回对应的 value,若 key 不存在,且传入第二个参数,那么返回该参数,若无第二个参数,则返回 None。
from collections import defaultdict
my_id = defaultdict(list)
my_id.update({'name': 'wincer'})
>>> mydict['name']
'wincer'
>>> mydict['age']
[]
defaultdict 需要指定一个 factory,当查询 key 不存在时,会创建一个空的 factory 返回。推荐使用这种方式来处理 key 不存在的情况,因为该方法不仅可用于读取 value 值,还可随时用 append 来更新 value。同时需注意:defaultdict 中的参数只会在 __getitem__ 中被调用。如 dd 是一个 defaultdict,k 是一个不存在的键,dd[k] 用 factory 来创造一个默认值,但 dd.get(k) 却仍会返回 None。
当我们调用 my_dict[key] 时,如果 key 是一个字符串,我们会需要用 my_dict[’name’] 来获取,如果你觉得比较麻烦,想直接用 my_dict[name] 的话,可以采用如下方法:
from collections import UserDict
class StrKeyDict(UserDict):
def __missing__(self, key):
if isinstance(key, str):
raise KeyError(key)
return self[str(key)]
def __contains__(self, key):
return str(key) in self.data
def __setitem__(self, key, item):
self.data[str(key)] = item
d = StrKetDict([('1', 'one'), ('2', 'two')])
>>> d[2]
'two'
有时我们可能更懒,想要用类属性类似的 my_dict.name 方法来获取 value,这时,可以使用 __getattr__ 方法:
from collections import UserDict
class AttrDict(UserDict):
def __getattr__(self, attr):
return self[attr]
d = AttrDict([('name', 'wincer'), ('age', '20')])
>>> d.name
'wincer'
并不推荐这样做,因为在 dict 实现中,并没有要求 key 一定为合法标识符,只需要是可散列对象即可,而上面的写法一旦 key 不为合法标识符,会 raise 一个 SyntaxError:
d.update({(0): 'zero'})
>>> d[(0)]
'zero'
>>> d.0
SyntaxError: invalid syntax
如果非常想使用 . 来获取 value 的话,建议使用 namedtuple
当然这也就意味着必须使用合法标识符了:
from collections import namedtuple
ID = namedtuple('ID', 'name age')
me = ID('wincer', 20)
>>> me.name
'wincer'
ID = namedtuple('ID', '(1, 0) age')
ValueError: Type names and field names must be valid identifiers: '(1'
同样借助键查询,可以实现 Python 中没有的 switch … case 结构:
def foo(x):
data = {
0: 'zero',
1: 'one',
2: 'two',
}
return data.get(x, None)
所以说 Python 不设计 switch … case 语句是有原因的,看上面的实现,比 switch … case 不知道高到哪里去了。
在添加键的时候会按顺序添加,同时 .popitem 是会删除并返回字典的最后一个元素而不是像 dict 里面一样可能会删除任意元素。
这个映射会给键一个计数器,每次更新键时都会增加这个计时器,所以这个类型可以用以给可迭代类型计数:
from collections import Counter
ct = Counter('hfkjahfkakhf')
>>> ct
Counter({'a': 2, 'f': 3, 'h': 3, 'j': 1, 'k': 3})
ct.update('fdjlahkla')
>>> ct
Counter({'a': 4, 'd': 1, 'f': 4, 'h': 4, 'j': 2, 'k': 4, 'l': 2})
>>> ct.most_common(2)
[('h', 4), ('f', 4)]
用法见键查询。
在 Python 3.3 后的版本,types 模块引入一个名为 MappingProxyType 的类。如果给这个类一个映射,它会返回一个只读的映射视图。但它是动态的,如果原映射改动,那么它也会相应改动。
>>> int.__dict__
mappingproxy({'__abs__': ,
'__add__': ,
...})
from types import MappingProxyType
d = {1: 'A'}
d_proxy = MappingProxyType(d)
>>> d_proxy[2] = 'x'
TypeError: 'mappingproxy' object does not support item assignment
d[2] = 'B'
>>> d_proxy[2]
'B'
]]>
相对于任何宏伟愿景,对细节的关注甚至是更为关键的专业性基础。首先,开发者通过小型实践获得可用于大型实践的技能和信用度。其次,宏大建筑中最细小的部分,比如关不紧的门、有点儿没铺平的地板,甚至是凌乱的桌面,都会将整个大局的魅力毁灭殆尽。这就是整洁代码之所系。
本书「序」中的这段话完美的诠释了作者写本书的意义。(简评在最后)
有人也许以为,关于代码的书有点落后于时代——代码不再是问题:我们应当关注模型和需求。……扯淡!我们永远抛不掉代码,因为代码呈现了需求的细节。在某些层面上,这些细节无法被忽略或抽象,必须明确之。将需求明确到机器可以执行的细节程度,就是编程要做的事。而这种规约正是代码。
勒布朗(LeBlanc)法则:稍后等于永不(Later equals never)。
多数人都知道一幅画是好还是坏。但能分辨优劣并不表示懂得绘画。能分辨整洁代码和肮脏代码,也不意味着会写整洁代码!
Bjarne Stroustrup(C++ 语言发明者):我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,高处一堆混乱来,整洁的代码只做好一件事。
Grady Booch(《面向对象分析与设计》作者):整洁的代码简单直接。整洁的代码如同优美的散文。整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直截了当的控制语句。
Ron Jeffries(《极限编程实施》作者):简单代码,依其重要顺序:
Ward Cunningham(Wiki 发明者):如果每个例程都让你感到深合己意,那就是整洁代码。如果代码让编程语言看起来像是专为解决那个问题而存在,就可以称之为漂亮的代码。
光把代码写好可不够。必须时时保持代码整洁。
名副其实:变量、函数或类的名称应该已经答复了所有的大问题。它该告诉你,它为什么会存在,它做什么事,应该怎么用。
避免误导:应当避免使用与本意相悖的词。别用 accountList 来指称一组账号,除非它真的是 List 类型。用 accountGroup 或 bunchOfAccounts,甚至 accounts 都会好一些。
做有意义的区分:以数字系列命名(a1、a2,……aN)是依义命名的对立面。这样的名称纯属误导——完全没有提供正确信息;没有提供导向作者意图的线索。
public static void copyChars(char a1[], char a2[]) {
for (int i = 0; i < a1.length; i++) {
a2[i] = a1[i];
}
}
如果参数名改为 source 和 destination,这个函数就会像样许多。
使用读得出来的名称:
private Date genymdhms; // 生成日期,年、月、日、时、分、秒
private Date generationTimestamp;
使用可搜索的名称:窃以为单字母名称仅用于短方法中的本地变量。名称长短应于其作用域大小相对应。
避免思维映射:不应当让读者在脑中把你的名称翻译为他们熟知的名称。
类名:类名和对象名应该是名词或名词短语,如 Customer、WikiPage。避免使用 Manager、Data 这样的类名。
方法名:方法名应当是动词或动词短语,如 postPayment、deletePage 或 save。
每个概念对应一个词:给每个抽象概念选一个词,并且一以贯之。
别用双关语:避免将同一单词用于不同目的。
函数的第一规则是要短小。第二条规则是还要更短小。
函数应该做一件事。做好这件事。只做这一件事。
别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。
最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了。
函数要么做什么事,要么回答什么事,但二者不可兼得。函数应该修改某对象的状态,或是返回该对象的有关信息。
重复可能是软件中一切邪恶的根源。许多原则与实践规则都是为控制与消除重复而创建。
我写函数时,一开始都冗长而复杂。有太多缩进和嵌套循环。然后我打磨这些代码,分解函数、修改名称、消除重复。我缩短和重新安置方法。有时我还拆散类。
大师级程序员把系统当作故事来讲,而不是当作程序来写。他们使用选定编程语言提供的工具构建一种更为丰富且更具表达力的语言,用来讲那个故事。
过程式代码便于在不该动既有数据结构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类。
得墨忒耳率认为,类 C 的方法 f 只应该调用以下对象的方法:
方法不应调用由任何函数返回的对象的方法。换言之,只跟朋友谈话,不与陌生人谈话。
对象曝露行为,隐藏数据。便于添加新对象类型而无需修改既有行为,同时也难以在既有对象中添加新行为。数据结构曝露数据,没有明显的行为。便于向既有数据结构添加新行为,同时也难以向既有函数添加新数据结构。
名词 | 基础定义 |
---|---|
限定资源 | 并发环境中有着固定尺寸或数量的资源。例如数据库连接和固定尺寸读/写缓存等 |
互斥 | 每一时刻仅有一个线程能访问共享数据或共享资源 |
线程饥饿 | 一个或一组线程互相等待执行结束。 |
死锁 | 两个或多个线程互相等待执行结束。 |
活锁 | 执行次序一致的线程,每个都想要起步,但发现其他线程已经「在路上」。 |
对象是过程的抽象。线程是调度的抽象。
并发是一种解耦策略。它帮助我们把做什么(目的)和何时(时机)做分解开。
并发软件的中肯说法:
生产者-消费者模型:一个或多个生产者线程创建某些工作,并置于缓存或者队列中。一个或者多个消费者线程从队列中获取并完成这些工作。生产者和消费者之间的队列是一种限定资源。
读者-作者模型:当存在一个主要为读者线程提供信息源,但只是偶尔被作者线程更新的共享资源,吞吐量就会是个问题。增加吞吐量,会导致线程饥饿和过时信息的积累。协调读者线程不去读取正在更新的信息,而作者线程倾向于长期锁定读者线程。
宴席哲学家:许多企业级应用中会存在进程竞争资源的情形,如果没有用心设计,这种竞争会遭遇死锁,活锁,吞吐量和效率低等问题。
本书后几章主要侧重于讲解 Java 代码的一些例子,对其它语言帮助不大,在这里就不做整理了。
正如我在 上一篇读书笔记 中所说的:每一本中都会充斥着许多作者的自己的观点、看法,而唯有价值观相符合或相接近的人才会觉得本书写得很不错,上一本《黑客与画家》是,这本《代码整洁之道》也是,你可能很难认为变量的命名需要有那么考究,函数的长短有那么重要,心里想着程序能运行就没事,甚至连 WARNING 都忽视掉,这类人想必并不是本书的目标群体。而本书的目标群体在开头已经注明了:你想成为一个更好的程序员。其实我觉得目标群体还可以加上一小撮人:有强迫症的程序员——比如我。
我曾经看自己四个月前的代码能羞愧得钻进地里,心想着怎么能写出这么烂的 代码 。这四个月固然有我对该门语言较高层级的数据结构更加熟悉,能更熟练的操作它们,但更多的是编程观念的改变:需要用心来写代码,不要简单敷衍了事,不要认为程序只要能运行就算成功。程序毕竟还是写给人看的,就算不是为了别人,看着意义明确的变量,缩进优美的段落,结构分明的函数,想必自己心里也会很舒畅的。
]]>我一直很不相信国内的那些云服务提供商(尤其是在李彦宏发表的讲话「中国用户对隐私问题没那么敏感,在个人隐私方面更加开放,一定程度上愿用隐私换方便和效率」后),因为怕隐私得不到保障,故而我的一些隐私数据都是存放在国外的云盘(如 Dropbox、Drive 等)上。
可这俩在国内都被墙了,而手机翻墙总是显得有些不够方便,与是我就琢磨着自己搭建一个云服务,随后就发现了 Nextcloud 这一开源云服务。而网上的教程都太过复杂了,对新手太过不友好,于是乎——一篇近乎傻瓜式的 Nextcloud 教程诞生了。
这里采用 Docker 容器方式来安装 Nextcloud,这样就不用担心各种环境依赖了(Nextcloud 的依赖简直多得吓人,而 Dockerfile 会帮你把依赖都配置好)
注:Docker 仅支持 64-bit 的系统
Docker 现已被各大发行版的仓库收入,采用正常安装命令即可:
yum -y install docker
随后,启动 Docker 守护进程:
# systemctl
systemctl start docker
# Service
service docker start
有了 Docker 后,就可以几行代码安装 Nextcloud 了:
# clone nextcloud 的 docker 容器
git clone https://github.com/nextcloud/docker.git
# 耐心等待安装
docker run -d -p 8080:80 nextcloud
安装完成后先别忙着启动,docker ps -a -q
查看一下容器的 id,是一串 12 位的字符串,为了便于记忆,重命名一下:
docker rename ××× nextcloud # ××× 即为容器 id
随后就可以采用如下命令启动了:
docker start nextcloud
# 测试
curl http://localhost:8080
这样,就完成了 Nextcloud 的安装工作。
方法一只能安装最新版的 Nextcloud,而最新版缺少部分功能,如:无法添加 Drive 和 Dropbox 的外置存储。如果你对外置存储不是很 care 的话,那就按照方法一安装就可以了。
git clone https://github.com/nextcloud/docker.git
cd docker/12.0/apache
这里采用官方编写的 Dockerfile 手动构建,所以时间会花得比较久。
docker build -t nextcloud .
这时候用 docker images
应该可以看到刚刚创建的镜像了,随后创建容器:
docker create -v /var/www/html/apps/:/var/www/html/apps -v /var/www/html/config/:/var/www/html/config -v /var/www/html/data/:/var/www/html/data -p 127.0.0.1:8080:80/tcp --name nextcloud nextcloud
稍稍解释一下参数:
由于 NextCloud 已经占用了 8080 端口,这里采用 Nginx 做反向代理,将域名直接解析至 8080 端口。
server {
listen 80;
server_name cloud.example.com;
location / {
proxy_pass http://localhost:8080;
}
}
重启 Nginx 服务后,就可以通过 cloud.example.com 来访问云服务了。
# 进入容器内的 bash
docker exec -i -t nextcloud bash
有时候 NextCloud 会自己定向至本地的 8080 端口,所以需要手动重写正确的地址:如果提示不能定位软件包,先执行 apt-get update
。
vim config/config.php
# 加上下面这行
'overwritehost' => 'cloud.example.com',
重启,让配置生效:
docker restart nextcloud
由于我 VPS 的内存比较小,所以并没有启用 MySQL/MariaDB 数据库(怕爆内存),而是采用了 SQLite,反正也是我一个人用,问题不大。
开启了 MySQL 后发现内存也就多了 20M(但性能的提升可不是一点半点),遂还是改成 MySQL 了:
安装 MySQL(这里采用 MariaDB 分支)
yum -y install mariadb-server mariadb-client
# 设置一下 root 密码等
mysql_secure_installation
开启 daemon 服务
systemctl start mariadb
systemctl enable mariadb
mysql -uroot -p
登录 MySQL
# 创建 nextcloud 数据库
CREATE DATABASE nextcloud CHARACTER SET = utf8 COLLATE = utf8_general_ci;
# 创建 nextcloud 的用户
CREATE USER nextcloud IDENTIFIED BY 'admin123';
# 赋予对数据库所有的权限
GRANT ALL ON nextcloud.* TO nextcloud;
创建管理员帐号和密码
数据库就选择 MySQL/MariaDB,其它参考下表:
名称 | 值 |
---|---|
用户名 | nextcloud |
密码 | admin123 |
数据库名 | nextcloud |
地址 | 172.17.0.1 |
注意:这里的地址千万不要填写成了 localhost 或者 172.0.0.1,因为这里的地址需要容器内与外部通信。
点击完成后,等待几秒就可以使用了。
由于我 VPS 的容量只有 10g,故而放不了过多的视频,就考虑采用外部存储的方法,将 Drive、Dropbox 挂载至 Nextcloud 外部存储或者 VPS。
注:Nextcloud 13 已经取消对 Drive、Dropbox 外部存储的支持(这时候你也可以选择把 Drive 直接挂载至 VPS 本地目录,再通过外部存储链接至挂载目录来完成)。
在应用页面,启用 External storage support
插件:如果提示:「没有安装 “smbclient”无法挂载 “SMB / CIFS”, “SMB / CIFS 使用 OC 登录信息”. 请联系您的系统管理员安装」
解决办法:
# 进入容器的内的 console
docker exec -i -t nextcloud bash
apt-get install libsmbclient-dev
这里简单说一下,不管你的 VPS 原本的系统是 CentOS、RedHat、Debian,统一都用 apt-get 安装,因为现在处于的是 docker 容器内的系统,与 VPS 的系统是分离的。
接着再安装 smbclient:
pecl install smbclient
同时会提示:「You should add “extension=smbclient.so” to php.ini」,这里又被小坑一把,网上大部分教程所说的 /etc/php.d/php.ini
并不存在,在 docker 容器内部该文件是在:
vim /usr/local/etc/php/conf.d/docker-php-ext-intl.ini
# 加上下面这行
extension=smbclient.so
随后重启 Nextcloud 服务就应该就 OK 了。
随后如果你安装的 Nextcloud 是 13 版本及以上的话,就只有考虑用 box 提供的 WebDAV 来作为外置存储了,不过只有 10G 的容量,且最大文件限制是 250MB。如果是用的 12 版本及以下的话,就可以考虑采取 Drive 作为外置存储了:
访问 Google 开发者平台 :
点击「启用 API 和服务」
点击「Google Drive API」
点击「启用」
点击左侧的凭据 -> OAuth 同意屏幕:
按照以上格式填写,点击保存
随后创建凭据:
应用类型选择网页应用,其它的参考以下:
点击创建后,会弹出悬浮框告诉你 ID 和 Key。
登录 Nextcloud,转至管理页面,点击「外部存储」,选择 Google Drive,填入 API 和 Key,点击授权,若授权时出现 400 错误,那么是重定向的 URI 出问题了,再添加如下一条:
https://cloud.example.com/index.php/settings/admin/externalstorages
如果提示「此应用未经过验证」,点击高级 -> 转至 example.com,忽略掉就行。
当出现了绿色按钮,就表示配置成功了。
参考:
]]>「你说,没有希望的事,还有坚持的必要吗?」
确实是没想到,看国产青春剧也能看出了共鸣。
忘记很早之前在哪看到过一句话:人会长大三次。第一次是在发现自己不是世界中心的时候;第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候;第三次是在明知道有些事可能会无能为力,但还是会尽力争取的时候。
最初看这句话还没什么感觉,最近看了「最好的我们」后,突然就触动了。
那种触动,想来就是怎么也绕不开的「成长」了:自己做了想做的事,而生活却没有给自己想要的结果。于是乎,以后再遇见了想做的事,开始犹豫了,开始畏缩了,开始计较得失了,因为有了之前的经历,担心自己做了,却也得不到自己想要的结果。
你当然可以说是因为自己长大了,会计较得失了、不会像小时候一样:想干什么就去干什么。是啊,第二次成长的你知道了有些事情即使再怎么努力,也不会得到满意的结果,于是干脆就不去做了。
可目前处于不明成长阶段的我啊,又觉得凡事要是都仔细衡量得失后再去想做还是不做的话,那人生想必会少掉许多乐趣、会错过许多事情。
我果然还是不适合看电视剧,花了一个月时间才把《最好的我们》看完(小时候那种看电视剧甚至广告时间都不愿意转台生怕错过衔接部分的劲儿都不知道哪去了,以后有时间还是多看看电影和书),听说还有几部青春剧也挺不错(《你好,旧时光》、《一起同过窗》等),就不看了,虽说确实能勾起高中时的那些或苦涩或美好的回忆,可那些回忆却再也不可得了。
也不想总是陷在回忆里,毕竟我,到底是已经长大了。
]]>大概在五天前,忽然发现一直在用的网易云解析不能用了,去作者的项目查看才知道原来是网易云更换了新的接口,旧接口的请求现在统一返回 403。于是乎,便萌生了自己写一个接口的想法。
其实网易云的外链获取目前还是有几种可用方案,比如:
https://music.163.com/song/media/outer/url?id=[].mp3
,将中括号改为歌曲 id,即为外链这两种方法其实大同小异,都会 302 至歌曲的缓存地址,但也存在一个身为强迫症的我无法忍受的缺点——缓存地址的协议是 现在第二个方法已经会直接 302 至 HTTPS 协议了。于是决定暂时放弃掉网易云,换其他家的顶着。HTTP
(从云音乐官网现在还有大量的 Mixed Content 就可以看出网易对这方面并不上心),而且自己将协议修改成 HTTPS
后访问部分歌曲又会有机率出现 403,这可真是逼死我了
我又用回了网易云接口,并编写了一个 API 文档 ,欢迎使用。
考虑了一圈,还是决定选 QQ 音乐。在网上也找到了 QQ 音乐所提供的接口:
https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg
https://y.qq.com/n/yqq/song/[].html
括号部分'C00' + songmid + '.m4a'
int(random() * 2147483647) * int(time() * 1000) % 10000000000
综上,歌曲的请求地址为:https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg?format=json&cid=205361747&uin=0&songmid=[smid]&filename=[filename]&guid=[guid]
向这个地址请求后,会得到一个 JSON 格式的数据文件,包含了我们需要的信息:vkey
curl 'https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg?format=json&cid=205361747&uin=0&songmid=[smid]&filename=[filename]&guid=[guid]'
{"code":0,"cid":205361747,"userip":"××.××.××.××","data":{"expiration":80400,"items":[{"subcode":104001,"songmid":"000uhMwj387EBp","filename":"C00000uhMwj387EBp.m4a","vkey":"B6BB8F604606DFDC82FD81CE33BC9C0277365D4B8B1BC8BCC909E408EAC9822315B2B9D021F42B495FA14AADCB598B21BCDB867931B7A953"}]}}
得到了最重要的 vkey
字段后,就可以解析出歌曲的「真实链接」了:
https://dl.stream.qqmusic.qq.com/[filename]?vkey=[vkey]&guid=[guid]&uin=0&fromtag=66
你可能注意到返回的信息中还包含了 expiration
字段。是的,vkey
只有在该时间段内才有效,当然这个问题很好解决,可以把该程序部署至服务器,而从服务器发起请求获取链接后 302 至歌曲链接。
而当我满心欢喜的把这个脚本向服务端部署的时候,却失败了:原因是接口的请求地址只支持国内的(想来是因为 QQ 音乐只拿到了在大陆地区的版权),而我的服务器在美国,这个问题就有些难解决了(我没有国内的服务器)。
于是我想另辟蹊径。
既然服务端无法解析,那就用 JS 在用户端解析。
但又带来了另一个问题——跨域。
目前跨域请求比较好的解决方案有两种:CORS 和 JSONP,其中 CORS 需要服务器端设置 Access-Control-Allow-Origin
,所以也就只有使用 JSONP 了。
注意:跨域请求失败原因浏览器端阻止显示,并非服务器端无法返回数据
使用 JSONP 时要求服务端返回的是满足 JSONP 模式 的文件,不能是纯 JSON 文件,举个例子:
var url1 = "https://lab.itswincer.com/jsonp/without-callback.js";
var url2 = "https://lab.itswincer.com/jsonp/with-callback.js";
function foo(data) {
alert(`Hi, I am ${data.name}`);
}
var script = document.createElement('script');
script.setAttribute('src', url1);
document.body.appendChild(script);
script.setAttribute('src', url2);
document.body.appendChild(script);
其中 without-callback.js
返回的是纯 JS 文件,with-callback.js
返回的是满足模式的 JS 文件。可以运行上面代码看看结果。
本想直接用现成的 ajax
,考虑到并非所有的网站都引入了 jQuery,而为 ajax
就引入一个那么庞大的库又有些没必要。
于是就自己封装了一个 getJSONP()
接口来搭配 getMusic()
使用。
项目已开源,具体的代码见 这里 ,有很详细的注释。
为了使接口更干净,没有使用 callback 函数,而是使用了 ES7 的新特性 async、await。尝试过使用 Babel 等工具转换成兼容更好的 ES5 代码,但是并没有成功,故而浏览器的兼容可能存在问题。
引入 这个 JS 文件 :
接口:await getMusic()
,如下图:
注意:在调用 getMusic() 的时候一定要加上 await 关键字,否则返回的就是一个 Promise 对象了
由于使用了 ES7 的新特性:async 和 await,故而 Aplayer 的配置文件也需要稍加改动:需要将原配置信息放至包含 async
关键字的函数内,随后调用这个函数,如下:
async function syncHand() {
new Aplayer(...);
}
syncHand();
越来越认同保罗 · 格雷厄姆那句「黑客就像画家,工作起来是有心理周期的有时候,你有了一个令人兴奋的新项目,你会愿意为它一天工作 16 个小时。等过了这一阵,你又会觉得百无聊赖,对所有事情都提不起兴趣。」话了,简直就是我的写照:这四天大约花费了 30 小时(当然有很大一部分缘由是之前没怎么学过 JavaScript,修改一下别人的代码还行,自己写就有点「捉襟见肘」了),而估计后几天又会陷入「空窗期」了。
而 JavaScript 又是一门有很多坑爹特性的语言,也让我把初学者的坑基本上都踩完了(还是写 Python 爽)。同时也感觉学习新东西的最好、最快的方法就是实战,换句话说,抱着解决问题的目的去学习所学到的知识远比你抱着单纯学习目的所学的知识要更快、更牢靠。
其实像 Hexo 这样的静态博客框架本不需要服务器的,GitHub Pages 就提供免费的托管服务、且不限流量,但内心那点不安分因素总是撩拨着我:比如可以自定防护规则、可以搭建私有 Git 服务、可以搭建自己的 API(这个比较重点)、还能自己搭建 SS 服务,于是乎就买了一台 VPS。
由于我的博客使用了 Cloudflare 作为 CDN 服务商,而国内的电信和联通用户是默认解析到 Cloudflare 的美西结点,只有移动用户是解析到香港节点,所以为了 API 的快取速度(即:本机 -> Cloudflare -> VPS -> Cloudflare -> 本机),将服务器选在了洛杉矶,每年 20$、1T 流量、10G 固态、512M 内存,搭建一个静态博客和几个 API 足够了。
SSH 的安全验证有两种级别:
这里介绍第二种方法。
如果在使用 GitHub 的时候已经生成过,那么这一步可以略过
ssh-keygen # 默认生成长度为 2048 位的 RSA 密钥
ssh-keygen -b 4096 # 可以通过添加参数 -b 设定长度
随后就会生成一对密钥,默认为:id_rsa(私钥)、id_rsa.pub(公钥)
使用 ssh-copy-id 命令
ssh-copy-id username@server-addr
需要输入远程服务器的登录密码,随后 id_rsa.pub(公钥)会自动上传至服务器的 ~/.ssh/authorized_keys
文件中
随后再进行 SSH 连接时,就不需要再输入密码了
虽不用输入密码,但仍需要输入服务器登录名和 IP 地址,所以需要将配置写入 ~/.ssh/config
中:
Host wincer # 这里填写简化名称
HostName ××.××.××.×× # 服务器 IP
Port 22 # 端口号
User root # 远程登录用户名
随后再进行 SSH 连接时,输入 ssh wincer
就可以登录了
scp
命令也可以简化成以下:
scp FILENAME wincer:PATH
目前我的博客仍然在 该仓库 的 master 分支上保留有静态文件,仅作备份。
首先为 DNS 解析添加一条 「A 记录」,记录值为 VPS 所分配的 IP
SSH 登录后,编辑 Nginx 的配置文件 vim /etc/nginx/nginx.conf
:
server {
listen 80;
server_name blog.itswincer.com;
index index.html;
root /data/www/hexo;
}
可部署多个子域名,只需将 server_name
和 root
替换成相应的子域名和文件夹就可以了
可以先创建一个 index.html
测试一下,访问 blog.itswincer.com
看看是否成功
这一步可选,你也可以手动用 scp
命令将每次 hexo g
生成的静态文件上传至服务器,只不过略微麻烦。
Travis CI 的终端并不能支持用户输入密码,而 GitHub 的 Token 又无法在自己的服务器使用,故而只能采取[简化 SSH 登录](#简化 SSH 登录)这步中类似的方法,即用私钥(即 id_rsa)去确认登录的身份,而将私钥公开至 GitHub 又是很危险的,所以我们需要将私钥加密:
gem install travis # 需要安装 gem,自备梯子
travis login # 输入 GitHub 的账户密码
travis encrypt-file ~/.ssh/id_rsa --add # 加密私钥,同时解密命令会添加至 travis.yml
Travis CI 上的 known_hosts 只添加了 GitHub 下的三个域名,在使用 SSH 登录时,会提示是否添加该主机,同样因为终端无法输入,所以需现将服务器的 IP 与端口号添加至 known_hosts:
addons:
ssh_known_hosts: ××.××.××.××
这里忍不住吐槽一下 Travis CI 的加密:居然无法同时加密两个文件,而官方提供的方法是先把需要加密的文件压缩后加密,再解压。
Hexo 这类静态博客所需的内存其实是挺少的,只需在后台运行一个 Nginx 进程就可以了,只运行一个 Nginx 进程时用「搬瓦工」的管理面板查看发现一共内存才使用了 40M,才用了不到 10%,所以就想着可以将之前写的「一言」API 放到我的服务器上,毕竟 Heroku 在国内访问还是挺不稳定的。
之前是用 Python3 写的,后来发现 VPS 自带的 Python 版本居然是 2.7,深知其中坑的我就没打算再用 Python 了,于是就是用 Node.JS 写了一个,本地调试了一下,就扔到服务器上了。第二天早上起来一看,发现内存占用居然到了 110M,一查看原来都是 Node 的占用,其中每一个 API 请求,平均就会多占用 2M 的内存,而且这个请求所占用的内存并不会释放,这样下去怕是没两天服务器就要爆内存了。
后来我也想过解决办法,比如用 PM2 这个工具来限制运行的内存,超过就重启 Node 环境,也想过定时重启服务器,再转念一想,我是大爷诶,凭啥要我去迁就辣鸡 Node.JS 的内存管理,你不行那我换一个具有垃圾回收的语言不就好了,那就 Java?好像也不太行,毕竟服务器就那么点硬盘,JDK 和 JRE 不知道要占用我多少空间,再者说来毕竟我可是 「Java 黑」。
那么就归纳一下我的需求:「占用内存小、部署方便、有垃圾回收(不会爆内存)、不要 Java」,然后考虑到编译型程序比解释型程序占用的内存更小,所以也就没考虑 Ruby & Python,满足这些要求的好像也只有 Golang 了。
写了那么久的动态类型语言,突然要我写静态类型语言还真是有点不适应。在网上找了个例子,自己捣鼓了一个下午,就写出来了,算是一个勉强遵循「RESTful」风格的 API,开始还有日志功能,后来想想没必要,Nginx 也可以监控端口的访问日志,就删去了。
然后在 Nginx 配置端口
server {
listen 80;
server_name api.itswincer.com;
location / {
add_header Content-Type 'application/javascript';
proxy_pass http://localhost:520;
}
}
而且 Golang 的部署也是很方便,将 *.go 拷贝就行了。跑了几天,内存占用稳定在 10M 上下。
该项目 已托管至 GitHub。
搬瓦工的 SS/SSR 搭建可以说是非常的方便了:
Shadowsocks Server
按钮install Shadowsocks Server
按钮大约半分钟后,会提示已经安装完成:
再点击 Go back
按钮,回到以下界面,再点击 Start
:
SS 服务配置完成了,将以上信息填入 SS 客户端即可使用。
不过由于服务器是在美西,所以无论怎么优化(BBR),延迟都会在 160ms 以上,当然这对浏览网页看视频来说也没有什么影响。
注意:当你使用 VPS 翻墙时,会同时计算上行、下行流量,也就是说如果翻墙使用 1G 流量,其实等于使用了 VPS 的 2G 流量。
我最近有在思考私有云笔记的必要性,毕竟有了博客,那云笔记的作用可能就鸡肋了一点。但我还是选择了搭建。我的想法是:博客用于存放、发布一些较正式的文章,而笔记可以休闲一点(类似作文和日记的区别)。
回到正题,目前来说,体验好的云笔记要么需要会员、要么存在诸多功能限制,而我又不想多浪费钱,那么选择一个支持多设备(其实主要是解决手机设备)的同步方案并借助私有的服务器架设自然也就是最好的解决办法了。我选择了 Nextcloud 作为解决方案 ,并借助他的 WebDAV 功能作为多端同步工具。
手机端笔记软件选择的是 易码 ,支持 Markdown 语法和 WebDAV 同步,电脑端可以选择直接用浏览器访问 Nextcloud,可以在线 Markdown 编辑和预览,当然也可以选择用 Nextcloud 同步至本地文件夹,并用其它编辑器打开就可以了。
]]>去年年底那会,花了大概一周多时间,阅读完了《黑客与画家》这本书,收获颇丰。可惜当时确实没多少时间整理出读书笔记,期末考试结束后,回到家中,本想着有时间能好好补一下博客,结果回家之后也没有想象中的空闲,看着「搬瓦工」把每年 20$ 的套餐补货了,于是就购置了一台服务器,将博客源码从 GitHub 上转移到了自己的服务器上,还拿 Golang 重写了一下「一言」的 API(扯远了,服务器的事等以后再开一篇博客说说),还补了一部早已加入想看列表却一直没看的番——「反叛的鲁路修」(嘻嘻 😌)。
直到今天,才终于有时间能把这篇读书笔记给整理出来了,笔记是直接在 Kindle 上标注的,然后用「 Clippings.io 」这个工具导出(为什么 Kindle 不能开发一个好用一点的笔记管理系统呢!?)。
好在 azw3 版本在 Kindle 上的体验还不错,即使有代码段排版也没有垮掉,所以决定原谅你。
(👇以下为文摘)
在一个人产生良知之前,折磨就是一种娱乐。
程序写出来是给人看的,附带能在机器上运行。(这句话的出处是在《SICP》这本书的卷首语,作者引用了)
如果有必要的话,大多数物理学家有能力拿到法国文学的博士学位,但是反过来就不行,很少存在法国文学的教授有能力拿到物理学的博士学位。
人们喜欢讨论的许多问题实际上都是很复杂的,马上说出你的想法对你并没有什么好处。
小时候,每个人都会鼓励你不断成长,变成一个心智成熟、不再耍小孩子脾气的人。但是,很少有人鼓励你继续成长,变成一个怀疑和抵制社会错误潮流的人。 如果自己就是潮水的一部分,怎么能看见潮流的方向呢?你只能永远保持质疑。
不服从管教,其实是黑客之所以成为优秀程序员的原因之一。
公民自由并不仅仅是社会制度的装饰品,或者一种很古老的传统。公民自由使得国家富强。
经济学里有一条拉弗曲线(Laffer curve),认为随着税率的上升,税收收入会先增加后减少。我认为政府的力量也是如此,随着对公民自由的限制不断上升,政府的力量会先增加后减小。
极权主义制度只要形成了,就很难废除。(咳咳)
一定数量的盗版对软件公司是有好处的。不管你的软件定价多少,有些用户永远都不会购买。如果这样的用户使用盗版,你并没有任何损失。事实上,你反而赚到了,因为你的软件现在多了一个用户,市场影响力就更大了一些,而这个用户可能毕业以后就会出钱购买你的软件。
首先,管理企业其实很简单,只要记住两点就可以了:做出用户喜欢的产品,保证开支小于收入。
一个大学毕业生总是想「我需要一份工作」,别人也是这么对他说的,好像变成某个组织的成员是一件多么重要的事情。更直接的表达方式应该是「你需要去做一些人们需要的东西」。即使不加入公司,你也能做到。公司不过是一群人在一起工作,共同做出某种人们需要的东西。真正重要的是做出人们需要的东西,而不是加入某个公司。
要鼓励大家去创业。只要懂得藏富于民,国家就会变得强大。让书呆子保住他们的血汗钱,你就会无敌于天下。
财富是用工作成果衡量的,而不是用它花费的成本衡量的。如果我用牙刷油漆房屋,屋主也不会付给我额外工资的。
好设计是艰苦的设计。如果观察那些做出伟大作品的人,你会发现他们的共同点就是工作得非常艰苦。如果你工作得不艰苦,你可能正在浪费时间。
并非所有的痛苦都是有益的。世界上有有益的痛苦,也有无益的痛苦。你需要的是咬牙向前沖刺的痛苦,而不是脚被钉子扎破的痛苦。解决难题的痛苦对设计师有好处,但是对付挑剔的客户的痛苦或者对付质量低劣的建材的痛苦就是另外一回事了。
等到你逐渐对一件事产生热情的时候,就不会满足于模仿了。
「你用什么语言并不重要,重要的是你对问题是否有正确的理解。代码以外的东西才是关键。」这当然是一派胡言。各种语言简直是天差地别。
语言设计者之间的最大分歧也许就在于,有些人认为编程语言应该防止程序员干蠢事,另一些人则认为程序员应该可以用编程语言干一切他们想干的事。
允许你做某事的语言肯定不差于强迫你做某事的语言。
它们(指某些语言)的内核设计得并非很好,但是却有着无数强大的函数库,可以用来解决特定的问题。(你可以想象一辆本身性能很差的小汽车,车顶却绑着一个飞机发动机。)有一些很琐碎、很普遍的问题,程序员本来要花大量时间来解决,但是有了这些函数库以后,解决起来就变得很容易,所以这些库本身可能比核心的语言还要重要。所以,这些奇特组合的语言还是蛮有用的,一时间变得相当流行。车顶上绑着飞机发动机的小车也许真能开,只要你不尝试拐弯,可能就不会出问题。(内心 OS:我可没有针对 C++ 😏)
当我说 Java 不会成功时,我的意思是它和 Cobol 一样,进化之路已经走到了尽头。
如果摩尔定律依然成立,一百年后计算机的运行速度将是现在的 74 乘以 10 的 18 次方倍(准确地说是 73 786 976 294 838 206 464 倍)。
即使最后只是略微快了 100 万倍,也将实质性地改变编程的基本规则。如果其他条件不变,现在被认为运行速度慢的语言(即运行的效率不高)将来会有更大的发展空间。
效率低下的软件并不等于很烂的软件。一种让程序员做无用功的语言才真正称得上很烂。
自下而上的编程方法意味着要把软件分成好几层,每一层都可以充当它上面那一层的开发语言。这种方法往往会产生更小、更灵活的程序。它也是通往软件圣杯——可重用性(reusability)——的最佳路线。
罗伯特·莫里斯和我都很了解 Lisp 语言,我们相信自己的直觉,找不出任何不使用它的理由。我们知道其他人都用 C++ 或 Perl 开发软件,但是我们不觉得这说明了什么问题。如果别人用什么技术,你也用什么技术,那么你大概只能使用 Windows 了(日常黑 Windows)。
编程语言的特点之一就是它会使得大多数使用它的人满足于现状,不想改用其他语言。
如果从图灵等价(Turing-equivalent)的角度来看,所有语言都是一样强大的,但是这对程序员没有意义。
最不用担心的竞争对手就是那些要求应聘者具有 Oracle 数据库经验的公司,你永远不必担心他们。如果是招聘 C++ 或 Java 程序员的公司,对你也不会构成威胁。如果他们招聘 Perl 或 Python 程序员,就稍微有点威胁了。至少这听起来像一家技术公司,并且由黑客控制。如果我有幸见到一家招聘 Lisp 黑客的公司,就会真的感到如临大敌。
你的经理其实不关心公司是否真的能获得成功,他真正关心的是不承担决策失败的责任。
黑客欣赏的一个特点就是简洁。黑客都是懒人,他们同数学家和现代主义建筑师一样,痛恨任何冗余的东西或事情。
简洁性是静态类型语言的力所不及之处。只要计算机可以自己推断出来的事情,都应该让计算机自己去推断。举例来说,hello-world 本应该是一个很简单的程序,但是在 Java 语言中却要写上一大堆东西,这本身就差不多可以说明 Java 语言设计得有问题了。
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
如果你从来没没有接触过编程,看到上面的代码可能会很奇怪,让计算机显示一句话为什么要搞得这么复杂?有意思的是,资深程序员的反应与你一样。
语言设计者应该假定他们的目标用户是一个天才,会做出各种他们无法预知的举动,而不是假定目标用户是一个笨手笨脚的傻瓜,需要别人的保护才不会伤到自己。如果用户真的是傻瓜,不管你怎么保护他,他还是会搬起石头砸自己的脚。
对黑客来说,选择编程语言的时候,还有一个因素比简洁更重要,那就是这种语言必须能够帮助自己做到想做的事。
(👇以下为简评)
这本书算是我从去年 7 月以来看完的第一本书了(《计算机程序的构造和解释》这本书太难了,看了前两章就没时间看,到还书的日期了),主要也在于作者 Paul Graham 的行文十分流畅,阮一峰的翻译也很到位,没有什么阅读障碍,还有「读至好几处都有一拍大腿,哎呀妈呀我也是这么想的啊」的想法,读完之后,思想也似乎豁然开朗了些。
关于第六章——「如何创造财富」,财富的获得是看你最终的结果,不是看你的付出(过程)。你做出了别人需要的产品,没人在乎你是做了三天还是三十天,他并不会因为你只做了三天就完成而少付给你报酬,更不会因为你是三十天完成而多给你报酬。还有关于「财富并不是固定不变的」这个理论,他给出了一个例子:你拥有一辆老爷车,你可以不去管它,也可以自己动手把它修葺一新这样的话,你就创造了财富:世界上因此多了一辆新的车,财富就变得多了一点,如果你把车卖掉,你得到的卖车款就会比以前更多,与此同时,你并没有使任何人变得更贫穷。正因为这个理由,他也建议我们多多创业,但也给我们泼了一盆「凉水」:创业的付出与回报总体上是成比例的,但是在个体上是不成比例的,不要把创业过于神话,但创业的确给了我们更多的可能。
还有就是关于编程语言的争论,作者似乎和我一样很喜欢黑 Java,认为 Java 是「进化之路已经走到了尽头」,因为编程语言并不应该限制程序员去做某些事情,即使这些事情是有害的。同时也抛出了另一个很新颖的说法:关于一百年以后,我们该使用什么样的编程语言?按照摩尔定律:预计 18 个月会将芯片的性能提高一倍,那时候电脑的运行速度将是现在的 73 786 976 294 838 206 464 倍,所以他认为现在某些因为运行速度略慢但编程起来更舒服的语言在未来反而是主流,即有更大的发展空间,同时作者似乎很推崇动态类型语言,因为写起来比静态类型语言方便、看起来也比较简洁。作者也不止一次的推崇了 Lisp,甚至不惜黑 Oracle 数据库、C++、Java(见上面第 30 条)。
最后,这本书算是 Paul Graham 的一本随笔文集,其中自然充斥着许多作者的价值观,如果这些价值观与你的价值观符合,那么你就会像「捡到宝」一样的对待这本书,反之,你会认为这本书的观点完全是和「邪教信条」一般,很庆幸,我是前者。
处于马上步入社会的我啊,在迷茫的时候,不妨也多阅读几本好书。
]]>前段时间忙于备考,博客有段时间没更新了。其实早就有写这篇博客的想法了,原因嘛——我是比较喜欢看电影的,而且近来也对数据分析颇感兴趣,于是花了一天时间,先是爬取数据,再分析整理,数据可视化。
其实豆瓣对爬虫的防范算是比较高级了,即使伪造了 Cookie,还是会封禁 IP(还好我的代理 IP 多😏),甚至还会把你的帐号暂时冻结,其实要不是有一些电影词条必须登录才可见,也不用伪造 Cookie 这么麻烦。
之前爬取都是用的正则匹配,这次首次接触了「 Beautiful Soup 」这个库,相见恨晚啊,不多说,先上代码:
def get_info(url):
movie = {}
proxies = {'https': "socks5://127.0.0.1:1080"}
info = get(url, cookies=read_cookie(), proxies=proxies).text
soup = BeautifulSoup(info)
try:
# get movie name
name = soup.find(property='v:itemreviewed').get_text()
movie['name'] = name.split(' ')[0]
# get movie year
year = soup.find(class_='year').get_text()
movie['year'] = year[1:-1]
# get movie info
info = soup.find(id='info').get_text().replace(' ', '').split('\n')
info = [x for x in info if x is not '']
for item in info:
if '导演:' in item:
movie['director'] = item[3:].split('/')
if '主演:' in item:
movie['actors'] = item[3:].split('/')
if '类型:' in item:
movie['type'] = item[3:].split('/')
if '国家/地区:' in item:
movie['region'] = item[8:].split('/')
if '语言:' in item:
movie['language'] = item[3:].split('/')
if '片长:' in item:
time = [
search(r'[\d]*', x).group() for x in item[3:].split('/')
]
movie['time'] = sorted(time, reverse=True)[0]
# get top250 info
movie['rank'] = soup.find(class_='top250-no').get_text()
movie['number'] = soup.find(property='v:votes').get_text()
except Exception as e:
print(e)
return movie
其中片长取得是无删减版的片长,即不同版本中最长的。
地区、导演、语言等由于会出现多项内容,采取列表存放。
以下统计数据截止至 2018/01/20
其中由「宫崎骏」和「克里斯托弗·诺兰」贡献最多,均为 7 部,具体为:
宫崎骏(日本):
克里斯托弗·诺兰(英国):
其中由「张国荣」贡献最多(前三居然都是香港地区的演员),有 8 部,分别是:
其中「美国」地区一枝独秀,超过半数以上电影的制片地区均为「美国」,且远超第二名「英国」。
统计名称 | 数值 |
---|---|
中位数 | 118.0 |
均值 | 124.0 |
众数 | 98.0(10 次) |
标准差 | 34.1 |
极差 | 218.0 |
其中片长最长的电影为《指环王3:王者无敌》,导演是「彼得·杰克逊」,片长为 263 mins,排名是 No.30。
其中片长最短的电影为《萤火之森》,导演是「大森贵弘」,片长为 45 mins,排名是 No.150。
统计名称 | 数值 |
---|---|
中位数 | 2002.0 |
均值 | 1998.6 |
众数 | 2004(13 次) |
标准差 | 15.6 |
极差 | 85 |
其中距今最久远的电影是《城市之光》,导演是「查理·卓别林」,年份为 1931 年,排名是 No.210。
其中距今最接近的电影有 5 部,均为 2016 年上映:
嘿嘿,没想到吧,贡献电影最多的年份并不是「Top 250」前四名中有三部的 1994 年,而是 2004 年。
统计名称 | 数值 |
---|---|
中位数 | 8.70 |
均值 | 8.78 |
众数 | 8.7(44 次) |
标准差 | 0.27 |
极差 | 1.40 |
其中最高分为 9.6 分,为两部电影所获得:
其中评分最低的电影为《疯狂的石头》,分数是 8.2 分,导演为「宁浩」,评分人数为 312083 人,排名为 No.230
可以看出豆瓣在进行「Top 250」排名时,并不是仅看评分,其中评分人数也占了很大的一部分比重,且似乎还有一些其它的因素,比如《血战钢锯岭》这部电影,评分 8.7,评分人数为 310624 人,却并没有上榜,同为评分 8.7,评分人数为 314940 的电影《看不见的客人》排名却早已进前百(No.83)。
最后,本人并非专业电影人士,无法针对以上数据提出建设性的建议,所做统计也仅仅是出于爱好,也愿自己能在闲暇时间里,多看几部电影。
]]>关于 2017 年,其实还真的有挺多想说的,也早就有想写一篇博客的想法了,差不多到今天才抽得出时间写。
前几天和朋友聊天时谈到关于今年最有成就感的一件事,我想了一会,应该是搭建了这样一个博客。
当初搭建博客的初衷其实很单纯:就是为了好玩,谁知从此就沉迷于此了。在之后的写博客的过程中甚至产生了一种当一个作家也还不错的想法(当然前提是我的文章还有人看😋),现在想想,与高中时期相比,我的想法是发生了一些转变(在高中时期的我是绝不可能产生这种想法的)。正如开始所说的,现在遇到点什么事就想写下来,在往年,我一直没有写年末总结的习惯。这种「创作欲」,类似作家:将自己内心的想法写成作品,实则是把自己的内心剖析给别人看。也渐渐有些明白卡尔·雅斯贝尔斯的那句「文学和科学相比,的确没什么用处,但文学最大的用处,也许就是它没有用处」的意思了。
买了 kpw3 后,我很乐意培养自己的阅读习惯。大学时间其实还是比较宽松的,但我反而不能每天抽出一小时阅读时间。有时候看书没看两分钟,随便手机一个通知消息就能让我转移注意力——这也是我的缺点:当自己没有全神贯注的时候,很容易被其它的事情所吸引注意力(这也算我很迫切想改掉的一个坏习惯),也导致看了近两周才把《黑客与画家》这本书看完(书推过两天会补上)。
是太浮躁了,也太焦虑了,或许是因为到了大三,面临找工作的压力,这压力不仅体现在看书上,有时我就莫名想快些完成正在做的事情,后来多次发现快速完成的事情必然是敷衍的,而事后一旦想起这件「敷衍」的事情,会更加浮躁。其实这样并不好,道理古人都说给我们听了:「欲速则不达」,以后我会尽量放慢自己做事情的速度,投入自己的内心,问问自己真正想要的是什么。
今年有过一段恋情,对我产生了一些影响,有好,也有坏,让我成长了许多,也意识到了自己的不足。是的,一段感情之后一定会让你成长的。我在这个过程中有开心、难过、有挂念一个人,甚至有些「病态」的想法——不论好坏,这些特殊的情感都是之前没有体会过的。
恋爱的时候,双方的关系一定需要去协商、磨合,这也会让你学会更好的与人沟通,同时你会发现有些问题如果脱离恋爱范围的话根本就不是问题。在一段恋情过后,我们获得的不仅仅是恋情,还有更好的、获得了成长的自己。你会更了解自己,也更了解你需要找一个什么样的人。
要有自己的生活,要坚持做自己。要学会去爱,但要先学会爱自己。
接下来说说工作。
其实我很反感工作——即学校安排给你作为学生所必须学习的课程,所以这学期的课我基本没怎么去,因为去了我也不会听:我无法强迫自己去听那些完全没有兴趣的课程,那有点像是别人强迫要你去做的事情,我天生是一个「猫型人格」(即:你让我向左转,我会不由自主的向右转,同时心里还有一点歉疚),所以有些不由自主地抗拒。
在 之前写的那篇文章 中就说到,我想更加追随自己内心的意愿去活着。具体到工作的说法就是:我想开始「不以找到工作为目的的学习」,学习自然指的是编程。
编程这么有趣的事,竟然还有钱赚 ——by c++ 之父
我喜欢编程,我愿意将自己的时间花费在上面做一些有趣的小程序,即使这在旁人看来对以后的工作没有什么帮助,我不想抱着太强的目的性、太多的功利心去学习,因为这样,会让学习变味。同时我也乐于看着指尖下的一串串字符到显示器上显示出成果,会有一种小小的满足感。
最后,小小说一下对 2018 年的展望(这绝对不是 FLAG!)
我是一个很注重隐私的人,所以对密码学也就很感兴趣,这学期本着想进一步了解密码学的念头选了一门应用密码学的选修课(其实是为了混学分),虽说也没去过几次,但总想着这门课都快结束了总不能像没上过一样。这次借着 GnuPG(以下简称 GPG) 软件的使用也聊聊目前现代密码学中以密钥性质进行区分的两大加密方式。
大概半年前,写过一个暴力破解加密压缩文件的程序,说白了就是跑字典,不断的试密码,这只能破解常用密码,一旦用户采用随机生成的密码就无从下手。我们平时所用到的压缩加密大多都是对称性加密,即我们用同一字符串对文件进行加密,又用同一字符串进行解密(此时为了保证安全,密码需越复杂越好)。
明文 <–> 密钥 <–> 密文
对称加密很方便也很快速,但是也带来了一个很大的缺点,由于加密和解密用的都是同一密钥,在传输的过程中,要求双方取得相同的密钥,这会大大降低加密的安全性(注意:这里所说的不安全不是说对称加密算法不安全,而是从密钥的获取程度来说的,即密钥知道的人越少越安全)。
在如今的互联网时代,通信双方分隔异地且素为谋面,则对称加密要求事先交换共同密钥的安全性也无法得到保障。
那么为了解决对称加密的安全隐患,非对称加密诞生了。 与对称加密不同的是,非对称加密的加密和解密所需要的密钥是不同的,而且知道了其中一方,想推导出另一方(需要解决一个数学难题),在量子计算机时代来临之前,基本是不可能完成的。 因此公开其中一个密钥,并不会对密钥对的安全性有影响。 我们常说,公钥可以公开,私钥需要保密,但其实公钥和私钥在生成过程上,并无什么不同。并不是因为公钥公开后,解密出私钥困难,如果公开的是私钥,解密出公钥也同样困难。也就是说我们将一对密钥公开的那部分叫公钥,另一部分叫做私钥。并不是因为公钥,才能公开,私钥,就必须保密。
明文 <–> 公钥 <–> 密文 <–> 私钥 <–> 明文
前一段时间很火的勒索病毒就是采用的非对称加密中的 RSA-4096 加密算法。 想具体了解 RSA 加密原理的话, 点击这里 。 由于公钥加密在计算上相当复杂,导致其加密速度相对于对称加密来说慢。
其中对称加密还有一个用处:数字签名。 对称加密的公钥和私钥在使用顺序上并没有什么要求,你可以用公钥加密,私钥解密,这就是非对称加密算法,同样可以用私钥加密,公钥解密,而这就成为数字签名。 由于私钥是发送者保存的,发送者用私钥加密后的信息,任何拥有该发送者的公钥的人都可以解密该信息。如果接收用发送者公开的公钥解开了,那么说明这个信息是确实是发送者发送的(没有被篡改,也不是伪造的)。公众也可以信赖这条信息确实来自与该用户,用户无法否认。
一般来说,不直接对消息进行签名,而是对消息的哈希值进行签名,并将签名附赠在消息一起发送。
总结一下二者的优点与缺点:
故现在多将二者结合使用:需要加密的主体内容使用对称加密,对称加密的密钥使用非对称加密。
下面说说如何使用 GPG 软件加密文件。
GPG 支持的算法有很多:
公钥:RSA, ELG, DSA, ECDH, ECDSA, EDDSA
对称加密:IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256,
TWOFISH, CAMELLIA128, CAMELLIA192, CAMELLIA256
散列:SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
压缩:不压缩,ZIP,ZLIB,BZIP2
使用对称加密很简单,只需要一行就可以:
gpg --cipher-algo [对称加密算法名称] -c FILENAME
随后会让你输入两次密码,就会生成一个 FILENAME.gpg 的文件在同目录下。
解密:
gpg -o FILENAME -d FILENAME.gpg
更多参数请输入 gpg -h
自行查阅。
(这里如果输入的是 --gen-key
的话,会省去一些步骤:自动设置密钥尺寸为 2048 位、有效期限为 2 年、注释留空):
gpg --full-generate-key
回车后,出现以下文字:
gpg (GnuPG) 2.2.3; Copyright (C) 2017 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
请选择您要使用的密钥种类:
(1) RSA and RSA (default)
(2) DSA and Elgamal
(3) DSA (仅用于签名)
(4) RSA (仅用于签名)
您的选择?
选择 1:
RSA 密钥长度应在 1024 位与 4096 位之间。
您想要用多大的密钥尺寸?(2048)
选择 4096:
请设定这把密钥的有效期限。
0 = 密钥永不过期
= 密钥在 n 天后过期
w = 密钥在 n 周后过期
m = 密钥在 n 月后过期
y = 密钥在 n 年后过期
密钥的有效期限是?(0)
如果想设置 5 年过期,输入 5y,我这里是自己私人用,选择 0,随后会让你确认以上信息正确与否,输入 y,系统会要求你提供一下个人信息:
You need a user ID to identify your key; the software constructs the user ID
from the Real Name, Comment and Email Address in this form:
"Heinrich Heine (Der Dichter) "
真实姓名:
电子邮件地址:
注释:
注释这一栏可以留空。
随后:
您选定了这个用户标识:
"×××××× <××@×××.com>"
更改姓名(N)、注释(C)、电子邮件地址(E)或确定(O)/退出(Q)?
输入 o,会弹框提示设置一个密码,用于保护私钥。
与此同时,系统也会提示:
我们需要生成大量的随机字节。这个时候您可以多做些琐事(像是敲打键盘、移动
鼠标、读写硬盘之类的),这会让随机数字发生器有更好的机会获得足够的熵数。
几秒后,系统就会提示密钥已经生成。
显示系统的私钥:
gpg -K
显示系统的公钥:
gpg -k
删除密钥:
gpg --delete-keys [uid]
gpg --delete-secret-keys [uid]
其中 uid 可以使用邮箱代替,下同。
导出公钥:
gpg -o public.key --export [uid]
导出私钥:
gpg -o private.key --export-secret-keys
这样导出的 key 文件是二进制,不可读,加上 –armor 参数可以保存为 ASCII 码形式。
导入密钥:
gpg --import [key 文件]
gpg -r [uid] -o FILENAME.gpg -e FILENAME
-r 指定用户的公钥,如自己使用改为自己邮箱即可,-o 指定加密后输出文件名称。
gpg -o FILENAME -d FILENAME.gpg
会让你输入密码,即用于保护私钥的密码。
参考:
]]>为了针对我的网站提供更好的浏览体验(或者说更接近原生 App 的用户体验),在之前我就已经 开启了 Service Worker 技术 ,针对离线或者网速慢的情况下改善用户体验。但只有少数几个浏览器支持 (Chrome、Firefox、Opera),对目前手机端用户数最多的 QQ 浏览器、UC 浏览器却没有支持,也就是说该方法针对 QQ 浏览器和 UC 浏览器并没有什么实际优化。
而且对于 Service Worker,它并不能减少你的 HTTP 连接数量,只是拦截你的请求,减少 Stalled、Request sent 和 TTFB 的时间,见下图:
针对以上两个问题,本博客采用另一种 HTML5 新技术 —— localStorage。
localStorage 是在 HTML5 中新引进的一项存储技术,(如果不被清除)存储没有时间限制,但是有大小限制,一般(不同浏览器的限制有所差别)对于每个域名是 5 MB,对于存储一些纯字符串脚本,足够了。且目前 大部分主流浏览器 均支持此项技术。
但是需要注意,Service Worker 是可以将所有的 HTTP 请求全部拦截,无论服务器的 Response Headers 中的 Content-Type 是什么类型都可以拦截从本地加载。而 localStorage 仅能存储静态资源(JavaScript/CSS)。
而存储在 localStorage 的中的静态资源所带来的优点就在于再次加载时不需要发起 HTTP 请求(Queueing、Stalled、Request sent、TTFB、Content Download 这些都不需要),这可以大大改善不支持 SW 技术的浏览器在访问我网站时的浏览体验。
本博客采用的是 basket.js 方案,将 JavaScript 在 localStorage 中,利用 localStorage 的特性,减少 HTTP 连接的次数,以达到改善页面加载体验的目的。
目前(2018/09)得益于 新主题的 SPA 特性 ,我已经移除 localStorage 的功能了:一方面是因为用户每次点击并不用重新请求 JavaScript 和 CSS 和 HTML,存储 JavaScript 有些没必要;另一方面是 localStorage 还存在安全隐患,故暂时去除 localStorage。
为了避免每次刷新页面 main.css 加载先后页面出现抖动的问题,默认不将 main.css 放入 localStorage 中存储。
另一个问题是 NexT 在设计之初就很依赖于 js(会加载大量的 js 文件),而这些 js 文件的加载顺序是有要求的,jquery 必须优先被加载,否则就会出现奇怪的 bug,好在 basket.js 提供了控制加载先后顺序的方案。
在 这篇知乎回答 中,很详细的列出了 localStorage 的优点和缺点。
其中最危险的是网站出现 XSS 漏洞,就会被人利用将恶意代码注入到 localStorage 中,导致即便修复了 XSS 漏洞存储的代码依然是被篡改的。
好在 basket.js 可以提供将 localStorage 中的代码重新从网络加载的问题。具体见 官方文档 。
]]>一入外设深似海,从此钱财是路人。
第一次知道外设这个概念,是在高中的时候,在网上偶然逛到机械键盘贴吧,只是当时忙于准备高考,而外设又价格不菲,于是念头便搁置了。
后来上了大学,买了笔记本,敲着笔记本自带的键盘「 shit 」一般的手感,才想到我应该买一把机械键盘了。于是就在网上找,看到一个段子说:
年轻人千万别碰哪些东西?
当时大一,看到这个段子就笑了一笑,面对从一百多到一千多价位不等的机械键盘,还是比较理智的,听人说凯华轴的手感也是最接近 Cherry 轴的,于是就买了贼鸥 87,用了快两年,这期间:鼠标换了两个,耳机也买了两个,键盘却一直在用这一个,最近有几个键不灵了,正好趁着双十一,想着干脆换一把新的。心中对 Poker 那独特的键位种草已久,可惜京东没有 Poker II 的红轴版本,于是便入手了一代。
不愧是「二手东」,这饱经沧桑的包装盒:
关于包装盒,去拿快递的时候还发生了一个小插曲:当时京东的人问我手机尾号,我告诉了她,然后又问我是什么东西,我说是一把键盘,然后他就去找,找了半天,没找到,然后就问另一个人,说:“尾号是 6 的快件都在这里了吧,怎么没有键盘啊?”,然后转头问我:“键盘应该是挺大的吧?”,我说:“不,不大,挺短的”,然后她又去找小一点的包裹,结果一找就找到了。
回到寝室,迫不及待的拆开了包装:
这便是全家福了,包含:键盘本体、USB 连接线、RGB 的大键键帽、说明书、拔键器。
其中连接线带有屏蔽磁环,做工也算精良。
Poker 这一系列,最大的特点应当就是 60% 尺寸的设计了,准确来说是 61 键。相对于普通 87 键的键盘,尺寸更加玲珑小巧,省去了方向键和功能键,改为用 FN
的组合键来实现相应功能。方向键是用 Fn
+ WASD 来实现,不过,对于用 Spacemacs 的我来说,没啥影响,哈哈。
真正拆开的时候才发现 60% 尺寸带来的冲击有多么大。
说到组合键,Fn
与组合键的功能在侧刻上都已标注:
Fn
与数字键组合就是 F1~F12。Fn
+ N、M、< 分别是音量 -、+、静音等。
说道机械键盘的核心,应当就是轴体和键帽了。
轴体方面,采用的是 Cherry 原厂轴体,大键也是卫星轴设计。手感嘛,自然是没话说了。我这里购买的是红轴的版本,毕竟用了两年,还是红轴最为顺手。
键帽采用的是 PBT 材质,对于 ABS 来说,PBT 的好处就是绝不会打油。
而且这款 PBT 键帽比我之前在网上购入的 PBT 键帽手感要更胜一筹,对着光看起来还闪着微弱的光,挺有意思。
在上图键 F、G、H 的侧面,可以看到有三个数字,分别是 15ms、0.1s、0.5s,这是允许用户调整按下键帽时的响应速度。这一点也是比较新奇。
相对与小巧玲珑的正面来说,背部就没有那么精致了:
四周是四个黑色的防滑垫,没有撑脚,可能是为了缩减体积来作出的取舍(当然键盘也设计成了前高后低的人体工学形状),防滑垫对我来说用处不大,因为我是把键盘放在鼠标垫上使用的。
中间那块金属铭牌上刻着一句英文:「The keyboard to cheer you up」(用这把键盘让你高兴起来!)
可能会注意到在底部的右侧有四个很小的指拨开关,作用分别是:
这一功能也算是 Poker 的特色了,目前还不是很了解,先放一放,过几天等了解了再补上。
换上附赠的 RGB 键帽后,白色素雅的 Poker 顿时骚了起来,哈哈。
由于是 mini 键盘,我的手托也就不那么合适(长了一截,无关紧要)。
一把 60% 键盘,精简了多余的按键和尺寸,为便携带来了许多好处(要是再赠送一个保护套就更完美了)。做工上乘,手感尚佳,不过大键的手感稍肉,Cherry 原厂轴加上 PBT 键帽,算的是 IKBC 的良心之作,值得入手。(怎么感觉写成了软文 23333
]]>最初是在手机上一个叫「一言」的 App 接触到 Hitokoto,一见倾心啊。之前我看书时遇到写的不错的句子就喜欢摘录下来,在有自己的博客之后,本想是单独写一篇博文来存放,后来分析了 NexT 的布局后,就想到在侧栏底部可以加上一个单独的模块。
最开始,是使用别人的 API,后来觉得不太好,有诸多限制,而我又没有主机,于是就自己用 Javascript 写了一个本地的脚本。后来发现这样也不太好,因为本地的脚本每次加载势必要加载存放 Hitokoto 的 JSON 文件一次,当记录越来越多时,会消耗不必要的资源。毕竟每次只需要加载一条。
最开始准备构建的时候,就遇到了一个问题:一言的数据库去哪里找。我翻便了 Google,基本都是提供 API 的,并不会将完整的数据库给你。这想想也正常,都把数据库给你了,那谁还用你的 API 呢。
我就花了一下午,写了一个爬虫,对准了几个提供 API 的网站,开始爬去数据。但是由于 API 产生的数据是随机的,难免会有重复。所以爬取之后又要查重,着实花费了我不少时间。
整个过程大概花了一天多,做成了一个 JSON 格式的文件,然后用 JS 导入成为数组,再随机访问数组的某一项,这便是最初“本地版”的「一言」了。
先前已经说过,一旦数据多了起来。那么数组的访问和加载都是问题,而访问慢的问题可以用数据库来解决。而这学期正好在学数据库这门课,于是便花了点时间将 JSON 格式的数据转化成 sqlite 数据库。JSON 格式的数据有需要的只有 3 项,分别是 ID(用以标识每个 Hitokoto)、HITOKOTO(每个 Hitokoto 的内容)、SOURCE(每个 Hitokoto 的出处)。知道了这些,转化的代码就呼之欲出了:
import json
import sqlite3
JSON_FILE = "hitodb.json"
DB_FILE = "HITODB.db"
conn = sqlite3.connect(DB_FILE)
with open(JSON_FILE, 'r') as load_f:
data = json.load(load_f)
for line in data:
print(int(line["id"]), line["hitokoto"], line["from"])
conn.execute(
'INSERT INTO HITOKOTO (ID, HITO, SOURCE) VALUES ({a}, \'{b}\', \'{c}\')'.
format(a=line['id'], b=line['hitokoto'], c=line['from']))
conn.commit()
print('Successfully')
conn.close()
截至至本文发布,该「一言」数据库共收录了 880 条记录,以后我还会陆续添加。
有了数据库,自然要构建一个 API,这里选用的是 Flask 框架提供的接口。
首先你需要安装 Flask,而 Python 是自带 sqlite3 模块的。直接上代码:
import sqlite3
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello World!'
@app.route('/api/')
def get_hito():
conn = sqlite3.connect('HITODB.db')
hito = conn.execute(
'select * from hitokoto order by random() limit 1').fetchone()
hitokoto = "{} ——「{}」".format(hito[1], hito[2])
return 'function hitokoto() { ' + 'document.write(\'{}\');'.format(
hitokoto) + '}'
@app.route('/api/json/')
def get_json():
conn = sqlite3.connect('HITODB.db')
hito = conn.execute(
'select * from hitokoto order by random() limit 1').fetchone()
hitokoto = {}
hitokoto['id'] = hito[0]
hitokoto['hito'] = hito[1]
hitokoto['source'] = hito[2]
return jsonify(hitokoto)
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)
保存为 run.py
。然后运行,打开 http://0.0.0.0/api/
如果没有意外的话,应当是成功了。接下来就是部署了。
一开始担心是没有主机,后来才知道有「 Heroku 」这个造福大众的云平台服务。
首先你需要安装 Heroku 客户端工具 ,安装完成后,输入以下命令来验证安装是否成功:
$ heroku --version
安装成功后,在本地命令行登录 Heroku:
$ heroku login
然后输入你的帐号和密码即可
可以在 网页端创建 ,也可以在命令行创建:
$ heroku create wincer-hito
这里或许会提示你名字已经被使用了,换一个就好。接下来要初始化本地和远程代码库。
$ mkdir hitokoto # 创建本地代码仓库
$ cd hitokoto # 切换至本地仓库目录
$ git init # 初始化本地仓库
$ heroku git:remote -a wincer-hito # 链接到远程仓库
除了代码和数据库外,两个必要的文件:requirements.txt
部署应用时,远程环境会自动安装 requirements.txt
文件中列出的依赖。我们 requirements.txt
文件内容如下:
Flask==0.12.2
gunicorn==19.4.5
接下来,我们如何告诉服务器如何运行这个文件呢?就要通过 Procfile
文件了。
web: gunicorn run:app
以上就是 Procfile
的内容。
另根据习惯,可自行添加对该项目的描述。
接下来就是激动人心的提交了:
$ git add .
$ git commit -m "Init commit"
$ git push heroku master
打开 https://wincer-hito.herokuapp.com/api/ 看看效果吧!
升级程序的时候,在所有的改动提交后,建议按照如下步骤升级:
$ heroku maintenance:on
$ git push heroku master
$ heroku run python run.py deploy # run.py改成自己的文件名
$ heroku restart
$ heroku maintenance:off
数据获取:
在你想使用「一言」的地方插入以下代码:
演示效果看侧栏。
注:由于是 Heroku 的主机是在美国,所以该 API 延迟可能会有一点高。
]]>去年暑假的时候,写了一篇如何装 Linux 和 Windows 10 双系统的文章发在了简书上,我写这篇文章的原因是当初装双系统确实是折腾了许久,网上也找不到一篇详尽的教程。由于去年对于写教程还不是熟练,而这一年多的使用过程也遇到了一些问题,所以就准备「Refactoring」这篇文章。
在教程正式开始之前,先花一点时间说明 EFI 分区的组成和作用。
首先,在你装了 Windows 之后,Windows 在装机过程中会将硬盘划分出一个约 100m 大小的分区,称为 EFI 分区这个分区就是起引导作用的。在资源管理器中是看不到的这个分区的,可以在磁盘管理中看到,管理则需要借助
DG 工具
。便于说明,在装好了 Linux 之后,我将 EFI 挂载至 boot 分区截图:
可以看到,该分区包含 3 个文件夹(如果你没有装 Linux 的话,就只有两个),分别是 Boot、Microsoft 和 Manjaro,其中 Boot 文件夹就是 UEFI 引导所必需的文件。
我们继续打开 Microsoft/Boot
文件夹:
这些文件就是启动 Windows 10 所必需的,包含了语言包、字体等,BCD 包含了 Windows 引导开始以后的信息。其中,bootmgfw.efi 是 Windows 默认引导文件。
以上是采用 UEFI 启动 Windows 10 的文件结构,也就是说,当你按下开机按钮的时候,首先 UEFI 找到 EFI 分区的 Boot 文件夹,然后加载 bootx64.efi
文件,读取文件信息,找到 EFI/Microsoft/Boot/bootmgfw.efi
,按照 bootmgfw.efi
的要求,加载所需的启动信息,启动 Windows 10。
在正式装系统之前,我们还需要做一些准备工作:
这个功能的作用是在于关机的时候不完全断电,类似将系统处于「休眠」状态,这样可以让开机更加迅速。但这也就导致了只能使用 Windows 系统。
在默认情况下,UEFI 固件只会加载那些被签名的引导程序。在缺少 Secure Boot 功能的传统 PC 机上,恶意的后门程序可以加载自身,进而摇身一变伪装成一个引导程序。这样的话,BIOS 就会在启动的时候加载后门程序,这样它就可以躲过操作系统,把自己隐藏得很深。 但是不得不说,这对我们安装 Linux 造成了很大的困扰,也是直接导致我们重启到 Windows 10 后进不去 Linux 的原因。 首先我们要关闭这个功能:进入 BIOS 找到 Secure Boot,选择 disabled,这样就关闭了。当然,有些人进入 BIOS 会发现 Secure Boot 这个选项是灰色的(比如我的就是),这时你需要先给你的 BIOS 设一个密码,然后就能关 Secure Boot 了。
所有的准备都已经完成,这时就可以准备刻录 U 盘了,不推荐 UltraISO,经亲测,软碟通仅刻录 Ubuntu 能成功,其它绝大多数发行版都会失败。推荐「
Rufus
」和「
USBWriter
」,这两个软件都可以。
刻录完成后,重启按 f12
,选择从 USB 设备启动,对于绝大多数发行版来说一路回车就行了,只需要注意一点:在选择挂载 boot 位置的时候,一定要挂载在 efi 分区,别的都不行。
重启之后,不出意外的话,你会直接进入 Windows 10,不要担心,这时 Linux 已经安装成功了,我们只需要将引导文件替换一下。
先用 DG 打开 EFI 分区,你会看到多了一个文件夹,名称取决于你安装的是哪一个发行版。我安装的是 Manjaro Linux,名称就是 Manjaro,打开之后会发现里面有一个名为 grubx64.efi 的文件,这就是启动 Linux 的引导文件。和 Windows 10 的 bootmgfw.efi 类似,我们想要用 grubx64.efi 引导代替掉 bootmgfw.efi,这样就可以用 GRUB 引导了。步骤:
bcdedit /set {bootmgr} path \EFI\Manjaro\grubx64.efi
#如果报错,就把 {bootmgr} 用单引号引起来,CMD 和 Powershell 语法的区别
至此,如果你安装的是除 Arch 之外绝大多数发行版,那么接下来就和你没有啥关系了,你已经成功了,好好享受吧!
开机之后会发现进入 GRUB 的引导了,通常会包含至少三个选项(以 Manjaro 举例):Manjaro、Manjaro 高级选项和 Windows Manager。这就代表你已经完美的解决了 Windows 和 Linux 双系统引导的问题。
这一点是我安装 Arch Llinux 的时候发现的,Arch Linux 安装过程是手动安装的,在编写 GRUB 的时候会扫描不到 Windows Manager 所在的分区(当然可能不是所有人都会遇到),所以在 GRUB 界面可能会看不到 Windows Manager 选项,导致进不去 Windows 10,这里就需要手动编辑 GRUB 信息,我们打开 /boot/grub/grub.cfg
文件,发现里面确实没有 Windows 10 的启动信息,在后面加上:
menuentry "Microsoft Windows 10" {
insmod part_get
insmod fat
insmod search_fs_uuid
insmod chain
search --fs-uuid --set=root $hints_string $fs_uuid
chainloader /EFI/Microsoft/Boot/bootmgfw.efi
}
注意:
这里的 $hints_string
,代表的是终端执行命令:
sudo grub-probe --target=hints_string /boot/efi/EFI/Microsoft/Boot/bootmgfw.efi
后的输出;
而 $fs_uuid
代表的是:
sudo grub-probe --target=fs_uuid /boot/efi/EFI/Microsoft/Boot/bootmgfw.efi
的输出。
然后保存。在终端执行命令:sudo grub-mkconfig -o /boot/grub/grub.cfg
,就 OK 了。
到此,Arch Linux 和 Windows 10 双系统也配置完毕了。
在使用这一年多的时间,遇到了以下的几个问题:
替换引导文件
的方法重新输入一遍命令就行。最后:祝使用愉快。
]]>If you don’t let go old things, new ones wouldn’t come. —— Nicolas Wincer
时间是在 9 月 27 日晚,我用了一年零 8 个月的 Kindle 正式宣布坏掉,原因是充不进电,我的第一反应是想着去修,后来还是打消了这个念头。主要是这个 Kindle 实在算是家族里的「老古董」了,我对 kpw3 的 300 ppi 也是种草许久,正好本着“旧的不去,新的不来”的观念,就入了一部 kpw3,其实在我想着要买 kpw3 的时候,是有点纠结 Voyage 的,因为用了快两年的 Kindle3 我已经习惯了实体翻页键,奈何囊中羞涩,只是为了这一个功能就要多花 600 +,有些不值当,想着等工作了之后直接上 Oasis。
其实我最近是比较少看书了,现在看的这本《雪中悍刀行》看了半年多才看了一半,上本《将夜》看了一年,一方面是看的书越多,品味自然也高了起来,现在写的好的小说是越来越少,之前一直很喜欢的几个作者要么更新是越来越慢(比方说:烽火戏诸侯,愤怒的香蕉)、要么是书的质量不如之前(比方说:烟雨江南、猫腻),有点担心自己看完了就书荒了。
我买 Kindle 不是为了亚马逊庞大的图书资源(我看书只自己在网上找),而是因为那块 E-ink 屏幕,而且因为 Kindle 那可怜兮兮的娱乐功能,用 Kindle 时可以更专注于看书。而国庆前几天一直忙于跑亲戚,所以直到今天才有空闲时间开箱。
这就是全部的配件(裸机 + 数据线)了:右边是卖家附赠的
参数 | 描述 |
---|---|
阅读灯 | 4 颗 |
解析度 | 300 ppi |
重量 | 205 g |
尺寸 | 169 × 117 × 9.1 mm |
屏幕 | 6 吋 |
容量 | 4 GB |
连接 | Wi-Fi |
运存 | 512 mb |
开完箱经过简单的设置之后,迫不及待的从电脑传了几本书(谁都阻止不了我想读书的心情!)。
吐槽一下,这里是无法像多看一样做成文件浏览的形式,也就是说,即使你把一些书放进新建的文件夹里(便于归类管理),它也是直接在首页显示。
这就是阅读界面的选项了,选项少的可怜,而且页边距太大!我这已经设置页边距最小了。
得益于 Kindle 这块 4:3 的屏幕,看漫画可以说是比手机更具优势。清晰度是够了,要是屏幕再大一些就好了:
如果想要购买正版书,就在上方的搜索按钮输入书名:
设置界面确实寒酸,不过想想要的只是纯粹的阅读体验,也就释然了:
使用了半个多小时两天多了,简单总结一下感受:
买 kpw3 之前其实还有一个顾虑,就是刷不了「多看」,我的电子书资源多是 「epub」格式的,而 Kindle 的原生系统是不支持「epub」格式的(我一直搞不懂为什么亚马逊不支持)现在我每一本书都要转成 「mobi」 才能在 Kindle 上看。
还有就是实体翻页键了,等我经济独立之后,一定要买 Oasis!
]]>Spacemacs 是一份 Emacs 的配置文件,将 Vim 的快捷键移植到了 Emacs 上,可以提供 Vimer 至 Emacs 的无缝衔接。有了 Spacemacs,你不需要花那么多时间去学习 Emacs 就可以真正用 Spacemacs 开始做一些事情。
$ mv ~/.emacs.d ~/.emacs.d.bak
$ git clone https://github.com/syl20bnr/spacemacs ~/.emacs.d
$ emacs
Clone 至本地后,第一次使用 Spacemacs 时要加载一些 Package,以及根据你的喜好所生成的配置,建议一路回车。
此时会加载很多的 Package,如果没有挂代理的话,就会很慢很慢,可以采用 emacs-china 的配置源。
Spacemacs 基本使用的是原生 Vim 的快捷键,此前请先熟悉 Vim 的操作。我这里只贴出个人认为比较常用的快捷键。
SPC f e d
快速打开配置文件
SPC f e R
同步配置文件
SPC q q
退出 Emacs
SPC q R
重启 Emacs
SPC f f
打开文件
SPC f t
neotree 方式显示文件路径
Ctrl s
搜索当前文件(需安装 ivy layer)
*
另一种搜索文件的姿势(需将光标置于需搜索的单词处)
n
下一个匹配N/p
前一个匹配r
改变范围:当前屏幕,当前函数,当前 buffere
编辑所有匹配(类似于替换)/
在当前 project 搜索SPC s c
清除搜索高亮
SPC f R
重命名当前文件
SPC f E
使用 sudo 来编辑文件(当某些文件的权限是只读的时候)
SPC f D
删除当前文件
SPC f r
打开最近文件列表(需安装 ivy layer)
SPC f y
复制当前文件的绝对路径
SPC f c
复制文件
SPC b b
显示已经打开的 buffer
SPC b d
关闭当前 buffer
SPC b h
进入 Spacemacs 初始界面
SPC b N
新建一个 buffer
SPC b R
从自动备份的文件中恢复
SPC b Y
复制整个 buffer 的内容
SPC b P
将剪贴板的内容粘贴到整个 buffer
SPC Tab
切换至上一个 buffer
SPC n(number)
跳转至第 n 号窗口
SPC 0
跳转至 neotree 侧边栏
SPC w m
当前窗口最大化
SPC w s
或 SPC w -
水平分割窗口
SPC w v
或 SPC w /
竖直分割窗口
SPC w =
平衡窗口
SPC w d
删除当前窗口
SPC w o
切换至其他窗口
SPC t g
将当前窗口与其他窗口 黄金分割
SPC p f
在当前 project 中查找文件
SPC p p
切换项目
SPC /
在该项目中搜索字符串
SPC p R
在项目中替换字符串,先输入「匹配」的,再输入「替换」的字符串(我一般不使用这种方式,我用*
来替换)
SPC j =
自动对齐
SPC m =
美化代码(不适用于所有语言)
SPC '
打开/关闭 Eshell(需安装 shell layer)
SPC a s
打开其它种类的 Shell
C g
输错命令时,可取消该次输入
将 dotspacemacs-line-numbers
的值改为 ‘relative
Spacemacs 中集成了 Git 管理工具,需先安装 git layer。
常用的快捷键:
git | magit |
---|---|
git init |
SPC g i |
git status |
SPC g s |
git add |
SPC g s 弹出然后按 s |
git add currentfile |
SPC g |
git commit |
SPC g c c |
git push |
SPC g P |
git log |
SPC g l l |
git checkout xxx |
SPC gn C |
git checkout -- xxx |
SPC g s 弹出然后按 u |
git reset --hard xxx |
SPC g s 弹出然后按 x |
终端使用 emacs -daemon
以守护模式开启 emacs:
$ emacsclient -c
打开 Emacs GUI
$ emacsclient -t
打开 命令行 Emacs
当开启守护进程时,点击关闭按钮后进程还是会保留在后台,如果想要彻底关闭 Emacs 可以:SPC q q
或者$ killall emacs
以下是我针对我常用的一些语言做的一些特殊的设置:
我没有采用 Spacemacs 提供的 c/c++ layer,而是采用的 Irony-Mode ,因为原生的 c/c++ layer 自动补全需要 ycmd,而 ycmd 安装配置起来实在太麻烦了。
:gdb
启用 gdb 调试
SPC c C
编译程序
cmake
编译,可以替换成 clang/gcc -g main.C -o main
(这些参数会被记住)Python 用的 Spacemacs 自带的 python layer,添加了一些参数:
(python :variables
python-enable-yapf-format-on-save t ;; 当保存的时候自动 `yapf' 美化
python-fill-column 80 ;; 开启 80 列的提示
python-sort-imports-on-save t) ;; 当保存的时候自动排序导入的包
, c c
运行当前文件
, =
美化代码
, '
打开 IPython repl
, g
跳转至定义处:
, g g
在当前窗口跳转至定义处, g G
在另一窗口跳转至定义处, g b
回到原处, s
将当前文件发送至 repl:
, s b
将当前 buffer 发送至 repl, s f
将当前 defun 发送至 repl, s r
将当前选中内容发送至 repl我将 JavaScript layer 自带的 repl 换成了 nodejs,自带的不太好用。
(javascript :variables
tern-command '("node" "/home/wincer/.npm-global/bin/tern") ;; 指定 `tern' 的路径
javascript-disable-tern-port-files nil)
设置了一些快捷键:(o 开始的默认为用户自定义的)
SPC o s i
启动 nodejs repl
SPC o s b
将当前 buffer 发送至 repl
SPC o s r
将选中内容发送至 repl
SPC o s l
将当前行发送至 repl
我是在学 sicp 时才用到 Scheme,所以采用的 Scheme 实现是 MIT-Scheme,并将其设置为默认 repl:
, '
切换至 repl
, s
评估算式:
, s b
计算当前 buffer, s e
计算最后一个表达式, s f
计算当前定义的函数, s r
计算当前选中的内容我的 Spacemacs 配置放在了 GitHub 上, 这是地址 。
]]>凡心所向,素履所往,生如逆旅,一苇以航。
一直很喜欢海子对于时间的说法——“打马而过”。就像我还没来得及细数,20 个年头匆匆已逝。没有那么多时间细想,这一天就这么来临了,来不及回忆过去,也来不及憧憬未来,一眨眼,就发现自己已经 20 岁了。
在许久之前,我便对自己的 20 岁有过憧憬,想着,20 岁的我会在哪里,做着什么事情。是有了一项划时代的发明,成为震惊世界的奇才;还是偏居一隅,发出「天地与我并生 万物与我为一」的感慨。是的,我希望自己能真实的活着,不像那些忙忙碌碌一辈子不知道为谁而活的人那样。不在意别人的眼光,不为了生存而活。
但是,现在的我,也就只是在大学里,做着大多数人应该做的事情,过着大多数人应该过的生活。看来在这二十年的生命中,我还是不够坚韧。
我想我是不甘于于平凡的,很小的时候,我就会告诉自己,不要去重复别人做过的事情,因为我是独一无二的(后来才知道原来小孩都会有这样的想法),我有自己的事情去做。现在回想起来,还真的觉得挺可爱的。
《搏击俱乐部》里泰勒抢了一个便利店员(雷蒙)的钱包并拿枪指着他的后脑勺,雷蒙跪在地上颤抖着,泰勒问他想做什么,同时扳下击锤,雷蒙颤抖得更厉害了。
“兽医”,雷蒙颤几乎是带着哭腔说了出来。
“我知道了,我要拿走你的驾照。我随时会去看你,我知道你住在哪”,泰勒说。
“要是在六星期内你没当上兽医,你就死定了”,泰勒把钱包还给他了,并让他跑回了家。
同行的杰克表示不理解:“拜托 那有什么好玩的?那样做有什么意义?”
泰勒背对着他,“明天会是他一生中最美的一天,他的早餐会比我们吃过的都甜美。”
蒋勋在《孤独六讲》中写到,好像只有孤独,生命可以变得丰富而华丽。
无人理解的泰勒,他的人生想必是华丽到了极点。他内心所真正向往的地方是只有自己知晓的一方天地,他会去做自己想做的事,并因此让自己的生命变得有意义起来。
这一切都是因为做自己喜欢的事情,无关别人,只是为了自己的热爱。
从小到大,父母乃至老师灌输的思想就是:用心念书,从市重点初中,到省重点高中,再到一本大学,过更好的生活。
是的,过去二十年我仿佛就是按照这个既定的轨迹,一步一步活成了别人眼中的自己。等到我现在可以反思我的生活时,才发现我想做的事情和我应该做的事情那条清楚的界限早已模糊不清,长期的压力仿佛让自己对一切都失去了兴趣。
我抗争过吗?当然抗争过,不过一个人的力量终究是难以改变什么,泰勒也深知这一点,才会成立“搏击俱乐部”。
这样的生活很可怕。
《三傻大闹宝莱坞》兰彻对法汗说:
知道我为什么第一名吗?因为我热爱机械,工程学就是我的兴趣所在,知道你的兴趣吗?这就是你的兴趣……跟工程学说拜拜,跟摄影业结婚,发挥你的才能,想想迈克尔杰克逊的爸爸硬逼他成为拳击手,拳王阿里的爸爸非要他去唱歌,想想后果多可怕?
是的,被别人强迫去做自己不喜欢的事情,是很可怕的。更可怕的是,被强迫的多了,就会麻木。
从小学到高中,我的生活一直像父母要求的那样,努力,不轻言放弃。被强迫穿着这许多外衣的我,沿着既定的轨迹一点一点的行进。
如果说,之前的我,不是为自己而活,那么从此时此刻,我就要像小时候自己想的那样,不为别人而活,为自己真实地活着。去寻找自己喜欢且甘之如饴的事情。
作家吴晓波在《把生命浪费在美好的事情上》中写到:
喜欢,是一切付出的前提。只有真心的喜欢了,你才会去投入,才不会抱怨这些投入,无论是时间、精力还是感情。
在这个世界上,不是每个国家每个时代每个家庭的年轻人,都有权利去追求自己所喜欢的未来,所以,如果你侥幸可以请千万不要错过。
我还年轻,以后的路还很长,我可以做得更好。
不要害怕前路,我会迈着缓慢而坚定的步伐走下去。
我不要自己做到最好、最优秀,只希望能在接下来的时光里,变得柔软而坚韧。
最后,二十岁快乐,送给自己。
]]>没错,我又双叒叕换评论系统了,从最初的网易云跟帖,到后来的 LiveRe,再到现在的 Disqus,两个多月就换了好了三四次(中间从 LiveRe 切换过一次 Disqus,后来又换回来了)了,仿佛我在折腾这些非博客主体的路上越走越远,也幸好我的博客才建成,没啥人留言,不然就得不偿失了。
其实 LiveRe 真的做的挺棒的,中国的本地化做的更是没话说,支持国内的社交媒体:微信、QQ、百度、人人、豆瓣、新浪,国外的支持的就更多了,上次我因为评论框颜色的问题发送了邮件,结果不到 12 个小时 LiveRe 中国区的负责人亲自发邮件解答了这个疑问,就这点来说简直太良心了。
但是美中不足的是:
我最不能忍受的就是第三点了,由于我博客是采用了 CloudFlare 的 Keyless SSL 技术,流量都会走 CloudFlare 的 CDN 节点,但是由于节点在国外,国内访问速度实在是太慢了,每次点开网页都会看到圈圈不停的转,这简直不能忍啊,于是我就想做一个延时加载的,后来想想,既然都要做延时加载的了,那我为什么不干脆换成 Disqus 呢?
那么说到 Disqus,之前为什么会不用 Disqus 呢,主要还是担心国内不会翻墙用户无法评论的问题,后来想想其实这点不重要,因为:
这样优化过后,总算好多了。
原理嘛,先用 ajax 异步发送一个 get 请求至 Disqus 服务器,接收成功则屏蔽按钮,加载评论;超时则自动断开,并显示加载按钮:
<%
/*
延迟加载 disqus,timeout 可以自己设置时长
*/
%>
<%
/*
由于我超时时长设置得比较短,所以可能翻墙了还是没有自动加载评论,这时就需要手动点击加载了
*/
%>
后续:我在这篇文章里面用了一种更合适的方式来 载入评论
最后,列一下我对博客的优化:
每一点优化我都有写文章,文章链接可以通过搜索关键字获取。
]]>静态博客的内容是很适合用缓存来加速访问的,除了采用常见的 CDN 加速和压缩博文等方法,通过客户端也可以实现加速访问,本文介绍的是「服务工作线程—— Service Worker」。关于 Service Worker 的具体介绍见 这里 。本文主要需要的是它的离线加载的特性。
本博客使用 Service Worker 可分为两个阶段,在我最初撰写本文的时候,使用的是 Service Worker 原生的接口。在不久之后,Google 推出了 sw-toolbox 和 sw-precache 用以让用户更全面的掌控 Service Worker 缓存的方式:包括版本控制、文件缓存级别、具体路径等,于是在我经历了漫长的实践后(其实是因为懒),有了本文 Version 2.0。
以下注册代码需要在网站的根目录添加,这样才能保证接管整个网站的全部资源。
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(function() {
console.log('A new service worker is being installed.');
})
.catch(function(error) {
console.log('Service worker registration failed:', error);
});
} else {
console.log('Service workers are not supported.');
}
将以上代码加入主题中,至于加在哪需要根据主题的结构决定。你只需要保证生成的静态资源中包含以上代码,那么就算添加成功。以 NexT 为例,你可以把以上代码添加到 ``/next/layout/_thrid-party/comments/ ` 下的任一评论配置文件中(前提是你开启了该评论组件)。
将以下代码保存为 sw.js
,并确保生成静态文件的时候,sw.js
在网站根目录下(你可以把它放在 source
文件夹内)。
"use strict";
(function() {
var cacheVersion = "-180503";
var staticCacheName = "asset" + cacheVersion;
var maxEntries = 100;
self.importScripts("https://cdn.jsdelivr.net/npm/sw-toolbox@3.6.0/sw-toolbox.js");
self.toolbox.options.debug = false;
self.toolbox.options.networkTimeoutSeconds = 1;
/* staticImageCache */
self.toolbox.router.get("/(.*)",self.toolbox.cacheFirst, {
cache: {
name: staticCacheName,
maxEntries: maxEntries
}
})
})();
首先指定 cacheVersion,在刷新缓存的时候会进行匹配;其次是一个 Cache Storage 名称的有关变量,我这里只是简单划分为静态资源——全部从缓存中加载的资源;关闭 debug 模式,设置 Timeout 时间为 1s。
其中 sw-toolbox 的 缓存级别 共有 5 个(网络优先、缓存优先、速度优先、仅缓存、仅网络)。
我这里采用的是 cacheFirst
,即缓存优先加载。可针对具体的资源进行不同的缓存级别分配。
其中 self.toolbox.router.get
表示每一个你需要操作的资源,第一个参数表示匹配的网址,第二个表示缓存级别,第三个是回调函数。
具体到以本站为例的话,你可以参考本站的 配置文件 。
可以看到在启用了 Offline
仍然可以加载页面
刷新页面可以看到许多资源是直接 ( from ServiceWorker ) 加载的,并未发起新的 http 请求。
is Serviceworker ready 详细列出了所有浏览器支持的情况。
服务器工作线程只能工作在 HTTPS 加密的网站上,本地的 localhost
是默认安全。
参考文章:
]]>我为什么会想到要写一个下载器呢,实在是被百度云给逼的没招了,之前用 Axel 配合直链在百度云下载视频能达到满速,结果最近两天 Axel 忽然不能用了,于是我就想着要不干脆自己写一个吧,就开始四处查询资料,这就有了这篇博客。
我假设阅读这篇博客的你已经对以下知识有所了解:
获取文件采用的是 requests 库,该已经封装好了许多 http 请求,我们只需要发送 get 请求,然后将请求的内容写入文件即可:
import requests
r = requests.get('http://files.smashingmagazine.com/wallpapers/july-17/summer-cannonball/cal/july-17-summer-cannonball-cal-1920x1080.png')
with open('wallpaper.png', 'wb') as f:
f.write(r.content)
随后看看文件夹,那张名为 wallpaper.png
的图片就是我们刚刚下载的。
但是这个功能太简单了,甚至简陋,我们需要多线程并发执行下载各自的部分,然后再汇总。
为了拆分,首先得知道数据块的大小,HTTP 报文首部提供了这样的信息:
Content-Length
字段(上文图片大小为 261258 bytes)import requests
headers = {'Range': 'bytes={}-{}'.format(0, 100000)}
r = requests.get('http://files.smashingmagazine.com/wallpapers/july-17/summer-cannonball/cal/july-17-summer-cannonball-cal-1920x1080.png', headers = headers)
with open('wallpaper.png', 'wb') as f:
f.write(r.content)
我们得到了图片的前 100001 个字节(Range 的范围是包括起始和终止的),打开 wallpaper.png
你应该能看到一幅“半残”的图。
这样我们里目标更近了一步,继续:
part = size // nums
for i in range(nums):
start = part * i
if i == num_thread - 1: # 最后一块
end = file_size
else:
end = start + part
def down(start, end):
headers = {'Range': 'bytes={}-{}'.format(start, end)}
# 这里最好加上 stream=True,避免下载大文件出现问题
r = requests.get(self.url, headers=headers, stream=True)
with open(filename, "wb+") as fp:
fp.seek(start)
fp.write(r.content)
嘛,线程多了起来就扔到线程池让它来帮我们调度。
功能复杂了,用对象来封装整理一下:
class Downloader():
def __init__(self, url, num, name):
self.url = url
self.num = num
self.name = name
r = requests.head(self.url)
self.size = int(r.headers['Content-Length'])
def down(self, start, end):
headers = {'Range': 'bytes={}-{}'.format(start, end)}
r = requests.get(self.url, headers=headers, stream=True)
# 写入文件对应位置
with open(self.name, "rb+") as f:
f.seek(start)
f.write(r.content)
def run(self):
f = open(self.name, "wb")
f.truncate(self.size)
f.close()
futures = []
part = self.size // self.num
pool = ThreadPoolExecutor(max_workers = self.num)
for i in range(self.num):
start = part * i
if i == self.num - 1:
end = self.size
else:
end = start + part - 1
# 扔进线程池
futures.append(pool.submit(self.down, start, end))
wait(futures)
至此,核心功能都完成了,剩下的就是实际体验的优化了。
完整的代码已托管至 GitHub,地址见 这里 。
很可惜,我写的这个下载器还是不能下载百度云直链,不过嘛,好多人都说结果不重要,都说重要的是过程,不是么?写这个下载器我也确实学到了许多,至于一开始我是出于什么样的目的?管他呢
]]>从 2013 年开始,手机 QQ 就已经不支持私人聊天记录的导出功能了(群聊的记录还是可以导出),目的当然是为了推广超级会员,毕竟超级会员的聊天记录有 2 年漫游时间,而不想给腾讯送钱的我,就只好另辟蹊径了。
配合视频教程食用更加哦~:https://youtu.be/Y4y-UWg5vco
我并不算是那种埋头造轮子的人,所以遇到问题总是先问谷歌,确实也寻找到了一些工具,可惜有的不能用能用的还要收费。看来还是需要自己动手(当然,不动手也就没有这篇文章了)。
安卓手机 QQ 的数据库文件保存在 data/data/com.tencent.monileqq/databases/[QQ 号].db
下,所以需要 Root(更改 AndroidManifest.xml 的 debuggable 属性之后可以使用 adb 工具导出),这里并非本文的重点,就不展开说了。数据库里面不仅有聊天记录,基本上包括了 QQ 号的所有信息。
不幸的是,里面的重要数据被加密了。
另外很幸运的是,加密方式采用的是「 异或加密 」,而用于加密的字符串就是你手机的 IMEI ,所有手机的 IMEI 都是不同的,这样也可以确保加密后的数据是唯一的,既然知道了加密方式和密钥,那么解密自然也就不是难事了。
先想一下,我们聊天记录想导出成什么格式:
我的想法是:时间--发信人--内容
这样的格式。打开数据库文件:
如下图,mr_friend_** **_New 就是你与每一个好友聊天的信息,包括昵称、备注、qq 号码、聊天记录等,直接查看就会发现是被加密过的。这一串 32 位的字符串就是 QQ 号码的 md5 值。
由于 QQ 在手机端使用的数据库是 sqlite,Python 有很方便的 sqlite 的工具,而且 Python 针对字符串处理很方便,这里就采用 Python 来解密。
用浏览工具打开数据库,以我的数据库为例:
msgData
保存的就是聊天记录,selfuin
就是聊天对象的 QQ 号码,time
就是发送消息的时间,既然知道了这三个就是我们想要的,那么接下来的就好办多了,解密这三个就好了。
既然牵扯到解密,自然也就逃不掉编码和解码。尤其是 msgData
项,确实是花费了我好久才解决(哼,我才不会说这是因为我对 Python 的编码不熟悉呢)。
代码已托管至 Gist,见 这里 。
参考:
]]>6 月 23 日。马龙、樊振东、许昕宣布退出 2017 年国际乒联中国公开赛。
本来针对这个事,我想写一篇长文来讲讲最近中国某些「魔幻」的地方,奈何后天就要考网络,只好暂且放下。
我一般不在博客上转载文章,不过鉴于这几天实在没有时间写,于是决定转载一下微博的一篇文章,出处已无法考证。
憋了一晚上了,大半夜了,看着事情从最初了英勇豪迈,从振奋人心,担忧,到最后深深的无能为力。 看它空降热搜,一路爬到第一,半个榜内都是国乒。 看它被封,被禁,被删博。 截了一路的图。 这是国球。
国家体育总局关心的是那几块闪闪发亮的金牌。所以呢,给他们空降教练组长,突然之间毫无征兆的给他们的恩师「另谋高就」,他们没法反抗,禁言,被收手机,不能发声,不能上微博。 金牌可以再有,但国乒,这个从 2006 到 2016 年,敢在体育总局面前下军令状的梦之队,被瓦崩了,就不能重来。 看到了吗,从杜塞世乒赛临阵换走孔令辉,到成都公开赛明升暗降刘国梁,风云诡辩,赛场上闪闪发亮的新老双子星,四个大满贯,转眼间就只剩下了马龙一人。 有人说他们,不爱国,退赛,逃避,搞得和国旗上印的是刘国梁的脸一样。 那你是没有看过,孔令辉在夺冠后的瞬间扯起衣服亲吻胸口的那个国旗小标。你是没有看过,许昕在赢下赛点后,扯着衣服指给全世界看,他是中国人,然后指着背后的 CHINA 留给世人一个坚不可摧的背影。 你是没有见过,一个体育项目,2006 至 2016,在漫长的十年里,一个队伍包揽了世界大赛中,他们在制度约束下所能获得的所有金牌银牌,以及铜牌。 是中国乒乓球。 爱不爱国,铁骨铮铮,天地可鉴。 所以为什么他们会放弃他们热爱的赛场,扣除上百的世界排名积分,在一个不用升国旗奏国歌的比赛中以这种极端的方式伸张正义? 他们心寒。 孔令辉走了,刘国梁走了。捧起国球一片天,使这个精神漫漫延续的人,都以这样的方式离开了他们所热爱的赛场,不是功满圆退,而是二话不说让你离开。 国乒在今年刚刚重新聘选重组完教练组,刘国梁说不想从政只想呆在球场。马琳王皓,曾经世界冠军重新回到了国家队,以另一种方式,教练员的方式。 然而,半年未到,体育总局下令国乒取消总教练主教练职位,设立组长分管男女队,让刘国梁去做乒协副主席。 连李永波在内,有 20 多位的乒协副主席。 毫无征兆。 你让他们如何接受。 这几天里,从东京到成都,他们到底经历了什么,只有他们自己知道。 下午蔡振华蔡局到了成都,对刘国梁的卸任发表了态度。 晚上,所有能发生声的运动员,无论是国家一队二队还是省队,退役没退役,还有教练员,都发了一条微博。他们都选择了用这种方式来抗议。 后果是什么,禁赛,谩骂,卸甲归田? 他们知道吗,同样也不知道。 马龙,赌上了他最好的现在,樊振东,赌上了他前途无量的未来,许昕,赌上了奥运后好不容易重拾的好状态。 他们怎么可以这么傻。 国乒不是没了刘国梁就转不下去,也不是没了许昕马龙张继科樊振东就转不下去,事实上,马龙,刘国梁在里约后都有过退役的念头,但是又是什么让他们选择了依旧留在了赛场上? 是那方寸球台,和牵动着万千国人心的白色小球。 刘国梁,大满贯。退役后的第一天站在了教练员的位置上,至此,整整十四年。 在这十四年,他有了两个可爱的女儿,一个叫赢赢,一个叫一一。 赢,是中国队赢。一,使中国队第一。 他这半生,都叱咤风云于这赛场,这一生中最重要的人,也与乒乓球挂了勾。
发声,这只是见不惯也不能接受一代功臣沦落如此地步。 不能反抗,唯有自燃。
没有任何一个项目可以做到如此地步。教练员,运动员,把自己串在一起,做同一条绳上的蚂蚱。 没有任何一个项目可以做到如此地步。女队教练员不够,男队教练去补,男队教练员不够,那运动员还可以坐镇场外。 没有一个项目任何一个项目,和乒乓球一样可以看到四面五星红旗闪闪升旗的模样。
拿什么赌?拿自己赌,拿世界第一去赌,拿整个职业生涯去赌。 他们是连赛后忘记握手都要大肆报道严重批评的运动员,罢赛简直是想都不敢想的事情,而如今却发生了。 为何会做出如此举动,我们都应该明白。
这是中国乒乓球队历史上最没有把握的一次比赛。 但我希望他们赢。
那句话没有变,无论怎样都不会变,国乒长虹,剑指东京。
不知道他们在酒店怎么样,有没有手机,看不看得到我们。
如果看得到,想告诉他们。
整个中国都在支持他们。
很奇怪,国乒为什么要改革?改革不是应该改掉不好的吗?日本队今年复制中国管理模式,韩国金泽洙回来重新凝聚团队,各国都在学习认可的管理模式,就因为上层的政治斗争,就随意改革?
要建立一个国乒体系花了刘国梁 20 年的心血,而毁掉只需要一个会议。
教练组扁平化,这是好听的说法,真实意思是业务和权力分开,由官僚进行垂直管理,中央集权。新建的“管理组”谁来空降?懂不懂乒乓球?会比一群世界冠军的教练还要懂?和教练组有分歧,谁听谁的?外行领导内行?
政治斗争,高于金牌利益,高于项目,高于运动员。这就是中国体育界。
为什么每一次,都要在巅峰的时候收割别人的心血,提走功勋,然后等低谷了再急巴巴请人来「临危受命」?前有中国女排,后,可能就是乒乓球。
不仅是体育总局,其实这就是中国的现实。
我很乐意看到中国这样最后会变得怎么样 :D
]]>目前关于破解百度云限速的方法网上提供了许多种,实则是殊途同归,即:高速链接 + 多线程下载工具。而目前获取的链接的方法并非完美且存在一些限制,但聊胜于无。我将目前网上能搜集到的方法一一列举,同时也会针对每个方法的适用性与方便性做出评价。
Windows 用 PanDownload 。
其它平台用 PanDownload 网页版 。
这一类方法都是以插件获取下载直链,再辅以多线程下载工具来达到不限速的目的,以下为各个插件的具体说明:(有关多线程下载工具 Axel 见文章末端)
此方法适用性应当比较高。以下是 Axel 开启 128 线程的示例:
缺点就是有些麻烦:需复制 Header 信息才可掉调用下载工具(如 Axel 等)下载,获得 Header 的方法就是打开调试窗口,粘贴该链接在 Chrome 地址栏,在 Network 选项卡中查看该链接的 Request Headers,至少需要将 Cookie、User-Agent 两项传入给下载工具。
注意:这里的 Cookie 并不是当前域名(pan.baidu.com)的 Cookie,是
pcs.baidu.com
的 Cookie,其实所需要的仅仅是 Cookie 的BDUSS
和pcsett
值
在提供 Windows 客户端的同时,PanDownload 还于最近提供了网页版,可直接将分享的文件提取出直链 :
打开 PanDownload 网页版 ,输入分享链接和提取码
会生成一个包含你想要下载文件的页面,点击
会进入这样的界面:
直接点击即可通过浏览器下载(如果你想用其它的工具下载,记得传递 Cookie),当然你也可以使用 Aria2 RPC 下载。
这是我使用 Aria2 下载的速度,配置文件见 这里 :
此方法的优点在于不用安装额外的浏览器插件,也不用下载客户端,比较方便手机和 Linux 用户。缺点在于 Aari2 的线程仍然有限,所以速度不会特别快,但也算比较理想了。
~~自本文最近一次更新起,该方法获取的链接已无法在 Axel 中使用,原因是 URL 参数中的 app_id 失效,但这失效的 app_id 的 URL 却仍然可以用 aria2c 下载。~~可以使用我 Fork 后修改 的版本作为代替。
我的小号在使用这个方法的时候被封了,直接 403,更换帐号后可正常下载。被封之后大概两天内会解封。建议线程数不要开太多,被封之后可以更换速盘下载。
clone 该仓库
Chrome -> 更多工具 -> 扩展程序 -> 加载已解压的扩展程序(需勾选开发者模式) -> chrome/release(文件夹)
进入想要下载文件的界面
勾选,点击 导出下载 -> 文本导出 -> 拷贝下载链接:
复制链接后,是一串格式类似以下内容的命令:
axel -o "xxxxxx" -H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" -H "Cookie: BDUSS=9aRnpJYjF-THlHUbbjxkTYUnjk^&8naddR2NscTF-cFZJVWV3cDBvVkVaeHpHOFNJcXRhQVFBQUFBJCQAAAAAAAAAAAEAAADvjlIvY3cwODI5OQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABKVg1oSlYNaS0; pcsett=4789643579-hukfa445465a15156c1515a5f12cxzw4" "URL" -n 233
其中包含两个 HTTP 首部信息:分别是 UA、Cookie,这两个信息在上一步骤的框里均会显示,不要直接复制我的,Cookie 会过期。其中最后一个参数 -n 233
是需要你手动输入的线程数量,即为采取 233 个线程下载。
该项目 算是目前比较完美的解决方案了,以下是使用 Axel 开启 256 个线程后的速度(不要在意中间的乱码):
原项目是将链接导出至 ariac2 下载,但是 ariac2 却只能最多开启 16 个线程,这对一般下载任务也够了,但是对于百度这种老流氓来说(每个连接限速至 10Kb/s ),还是不够用的,所以我 Fork 后采用 Axel 代替 ariac2,Axel 可以设置任意连接数)。
此方法也是我目前一直在使用的方法。
这个方法是酷安上流传已久的方法,但我一直都无法满速,还是在这里提一下吧,造福一下手机党:
netdisk;7.8.1;Red;android-android;4.3
同样该方法并非完美,用「ES 文件浏览器」获取的百度云链接只能在该手机端使用,因为该链接是通过本地端口远程链接所生成的,故还是有一些限制,速度不稳定(会有一些不稳定的波动):
这一类方法使用的是别人已经封装好了的第三方客户端,其实就相当于把 获取链接的插件 + 多线程下载工具 打包好成为一个客户端。比上一类方法方便性会强一些,但适用性会弱一些。
该软件只有 Windows 版。
该软件 不仅支持加速下载,并支持在线解压缩,并且文档很详细。
并且个人认为比速盘好,我有时候使用速盘下载提示限速,但此软件却能达到满速,推荐使用。
该软件 同样是由爱吾一位大神创作,同样只有 Windows 版。
与其它软件的不同之处在于它还支持网盘资源搜索的功能。
据说是支持直接通过分享链接下载的,但我试了一下通过分享链接下载总是报错。但登录后下载还是可以的。
该项目 是使用 Go 语言编写的命令行客户端,支持下载、分享、上传、离线下载等功能。
我没有使用过该项目,但该项目在 GitHub 上收获 6.5k 的 star,应当是获得了许多人的认可。
我为什么提倡使用 Axel 来代替 aria2c 作为多线程下载工具呢,原因是 aria2c 最多只能设置 16 线程下载,虽说网上有修改成 512 线程的版本,但我试了之后发现速度并没有提升。而 Axel 对此则没有限制。
该软件已经附在各发行版的源仓库中了,直接安装就行。对于 Windows 来说可以自行从源码编译,这是 教程 。
终端输入 axel --help
:
用法: axel [选项] 地址1 [地址2] [地址...]
--max-speed=x -s x 指定最大速率(字节 / 秒)
--num-connections=x -n x 指定最大连接数
--output=f -o f 指定本地输出文件
--search[=x] -S [x] 搜索镜像并从 X 服务器下载
--no-proxy -N 不使用任何代理服务器
--quiet -q 使用输出简单信息模式
--verbose -v 更多状态信息
--alternate -a 文本式进度指示器
--help -h 帮助信息
--version -V 版本信息
本文持续更新中。
]]>从去年 8 月到现在,终于无法忍受 Ubuntu 了,原因有以下几点:
考虑到以上三点,我选择了 Arch 系的 Manjaro Linxu ,选择 Arch 是因为去年有装过,是滚动更新模式,提供最新版本的软件,而不直接用的原因是安装步骤太过繁琐,没有必要,故而选择了基于 Arch 的 Manjaro 发行版。
建议采用「 rufus 」烧制到 u 盘,制作的时候选择 dd 模式,不要选择 iso 模式,否则会无法从 u 盘启动,随后一路点点点。
安装完成后,界面挺丑的,首先:
]]>
使用 Hexo 在 GitHub Pages 搭建博客时,博客作为一个单独的 GitHub 仓库存在,但是这个仓库只有生成的静态网页文件,并没有 Hexo 的源文件。这样一来换电脑或者重装系统后,再想找回源文件就比较麻烦了,这里推荐一种比较完美的方法解决备份问题。
clone
至本地,将之前的 hexo 文件夹中的 _config.yml、themes/、source/、scaffolds/、package.json 和 .gitignore 复制至 WincerChan.github.io 文件夹;.git/
删除,否则无法将主题文件夹 push(也可以将主题文件夹使用
子模块
的方式添加到该仓库);npm install
和 npm install hexo-deployer-git
(这里可以看一看分支是不是显示为 hexo);git add
、git commit -m ""
、git push origin hexo
来提交 hexo 网站源文件;hexo g -d
生成静态网页部署至 Github 上。这样一来, WincerChan.github.io 仓库就有 master 分支和 hexo 分支,分别保存静态网页和源文件。
在本地对博客修改(包括修改主题样式、发布新文章等)后:
git add
、git commit -m ""
和 git push origin hexo
来提交 hexo 网站源文件;hexo g -d
生成静态网页部署至 Github 上。即重复备份的 7-8 步骤,以上两步没有严格的顺序。
重装电脑后,或者在其它电脑上想修改博客:
git clone git#github.com:WincerChan/WincerChan.github.io.git
将仓库拷贝至本地;npm install hexo-cli -g
、npm install
、npm install hexo-deployer-git
。这里稍作说明:
ssh-keygen -t rsa -C "yourname#email.com"
,一路回车;id_rsa
、id_rsa.pub
两个文件,这就是密钥对,id_rsa 是私钥,千万不能泄漏出去;id_rsa.pub
文件的内容,注意不要粘贴成 id_rsa
,最后点击「Add SSH Key」。这里说一下步骤 4 为什么只需要拷贝 6 个,而不需要全部:
_config.yml
站点的配置文件,需要拷贝;themes/
主题文件夹,需要拷贝;source
博客文章的 .md 文件,需要拷贝;scaffolds/
文章的模板,需要拷贝;package.json
安装包的名称,需要拷贝;.gitignore
限定在 push 时哪些文件可以忽略,需要拷贝;.git/
主题和站点都有,标志这是一个 git 项目,不需要拷贝;node_modules/
是安装包的目录,在执行 npm install
的时候会重新生成,不需要拷贝;public
是 hexo g
生成的静态网页,不需要拷贝;.deploy_git
同上,hexo g
也会生成,不需要拷贝;db.json
文件,不需要拷贝。其实不需要拷贝的文件正是 .gitignore
中所忽略的。
关于如何使用 CI/CD 持续部署可以参考我 这篇文章 。
]]>5 月马上就要过去
似乎还没开始的这个学期,怎么就快结束了
时间怎么这么快?
长眠于 4 月之前的海子,对于时间,有一个生动的说法叫「打马而过」
有时觉得,这种匀速流淌不可改变的东西,才是真 TMD 残忍。
雨滴的出生到结束,就是从天空落向大地
似乎它的宿命就是滴落大地
回到家中,躺在沙发上
空气中弥漫着「熟悉的味道」
想必那就是家的味道吧?
临近期末
似乎应该很担心成绩挂科
我理想中的大学不是这样的
我讨厌把自己的能力和思考,都锁在一个 for 循环里
循环的条件是:你是一个大学生
那样该多无趣啊
我想 break,就像现在躺在沙发上
可以暂时跳出这个循环
突兀的断网
调试了许久之后
终于意识到可能没有网了
这是 break 出循环的代价?
在家里总可以敞开去吃
喝啤酒到胃涨还能强迫去吃饭
吃的饱了
总算能真切感受到自己不在循环里
明媚而灿烂的五月啊
要是心情烦躁的时候,写写博客吧
这也是断网唯一能做的开心事了
]]>有个白狐儿脸,佩双刀绣冬春雷,要做那天下第一;
湖底有白发老魁爱吃荤;
缺门牙老仆背剑匣;
山上有个骑青牛的年轻师叔祖,不敢下山;
有个骑熊猫扛向日葵不太冷的少女杀手;
这个江湖,高人出行要注重出尘装扮,女侠行走江湖要注意培养人气,宗派要跟庙堂打好关系;
而主角,则潇洒带刀,把江湖捅了一个通透;
江湖是一张珠帘。大人物小人物,是珠子,大故事小故事,是串线。情义二字,则是那些珠子的精气神。
年少时,看武侠电视剧里的侠踪剑影,总是莫名憧憬,甚至意犹未尽处,还会忍不住幻想自己是位飞流倜傥、快意恩仇的大侠,剑收于鞘时渊渟岳峙、剑起时又能挥出一片水银泻地云卷云舒。
日思夜想久了,于是便在心中有了一片江湖,有了一场江湖梦。
我想不止我是如此。
有句老话说,一千个读者的心中,就有一千个哈姆雷特。
同样的,一千个被现实社会的条条框框桎梏住的俗人们心间,便也有一千座不同的江湖。
求名者得名、求利者得利、求快意者得快意、求安稳者得安稳——这些在现实中并不存在的江湖就像是我们圆梦的地方,我们被称之为“规矩”“方圆”“社会准则”的枷锁束缚得越紧,就越是想要在内心最深处那片江湖里翻江倒海自在逍遥。
说白了,我们心中江湖上的那个状若侠客的自己,才是我们真正想成为的自己。
但人生有太多弯路,太多不可回头的路,一步踏错便再无转圜的余地,南辕北撤说的便是这个道理——有时候回头看,我们一路行来的方向,竟是和最初的梦想背道而驰,可我们被所谓社会的进步、所谓年轻人的成熟、所谓命运的安排这一类的东西追迫着、驱赶着,又着实没有时间停下来感伤,于是渐行渐远、梦想和现实也被拉扯得越来越沧海桑田。
到最后,我们的梦想,只剩下一副骨架、一副残骸,即是那座每每热血沸腾时便在心间浮起的海市蜃楼般的偌大一个江湖。
在那个江湖里,我们是最自在最洒脱不羁的那位侠客。
李淳罡:
大雨依旧磅礴。
她不起身,徐凤年便一直撑着伞。
老剑神李淳罡望向这一幕,瞪大眼睛。
随即眼中黯然落寞缅怀追忆皆有。
那一年背负那女子上斩魔台,一样是大雨天气,一样是撑伞。
世人不知这位剑神当年被齐玄帧所误,木马牛被折并不算什么,只剩独臂也不算什么,这都不是李淳罡境界大跌的根由,哪怕在听潮亭下被困二十年,李淳罡也不曾走出那个自己的画地为牢。
原本与世已是无敌,与己又当如何?
李淳罡想起她临终时的容颜,当时她已说不出一个字,可今曰想来,不就是那不悔两字吗?
李淳罡走到大雪坪崖畔,身后是一如他与绿袍女子场景的撑伞男女。
她被一剑洞穿心胸时,曾惨白笑言:“天不生你李淳罡,很无趣呢。”
李淳罡大声道:“剑来!”
徽山所有剑士的数百佩剑一齐出鞘,向大雪坪飞来。
龙虎山道士各式千柄桃木剑一概出鞘,浩浩荡荡飞向牯牛大岗。
两拨飞剑。
遮天蔽日。
这一日,剑神李淳罡再入陆地剑仙境界。
洪洗象:
正在经楼找寻一部典籍的陈繇踉跄跑到窗口,颤颤巍巍推开窗户,老泪纵横,嘴唇颤抖道:“王师兄,小师弟成了!”
山中炼丹的宋知命顾不得一鼎炉被凡人视作仙物的丹药,扑通一声跪下去,磕头道:“武当三十六弟子宋知命,恭迎祖师爷!”
在东海寻觅到一名骨骼清奇闭关弟子的俞兴瑞,正坐蒲台上传授那名弟子内功心法,抚掌大笑,笑出了眼泪,激动万分道:“李玉釜,你掌教师叔终于要下山了!”
七十二峰朝大顶,二十四涧水长流。其中最长一条飞流直下的瀑布犹如神助,低端被掀起拉直,通向毗邻那座唯有一名年轻道人修习天道的小莲花峰,瀑布如一条白练横贯长空,数万香客见到此景,仿佛置身仙境,更加寂静无声,偌大一座武当山,几乎落针可闻。水起作桥为谁横?齐仙侠亲眼见到古剑连鞘飞出太虚宫,尾随其后,沿着悬挂两峰峰顶水桥奔掠向小莲花峰,看到骑牛的怔怔靠着龟驼碑,喃喃自语:“今曰解签,宜下江南。”
一身朴素道袍的洪洗象拍了拍尘土,骑上一只体型巨大的黄鹤,望向江南。
江南好,最好是红衣。
徐脂虎缓缓转头,问道:“你到底是谁?” 一直被寄予厚望去肩扛天道的年轻道士羞赧嚅喏道:“洪洗象啊。”
徐脂虎重复问道:“你来做什么?”
年轻道士壮着胆子说道:“那年在莲花峰,你说你想骑鹤。”
她转过身,背对着这个胆小鬼。
这个放言要斩断赵氏王朝气运的道人,深呼吸一口,笑道:“徐脂虎,我喜欢你。”
“不管你信不信,我已经喜欢你七百年。”
“所以这世上再没有人比我喜欢你更久了。”
“下辈子,我还喜欢你。”
丫鬟二乔眨巴眨巴水灵眸子,小脑袋一团浆糊,只看到小姐捂着嘴哭哭笑笑的,就更不懂了,唉,看来小姐说自己年纪小不懂事是真的呀。
年轻道士伸出手,轻声道:“你想去哪里,我陪你。”
这一曰,武当年轻掌教骑鹤至江南,与徐脂虎骑鹤远离江湖。
仙人骑鹤下江南,才入江湖,便出江湖。
年轻道士深呼吸一口,等女子依偎在他怀中,那柄横放在龟驼碑边缘的所谓吕祖佩剑出鞘,冲天而起,朝天穹激射而去,仿佛要直达天庭才罢休。
九天之云滚滚下垂。
整座武当山紫气浩荡。
他朗声道:“贫道五百年前散人吕洞玄,五十年前龙虎山齐玄帧,如今武当洪洗象,已修得七百年功德。”
“贫道立誓,愿为天地正道再修三百年!”
“只求天地开一线,让徐脂虎飞升!”
年轻道士声如洪钟,响彻天地间。
“求徐脂虎乘鹤飞升!”
黄鹤齐鸣。
吕祖转世的年轻道士盘膝坐下,望着注定要兵解自己的那下坠一剑,笑着合上眼睛。
陈繇等人不忍再看,老泪纵横。
有一虹在剑落后,在年轻道士头顶生出,横跨大小莲花峰,绚烂无双。
千年修行,只求再见。
轩辕敬城:
修身在正其心。
莫道书生无胆气,敢叫天地沉入海。
成事者,不惟有超世之才,亦必有坚韧不拔之志。
轩辕青锋脑海中走马观灯,那些诗词文章一一浮现。
“我入陆地神仙了。”
轩辕敬城闭上眼睛,只见他七窍流血,却神情自若地双手摊开,似乎想要包容那整座天地。
以他为圆心,大雪坪积水层层向外炸起。
那一瞬间,有九道雷电由天庭而来。
辕敬城每年酿当归酒三坛,两坛都让人送来庭院,自己只余一坛。
所以他从来都是喝不够酒,而这里却是从来不喝,任由年年两坛酒搁着闲置,年复一年,酒坛子越多,酒香也愈发醇厚。
她终于启封一坛酒,搬来一套尘封多年的酒具,酒具是那男人自制而成。 反正除了习武,那人仿佛没有不擅长的事情。
独坐的她盛了一杯酒,放在桌上,好似对于喝不喝酒,犹豫不决,她没来由开始恼恨自己,伸手猛地拍掉酒杯。
半响后她起身去拿回酒杯,才发现杯底刻有两行小字,字迹清逸出尘。
人生当苦无妨,良人当归即好。
许涌关:
一刹那。
瞎子老许头脑一片空白。
他既然能活着走下累累白骨破百万的沙场,能是一个蠢蛋?
在北凉,谁敢说这一句徐骁不过是驼背老卒?
除了大柱国,还有谁?
瞎子老许那一架需要拐杖才能行走的干枯身体剧烈颤颤巍巍起来。
最后这位北凉赖活着的老卒竟是泪流满面,转过头,嘴唇颤抖,哽咽道:“大柱国?”
那人并未承认也未否认,只是喊了一声瞎子老许:“许老弟。”
只见瞎子老许如同癫狂,挣扎着起身,不顾大柱国的阻止,丢掉拐杖,跪于地上,用尽全身所有力气,用光了三十年转战六国的豪气,用光了十年苟延残喘的精神,死死压抑着一位老卒的激情哭腔,磕头道:“锦州十八-老字营之一,鱼鼓营末等骑卒,许涌关,参见徐将军!”
锦州十八营,今曰已悉数无存,如那威名曰渐逝去的六百铁甲一样,年轻一些的北凉骑兵,最多只是听说一些热血翻涌的事迹。
鱼鼓营。
号称徐字旗下死战第一。
最后一战便是那西垒壁,王妃缟素白衣如雪,双手敲鱼鼓营等人高的鱼龙鼓,一鼓作气拿下了离阳王朝的问鼎之战。近千人鱼鼓营死战不退,最终只活下来十六人,骑卒许涌关,便是在那场战役中失去一目,连箭带目一同拔去,拔而再战,直至昏死在死人堆中。
其实,在老卒心中,大柱国也好,北凉王也罢,那都是外人才称呼的,心底还是愿意喊一声徐将军!
被徐骁搀扶着重新坐在木墩上的瞎子老许,满脸泪水,却是笑着说道:“这辈子,活够了。徐将军,小卒斗胆问一句,那徐小子莫不是?”
徐骁轻声道:“是我儿徐凤年。”
老卒脸贴着被大柱国亲手拿回的拐杖,重复呢喃道:“活够了,活够了……”
鱼鼓营最后一人,老卒许涌关缓缓闭目。
徐将军,王妃,有一个好儿子啊。
我老许得下去找老兄弟们喝酒去了,与他们说一声,三十万北凉铁骑的马蹄声只会越来越让敌人胆寒,小不去,弱不了。
徐字王旗下,鱼龙鼓响。
老卒许涌关,死于安详。
温华:
一个时辰后黄龙士缓缓走下马车,马车渐渐远去,消失于风雪中。
黄龙士没有急于入院,而是在巷弄来回走了两趟,这才推开门扉。
短短一炷香后,一名年轻男子断一臂,瘸一腿,自断全身筋脉,只存一条性命,只拎上那柄原本就属于自己的木剑,离开了院子。
巷中雪上长长一条血。
“在老子家乡那边,借人钱财,借你十两就还得还十二三两,我温华的剑,是你教的,我废去全身武功,再还你一条手臂一条腿!”
他在院中,就对那个黄老头说了这么一句话。
然后这个雪中血人在拐角处颓然蹲下,手边只剩下一柄带血木剑。
年轻游侠儿泪眼模糊,凄然一笑,站起身,拿木剑对准墙壁,狠狠折断。
此后江湖再无温华的消息,这名才出江湖便已名动天下的木剑游侠儿,一夜之间,以最决然的苍凉姿态,离开了江湖。
刺骨大雪中,他最后对自己说了一句。
“不练剑了。”
徐凤年:
徐凤年闭上眼睛,双手搭在春雷上,有些明白一些事情了,为何徐骁如今还像个老农那般喜欢缝鞋?轩辕敬城本该像张巨鹿那般经略天下,最不济也可以去跟荀平靠拢,却被自己堵在了一家三口的家门以外,堵在了轩辕一姓的徽山之上,即使一举成为儒圣,仍是不曾跨出半步。骑牛的最终还是下了山,但这种下山与在山上,又有什么两样?羊皮裘李老头儿十六岁金刚十九岁指玄二十四岁达天象,为何断臂以后仍是在江上鬼门关为他当年的绿袍儿,几笑一飞剑?
说到底,都是一个字。
徐凤年想着她的酒窝,摇晃站起身。
他就算不承认,也知道自己喜欢她。不喜欢,如何能看了那么多年,却也总是看不厌?
只是不知道,原来是如此的喜欢。
既然喜欢了,却没能说出口,那就别死在这里!
徐凤年睁眼以后,拿袖口抹了抹血污,笑着喊道:“姜泥!老子喜欢你!”
拓跋春隼冷笑不止,只不过再一次笑不出来。
一名年轻女子御剑而来,身后有青衫儒士凌波微步,逍遥踏空。
女子站在一柄长剑之上,在身陷必死之地的家伙身前悬空。
她瞪眼怒道:“喊我做什么?不要脸!”
李当心:
唉,闺女,等你大些,就会明白只要在一个男人心中好看,你就是天下最好看的姑娘了。”
“啊?可徐凤年说我长得一般呐,完了!”
“闺女真是长大了,娘很欣慰呐。闺女,娘真不好看?不行,再下山一趟,还得买些胭脂水粉,多扑一些在脸上就好看了。”
“娘你又乱花钱,爹肯定要跟笨南北蹲墙角唠叨去了,他们一起叨叨叨,可烦了。”
“让他们叨叨去。哪天不叨了才不好。”
这娘俩,似乎挺俗气。
亏得各自身后爱慕着她们两个的光头,是那般佛气。
小和尚将洗好的袈裟晾好,望向房内自语到,“又是一个天晴的好日子。李子,师父说我没悟性,你也说我笨,咱们寺里两个禅,我都不修。你便是我的禅,秀色可参。”
千山以外是千山,这就是江山;六宫粉黛独看你,这就是美人。
白衣僧人笑道:“去吧,睡觉去。” 小和尚嗯了一声,道:“东西怕打雷,我去门外给她念经去。” 白衣僧人摸了摸自己光头,这徒弟。站在千佛殿门口,看到在泥泞中奔跑顾不得雨水的笨南北,白衣僧人呢喃道:“笨南北啊,你有一禅,不负如来不负卿。
少妇才喊完,嗖一下,一名白衣僧人就以屁滚尿流的姿态窜出那栋巍峨阁楼,来到少妇面前,笑呵呵道:“媳妇,走累了没,给敲敲腿?”
若是外人在场,定要认为以这女子一路行来表现出的蛮横,肯定要好生拾掇一番白衣僧人才会罢休,但真见着了自己男人,她却是轻柔说道:“不累呢,只是好几天没见着你,有点想你啦。”
本名原来是李当心的白衣僧人笑容醉人,也不说话。
既然有她,天下无禅。
在朋友的推荐下,这个简易的博客搭建起来了。
折腾了一天多,第一天结束的时候在 Github Pages 上看到自己的博客加载出来的时候,突然有种错综复杂的恍惚感。是的,它不是自己的 QQ 空间,不是新浪博客,不是豆瓣小站,也不是百度贴吧。它更像是属于自己的一块小小的领地,因而我满足这种归属感。我愿意、更乐于在上面安静劳作。
一个之前为地主打工的农民,现在通过自身努力终于分到了一块地,不再需要帮地主的土地创造价值时,于是,这个农民重生了,他可以自豪的宣告:Hello World。当然,这个农民确切的来说是个码农。
主题采用的是 Next,很好看的主题,使用文档见 这里 。
]]>