This commit is contained in:
parent
729704984c
commit
4bd975796e
148 changed files with 4865 additions and 27561 deletions
|
@ -1,12 +0,0 @@
|
|||
|
||||
site_nav_links:
|
||||
所有文章: /
|
||||
归档: /archives
|
||||
|
||||
site_links:
|
||||
- name: "thislight@mastodon.social"
|
||||
href: "https://mastodon.social/@thislight"
|
||||
rel: "me"
|
||||
class: "mastodon"
|
||||
- name: "Subscribe (Atom)"
|
||||
href: "/atom.xml"
|
|
@ -1,97 +0,0 @@
|
|||
---
|
||||
title: 博客2021年最终功能更新
|
||||
date: 2021-12-30 21:26:06
|
||||
tags:
|
||||
- Hexo
|
||||
- logbook
|
||||
- 博客功能更新
|
||||
---
|
||||
|
||||
我对博客功能的要求是拒绝花里胡哨,一切为阅读服务。现在是2021年年底,正好我要为我对年终总结的一些设想给博客更新一些功能:快速引用素材、Steam游戏卡片、引用Wikipedia条目。
|
||||
|
||||
<!--more-->
|
||||
|
||||
## 快速引用素材
|
||||
|
||||
之前我引用图片一直都是用图片的完整路径,实在是非常麻烦,所以我一直期待能找到一个简单的方法引用素材。原先的考虑是用[hexo-asset](https://github.com/cnzsb/hexo-asset),但是在一番简单搜索后发现hexo-render-marked在3.1.0+已经携带了类似功能了:https://hexo.io/docs/asset-folders.html#Embedding-an-image-using-markdown 。直接在_config.yml里打开就行。
|
||||
|
||||
````
|
||||
post_asset_folder: true
|
||||
marked:
|
||||
prependRoot: true
|
||||
postAsset: true
|
||||
````
|
||||

|
||||
|
||||
## Steam游戏卡片
|
||||
|
||||
{% steamgame 22380 %}
|
||||
|
||||
{% steamgame 412020 "《地铁:离乡》确实是非常不错的半开放世界线性流程FPS。" %}
|
||||
|
||||
搜刮到[hexo-tag-steamgames](https://github.com/HCLonely/hexo-tag-steamgame)可以实现这个。
|
||||
|
||||
## 引用Wikipedia条目
|
||||
|
||||
{% wikipedia title:Wikipedia lang:zh wikiButton:true %}
|
||||
|
||||
原来我是想用[hexo-tag-wikipedia](https://github.com/tuanna-hsp/hexo-tag-wikipedia)。但是:
|
||||
|
||||
1. 这个插件用的不是新的Restful API,实际获取到的字符串千奇百怪。
|
||||
2. 这东西一开始用不了,我一看控制台发现一串`$.getJSON`:它插入的脚本用的JQuery的API。然而我的网页上并没有JQuery。
|
||||
|
||||
最后我改了一下把它改成用XMLHTTPRequest从[Wikipedia的Restful API](https://en.wikipedia.org/api/rest_v1/#/)拉取数据。脚本很简单:
|
||||
|
||||
````
|
||||
|
||||
function buildArgsHash(args) {
|
||||
let argsHash = {};
|
||||
args.forEach(arg => {
|
||||
const params = arg.split(':');
|
||||
argsHash[params[0]] = params[1];
|
||||
});
|
||||
|
||||
return argsHash;
|
||||
}
|
||||
|
||||
function generateWikipediaTagHtml(args, content) {
|
||||
const argsHash = buildArgsHash(args);
|
||||
const title = argsHash['title'];
|
||||
|
||||
const lang = argsHash['lang'] !== undefined ? argsHash['lang'] : 'en';
|
||||
const baseUrl = `https://${lang}.wikipedia.org`;
|
||||
|
||||
const url = `${baseUrl}/api/rest_v1/page/summary/${title}?redirect=false`;
|
||||
|
||||
const tagId = Math.round(Math.random() * 100000);
|
||||
const embeddedScript = `
|
||||
window.addEventListener('load', function() {
|
||||
var element = document.getElementById('${tagId}');
|
||||
var req = new XMLHttpRequest();
|
||||
req.addEventListener("load", function() {
|
||||
var result = this.response;
|
||||
const extract = result.extract;
|
||||
element.prepend(extract);
|
||||
});
|
||||
req.addEventListener("error", function() {
|
||||
element.prepend('Failed to fetch wikipedia data for "${title}".');
|
||||
});
|
||||
req.open('GET', '${url}');
|
||||
req.responseType = 'json';
|
||||
req.setRequestHeader('accept', 'application/json; charset=utf-8; profile="https://www.mediawiki.org/wiki/Specs/Summary/1.4.2"');
|
||||
req.send();
|
||||
});
|
||||
`;
|
||||
let contentText = `<script>${embeddedScript}</script>`;
|
||||
if (argsHash['wikiButton'] === 'true') {
|
||||
contentText += `<p><a href="${baseUrl}/wiki/${title}">Wikipedia:${title}</a></p>`;
|
||||
}
|
||||
|
||||
return `<blockquote id='${tagId}'>${contentText}</blockquote>`;
|
||||
}
|
||||
|
||||
hexo.extend.tag.register('wikipedia', generateWikipediaTagHtml);
|
||||
|
||||
````
|
||||
|
||||
我把它塞到了我的fork里( https://github.com/thislight/hexo-tag-wikipedia ),找时间我可能问问作者再把它合并到上游,因为有一个breaking change。我打算后面把它改成在服务器上获取,这样动态插入一大段文字的体验挺糟糕,而且每一个访客都要获取一次有点滥用资源的意思。
|
BIN
source/_posts/Blog-2021-Final-Update/mastodon_Elephant_Friend_Curious.png
(Stored with Git LFS)
BIN
source/_posts/Blog-2021-Final-Update/mastodon_Elephant_Friend_Curious.png
(Stored with Git LFS)
Binary file not shown.
|
@ -1,68 +0,0 @@
|
|||
---
|
||||
title: 新年新主题!博客新主题Buck介绍
|
||||
tags:
|
||||
- 博客功能更新
|
||||
- Hexo
|
||||
date: 2024-01-11 19:36:34
|
||||
---
|
||||
|
||||
|
||||
请允许我隆重介绍本博客的新主题——Buck!跟原先一样,这是一个老Material Design风格的主题。Buck从头开始设计,重写了所有样式和代码,加入了新的考量和原则。让我自豪地在这里为你介绍一下Buck带来的改进和变化。
|
||||
|
||||
<!--more-->
|
||||
|
||||
## 平平淡淡不是真
|
||||
|
||||
如果你曾经访问过我的博客,你很可能会感觉本博客的设计非常素。低情商一点,可以说很无聊。旧的CMD(Classic Material Design)主题中,强调色几乎不存在,而且字体大小非常保守,元素又缺乏变化。在我的实际体验中,这样的问题使得不同元素之间缺乏对比,页面缺少“锚点”抓住眼球,显得很平淡。
|
||||
|
||||
有人说“平平淡淡”才是真,但是!在阅读长篇文章的时候,如果没有一些奇怪的东西打扰你的眼睛,很有可能你就会睡着了。说实话,我在看《编译原理》的时候就睡着过好几回,所以我想要偶尔有一些醒目的东西。当我将h1到h6元素映射到Material Desgin Typography的Display4到Title的时候,我发现效果意外的好:它就像直接拍到我的脸上。我的视线在Display4切换到主要文字的Body1时甚至需要重新对焦,但它又不会特别恼人。文章的展示意外地有了足够的层次感。
|
||||
|
||||
颜色也是一种对比。博客原本就有一个粉色的强调色,但是非常非常少用,因为我也不知道怎么放。这次我想到了一个好主意:将有超过一篇文章使用的标签用强调色标出。现在主页终于不再是蓝色、白色和黑色了。当然,主题色也做了一些调整:蓝色变深了,提高了与白色的对比度。
|
||||
|
||||

|
||||
|
||||
除此之外,当你将鼠标放到各种物件上时,都会有新的强调样式。在Buck中,我非常重视即时反馈,几乎每个物件都有自己的强调样式,包括代码块、块引用,甚至列表和表格。在文章中,各种物件的样式不再使用浏览器样式,而是使用接近Material Design的样式,看起来更漂亮了。
|
||||
|
||||
| 物件 | 基本反馈 | 增强反馈 |
|
||||
| -- | -- | -- |
|
||||
| 代码块 | 有 | 无 |
|
||||
| 块引用 | 有 | 无 |
|
||||
| 列表 | 有 | 无 |
|
||||
| 表格 | 有 | 有 |
|
||||
| 图片 | 无 | 无 |
|
||||
| 标签 Chip | 有 | 无 |
|
||||
| 卡片 | 有 | 无 |
|
||||
| 链接 | 有 | 无 |
|
||||
|
||||
与此同时,Buck能更充分地利用屏幕空间,不再像CMD一样将页面主题的宽度限制得太小。首页的内容不再挤在中间显示,而文章也有更大的空间舒展文字。
|
||||
|
||||
## 渐进式增强网页
|
||||
|
||||
Buck的JS、CSS以及其它资源现在使用esbuild打包。得益于打包工具的魔法,我能够轻易为Buck的网页进行“渐进式增强”。简单来说,“渐进式增强”就是在条件允许的时候提供相应的功能,在条件不允许的时候也有一个备用方案,而不是简单地告诉用户页面无法使用。
|
||||
|
||||

|
||||
|
||||
当你在表格的某一列的一个单元格停留时,相应列的标题就会亮起,这个效果使用了JavaScript来实现。所以当你关闭JavaScript的时候,这个效果就会消失,但是页面仍然能够观看。
|
||||
|
||||
除此之外,还有数个地方使用了JavaScript增强效果。我还准备实现一个图片全屏显示工具,得益于全新的代码架构,这个目的比之前更容易完成`:)`
|
||||
|
||||
## 可及性(Accessibility)
|
||||
|
||||
相比CMD,Buck一定程度上优化了可及性。我有相当一部分精力放在了“减少触摸的二义性”上。
|
||||
|
||||

|
||||
|
||||
在原先的CMD中,文章列表中的一个项目是一个纸片,点击这个纸片就会进入文章页面。这听起来是很棒的主意。但是,如果用户注意到这个纸片中内容梗概也包含可操作内容的话,就会产生二义性,用户难以预测触摸的结果。因为使用手指触发触摸屏会在屏幕上产生一个椭圆形的触摸区域:
|
||||
|
||||
1. 物理上:用户确实无法确定实际上点击了哪个位置。
|
||||
2. 心理上:手指遮挡了触摸区域,增加了用户心理上的不确定。
|
||||
|
||||
所以,在Buck中,进入文章的点击区域被限制在了标题处,避开内容梗概内的可操作内容;同时,可点击区域也有明显的标识。避免这样的二义性后,触摸的结果就更容易被用户预测到。在其它地方的可操作内容中,我也会采取了增加空白等方式,以求减小这种二义性的影响,我个人认为效果还可以。
|
||||
|
||||
除此之外,在键盘导航、颜色对比度、字体等方面,Buck相对CMD也有很多改进。
|
||||
|
||||
## 漫漫长路
|
||||
|
||||
开始一个新项目,也有点像在打RPG:最开始零级出厂、慢慢升级,中间有可能提前结局。Buck断断续续地写了快一年,终于有了点底子,但是还是有很多问题需要修正。如果大家发现了bug,还请多多包涵。我是在Webkit、Gecko和Chromium三家引擎上测试的,bug我大多已经发现了,只是需要时间修。但是,真的,我博客没写几篇,博客自己的代码倒是写了不少XD
|
||||
|
||||
在2024年的开头能把它做出来,也许是好事开端的预兆?我是希望如此。
|
BIN
source/_posts/New-Theme-for-Blog-at-the-start-of-2024/post-item-showcase.png
(Stored with Git LFS)
BIN
source/_posts/New-Theme-for-Blog-at-the-start-of-2024/post-item-showcase.png
(Stored with Git LFS)
Binary file not shown.
BIN
source/_posts/New-Theme-for-Blog-at-the-start-of-2024/table-enhanced-react-showcase.png
(Stored with Git LFS)
BIN
source/_posts/New-Theme-for-Blog-at-the-start-of-2024/table-enhanced-react-showcase.png
(Stored with Git LFS)
Binary file not shown.
BIN
source/_posts/New-Theme-for-Blog-at-the-start-of-2024/tags-new-ui.png
(Stored with Git LFS)
BIN
source/_posts/New-Theme-for-Blog-at-the-start-of-2024/tags-new-ui.png
(Stored with Git LFS)
Binary file not shown.
|
@ -1,67 +0,0 @@
|
|||
---
|
||||
title: 博客最近的架构改动
|
||||
tags:
|
||||
- 博客功能更新
|
||||
date: 2024-06-07 23:52:10
|
||||
---
|
||||
|
||||
|
||||
嗯,是的,我又回来了。最近打算写一篇关于数据驱动UI的博文,于是我又关注起了我的博客,然后发现有个事项好像还没做:更新博客的构建。正好我自己的Git托管服务也搭得差不多了,我就考虑把构建迁移到我自己的服务上。当然,现在这种情况再用GitHub Pages不方便——于是网页就换成Cloudflare Pages托管。
|
||||
|
||||
这就是这次架构更新的大概方向。不过,这次改动中我确实遇到了一些问题,这些才是有意思的部分。
|
||||
|
||||
<!--more-->
|
||||
|
||||
## Hexo的增量构建
|
||||
|
||||
不知道之前有没有人注意博客文章的时间,所有博客文章的更新时间都是一样的,而且实际上没有任何更新(xD)。我一直以为Hexo只能一次构建全部页面,但是这次我想顺便看看有没有增量更新的方案——然后我就发现Hexo其实是支持增量更新的!([GitHub issue](https://github.com/hexojs/hexo/issues/2920))只需要保留`db.json`和生成产物文件夹`public`就可以了。
|
||||
|
||||
所以我在构建的时候,直接将它们缓存起来,这样就可以最小化每次构建的变更([Actions工作流描述文件](https://code.lightstands.xyz/Rubicon/blog/src/commit/4bdac4215e3562a9d41ea9307df6a986fab4084f/.forgejo/workflows/depoly.yml))。
|
||||
|
||||
````yaml
|
||||
# ...
|
||||
- name: Cache Generated Files
|
||||
id: files-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
public/**
|
||||
db.json
|
||||
key: generated-files-1
|
||||
# ...
|
||||
````
|
||||
|
||||
当需要重新构建整个网站的时候,只需要改一下key就可以了。我想到这回事是因为另一个惨痛教训……
|
||||
|
||||
## actions/checkout@v4无法检出LFS文件
|
||||
这次迁移还涉及到另一个我想试用的新东西:[Git LFS](https://git-lfs.com) (Large File System)。作为一个博客,有点二进制大文件再正常不过了:图片、pdf、幻灯片……所以我按照说明在本地安装了程序、设定参数、Commit、推送、等待部署。看起来一切都挺好,然后我就睡觉去了。
|
||||
|
||||
第二天起床打开某篇文章一看:我的图片呢?打开开发者工具查看一下网络请求,是有这个文件,但是好像不是图片,而且统一在130字节左右。我知道检出LFS需要一个额外的下载过程,所以我检查了一下actions/checkout@v4,发现它有一个默认不启用的`lfs`参数。
|
||||
|
||||
看来就是这个问题,我就打开了这个功能,等待图片回到我的网站。
|
||||
|
||||
````
|
||||
[command]/usr/bin/git lfs fetch origin refs/remotes/origin/master
|
||||
fetch: Fetching reference refs/remotes/origin/master
|
||||
...
|
||||
error: failed to fetch some objects from 'https://code.lightstands.xyz/Rubicon/blog.git/info/lfs'
|
||||
::remove-matcher owner=checkout-git::
|
||||
::error::The process '/usr/bin/git' failed with exit code 2
|
||||
````
|
||||
|
||||
“?”
|
||||
|
||||
是不是遇上了和Forgejo Actions一样的网络问题(DNS不生效导致我没办法隔离执行actions的网络)?我怀疑是因为我手动将代码托管网站指定为内网地址造成的,于是我登上服务器修改了hosts,问题依旧。
|
||||
|
||||
然后我就只能在网络上漫游,寻找一个答案。然后,啊哈,我发现你啦!就是这个:[#164 'actions/checkout@v3' with LFS fails because of double auth header on gitea.com/gitea/act_runner](https://gitea.com/gitea/act_runner/issues/164#issuecomment-739631)。
|
||||
|
||||
原来这个是actions/checkout的bug,它会为两个相同的域名设置相同的HTTP头用作验证,导致HTTP头重复了。如果HTTP服务器比较严格的话就会拒绝服务。因为我没在Nginx的日志里找到相关信息,只能死马当活马医,先试试他的workaround,结果就能用了。
|
||||
|
||||
但是增量更新没有更新老文件,于是我再重新生成整个网站,图片这下就出现了。
|
||||
|
||||
大概是LFS用的人真的不多吧。
|
||||
|
||||
## Robots.txt
|
||||
这个是我偶然发现的。我想用Google的PageInsight看一下我网页的加载速度,自我得瑟一下(xD)。然后发现在SEO这项下面提示Robots.txt格式错误,里面是我的主页。
|
||||
|
||||
这大概是因为Cloudflare Pages对所有文件名找不到的请求返回主页(嗯,`index.html`,有人把这个叫做“伪静态”),所以如果没有Robots.txt,它也会返回主页。这也是个GitHub Pages没有的功能,所以我也没注意。为了正常被索引,我只好自己添加了一个空的Robots.txt。
|
|
@ -1,72 +0,0 @@
|
|||
---
|
||||
title: Flutter, Dart和Signal范式
|
||||
date: 2023-12-09 20:38:24
|
||||
tags:
|
||||
- Flutter
|
||||
- Dart
|
||||
- 前端开发
|
||||
---
|
||||
|
||||
最近看到很多在Flutter上实现Signals范式的项目,其实我自己也试了一下。感想是:Dart缺少太多语法特性了……真正的Signals范式需要很多胶水代码,在JS上这些胶水代码都是用代码生成器生成的,但是Dart和Flutter让这个生成器不是那么的好写,或者没法方便的用自带的特性做类似的功能。
|
||||
|
||||
<!--more-->
|
||||
|
||||
比如说最关键的自动依赖跟踪,这是实现这个Signals的项目都有一大堆胶水代码的关键原因。举个JSX的例子:
|
||||
|
||||
```tsx
|
||||
<Hello name={isWorld() ? "world" : name}/>
|
||||
```
|
||||
|
||||
|
||||
会被转译成类似下面的JS代码。
|
||||
|
||||
```js
|
||||
createComponent(Hello, {
|
||||
get name() {
|
||||
return (isWorld() ? "world": name);
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
这样才能做到两个功能:
|
||||
|
||||
1. lazy evaluation。只有从props里面获取name时才会evaluate相应表达式;
|
||||
2. 自动依赖跟踪。通过模拟一个dynamic-scope variable,lazy evaluation可以让signal在被访问时获得这个变量的值来跟踪依赖。这个是最简单而且计算最少的实现方法(时间复杂度可以做到常数级)。
|
||||
|
||||
但是在Dart和Flutter Widget里面,你很难处理成这样。
|
||||
|
||||
为了理解这个挑战,可以考虑一下下面这样的API怎么在Dart和Flutter上实现:
|
||||
|
||||
```tsx
|
||||
// 怎么恰当处理组件的类型?
|
||||
const Hello: Component<{name: string, effectName: string}> = (props) => {
|
||||
const [isWorld, setIsWorld] = createSignal(false);
|
||||
|
||||
createEffect(() => {
|
||||
if (!isWorld()) {
|
||||
console.log(props.name); // 如何跟踪这个依赖?
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
console.log(props.effectName); // 如何将这个的更新与上面那个区别开?
|
||||
});
|
||||
|
||||
return <div>
|
||||
<p>Hello, {isWorld() ? "World" : name}</p> {/* 这个表达式该如何处理? */}
|
||||
<button type="button" onClick={() => setIsWorld(true)}>Toggle</button>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default Hello;
|
||||
```
|
||||
|
||||
如果你通过InheritWidget这种来代替dynamic-scope variable做(依赖跟踪),它需要在Element树向上查找,这个性能损失太大了。Dart也没有好用的proxy范式,所以很难简单的实现lazy evaluation。如果你的参数收Signal对象,那也是胶水代码的重要来源。
|
||||
|
||||
Getx虽然很“脏”,但是它确实充分发展了Dart和Flutter提供的东西。
|
||||
|
||||
如果要在Flutter上实现Signal范式,我估计有几个是必须的:
|
||||
|
||||
- 计算类型
|
||||
- inline object或者inline class
|
||||
- 不再围绕Widget设计API(Signal系统可以提供的粒度其实比现在的Widget更小,它完全可以直接控制Element)
|
|
@ -1,46 +0,0 @@
|
|||
---
|
||||
title: 最后时分
|
||||
date: 2024-01-14 15:49:03
|
||||
tags:
|
||||
- 小说
|
||||
---
|
||||
|
||||
看着远处的黑色圆盘,我感到从未有过的平静。
|
||||
|
||||
我是说——我从未有过一次这样的经历,在宇宙中孤独地向前移动。遥远的“臂”上散落颗颗星辰,而在臂之外的地方,只留下一片漆黑的空白。
|
||||
|
||||
<!--more-->
|
||||
|
||||
我不禁思考:巨大的宇宙中一个渺小的人类,他在里面连一个黑点都算不上,却能欣赏宇宙给他展示的奇妙画面。而且——并非隔着高强度的钢化玻璃窗口,是站在真空中并无阻隔地接受遥远的光线,尽管眼前还有另一块玻璃……
|
||||
|
||||
但是一切都不一样,这里没有灯光,也不再需要各种闪亮亮的仪器。轻轻伸出手,我仿佛能够直接感受到光线在我的皮肤上流动,这种幻想震撼到了我。
|
||||
|
||||
那个黑色圆盘离我又近了一点。我这时觉得有点惶恐:我怎么能有如此幸运,能够以如此,壮美的方式脱离尘世的纷纷扰扰呢?我告诉自己:我在期待那个时刻的到来。
|
||||
|
||||
“林克……”无线电里传出声音,“我非常抱歉……”
|
||||
|
||||
“这只是个意外,马丁。”我勉强转过身,看着越来越远的飞船。我也相信飞船上的人正在注视着我。
|
||||
|
||||
“我很好奇你平常的脏话一个词都没出现,你不觉得‘去你的怎么飞得那么远了’比较像你说的话一点吗?”我微笑道。
|
||||
|
||||
他干笑了两声。麦克风好像被谁抢了。
|
||||
|
||||
“林克先生,”我最好的学生和朋友,安妮:“时间不多了,如果你有什么话……要留给谁的话。”
|
||||
|
||||
我假装没听到抽泣声,转过去看了看越来越近的黑色圆盘。
|
||||
|
||||
我说:“能麻烦你们录一下吗?我想给萨拉利·蒙切尔留个言,他是我的律师。”
|
||||
|
||||
无线电安静了好一会,马丁打破了沉默:“开始录了。”
|
||||
|
||||
“尊敬的萨拉利·蒙切尔先生,我是朋特·林克。”我能听到一些沙沙声,但是我很确信无线电能清晰地把我的声音传递给他们。
|
||||
|
||||
“我即将在一次星际航行中牺牲,请您在确认我死亡后,根据我的委托条款对我身后的财产进行处理。向您致以我的敬意。”无线电又安静下来,但是我痛恨这种沉重的寂静。
|
||||
|
||||
“可以了。”我补充。我想象一粒光子静静的飞入远处那黑色圆盘,静静地绕着圆盘。我又转过身,凝视那艘飞船。我相信他们仍在看着我。
|
||||
|
||||
对他们来说,这是对“将死之人”的尊重。
|
||||
|
||||
我招了招手,背后越来越热。“宇宙永不沉默。”
|
||||
|
||||
还有二十秒。
|
|
@ -1,35 +0,0 @@
|
|||
---
|
||||
title: 人文社会科学的“客观真理”(文摘)
|
||||
date: 2022-02-12 21:00:03
|
||||
tags:
|
||||
- 文摘
|
||||
- 社会科学
|
||||
author: 潘绥铭
|
||||
---
|
||||
|
||||
> 本文摘录自潘绥铭为《中国人的男男性行为 性与自我认同状态调查》(2005)所作序言《自己领悟自己的路》,原小标题为“3.能不能自己研究自己?”。我认为,这篇序言中的一些观点不仅适用于关于MSM的研究,也适用于我们对待科学事物的态度。其中提到的一些现象,到今天也仍然盘踞在我们的社会之中。选节提出了一个我很赞同的观点:“人文科学中没有‘客观’”,其中的“MSM”是Man who have sex with man(有男男性行为的男人)的缩写。
|
||||
|
||||
最近几年来,凡是请我讲“同性恋问题”的人,我都对他们说:“同性恋”自己已经做了大量的研究,出版了许多书,拍了许多电影。你们为什么不请他们自己来讲一讲呢?结果他们都说:还是你讲吧,你讲的客观。言外之意就是,MSM自己研究自己是不客观的。
|
||||
|
||||
这又是一个“认识论”方面的大问题,一个中国人“被现代化”的思想方法产物,一个“模仿自然科学”的思维定势恶果。
|
||||
|
||||
<!--more-->
|
||||
对自然科学来说,确实存在着一个客观不客观的问题,你不可以在做实验的时候偷偷往里面加入一些别的东西,也不可以在试验结果里加进去自己的主观看法。但是,一些人文社会科学都不是研究“物”的,都是研究“人”的;尤其是,他们都是由人在研究人,而不是计算机在研究人。可是,研究者自己也生活在现实社会里,也不可避免地会受到社会的种种影响。无论他多么自觉地、努力地排除这种影响,在研究任何其他人的时候,怎么可能不加入自己的体验与看法呢?怎么可能保持所谓的“客观”呢?在学术上,这叫做“研究者的价值观不可能完全中立”。
|
||||
|
||||
自然科学的方法论有三大要素:受控条件下的、可重复的、试验。只有同时满足这三条,其结果才是“客观的”与“科学的”。例如:“水100度烧开”这个结论,就必须是至少在1个大气压和使用纯净水这两个受控制的条件下、无论谁去烧都是这样;并且必须真的去烧,而不是仅仅在推理。
|
||||
|
||||
可是,人文社会科学连一条也做不到。首先,无论你研究什么,你都不可能拿全人类做实验,甚至拿一个人去做实验都不可能。其次,人类的一切行为都是不可重复的,因为人是在不断成长中的。最后,人生活于其中的社会条件与历史条件,没有一个研究者能控制得了。
|
||||
|
||||
因此,非要拿人文社会科学去模仿自然科学,这根本就是一个认识论方面的原则错误。
|
||||
|
||||
MSM问题也是如此。且不论道德伦理与法律允不允许,仅仅是为了满足“受控条件下的”这个要求,我们有可能把一些MSM关在观察室里去研究他们吗?无论谁去看,难道他们都会做出产生同样表现的事情,以便满足“可重复”这个要求吗?最后,可以拿他们做实验吗?
|
||||
|
||||
因此说,非MSM的人去研究MSM,并不比他们自己研究自己更加“客观”,不仅因为被研究的MSM不是石头,也因为研究者自己并不是仪器。因此,如果你担心MSM自己研究自己可能会美化自己,那么你怎么不担心非MSM的研究者可能会丑化他们呢?而且,“美化”和“丑化”,是否有一个不变的“客观”的标准呢?
|
||||
|
||||
其实,严肃的人文社会科学研究者所主张的,根本就不是模仿自然科学,更不是全盘照搬,而是在研究的过程中,应该自觉地、尽可能地排除那些缺乏根据的、主观臆断的东西。反过来,严肃的学者都会在论述中不断地主动指出:哪些是被研究者自己的感受与说法,哪些是我自己的体验和看法。尤其是,严肃的学者绝对不会把自己的看法强加给被研究者,更不会宣称唯有自己的看法才是“真实的”、“客观的”、“科学的”。
|
||||
|
||||
我们中国人之所以容易喜欢“模仿自然科学”,完全是历史的产物。从小到大,我们听过无数遍“客观规律”、“科学真理”之类的宣传,终于使我们相信:对于人类、对于社会、对于我们这些活蹦乱跳的生命体,居然也有这样一类东西在冥冥之中控制着。结果,我们总是喜欢问“客观不客观”、“科学不科学”,却浑然不知,这些词汇根本不能套用到人文社会科学领域中来。
|
||||
|
||||
因此,无论是谁来研究MSM问题,都应该仅仅使用这个唯一的标准来判断。这其实是一个学术水平的问题、思维能力的问题,而不是性取向的问题。
|
||||
|
||||
> 有趣的是,哈耶克在《个人主义与经济秩序》中曾精彩地论证过一个相近的观点,他的论证围绕信息不对等和信息加工处理带来的主观性问题。如果说这些更多的是在哲学层面上的讨论,那么雨果 梅西耶和丹 斯铂伯的《理性之谜》更多的从心理学的角度讨论了类似的问题————理性。
|
|
@ -1,99 +0,0 @@
|
|||
---
|
||||
title: TypeScript和Service Worker
|
||||
tags:
|
||||
- Web
|
||||
date: 2024-10-16 22:58:32
|
||||
---
|
||||
|
||||
|
||||
在Service Worker中使用TypeScript还蛮麻烦的,因为Service Worker的类型属于内建类型库WebWorker下,默认情况下WebWorker的`self`是`WorkerGlobalScope & typeof globalThis`。但是Service Worker的`self`确实提供了一些Worker没有的服务,比如install事件,用`WorkerGlobalScope`确实不够。
|
||||
|
||||
<!--more-->
|
||||
|
||||
````json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "WebWorker"]
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
所以实际上,WebWorker也提供了`ServiceWorkerGlobalScope`……这个设计让人感觉自然又诡异。ServiceWorker确实是Worker的一种,但是:因为默认情况下你不能用不同类型覆盖值的类型(但是可以合并!这个不是本章重点,TS就是这么神奇),按照直觉的:
|
||||
|
||||
````ts
|
||||
declare let self: ServiceWorkerGlobalScope & typeof globalThis;
|
||||
````
|
||||
|
||||
很有可能会被报错:不能用不同类型覆盖值的类型声明。当然,这是一个很有必要的安全措施,否则类型就会随着声明的顺序变化。用`// @ts-ignore`可以隐藏这个错误,但是在代码联想中它仍然是`WorkerGlobalScope & typeof globalThis`,这不是我想要的。
|
||||
|
||||
在[TypeScript项目相关的Issue](https://github.com/microsoft/TypeScript/issues/14877)中很多人提到了一种绕过的方法:类型强转加上重命名。
|
||||
|
||||
````ts
|
||||
const sw = self as unknown as (ServiceWorkerGlobalScope & typeof globalThis);
|
||||
````
|
||||
|
||||
然后使用`sw`来做Service Worker专门的工作。这个workaround非常简单有效,但是我觉得它有点不够漂亮。首先是类型强转有风险,其次是我们明明有`self`,却要用`sw`,像是这串代码不是在Service Worker中工作的一样。那么,我们可以直接用一个局部新`self`覆盖掉全局`self`吗?
|
||||
|
||||
````ts
|
||||
const self: ServiceWorkerGlobalScope & typeof globalThis = self;
|
||||
let self: ServiceWorkerGlobalScope & typeof globalThis = self;
|
||||
````
|
||||
|
||||
上面的代码都不可以用。因为JavaScript的词法作用域在同一个作用域内是没有shadowing的,所以你只能得到一个“块级变量不能在声明之前引用“,包括这种:
|
||||
|
||||
````ts
|
||||
const self0 = self;
|
||||
const self: ServiceWorkerGlobalScope & typeof globalThis = self0;
|
||||
````
|
||||
|
||||
也属于这种错误。
|
||||
|
||||
那我们可以用一个函数把`self`抓住吗?
|
||||
|
||||
````ts
|
||||
const captureSelf = () => self;
|
||||
const self: ServiceWorkerGlobalScope & typeof globalThis = captureSelf();
|
||||
````
|
||||
|
||||
大成功!可以通过语法和类型检查。但是workbox构建时会出错:
|
||||
|
||||
````text
|
||||
Error: Unable to find a place to inject the manifest.
|
||||
This is likely because swSrc and swDest are configured to the same file.
|
||||
Please ensure that your swSrc file contains the following: self.__WB_MANIFEST
|
||||
````
|
||||
|
||||
静态分析中一般会把函数当作受副作用影响的部分,它们的返回值不会被看作是不变的。可能就是因为这样workbox-build就不再认识我们的`self`了。
|
||||
|
||||
那词法作用域不行,函数作用域呢?这样写并不会有语法错误:
|
||||
|
||||
````ts
|
||||
var self: ServiceWorkerGlobalScope & typeof globalThis = self;
|
||||
````
|
||||
|
||||
但这样做只会得到`undefined`。你可以尝试在浏览器的开发者工具执行下面的代码:
|
||||
|
||||
````js
|
||||
(function () {
|
||||
var window = window;
|
||||
console.log(window);
|
||||
})()
|
||||
````
|
||||
|
||||
因为在`var window`的时候这个变量已经在函数作用域中被声明了,所以当你`window = window`时就是`window = undefined`。
|
||||
|
||||
在那个相关的Issue中,有人把`self`转换类型传入一个单独的函数。这样做也不错,可以保留自己的`self`,不过还是要多一层栈,而且要调用函数,跟类型强转也没有太大区别。
|
||||
|
||||
如果可以调用函数……我们将会有is语法可以用:
|
||||
|
||||
````ts
|
||||
function isServiceWorker(self: WorkerGlobalScope): self is ServiceWorkerGlobalScope {
|
||||
return true;
|
||||
}
|
||||
````
|
||||
|
||||
然后将Service Worker专属的操作放进 `if (isServiceWorker(self)) { ... }`,一切正常!而且这个方法可以让我们分别做Service Worker和Worker的兼容。不过我们仍然要调用一个函数,而且会多一个branching,好在Service Worker的顶层代码不常执行,所以这个代价也能接受。
|
||||
|
||||
当然,其实类型强转到另一个变量的做法也可以,而且不需要branching的开销。虽然这样说,但其实影响都不大,主要还是看你比较喜欢哪个。
|
||||
|
||||
如果你对我如何处理Service Worker感兴趣的话,可以看看[图图的相关代码](https://code.lightstands.xyz/Rubicon/tutu/src/commit/46e7f1aaea91f742dd816369fbed0e8dc85944cb/src/serviceworker)。图图是一个在Web技术上构建的Mastodon客户端,如果你能试用并给我一些反馈的话就更好啦!
|
|
@ -1,81 +0,0 @@
|
|||
---
|
||||
title: 福岛第一核电站污水处理对策问与答
|
||||
date: 2023-08-25 20:52:31
|
||||
tags:
|
||||
---
|
||||
|
||||
作者并非该领域专业人士,这篇文章收集并整理了部分问题和回应,仅作为补充。
|
||||
|
||||
了解更多:
|
||||
- [常见问题 - 福岛第一核电站处理水排放 - 国际原子能机构](https://www.iaea.org/zh/zhu-ti/xiang-ying/fu-dao-di-yi-he-dian-zhan-chu-li-shui-pai-fang/chang-jian-wen-ti)
|
||||
- [问与答 - 处理水门户网站 - 东京电力](https://www.tepco.co.jp/zh-cn/decommission/progress/watertreatment/faq/index-cn.html)
|
||||
- [日本作业与国际监督 - 放射性物质海域扩散海洋资讯平台 - 行政院原子能委员会](https://tworis.aec.gov.tw/JapanAndSupervision)
|
||||
|
||||
<!--more-->
|
||||
|
||||
关于IAEA
|
||||
------
|
||||
|
||||
### IAEA是一家什么样的机构?
|
||||
IAEA是International Atomic Energy Agency的缩写,可叫作“国际原子能机构“。IAEA是一个独立政府间国际组织,作为政府间的科学技术协作论坛,致力于和平发展核技术和核能源。IAEA各成员国协商制定[原子能机构“安全标准”](https://www.iaea.org/zh/shu-ju-ku/an-quan-biao-zhun),并推荐各个成员国参照该标准制定法规。
|
||||
|
||||
### 为什么IAEA不审查福岛核电站污水处理对策的合法性和正当性?
|
||||
IAEA的主要目的是推动核技术的国际间协作。它能够审查污水处理对策是否符合安全标准,但无法确认污水处理处理对策是否合法或正当。
|
||||
|
||||
IAEA的特别工作组已经按照[原子能机构“安全标准”](https://www.iaea.org/zh/shu-ju-ku/an-quan-biao-zhun)审查日本政府和东京电力公司的排放计划和相关活动,并将独立进行监测,确证日本政府和东京电力公司公布的数据。
|
||||
|
||||
IAEA的审查结果对日本政府没有法律约束力,但提高了处理对策的总体透明度,有助于国际社会监督污水处理过程。
|
||||
|
||||
关于污水处理对策
|
||||
--------
|
||||
|
||||
### 福岛核电站直接将稀释的核污水排放到海洋中!
|
||||
按照排放计划,核污水不会排放到海洋中,排放到海洋中的是稀释后的”ALPS处理水“。污水经过多道工序处理才能得到ALPS处理水,除氚以外的物质都符合日本国家标准。
|
||||
|
||||
ALPS处理水在排放之前,还会用大量海水进行稀释,确保氚的浓度符合标准:排放液体中氚浓度低于1500Bq/L,远低于世界卫生组织饮用水标准(10000Bq/L)。
|
||||
|
||||
IAEA根据其全面评估得出结论,东京电力公司目前计划的ALPS处理水的排放对人们和环境的放射性影响可以忽略不计。
|
||||
|
||||
### 为什么日本相关机构在排放处理水之前不与周边国家达成一致?
|
||||
日本相关机构认为:周边其它国家在排放核电站废水时并不需要征得其它国家的同意。污水处理对策排放与废水相似的稀释处理水,所以也不需要与周边国家达成一致。
|
||||
|
||||
相关计划在2021年时就已经向社会宣布并邀请IAEA进行审查,包括韩国在内的多国政府已经表示同意该计划,但要求日本相关机构保持信息公开透明。
|
||||
|
||||
### 日本相关机构是否接受第三方监督?
|
||||
|
||||
* [IAEA的特别工作组(包含来自中国和韩国等国家的专家)会审查污水处理对策及其实施情况、验证日本相关机构的数据](https://www.iaea.org/zh/zhu-ti/xiang-ying/fu-dao-di-yi-he-dian-zhan-chu-li-shui-pai-fang/chang-jian-wen-ti);
|
||||
* 除了东京电力公司的海洋监测点外,[日本环境署、核规制委员会等机构也会在相应位置进行海洋监测](https://www.monitororbs.jp/index_en.html);
|
||||
* [东京电力公司邀请了日本原子能研究开发机构进行第三方分析](https://fukushima.jaea.go.jp/okuma/alps/index.html);
|
||||
* 东京电力公司设计并执行[海洋生物养殖实验,供公众了解排放水的情况](https://www.tepco.co.jp/zh-cn/decommission/progress/watertreatment/breedingtest/index-cn.html);
|
||||
* 韩国等国家的当地机构也会在当地进行监测。
|
||||
|
||||
|
||||
### 为何日本相关机构急着排放处理水?
|
||||
|
||||
* 存储容量即将用完,几乎不可能扩建;
|
||||
* 日本政府和东京电力公司需要推动废弃反应堆的进程;
|
||||
* 有因为地震或海啸导致液体泄露的风险。
|
||||
|
||||
|
||||
相关计划在2013年时就已经开始讨论并在2021年公布。选择海洋排放有以下原因:
|
||||
|
||||
* 海洋排放比较成熟,各地核电站都日常排放符合安全剂量的废水;
|
||||
* 海洋排放对于环境的影响比较容易监测,可以通过对水质和生物进行定点测试判别。
|
||||
|
||||
|
||||
关于核污染对策
|
||||
-------
|
||||
### 我是否该屯盐?
|
||||
没有必要。福岛核电站的污水处理对策公开透明,根据目前的科学证据来看,不会损害你的健康。如果实在担心,也可以购买些许,但请务必量力而行。
|
||||
|
||||
需要特别提醒的是,碘盐没有碘片的效果,请勿使用碘盐代替碘片,也不要过度食用碘盐。
|
||||
|
||||
### 碘片可以防辐射吗?
|
||||
目前来说,没有特别轻松就可以防止辐射的手段。碘片通常指碘化钾片剂,它不能防止辐射对你造成伤害。它只能避免碘的放射性同位素被你的身体吸收,从而减少体内放射性物质对你的伤害。污水处理对策中,排放的处理水符合日本国家标准,碘同位素的含量很低。
|
||||
|
||||
|
||||
* 碘片不是一般性的预防措施,勿在事件未发生时就使用碘片;
|
||||
* 除非长期暴露在污染下,不应长期服用碘片。
|
||||
|
||||
|
||||
如果你因为担心福岛核电站污水处理对策而已经购买碘片,请勿因此服用碘片。从目前的科学证据看,福岛核电站的污水处理对策不会损害健康。
|
|
@ -1,18 +0,0 @@
|
|||
---
|
||||
title: 早上好,欢迎来到世界!
|
||||
date: 2020-12-26
|
||||
---
|
||||
|
||||
新博客的第一篇博文,介绍一下博客文字的使用协议:
|
||||
|
||||
如无特别规定,博客中作者的文字、图片等非代码资源使用 [创作共享 保留权利-非商业性使用-禁止二次演绎(CC BY-NC-ND) 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/) 协议共享。
|
||||
|
||||
[](http://creativecommons.org/licenses/by-nc-nd/4.0/)
|
||||
|
||||
如无特别规定,博客中作者的代码使用 [GNU Affero General Public License, version 3](https://www.gnu.org/licenses/agpl-3.0.html) or later 协议共享,但是你可以通过邮件向作者请求帮助(包括额外的授权)。
|
||||
[](https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
<!--more-->
|
||||
不属于作者的,文章中引用的资源(包括但不限于文字、代码、图片、商标)由各自所有者所有并按照授权协议执行。
|
||||
|
||||
欢迎通过 [a09d124g7@foo.bar](mailto:a09d124g7@foo.bar) (请将foo.bar替换为relay.firefox.com)联系我(文章问题或是聊天都可以,但是我会无视掉我认为在问无意义问题的邮件,包括问一些网上查一下就知道结果的问题)!由于使用了Firefox Relay,请将邮件大小控制在150KB以内。
|
|
@ -1,30 +0,0 @@
|
|||
---
|
||||
title: Rubicon's Rubicon的RSS订阅功能介绍和技术细节
|
||||
date: 2021-10-13 18:36:53
|
||||
tags:
|
||||
- Hexo
|
||||
- RSS
|
||||
- 博客功能更新
|
||||
---
|
||||
|
||||
> RSS(英文全称:RDF Site Summary 或 Really Simple Syndication[2]),中文译作簡易資訊聚合[3],也称聚合内容[4],是一種消息來源格式規範,用以聚合多個網站更新的內容並自動通知網站訂閱者。使用 RSS 後,網站訂閱者便無需再手動查看網站是否有新的內容,同時 RSS 可將多個網站更新的內容進行整合,以摘要的形式呈現,有助於訂閱者快速獲取重要信息,並選擇性地點閱查看。 ———— [RSS - Wikipedia](https://zh.wikipedia.org/wiki/RSS)
|
||||
|
||||
之前花了两天时间完成了这个功能,后来心血来潮在网站上看看时发现有bug。修好bug之后我觉得还是水一篇文章吧,否则博客开了一年都没什么内容。这篇文章主要就是介绍RSS订阅功能以及实现时的一些技术细节,如果想要扒我(包含这个功能)的主题或者借以参考的话,我过一段时间会把这个主题整理开源出来。
|
||||
|
||||
<!--more-->
|
||||
|
||||
Rubicon's Rubicon同时支持全站订阅和按标签或目录订阅。全站订阅就在每个页面最下方的“Links”里面。
|
||||
|
||||
{% img /img/hexo-topic-feeds/site-feeds.png "Feed (Atom) 和 Feed (RSS)" %}
|
||||
|
||||
目前为止,对于Rubicon's Rubicon来说RSS和Atom订阅没有差别。
|
||||
|
||||
## 按标签或目录订阅
|
||||
|
||||
通过菜单栏里的"Archives"或右边的"Tags"进入任意标签或目录的页面,在标题下面就有“FEED (Atom)”和“FEED (RSS)”。
|
||||
|
||||
{% img /img/hexo-topic-feeds/topic-feeds.png "标题“诗集”下面有“FEED (Atom)”和“FEED (RSS)”" %}
|
||||
|
||||
## 技术细节
|
||||
|
||||
Rubicon's Rubicon基于静态博客生成器[Hexo](https://hexo.io)构建,所以当然没法根据参数动态生成订阅文件。不过[hexo-feed](https://github.com/sergeyzwezdin/hexo-feed)这个插件支持按照标签或者目录生成订阅。我做了一些配置,然后在相应的页面引用(拼链接)就可以使用了。
|
|
@ -1,911 +0,0 @@
|
|||
---
|
||||
title: 超文本传输协议(HTTP)快速入门
|
||||
tags:
|
||||
- 网络协议
|
||||
- HTTP
|
||||
date: 2024-10-04 14:00:00
|
||||
---
|
||||
|
||||
|
||||
- [请求一个页面](#请求一个页面)
|
||||
- [用词约定](#用词约定)
|
||||
- [从零开始的HTTP服务器生涯](#从零开始的HTTP服务器生涯)
|
||||
- [GET和POST](#GET和POST)
|
||||
- [参考资料和扩展阅读](#参考资料和扩展阅读)
|
||||
|
||||
<!--more-->
|
||||
|
||||
## 请求一个页面
|
||||
|
||||
这篇文章会用到[Curl](https://curl.dev/),Curl是一个用于在命令行中访问URI(Uniform Resource Indicator,统一资源标志符,我们常说的“网址”)的工具。我们用它作为例子,看看一个HTTP客户端如何从服务器获取网页。
|
||||
|
||||
在大多数Linux发行版中,这个工具都是默认安装的,你可以在终端模拟器中尝试`curl --version`:
|
||||
|
||||
````
|
||||
$ curl --version
|
||||
curl 7.85.0 (x86_64-redhat-linux-gnu) libcurl/7.85.0 OpenSSL/3.0.8 zlib/1.2.12 brotli/1.0.9 libidn2/2.3.4 libpsl/0.21.1 (+libidn2/2.3.3) libssh/0.10.4/openssl/zlib nghttp2/1.51.0
|
||||
Release-Date: 2022-08-31
|
||||
Protocols: dict file ftp ftps gopher gophers http https imap imaps ldap ldaps mqtt pop3 pop3s rtsp scp sftp smb smbs smtp smtps telnet tftp
|
||||
Features: alt-svc AsynchDNS brotli GSS-API HSTS HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz NTLM NTLM_WB PSL SPNEGO SSL threadsafe TLS-SRP UnixSockets
|
||||
````
|
||||
|
||||
如果你的系统已经安装了Curl,就会出现和上面类似的输出,而不是一条“找不到”的报错。
|
||||
|
||||
如果你使用Windows,[Windows 10和Windows 11已经预装Curl](https://curl.se/windows/microsoft.html)。但是在PowerShell的默认环境下,使用`curl`不会使用curl,你需要用`curl.exe`来代替接下来所有命令中的`curl`。(提示:如果你的命令窗口标题有PowerShell字样,说明你可能正在使用PowerShell)
|
||||
|
||||
你可以在命令行窗口中尝试`curl --version`:
|
||||
|
||||
````
|
||||
> curl.exe --version
|
||||
curl 7.79.1 (Windows) libcurl/7.79.1 Schannel
|
||||
Release-Date: 2021-09-22
|
||||
Protocols: dict file ftp ftps http https imap imaps pop3 pop3s smtp smtps telnet tftp
|
||||
Features: AsynchDNS HSTS IPv6 Kerberos Largefile NTLM SPNEGO SSL SSPI UnixSockets
|
||||
````
|
||||
|
||||
如果你正在使用没有预装Curl的Windows,你可以在 [curl.se/windows/](https://curl.se/windows/) 下载。
|
||||
|
||||
确认Curl可以使用,我们来试试访问`http://example.com`
|
||||
|
||||
````
|
||||
$ curl --http1.1 http://example.com -v
|
||||
* Trying 93.184.216.34:80...
|
||||
* Trying 2606:2800:220:1:248:1893:25c8:1946:80...
|
||||
* Immediate connect fail for 2606:2800:220:1:248:1893:25c8:1946: 网络不可达
|
||||
* Connected to example.com (93.184.216.34) port 80 (#0)
|
||||
> GET / HTTP/1.1
|
||||
> Host: example.com
|
||||
> User-Agent: curl/7.85.0
|
||||
> Accept: */*
|
||||
>
|
||||
* Mark bundle as not supporting multiuse
|
||||
< HTTP/1.1 200 OK
|
||||
< Age: 106060
|
||||
< Cache-Control: max-age=604800
|
||||
< Content-Type: text/html; charset=UTF-8
|
||||
< Date: Sat, 18 Mar 2023 06:39:37 GMT
|
||||
< Etag: "3147526947+ident"
|
||||
< Expires: Sat, 25 Mar 2023 06:39:37 GMT
|
||||
< Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
|
||||
< Server: ECS (sab/5707)
|
||||
< Vary: Accept-Encoding
|
||||
< X-Cache: HIT
|
||||
< Content-Length: 1256
|
||||
<
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Example Domain</title>
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: #f0f0f2;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
|
||||
}
|
||||
div {
|
||||
width: 600px;
|
||||
margin: 5em auto;
|
||||
padding: 2em;
|
||||
background-color: #fdfdff;
|
||||
border-radius: 0.5em;
|
||||
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
|
||||
}
|
||||
a:link, a:visited {
|
||||
color: #38488f;
|
||||
text-decoration: none;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
div {
|
||||
margin: 0 auto;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<h1>Example Domain</h1>
|
||||
<p>This domain is for use in illustrative examples in documents. You may use this
|
||||
domain in literature without prior coordination or asking for permission.</p>
|
||||
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
* Connection #0 to host example.com left intact
|
||||
````
|
||||
|
||||
在我们使用curl时,我们加上了两个额外参数,一个是`--http1.1`,一个是`-v`。`--http1.1`告诉Curl,我们要求它使用1.1版本的HTTP,否则Curl可能会自动选择其它版本的HTTP;`-v`则让Curl展示更详细一点的信息,包括连接服务器和HTTP请求。
|
||||
|
||||
让我们来关注`<!doctype html>`之前以`<`或`>`开头的行:
|
||||
|
||||
````
|
||||
> GET / HTTP/1.1
|
||||
> Host: example.com
|
||||
> User-Agent: curl/7.85.0
|
||||
> Accept: */*
|
||||
>
|
||||
|
||||
< HTTP/1.1 200 OK
|
||||
< Age: 106060
|
||||
< Cache-Control: max-age=604800
|
||||
< Content-Type: text/html; charset=UTF-8
|
||||
< Date: Sat, 18 Mar 2023 06:39:37 GMT
|
||||
< Etag: "3147526947+ident"
|
||||
< Expires: Sat, 25 Mar 2023 06:39:37 GMT
|
||||
< Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
|
||||
< Server: ECS (sab/5707)
|
||||
< Vary: Accept-Encoding
|
||||
< X-Cache: HIT
|
||||
< Content-Length: 1256
|
||||
<
|
||||
````
|
||||
|
||||
在这里,`>`的意思是发送到服务器,`<`是从服务器接收。我们向服务器发送的是“请求”(Request),而服务器向我们返回“响应”(Response),一次请求-响应是一次“事务”(Transcation)。Curl执行我们对于`example.com`的请求,首先向服务器发送了以下信息:
|
||||
|
||||
````
|
||||
GET / HTTP/1.1
|
||||
^^^ ~~~~~~~~~~~~ 请求方法 (Request Method)
|
||||
^ ~~~~~~~~~~ 路径(Path)
|
||||
^^^^^^^^ ~ 协议(Protocol)
|
||||
````
|
||||
|
||||
这一行可以很容易用大白话说明:“使用HTTP/1.1协议获取(GET)路径‘/’”。
|
||||
|
||||
接下来这一段通常被叫作“HTTP头”:
|
||||
|
||||
````
|
||||
Host: example.com
|
||||
User-Agent: curl/7.85.0
|
||||
Accept: */*
|
||||
````
|
||||
|
||||
每一行意味着一个键值对,用`Key: Value`的格式。`Host: example.com`告诉服务器我们想请求的域名是`example.com`,其它两个头我们留到稍后再了解。
|
||||
这些“HTTP头”跟第一行加起来组成了“HTTP请求头部”的数据。但是还没完,这里还有一个空行:
|
||||
|
||||
````
|
||||
|
||||
````
|
||||
|
||||
它标志着“HTTP请求头部”的结束。
|
||||
|
||||
另外,还有一个要注意的地方:HTTP的换行是CRLF格式,也就是大多数编程语言中的`\r\n`字符串转义,这个稍后我们自己编写HTTP服务器的时候才会变得重要。
|
||||
|
||||
我们接着来看看服务器发回给我们的数据,它跟请求有两点不同,第一个是:
|
||||
|
||||
````
|
||||
HTTP/1.1 200 OK
|
||||
^^^^^^^^ ~~~~~~~~~~ 协议
|
||||
^^^ ~~~~~~ 状态码(Status Code)
|
||||
^^ ~~~ 状态信息
|
||||
````
|
||||
|
||||
通常,状态码和状态信息一一对应,或者说,状态信息只是在解释状态码。在这里,`200`可以说是“成功”的意思。
|
||||
|
||||
第二个是,它在空行之后,即”HTTP响应头部“结束之后,带上了我们请求的页面`/`:
|
||||
|
||||
````
|
||||
<!doctype html>
|
||||
<html>
|
||||
...
|
||||
````
|
||||
|
||||
如果你的电脑上有[telnet](https://en.wikipedia.org/wiki/Telnet),你可以亲手发送一个HTTP请求!执行`telnet example.com 80`来连接到服务器的80端口。
|
||||
|
||||
````
|
||||
$ telnet example.com 80
|
||||
Trying 93.184.216.34...
|
||||
Connected to example.com.
|
||||
Escape character is '^]'.
|
||||
````
|
||||
|
||||
输入以下内容。就是上面HTTP请求的简化版,HTTP头只留下`Host`。
|
||||
|
||||
````
|
||||
GET / HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
````
|
||||
|
||||
别忘了空行!不必担心换行格式,telnet默认发送CRLF形式的换行,这跟HTTP的要求是一样的。
|
||||
|
||||
## 用词约定
|
||||
|
||||
### 套接字和Socket
|
||||
|
||||
Socket是一种对于网络逻辑接口的抽象,本文使用Socket一词。在有些资料中这个词被翻译成“套接字”。
|
||||
|
||||
### HTTP头部、头
|
||||
|
||||
为了尊重使用习惯,本文中的HTTP头和HTTP头部所指的内容是不一样的。为了方便理解,假设有这样的HTTP请求:
|
||||
|
||||
````
|
||||
GET /path/to/page HTTP/1.1
|
||||
Host: example.com
|
||||
User-Agent: sample-client/1
|
||||
|
||||
````
|
||||
|
||||
HTTP头部指的是结束标志(空行)以及之前所有部分,而HTTP头指的是第一行之后、结束标志之前的键值对部分。
|
||||
|
||||
### TCP
|
||||
|
||||
本文中的TCP是Transmission Control Protocol的缩写,中文翻译是“传输控制协议”。
|
||||
|
||||
## 从零开始的HTTP服务器生涯
|
||||
|
||||
我们已经了解HTTP请求和响应的结构,从这里开始,我们将使用Python来编写一个简单的HTTP服务器。如果你不会Python,可以看看[Python文档网站上的教程](https://docs.python.org/zh-cn/3/tutorial/index.html)。我们只会使用许多编程语言都具备的概念和特性,用其它编程语言实现应该也不会有太大障碍。
|
||||
|
||||
另外提一句,所有代码都在Fedora 37上使用Python 3.11测试,但是程序并没有使用特殊的特性,在其它平台和Python上多半也能正常使用。
|
||||
|
||||
你可能没有从零开始写HTTP服务器的经验,让我来为我们将要完成的代码划分几个部分:
|
||||
|
||||
1. 监听网络端口,等待连接
|
||||
2. 从连接读取HTTP请求
|
||||
3. 生成HTTP响应并返回
|
||||
|
||||
````python
|
||||
def main():
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
````
|
||||
|
||||
### 等待客户端连接
|
||||
|
||||
HTTP运行在TCP之上,我们需要打开一个TCP端口,等待客户端创建TCP连接。
|
||||
|
||||
我们将使用Python标准库里的[socket](https://docs.python.org/zh-cn/3/library/socket.html),这个模块应该已经随着你的Python安装了。没用过Socket API也没关系,我将对我们要进行的操作做一个简单介绍。要在一个端口上等待客户端连接,我们大概要进行以下工作:
|
||||
|
||||
1. 创建一个Socket(使用socket库里的`socket`类),配置使用TCP(HTTP在TCP上传输)
|
||||
2. 把一个端口bind到这个Socket上(使用Socket的`bind`方法)
|
||||
3. 将这个Socket设置为监听(使用Socket的`listen`方法)
|
||||
|
||||
完成这些工作后,我们就可以使用Socket的`accept`方法等待一个连接。
|
||||
|
||||
在main里添加代码之后:
|
||||
|
||||
````python
|
||||
from socket import socket, AF_INET, SOCK_STREAM # 导入我们需要的值
|
||||
|
||||
def main():
|
||||
# SOCK_STREAM配合AF_INET就是在IPv4上使用TCP的意思,HTTP在TCP上传输
|
||||
with socket(AF_INET, SOCK_STREAM) as server_port: # with会在运行离开这块代码之后关闭这个Socket
|
||||
server_port.bind(("127.0.0.1", 8989)) # 将127.0.0.1:8989这个地址绑定到这个Socket上
|
||||
server_port.listen() # 设置监听
|
||||
conn, addr = server_port.accept() # conn是相应连接的Socket, addr是地址
|
||||
````
|
||||
|
||||
要实验我们的代码是否有效,我们可以编写一个供telnet使用的Echo服务器。Echo服务器,回声服务器,顾名思义就是一个原样输出收到内容的服务器。以下是包含了这个Echo服务器逻辑的文件内容。
|
||||
|
||||
````python
|
||||
from socket import socket, AF_INET, SOCK_STREAM
|
||||
|
||||
def main():
|
||||
with socket(AF_INET, SOCK_STREAM) as server_port:
|
||||
server_port.bind(("127.0.0.1", 8989))
|
||||
server_port.listen()
|
||||
conn, addr = server_port.accept()
|
||||
with conn: # 运行离开这个with代码块时,with会帮我们关闭这个Socket
|
||||
print(f"Accepted {addr}")
|
||||
conn.settimeout(8)
|
||||
while True:
|
||||
data = conn.recv(4096) # 接收最多4096 bytes的数据
|
||||
if data != b"\r\n": # 如果是一个空行(只包含CRLF换行),就退出循环
|
||||
conn.send(data)
|
||||
else:
|
||||
break
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
````
|
||||
|
||||
假设这个Python脚本的名字叫作`server.py`,我们执行`python server.py`(因为需要Python 3,有些机器可能要使用`python3`代替`python`)。
|
||||
|
||||
````
|
||||
python server.py
|
||||
|
||||
````
|
||||
|
||||
现在我们可以使用`telnet localhost 8989`连接到我们的Echo服务器。输入一些内容、换行,看看服务器返回的内容。最后用空行退出。
|
||||
|
||||
````
|
||||
$ telnet localhost 8989
|
||||
Trying ::1...
|
||||
telnet: connect to address ::1: Connection refused
|
||||
Trying 127.0.0.1...
|
||||
Connected to localhost.
|
||||
Escape character is '^]'.
|
||||
Hello!
|
||||
Hello!
|
||||
|
||||
Connection closed by foreign host.
|
||||
````
|
||||
|
||||
### 读取HTTP请求
|
||||
|
||||
先让我们来总结一下我们之前了解到的内容:一次HTTP事务包括请求和响应,请求和响应发送的数据叫作HTTP信息(Message)。
|
||||
|
||||
一个信息包括两部分:头部和主体,一个空行代表头部结束。HTTP请求和响应的头部只有第一行的格式不同,第一行之后都是由键值对组成的HTTP头。
|
||||
|
||||
让我们新添加两个函数,分别用于读取HTTP请求和处理HTTP请求:
|
||||
|
||||
````python
|
||||
def read_http_request(conn):
|
||||
pass
|
||||
|
||||
def handle_http_request(conn):
|
||||
pass
|
||||
````
|
||||
|
||||
> HTTP在TCP连接上传输,TCP提供面向字节流传输,意思是:在同一个Socket上,无论你如何发送数据,TCP都将它们视为同一串数据,不保证它们分开到达目标(你可以试试搜索“TCP粘包问题”,这是一个存在又不存在的问题)。当然,TCP保证数据收到的顺序和发送的顺序一致。这也可以帮助你理解HTTP为何要如此设计。
|
||||
|
||||
为了简化代码,我们这里将使用比较简单的方法读取HTTP请求,并将其处理成三个返回值:请求方法、路径、头。请求方法和路径是字符串,头是一个Python字典(`dict`),保存HTTP头键值对。跟Python字典类似功能的东西,在其它编程语言中可能更习惯叫"Map"。
|
||||
|
||||
读取HTTP请求时,我们先原样读取出整个头部,存为字符串再解析。读取头部的函数命名为`read_http_request_header_string`。
|
||||
|
||||
````python
|
||||
def read_http_request_header_string(conn: socket):
|
||||
buffer = bytearray()
|
||||
while True:
|
||||
buffer.extend(conn.recv(4096))
|
||||
header_length = buffer.find(b"\r\n\r\n") # 两个连在一起的CRLF,第二个CRLF就代表空行
|
||||
if header_length != -1:
|
||||
return buffer[:header_length]
|
||||
````
|
||||
|
||||
这样我们就可以在`read_http_request`里使用它了,你可以先试着写一写这个`read_http_request`再看完整代码。
|
||||
|
||||
逻辑很简单,将读到的字符串按照`\r\n`分开,再分别处理第一行和剩余的行。需要注意的是,我们的`read_http_request_header_string`返回`bytes`(可以简单理解为一块内存),你需要用`decode`方法将其转换为字符串,该方法需要指定一个编码,你可以使用`"ascii"`。
|
||||
|
||||
````python
|
||||
def read_http_request(conn: socket):
|
||||
s = read_http_request_header_string(conn).decode("ascii")
|
||||
lines = s.split("\r\n") # 按CRLF切开
|
||||
# 解析第一行
|
||||
fstline = lines[0]
|
||||
method, path, protocol = fstline.split(" ") # 按空格切开第一行
|
||||
if protocol != "HTTP/1.1":
|
||||
raise RuntimeError("unknown protocol", protocol)
|
||||
method = method.lower() # 把方法转换成小写
|
||||
# 解析头
|
||||
header = {}
|
||||
if len(lines) > 1:
|
||||
for line in lines[1:]:
|
||||
key, value = line.split(": ")
|
||||
header[key] = value
|
||||
return method, path, header
|
||||
````
|
||||
|
||||
### 生成HTTP响应头部
|
||||
|
||||
相对于读取请求,生成响应要简单多了,只需要按照格式拼装信息:
|
||||
|
||||
````python
|
||||
STATUS_MESSAGES = {
|
||||
200: "OK",
|
||||
400: "Bad Request",
|
||||
404: "Not Found",
|
||||
500: "Server Error",
|
||||
}
|
||||
|
||||
def build_http_response_header(status_code, headers):
|
||||
lines = [
|
||||
f"HTTP/1.1 {status_code} {STATUS_MESSAGES[status_code]}"
|
||||
]
|
||||
for key, value in headers:
|
||||
lines.append(f"{key}: {value}")
|
||||
lines.append("") # 别忘了加空行
|
||||
lines.append("")
|
||||
return '\r\n'.join(lines).encode('ascii')
|
||||
````
|
||||
|
||||
请注意,这里为了增加空行,使用两次`lines.append("")`增加了两个空字符串。因为`'\r\n'.join`只在两个字符串中间增加分隔符`"\r\n"`。
|
||||
|
||||
举个例子:我们有`"A"`、`"B"`两个字符串,每个字符串一行。如果我们不增加空字符串,`'\r\n'.join`只会在A和B之间插入一个换行,结果是`"A\r\nB"`。
|
||||
|
||||
如果我们只在列表末尾增加一个空字符串,那么最后就只会是
|
||||
|
||||
````
|
||||
"A\r\nB\r\n"
|
||||
^ 新增加的空字符串
|
||||
````
|
||||
|
||||
,只是在B和增加的空字符串中间增加了一个换行;如果需要单独的空行,还需要一个额外空字符串,在第一个空字符串和第二个空字符串之间再插入一个换行,才会变为我们需要的`"A\r\nB\r\n\r\n"`。
|
||||
|
||||
### 处理HTTP请求
|
||||
|
||||
接下来我们正式在`handle_http_request`中处理HTTP请求。首先我们读取HTTP请求,如果请求出错,我们返回400 Bad Request。
|
||||
|
||||
````python
|
||||
def handle_http_request(conn: socket):
|
||||
try:
|
||||
method, path, headers = read_http_request(conn)
|
||||
except Exception as e:
|
||||
conn.send(build_http_response_header(400, []))
|
||||
print(f"- - 400 {STATUS_MESSAGES[400]}")
|
||||
raise e # 把错误重新抛出,方便你看错误堆栈
|
||||
````
|
||||
|
||||
然后我们检查方法是否是`get`,路径是否是`/`或者`/index.html`,满足条件的话我们就返回200 OK和一段HTML内容:
|
||||
|
||||
````python
|
||||
if method == "get" and (path == "/" or path == "/index.html"):
|
||||
headers = [
|
||||
("Charset", "UTF-8"),
|
||||
("Content-Length",str(len(DEFAULT_PAGE_HTML))),
|
||||
("Connection", "close"),
|
||||
]
|
||||
conn.send(build_http_response_header(200, headers))
|
||||
conn.send(DEFAULT_PAGE_HTML)
|
||||
print(f"{method.upper()} {path} 200 {STATUS_MESSAGES[200]}")
|
||||
````
|
||||
|
||||
你发现我们在这里设置了三个头,一个是`Charset`,我们将它设置为`UTF-8`,这是提示客户端,我们的内容使用UTF-8编码。我们的`DEFAULT_PAGE_HTML`定义如下:
|
||||
|
||||
````python
|
||||
DEFAULT_PAGE_HTML = """<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Default Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World!</h1>
|
||||
</body>
|
||||
</html>""".encode("utf-8")
|
||||
````
|
||||
|
||||
这里我们使用`.encode("utf-8")`将字符串转换为`bytes`。关于`decode`、`encode`方法和编解码可以阅读Python文档相应页面。
|
||||
|
||||
另一个头是`Content-Length`,这个头指定了HTTP主体(Body)的长度,也就是我们返回HTML的长度,接收端会根据这个长度读取主体内容。长度必须是十进制的字节数量。你可以看到我们在这里给了`DEFAULT_PAGE_HTML`的长度。
|
||||
|
||||
最后一个是`Connection: close`,这样设置意味着:无论是客户端还是服务器都可以在响应完成之后关闭相应连接。设置这个头是因为我们的服务器在响应HTTP请求之后就会自动关闭连接,但是HTTP/1.1默认不会关闭。
|
||||
|
||||
最后,如果不满足条件,返回404 Not Found,意味着这个页面没有找到。
|
||||
|
||||
````python
|
||||
else:
|
||||
conn.send(build_http_response_header(404, []))
|
||||
print(f"{method.upper()} {path} 404 {STATUS_MESSAGES[404]}")
|
||||
````
|
||||
|
||||
最后只要在我们的main函数使用这个`handle_http_request`就好了。
|
||||
|
||||
````python
|
||||
def main():
|
||||
with socket(AF_INET, SOCK_STREAM) as server_port:
|
||||
server_port.bind(("127.0.0.1", 8989))
|
||||
server_port.listen()
|
||||
conn, addr = server_port.accept()
|
||||
with conn:
|
||||
conn.settimeout(8)
|
||||
handle_http_request(conn)
|
||||
````
|
||||
|
||||
使用`python server.py`启动之后,使用`curl http://localhost:8989 -v`来看看效果。
|
||||
|
||||
````
|
||||
$ curl http://localhost:8989 -v
|
||||
* Trying 127.0.0.1:8989...
|
||||
* Connected to localhost (127.0.0.1) port 8989 (#0)
|
||||
> GET / HTTP/1.1
|
||||
> Host: localhost:8989
|
||||
> User-Agent: curl/7.85.0
|
||||
> Accept: */*
|
||||
>
|
||||
* Mark bundle as not supporting multiuse
|
||||
< HTTP/1.1 200 OK
|
||||
< Charset: UTF-8
|
||||
< Content-Length: 175
|
||||
< Connection: close
|
||||
<
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Default Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World!</h1>
|
||||
</body>
|
||||
* Closing connection 0
|
||||
</html>⏎
|
||||
````
|
||||
|
||||
我们的代码处理了一个新连接就退出了,如果要让它持续处理新连接,只要从`accept`方法开始放在死循环里就行:
|
||||
|
||||
````python
|
||||
def main():
|
||||
with socket(AF_INET, SOCK_STREAM) as server_port:
|
||||
server_port.bind(("127.0.0.1", 8989))
|
||||
server_port.listen()
|
||||
while True:
|
||||
conn, addr = server_port.accept()
|
||||
with conn:
|
||||
conn.settimeout(8)
|
||||
handle_http_request(conn)
|
||||
````
|
||||
|
||||
[完整代码请见Gist](https://gist.github.com/thislight/b0aaa23f247d62bb6d78cc650732b214)
|
||||
|
||||
### 使用浏览器开发者工具
|
||||
|
||||
浏览器开发者工具是调试你Web程序的利器!它就内置在你的浏览器里,包含多个有用的工具,快捷键通常是F12。接下来,我将演示用Firefox的开发者工具查看我们的服务器响应信息。
|
||||
|
||||
先打开一个新标签页。你有两种方法打开开发者工具:一种是在浏览器菜单里点击“更多工具”, 点击“Web开发者工具”。
|
||||
|
||||

|
||||
|
||||
另一种打开方法是按键盘上的F12。
|
||||
|
||||
打开后,你就会看到开发者工具,选择“网络(Network)”页面。
|
||||
|
||||

|
||||
|
||||
接下来,在地址栏中输入我们服务器的地址`http://localhost:8989`、确认访问,就可以在这个页面下看到浏览器产生的请求。点击单个项目可以展开详细信息。
|
||||
|
||||

|
||||
|
||||
## GET和POST
|
||||
|
||||
我们的服务器应该能够根据用户的输入进行不同的操作,不然为什么不直接提供一个HTML就好了呢?正好,HTTP给我们提供了一个工具:路径里面可以携带一个叫查询(Query)的部分,`?`之后就是我们的查询,查询里面可以直接携带URI未保留字符,这些字符在URI中没有特殊意义。比如在`/?something`中,`something`就是我们的查询。
|
||||
|
||||
> 查询其实是属于URI的一部分。我们在这里说查询是在“路径”中,这里的“路径”指的是前面我们所提到HTTP请求格式中的“路径”。
|
||||
|
||||
但是有一些字符不能直接放进URI里,因为它们是URI的保留字符,比如`@`,这时候就需要转义(Escape)这些字符。我们在这使用的编码叫[URL encoding(URL编码,也可以叫percent encoding,百分号编码)](https://en.wikipedia.org/wiki/URL_encoding)。
|
||||
|
||||
比如说我们要在路径里携带`example@example.com`,比如`/?email=example@example.com`,我们必须将其编码成`/?email=example%40example.com`。
|
||||
|
||||
虽然查询里面可以携带任何允许的字符,但是使用类似上面的格式仍然是比较常用的做法。也就是`key=val`代表键key的值是val,在上面就是`email`的值是`example@example.com`。如果要携带多对键值,可以用`&`从中间区分,比如`email=example%40example.com&nuke=1`,就有`email`和`nuke`两对键值。
|
||||
|
||||
接下来,我们一起改写上面的服务器,让它能够接受查询字符串,比如说在访问`/?name=HTTP`的时候可以返回"Hello HTTP!"。
|
||||
|
||||
### 在GET请求中接受用户输入
|
||||
|
||||
我们要修改以下部分:
|
||||
|
||||
- 修改访问路径匹配。我们之前使用`path == "/"`来确认正在访问的路径,这样的话`/?name=HTTP`就不能访问到`/`了。
|
||||
- 处理查询里的键值对。我们需要从请求中的路径取得访问路径和查询,将查询里的键值对解析成我们需要的数据结构,在这里是字典。
|
||||
- 让`DEFAULT_HTML_PAGE`的"World"可以自定义。我们在这里使用Python的`str.format`方法。
|
||||
|
||||
首先,我们需要从请求中的路径取得访问路径和查询:
|
||||
|
||||
````python
|
||||
def read_path(path: str): # 返回访问路径和查询
|
||||
parts = path.split("?", maxsplit=1)
|
||||
if len(parts) > 1:
|
||||
return parts[0], parts[1]
|
||||
else:
|
||||
return parts[0], ""
|
||||
|
||||
def handle_http_request(conn: socket):
|
||||
# ...
|
||||
onlypath, query = read_path(path)
|
||||
if onlypath == "/" and method == "get":
|
||||
# ...
|
||||
````
|
||||
|
||||
然后,我们需要将查询里的键值对解析成字典:
|
||||
|
||||
````python
|
||||
def parse_query(q: str):
|
||||
pairs_str = q.split("&")
|
||||
pairs = {}
|
||||
for s in pairs_str:
|
||||
if s: # 确保不是空字符串
|
||||
k, v = s.split("=")
|
||||
pairs[unquote_plus(k)] = unquote_plus(v)
|
||||
return pairs
|
||||
````
|
||||
|
||||
在这里,我们使用了`urllib.parse`的`unquote_plus`函数,你需要在文件顶部引入它:
|
||||
|
||||
````python
|
||||
from urllib.parse import unquote_plus
|
||||
````
|
||||
|
||||
我们使用这个函数将URL编码的字符串转换为普通字符串。
|
||||
|
||||
接下来,我们要让`DEFAULT_HTML_PAGE`可以接受自定义名字。因为`bytes`对象没有`format`方法,我们得去掉`.encode`,改在自定义名字之后完成。
|
||||
|
||||
````python
|
||||
DEFAULT_PAGE_HTML = """<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Default Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello {name}!</h1>
|
||||
</body>
|
||||
</html>"""
|
||||
````
|
||||
|
||||
当我们使用`format`方法时,就可以替换掉`{name}`的内容,生成实际要发送给客户端的页面。这时`DEFAULT_PAGE_HTML`就被叫作“模板”,生成这个页面的过程叫“渲染”。
|
||||
|
||||
````python
|
||||
if onlypath == "/" and method == "get":
|
||||
query_dict = parse_query(query) # 解析查询键值对
|
||||
# 渲染页面
|
||||
content = DEFAULT_PAGE_HTML.format(
|
||||
# 检查键值对中有没有键name,有并且值不为空的话就以其值替换{name},否则用World替换。
|
||||
name=(query_dict["name"] if query_dict.get("name") else "World")
|
||||
).encode("utf-8")
|
||||
headers = [
|
||||
("Charset", "UTF-8"),
|
||||
("Content-Length",str(len(content))), # 主体长度等于实际内容长度
|
||||
("Connection", "close"),
|
||||
]
|
||||
conn.send(build_http_response_header(200, headers))
|
||||
conn.send(content) # 发送渲染出来的页面
|
||||
# ...
|
||||
````
|
||||
|
||||
启动你的服务器。用`curl http://localhost:8989?name=HTTP -v`试试。
|
||||
|
||||
````
|
||||
$ curl http://localhost:8989?name=HTTP -v
|
||||
* Trying 127.0.0.1:8989...
|
||||
* Connected to localhost (127.0.0.1) port 8989 (#0)
|
||||
> GET /?name=HTTP HTTP/1.1
|
||||
> Host: localhost:8989
|
||||
> User-Agent: curl/7.85.0
|
||||
> Accept: */*
|
||||
>
|
||||
* Mark bundle as not supporting multiuse
|
||||
< HTTP/1.1 200 OK
|
||||
< Charset: UTF-8
|
||||
< Content-Length: 174
|
||||
< Connection: close
|
||||
<
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Default Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello HTTP!</h1>
|
||||
</body>
|
||||
* Closing connection 0
|
||||
</html>⏎
|
||||
````
|
||||
|
||||
你也可以用浏览器看看。修改一下`name`的值,看看给出什么结果。如果出现bug,尝试自己修一修。
|
||||
|
||||
修改一下模板,就可以直接在网页里面使用这个参数了。
|
||||
|
||||
````python
|
||||
DEFAULT_PAGE_HTML = """<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Default Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello {name}!</h1>
|
||||
<form method="GET">
|
||||
<input name="name" label="Hello to..." />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>"""
|
||||
````
|
||||
|
||||
HTML不在本文范围,敬请参阅MDN Web Docs相关页面。
|
||||
|
||||
[完整代码请见Gist](https://gist.github.com/thislight/d32a565609ee1f2379fc9b3af1d87012)
|
||||
|
||||
### HTTP POST
|
||||
|
||||
目前为止,我们都在使用HTTP的GET方法。另一个常用的HTTP方法是POST,为什么我们需要它呢?
|
||||
|
||||
- HTTP请求可以跟响应一样携带主体。GET请求不可以携带主体,但POST请求可以。放在路径里的数据经过编码后体积可能大幅增加;主体里的数据格式并没有规定,可以不用编码。
|
||||
- 理论上HTTP请求头部的路径可以无限长,但是客户端或者服务器可能会限制路径最大长度。
|
||||
- 在浏览器中,路径会被记录在浏览历史里,放在主体里的数据一般不会被记录。
|
||||
|
||||
如果我们使用HTML的form元素进行请求,POST请求主体使用的格式与我们之前查询键值对的格式相同。键是form元素内input元素name属性的值。
|
||||
|
||||
> 绝大部分情况下,你应该用GET方法展示数据、POST方法只用来记录数据。在刷新POST方法返回的页面时,浏览器需要重新提交请求,并会询问用户是否要这样做(因为这样做可能会导致不需要的副作用)。大部分情况下,这不是用户想体验的麻烦。
|
||||
> 如果你使用HTML的form直接处理POST提交,你可以在处理POST方法完成后返回[HTTP 303 See Other状态码](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303)并在`Location`头中指定重新请求的地址,让浏览器用GET方法重新请求并展示相应页面。
|
||||
> 我们接下来就是采用类似方法。
|
||||
|
||||
让我们来改写我们之前的服务器,让它可以接受POST方法的`default_name`参数,并将它设置为默认名字(当没有name参数时使用的名字)。
|
||||
|
||||
先添加一个全局变量:
|
||||
|
||||
````python
|
||||
global default_name
|
||||
default_name = "World"
|
||||
````
|
||||
|
||||
这是个比较大的改写,首先我们要先改写读取HTTP请求的部分,让它支持读取主体。然后我们要使用一个全局变量保存我们对默认名字的修改。
|
||||
|
||||
````python
|
||||
def read_http_request_header_string(conn: socket):
|
||||
buffer = bytearray()
|
||||
while True:
|
||||
buffer.extend(conn.recv(4096))
|
||||
header_length = buffer.find(b"\r\n\r\n")
|
||||
if header_length != -1:
|
||||
return buffer[:header_length], buffer[header_length+4:]
|
||||
|
||||
def read_http_request(conn: socket):
|
||||
s, rest = read_http_request_header_string(conn)
|
||||
s = s.decode("ascii")
|
||||
lines = s.split("\r\n")
|
||||
# 解析第一行
|
||||
fstline = lines[0]
|
||||
method, path, protocol = fstline.split(" ")
|
||||
if protocol != "HTTP/1.1":
|
||||
raise RuntimeError("unknown protocol", protocol)
|
||||
method = method.lower()
|
||||
# 解析头
|
||||
header = {}
|
||||
if len(lines) > 1:
|
||||
for line in lines[1:]:
|
||||
key, value = line.split(": ")
|
||||
header[key] = value
|
||||
return method, path, header, rest
|
||||
|
||||
def read_http_request_body(conn: socket, length: int, buffer: bytearray):
|
||||
while len(buffer) < length:
|
||||
buffer.extend(conn.recv(4096))
|
||||
return buffer[:length]
|
||||
````
|
||||
|
||||
我们在原先`read_http_request`函数的基础上增加`read_http_request_body`,并且让`read_http_request`返回多读的数据(在`\r\n\r\n`之后的数据)。在`handle_http_request`,我们这样读取主体:
|
||||
|
||||
````python
|
||||
def handle_http_request(conn: socket):
|
||||
global default_name
|
||||
try:
|
||||
method, path, headers, rest = read_http_request(conn)
|
||||
if "Content-Length" in headers:
|
||||
body = read_http_request_body(conn, int(headers["Content-Length"]), rest)
|
||||
# 我们在这只支持Content-Length,不支持Transfer-Encoding: chunked
|
||||
else:
|
||||
body = bytearray()
|
||||
except Exception as e:
|
||||
conn.send(build_http_response_header(400, []))
|
||||
print(f"- - 400 {STATUS_MESSAGES[400]}")
|
||||
raise e
|
||||
# ...
|
||||
````
|
||||
|
||||
接下来我们要改动检查路径和方法的代码:
|
||||
|
||||
````python
|
||||
onlypath, query = read_path(path)
|
||||
if onlypath == "/":
|
||||
if method == "get":
|
||||
query_dict = parse_query(query)
|
||||
# 渲染页面
|
||||
content = DEFAULT_PAGE_HTML.format(
|
||||
name=(query_dict["name"] if query_dict.get("name") else default_name)
|
||||
# 这里引用全局变量default_name的值,而不是硬编码"World"
|
||||
).encode("utf-8")
|
||||
headers = [
|
||||
("Charset", "UTF-8"),
|
||||
("Content-Length",str(len(content))),
|
||||
("Connection", "close"),
|
||||
]
|
||||
conn.send(build_http_response_header(200, headers))
|
||||
conn.send(content)
|
||||
print(f"{method.upper()} {path} 200 {STATUS_MESSAGES[200]}")
|
||||
return # 响应完成,直接返回
|
||||
elif method == "post": # 处理POST请求
|
||||
form_data_s = body.decode("utf-8")
|
||||
form_dict = parse_query(form_data_s)
|
||||
if form_dict.get("default_name"):
|
||||
default_name = form_dict["default_name"]
|
||||
conn.send(build_http_response_header(303, [
|
||||
("Connection", "close"),
|
||||
("Location", "."),
|
||||
])) # 让浏览器重新用GET请求并展示当前页面,Location是“.”
|
||||
print(f"{method.upper()} {path} 303 {STATUS_MESSAGES[303]}")
|
||||
return # 响应完成,直接返回
|
||||
conn.send(build_http_response_header(404, [])) # 没有匹配的路径或者方法,返回404
|
||||
print(f"{method.upper()} {path} 404 {STATUS_MESSAGES[404]}")
|
||||
````
|
||||
|
||||
最后,改动一下模板,这样我们就可以直接使用这个参数了:
|
||||
|
||||
````python
|
||||
DEFAULT_PAGE_HTML = """<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Default Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello {name}!</h1>
|
||||
<form method="GET">
|
||||
<input name="name"/>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
<h2>Default Name</h2>
|
||||
<form method="POST">
|
||||
<input name="default_name" />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>"""
|
||||
````
|
||||
|
||||
[完整代码请见Gist](https://gist.github.com/thislight/b69161000e5ea2904057a49f16b5ac8b)
|
||||
|
||||
### 再进一步
|
||||
|
||||
我们的代码尚不完善,你可以试试按以下方向改进。
|
||||
|
||||
- 处理并发连接
|
||||
|
||||
当前的代码只能依照顺序一个一个处理连接,你使用多线程或者Python的asyncio让我们的服务器能够并发处理连接。
|
||||
|
||||
参考资料:
|
||||
|
||||
- [Python文档:threading模块](https://docs.python.org/zh-cn/3/library/threading.html#module-threading)
|
||||
- [Python文档:asyncio](https://docs.python.org/zh-cn/3/library/asyncio.html#module-asyncio)
|
||||
|
||||
- 连接复用
|
||||
|
||||
当前代码在回应请求之后就直接关闭连接,你可以支持HTTP/1.1式的连接复用来提高连接使用效率。
|
||||
|
||||
参考资料:
|
||||
|
||||
- [MDN Web Docs:HTTP Keep-Alive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive)
|
||||
- [MDB Web Docs:HTTP Connection](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection)
|
||||
|
||||
- 增强兼容性
|
||||
|
||||
我们目前的代码作了一些假设,你可以改进代码以提高对不同客户端的兼容性。
|
||||
|
||||
- 支持[HTTP Transfer-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding),我们的代码中没有支持这种传输方式
|
||||
|
||||
- 包装和抽象
|
||||
|
||||
你可以将HTTP服务器包装起来,甚至进一步包装成可以使用的框架,为用户提供可用的API。例如:
|
||||
|
||||
````python
|
||||
from mymodule import MyServer
|
||||
|
||||
def handle_index(request):
|
||||
return request.ok(template="index.html")
|
||||
|
||||
if __name__ == "__main__":
|
||||
server = MyServer({
|
||||
"/": handle_index
|
||||
})
|
||||
server.run()
|
||||
````
|
||||
|
||||
你还可以试试阅读[Tornado的代码](https://github.com/tornadoweb/tornado),这是一个Python异步Web框架。
|
||||
|
||||
- 支持Cookie
|
||||
|
||||
HTTP是一个无状态协议:不同的响应-请求之间没有联系。HTTP Cookie是一项在不同响应-请求之间保留数据的技术。设计一个需要保留数据的功能,并在我们的HTTP服务器中实现它。
|
||||
|
||||
参考资料:
|
||||
|
||||
- [MDN Web Docs:Using HTTP cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies)
|
||||
|
||||
## 参考资料和扩展阅读
|
||||
|
||||
- [MDN Web Docs: HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP)
|
||||
- [Python Docs: socket](https://docs.python.org/zh-cn/3/library/socket.html)
|
||||
- [Wikipedia: URL encoding](https://en.wikipedia.org/wiki/URL_encoding)
|
||||
- [Wikipedia: Telnet](https://en.wikipedia.org/wiki/Telnet)
|
||||
|
||||
本文内容主要基于HTTP/1.1。如今,HTTP已经改进出HTTP/2和HTTP/3。这些新版本协议更加高效,更加适合我们当下的使用场景。虽然HTTP/2和HTTP/3与HTTP/1.1相比变化很大,但是基本概念并没有什么变动。
|
||||
|
||||
- [Web.dev: Introduction to HTTP/2](https://web.dev/performance-http2/)
|
||||
- [Couldflare: What is HTTP/3?](https://www.cloudflare.com/learning/performance/what-is-http3/)
|
BIN
source/_posts/http101/devtools-in-menu.png
(Stored with Git LFS)
BIN
source/_posts/http101/devtools-in-menu.png
(Stored with Git LFS)
Binary file not shown.
BIN
source/_posts/http101/devtools-inspect-request.png
(Stored with Git LFS)
BIN
source/_posts/http101/devtools-inspect-request.png
(Stored with Git LFS)
Binary file not shown.
BIN
source/_posts/http101/devtools.png
(Stored with Git LFS)
BIN
source/_posts/http101/devtools.png
(Stored with Git LFS)
Binary file not shown.
|
@ -1,31 +0,0 @@
|
|||
---
|
||||
title: 偷乐集
|
||||
date: 2022-04-29 13:40:24
|
||||
tags:
|
||||
- 诗集
|
||||
categories:
|
||||
- 诗集
|
||||
---
|
||||
|
||||
> 2022年第一季度写的两首小诗:《梅花》和《无题》。生活在不确定中让人疲累,没有紧急出口。
|
||||
|
||||
<!--more-->
|
||||
|
||||
## 梅花
|
||||
寒冬狠狠闭上梅花的眼
|
||||
花瓣落成雪花
|
||||
和哑巴
|
||||
|
||||
沉默的人尽力喧闹
|
||||
喧闹的人说不出话
|
||||
落雨走向起点
|
||||
泪珠串起结局
|
||||
还剩多少
|
||||
|
||||
## 无题
|
||||
在时钟的尽头
|
||||
跳着蹩脚的舞蹈
|
||||
经过沾满火焰的街道
|
||||
老人在偷笑
|
||||
多少干燥的徒劳
|
||||
自在逍遥
|
|
@ -1,31 +0,0 @@
|
|||
---
|
||||
title: 使用Makru和makru_langc管理现代C项目编译:引入&目录
|
||||
date: 2020-12-26 21:21:13
|
||||
updated: 2021-1-5 10:35
|
||||
tags:
|
||||
- Makru
|
||||
- C
|
||||
- 系列文章
|
||||
categories: 代码农场
|
||||
---
|
||||
|
||||
[Makru](https://gitlab.com/jinwa/makru)是一个用Python编写的全新编译工具。它的目的不是提供更多的编译逻辑,而是提供一个统一的平台方便开发和引入新的编译逻辑。Makru和目前主流的make-like编译系统最大的不同点再于:它没有使用类似宏语法之类的东西为配置文件提供超高的灵活性,而是直接选择了很多现代包管理工具正在使用的结构化数据配置文件(Makru使用的是yaml)。[makru_langc](https://gitlab.com/jinwa/makru_langc)就是在它之上构建的C语言项目编译插件。
|
||||
得益于这样的选择,makru_langc提供了非常流畅的编译管理体验。你可以快速理解并编辑配置文件,而不是在配置文件的海洋里晕头转向。另外,makru_langc还支持高自由度的自定义,你甚至可以利用它提供的工具编写自己的编译逻辑。
|
||||
|
||||
这个文章系列将会介绍一个用makru_langc来管理编译的小演示项目scat,它接收标准输入然后输出到标准输出。为了演示makru_langc的功能,scat会有一个子项目叫libcopy。这个子项目里只有一个函数:从第一个File输入,输出到第二个File。
|
||||
|
||||
<!-- more -->
|
||||
|
||||
makru_langc还自带直接从本地pkgconfig查询第三方库的功能,最后一篇文章会覆盖到这个特性,并以此说明如何为makru_langc编写自己的库查询逻辑。
|
||||
|
||||
## 环境说明
|
||||
文章中的开发环境是运行于Linux 5.9.15 x86_64之上的Fedora 33,Python 3.9以及Clang 11.0。Makru的版本是v0.1.0(最新beta版本),makru_langc的版本是尚未发布的r1(你可以直接检出master分支)。
|
||||
|
||||
## 目录
|
||||
- {% post_link "makru-tutor-2" "配置Makru和makru_langc" %}
|
||||
- 为可执行文件项目编写配置文件
|
||||
- 为库项目编写配置文件
|
||||
- 使用Pkgconfig Dependency Resolver引入第三方库
|
||||
- makru_langc的基本工作流程
|
||||
- 编写一个基于命令行输入的Dependency Resolver
|
||||
- makru_langc代码阅读指引
|
|
@ -1,148 +0,0 @@
|
|||
---
|
||||
title: 使用Makru和makru_langc管理现代C项目编译: 配置Makru和makru_langc
|
||||
date: 2021-01-04 18:49:20
|
||||
updated: 2021-01-18 16:37:00
|
||||
tags:
|
||||
- Makru
|
||||
- C
|
||||
categories: 代码农场
|
||||
---
|
||||
|
||||
在这一篇文章中,我会介绍makru_langc的安装。和普通的编译工具不同,makru_langc是作为Makru的一个插件安装的,而Makru的插件只能安装到项目上随项目源代码附带。这会带来一些不方便,但是能够保证每一个项目都能选择最适合的插件和插件版本而无需担心兼容成本。其实类似的方法在make和cmake的项目中也能经常看见,很多项目都会带有一些脚本或者预定义宏来帮助编译,只是Makru将它变成强制行为了。
|
||||
|
||||
<!-- more -->
|
||||
|
||||
这是一篇属于“使用Makru和makru_langc管理现代C项目编译”系列的文章,你可以在{% post_link makru-tutor-1 %}找到目录。
|
||||
|
||||
注意:makru_langc尚未考虑对于Windows的支持。
|
||||
|
||||
## 安装Makru
|
||||
Makru完全由Python编写而成,只需要你有Python。目前对于Python低版本的兼容性未知,但是你至少应该有3.6或更高版本的Python。我的环境使用的是Python 3.9.1。
|
||||
|
||||
如果你不想在系统级别安装Makru,你可以使用[virtualenv](https://pypi.org/project/virtualenv/)创建一个目录级的Python虚拟环境。不过其实一般情况下你不需要这样做。
|
||||
|
||||
````
|
||||
$ pip install makru
|
||||
Collecting makru
|
||||
Downloading makru-0.1.0b14-py3-none-any.whl (12 kB)
|
||||
Collecting semver<3.0.0,>=2.13.0
|
||||
Downloading semver-2.13.0-py2.py3-none-any.whl (12 kB)
|
||||
Collecting pluginbase<2.0.0,>=1.0.0
|
||||
Downloading pluginbase-1.0.0.tar.gz (41 kB)
|
||||
|████████████████████████████████| 41 kB 573 kB/s
|
||||
Collecting PyYAML<6.0.0,>=5.1.1
|
||||
Downloading PyYAML-5.3.1.tar.gz (269 kB)
|
||||
|████████████████████████████████| 269 kB 1.1 MB/s
|
||||
Building wheels for collected packages: pluginbase, PyYAML
|
||||
Building wheel for pluginbase (setup.py) ... done
|
||||
Created wheel for pluginbase: filename=pluginbase-1.0.0-py3-none-any.whl size=7747 sha256=7f4d815404165862e1c1a5544f115e566dd375355bb1caba1769b27be4d30f9a
|
||||
Stored in directory: /tmp/pip-ephem-wheel-cache-aisr91om/wheels/a6/31/1d/6f541ab6fdbb2e3fce5472e65c9769ecb782428d22d68ad938
|
||||
Building wheel for PyYAML (setup.py) ... done
|
||||
Created wheel for PyYAML: filename=PyYAML-5.3.1-cp39-cp39-linux_x86_64.whl size=44617 sha256=184ba6ba623bfa178270185f0afd6ba1ef695f08bb3f0298303ed87a665dc6e5
|
||||
Stored in directory: /tmp/pip-ephem-wheel-cache-aisr91om/wheels/69/60/81/5cd74b8ee068fbe9e04ca0d53148f28f5c6e2c5b177d5dd622
|
||||
Successfully built pluginbase PyYAML
|
||||
Installing collected packages: semver, pluginbase, PyYAML, makru
|
||||
Successfully installed PyYAML-5.3.1 makru-0.1.0b14 pluginbase-1.0.0 semver-2.13.0
|
||||
````
|
||||
可以看到Makru非常小,包含依赖在内只需要600KB左右。
|
||||
|
||||
让我们随便运行一下:
|
||||
````
|
||||
$ makru
|
||||
panic: could not found /some/random/path/makru.yaml.
|
||||
````
|
||||
好了,看起来Makru装好了。虽然它在抱怨在当前目录下面找不到配置文件,但是我们需要继续向前。配置文件是下一篇博文的问题。
|
||||
|
||||
## 安装makru_langc
|
||||
|
||||
还记得在文章第一段我提到的事情吗?“Makru的插件只能安装在项目上随源代码附带”。其实意思就是说Makru不存在能在整个系统或用户上生效的插件,所有插件的作用范围仅限于该项目。所以我们并不需要真正地“安装”makru_langc,只需要将它放在指定的文件夹下面就会生效,默认情况下这个目录是`<你的项目>/makru/plugins`。
|
||||
|
||||
现在我们给我们即将到来的scat项目创建一个文件夹,就叫`makru-tutor`。在文章里的完整路径是`/path/to/makru-tutor`,所以我们的默认插件目录就是`/path/to/makru-tutor/makru/plugins`。
|
||||
|
||||
### 直接放置源代码安装
|
||||
让我们直接开始吧!访问 [makru_langc的仓库](https://gitlab.com/jinwa/makru_langc),点击下载按钮然后选择合适的压缩包格式。把里面包含`__init__.py`的所有文件解压到`/path/to/makru-tutor/makru/plugins/makru_langc`下。
|
||||
|
||||
{% img /img/makru-tutor/2/screenshot-download-makru-langc-source.png "直接点击页面上很明显的下载按钮就可以选择压缩包格式了" %}
|
||||
|
||||
列出makru_langc文件夹的文件列表,要看见`__init__.py`才是正确的放法:
|
||||
````
|
||||
$ ls /path/to/makru-tutor/makru/plugins/makru_langc
|
||||
__init__.py
|
||||
...其它文件
|
||||
````
|
||||
|
||||
这样就安装好了!
|
||||
|
||||
但这篇文章还不应该结束,设想一下:大部分情况下升级插件都不会破坏兼容性,如果你想要享受最新功能和bug修复,每一次更新你都重复下载-解压的操作,这种重复性的操作让人感觉不便。我们还需要一个新的方案。
|
||||
如果你使用Git来管理你的项目,这里有一个更方便的方法:Git子模块。
|
||||
### 通过Git子模块安装
|
||||
让我们看看Git子模块的简单解释:
|
||||
|
||||
{% quote "Pro Git, the 2nd edition" https://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E5%AD%90%E6%A8%A1%E5%9D%97 "7.11 Git 工具 - 子模块" %}
|
||||
有种情况我们经常会遇到:某个工作中的项目需要包含并使用另一个项目。 也许是第三方库,或者你独立开发的,用于多个父项目的库。 现在问题来了:你想要把它们当做两个独立的项目,同时又想在一个项目中使用另一个。
|
||||
|
||||
我们举一个例子。 假设你正在开发一个网站然后创建了 Atom 订阅。 你决定使用一个库,而不是写自己的 Atom 生成代码。 你可能不得不通过 CPAN 安装或 Ruby gem 来包含共享库中的代码,或者将源代码直接拷贝到自己的项目中。 如果将这个库包含进来,那么无论用何种方式都很难定制它,部署则更加困难,因为你必须确保每一个客户端都包含该库。 如果将代码复制到自己的项目中,那么你做的任何自定义修改都会使合并上游的改动变得困难。
|
||||
|
||||
Git 通过子模块来解决这个问题。 子模块允许你将一个 Git 仓库作为另一个 Git 仓库的子目录。 它能让你将另一个仓库克隆到自己的项目中,同时还保持提交的独立。
|
||||
{% endquote %}
|
||||
|
||||
这正是我们需要的!实话说,我在使用Git两三年之后才接触到这个特性,我相信很多人从来没使用过它。不过它确实在一些情况下很有用,比如现在我们的情况。
|
||||
|
||||
好吧,废话少说。如果你需要Git的子模块功能的话你需要先把目录变成一个仓库。我们可以使用Git的魔法咒语来完成这件事:
|
||||
````
|
||||
$ git init
|
||||
已初始化空的 Git 仓库于 /path/to/makru-tutor/.git/
|
||||
````
|
||||
|
||||
然后再使用一个魔法咒语(你不需要自己手动创建`makru/plugins`文件夹,Git会帮你打理好这些):
|
||||
````
|
||||
$ git submodule add https://gitlab.com/jinwa/makru_langc.git makru/plugins/makru_langc
|
||||
正克隆到 '/path/to/makru-tutor/makru/plugins/makru_langc'...
|
||||
remote: Enumerating objects: 244, done.
|
||||
remote: Counting objects: 100% (244/244), done.
|
||||
remote: Compressing objects: 100% (137/137), done.
|
||||
remote: Total 250 (delta 145), reused 183 (delta 104), pack-reused 6
|
||||
接收对象中: 100% (250/250), 43.23 KiB | 48.00 KiB/s, 完成.
|
||||
处理 delta 中: 100% (146/146), 完成.
|
||||
````
|
||||
|
||||
然后就可以在相应目录下看到我们的插件:
|
||||
|
||||
````
|
||||
$ ls /path/to/makru-tutor/makru/plugins/makru_langc
|
||||
__init__.py
|
||||
...其它文件
|
||||
````
|
||||
(如果你有过Python编程的经验,到这里你应该就明白为何makru_langc要采取这种奇怪的项目布局了)
|
||||
|
||||
但是到这里事情还没完,我们还需要一个提交把我们的子模块放进树里。如果你需要了解Git,你可以看看上面引用的那本《Pro Git》,这本书有中文翻译。
|
||||
现在运行`git status`会多出来两个东西:
|
||||
````
|
||||
$ git status
|
||||
位于分支 master
|
||||
|
||||
尚无提交
|
||||
|
||||
要提交的变更:
|
||||
(使用 "git rm --cached <文件>..." 以取消暂存)
|
||||
新文件: .gitmodules
|
||||
新文件: makru/plugins/makru_langc
|
||||
````
|
||||
|
||||
要提交变更可以这样做:
|
||||
````
|
||||
git commit -m"随意产生的无意义信息(不"
|
||||
````
|
||||
所以我要这样:
|
||||
````
|
||||
$ git commit -m"add makru_langc to makru-tutor"
|
||||
[master(根提交) 0ed585d] add makru_langc to makru-tutor
|
||||
2 files changed, 4 insertions(+)
|
||||
create mode 100644 .gitmodules
|
||||
create mode 160000 makru/plugins/makru_langc
|
||||
````
|
||||
然后安装就完成了!
|
||||
|
||||
不过,到这里我们仅仅是“做了件事”安装了Makru和makru_langc,还没看到任何反馈。别担心,下一篇文章里我们就会得到点反馈了。
|
||||
|
||||
下一篇文章:为可执行文件项目编写配置文件
|
|
@ -1,75 +0,0 @@
|
|||
---
|
||||
title: 在Rope中测量"可达"
|
||||
date: 2021-11-3 UTC+8
|
||||
tags: ['Kache Development', 'Kache', '网络']
|
||||
---
|
||||
|
||||
观测你的猫的生死可不是件易事。<!--more-->当太阳明晃晃地照在可爱的Mudy身上时,它身上蓬松的毛反射了光线。光线经过许多介质进入你的眼中。哪怕不研究我们的身体如何处理这些莫名奇妙的光线,光从皮毛到你眼前的过程也需要时间。只不过这个时间太短:当你(在真空中)距离Mudy 299792458米时,这个时间是1秒。换句话说,当你在南极时,你看到北极的Mudy至少是0.05秒前的Mudy;当你在中国时,你看到北美的Mudy至少是0.1秒前的Mudy。
|
||||
|
||||
{% img /img/measure-peer-reachability/you-mudy-sun.png %}
|
||||
|
||||
Rope是为分布式应用框架Kache设计的抽象网络层。作为一个分布式应用框架,网络是最必要也是最麻烦的事情。在分布式网络里,知道一个Peer是否活着和能否连接上是重中之重。但是,在网络上观测一个Peer就如观测北极的Mudy或数千光年外的恒星一样麻烦。因为:1)你的朋友总是很麻烦,哪怕他们本意并不是想给你捣乱;2)你没办法不花时间就知道他们的情况,哪怕你和他们的延时只有1ms,你知道的也只是他们1ms前的情况,更别说我们不可能持续去监控他们的状况。
|
||||
|
||||
在描述一个Peer是否“可达”时,我们会变得混乱:我们事实上有无限多种方法“达”一个Peer。就像我们可以不用“看”就可以“听”到Mudy还活着。
|
||||
|
||||
{% img /img/measure-peer-reachability/you-mudy-sound.png %}
|
||||
|
||||
尽管我们有很多方法跟一个Peer交换信息,但却不是所有方法在所有时刻都有效。所以在描述一个Peer是否“可达”时,我们还需要描述其中一个方法是否“可达”目的地。
|
||||
|
||||
{% img /img/measure-peer-reachability/multiple-transports-to-peer.png %}
|
||||
|
||||
Rope使用PhysicalAddress和Peer分别描述路径和Peer的“可达”性。对于Peer而言,我们只需要知道它是否“活着”,即我们能否在网络上找到它。但对于PhysicalAddress而言,除了我们能否找到它,我们还需要知道我们是否能通过这条路径连接到Peer。
|
||||
|
||||
## Peer
|
||||
Peer,在中文中经常被翻译成“对等端”,我们对它的唯一要求就是活着。
|
||||
|
||||
````zig
|
||||
pub const Peer = struct {
|
||||
...
|
||||
aliveUntil: u64 = 0,
|
||||
aliveOffest: u64 = 0,
|
||||
...
|
||||
};
|
||||
````
|
||||
|
||||
这里我们采用了一种租期风格的方法来测量Peer是否活着:Peer会通过“租期”承诺自己在多少时间前会活着,Peer在租期过期前需要不停地续期,过期后我们就认为Peer已经死了。`aliveUntil`是这个租期的最后期限,`aliveOffest`则是Peer设置的租期时长。租期虽然被广泛使用(大部分的协议的心跳算法也使用租期),但它是一个很令人头疼的算法。
|
||||
|
||||
### 租期不定
|
||||
租期的令人头疼之处在于:租期时长可以是一个随意的值,但算法的表现跟租期时长有关,我们需要根据情况确定租期的值。较长的租期会使得Peer被错认为能连接上的时间会更长,它使得我们要测量的可达性变为“可能可达性”;较短的租期促使Peer更经常地续期,降低容错能力并且使用更多网络流量。
|
||||
|
||||
Google在它的Google Play Service中与服务器的心跳部分采取了自适应租期:随着连接上的时长增加,租期会逐渐变长。这种自适应租期的前提是,长期的“能连接上”可以预测接下来不太可能出现一段时间无法连接上的情况。自适应租期确实是个不错的方法,不过Rope中租期由Peer设置。Peer可以根据实际情况确定租期,目前这个数字还是固定值10秒。
|
||||
|
||||
|
||||
## PhysicalAddress
|
||||
能被找到和能连接上是有区别的。当我们通过`bind(2)`、`listen(2)`和`accept(2)`监听一个端口时,其他人哪怕有我们的地址和端口号也不一定能连上。我们经常需要一些特殊的技巧才能在现实中连接上其它人的机器。比如,如果我们和目标机器之间有NAT的话,我们必须要穿透NAT才能连上。尽管它确实存在,但是我们确实不一定能连接上。
|
||||
|
||||
所以,Rope的PhysicalAddress里面的可达性被分成了两个维度:Existence和Reachability。Existence指的是这个PhysicalAddress是否存在,Reachability指示这个PhysicalAddress是否有可能连接上。
|
||||
|
||||
````zig
|
||||
const PhysicalAddress = struct {
|
||||
...
|
||||
lastReachable: u64 = 0,
|
||||
lastFound: u64 = 0,
|
||||
lastDismiss: u64 = 0,
|
||||
promiseReachable: u64 = 0,
|
||||
...
|
||||
};
|
||||
````
|
||||
|
||||
上面lastFound和lastDismiss就是用来标识Existence维度的值,lastReachable和promiseReachable用来标识Reachability维度。它们都是Unix时间戳。
|
||||
|
||||
### Existence
|
||||
`lastFound`和`lastDismiss`用于标识Existence维度,这两个值分别跟两个事件有关:`_wire.found`和`_wire.down`。前一个在发现新的PhysicalAddress后发送,后一个在发现PhysicalAddress所代表的路径断开之后发送。它们会通过EventPub发送到网络上的其它Peer。
|
||||
|
||||
`_wire.found`事件会更新`lastFound`到一个时间,`_wire.down`事件会更新`lastDismiss`。当`lastFound`大于`lastDismiss`时,我们认为这个PhysicalAddress还存在于网络上。
|
||||
|
||||
我们是否可以用一个值标识这个维度,比如说单一个`lastFound`?最开始我也是只设计了`lastFound`。问题在于,如果我们这样做,这个Existence会变成一个租期风格的维度。但是这个事情明明我们已知,使用租期会出现“可能”。
|
||||
或者我们可以使用一个布尔值来代替这两个值,但是这样时间信息就会丢失。丢失时间信息会让程序在这两个事件频繁发生时变得混乱,特别是当发送该事件到接受该事件存在时间差时(EventPub使用泛洪法广播消息,在我们测量可达性的这个位置不保证事件能按照全局发送顺序收到)。考虑这个例子:两个Peer分别发送某个PhysicalAddress的found和down事件,found先发送但是最后收到,down后发送但是先收到。如果不分别保存两个时间我们只能简单地覆盖之前的结果,这时候状态就会变得奇怪。
|
||||
|
||||
### Reachability
|
||||
Reachability是一个完全独立的维度,它与Existence无关,跟这个PhysicalAddress是否连接上或是否正在传输数据有关。这是一个租期风格的维度。
|
||||
|
||||
### 理解"Reachable but not exists"
|
||||
在Reachability里我强调这两个维度是独立的。这样看起来会存在一种奇特的情况:Reachable but not exists(可达但不存在)。
|
||||
|
||||
既然可达为何不存在呢?这里的不存在不是真的不存在,而是在网络上不存在。试试考虑下面的情景:目前网络上存在A和B,它们互相是认为对方Reachable and exists的。现在有一个新节点C要加入,他连接A并开始广播`_ticktock`事件让大家知道它的存在。在这时候A和C互相之间可达,但在A的视角看这个PhysicalAddress的lastFound仍然是初始值0,即这个PhysicalAddress还不存在(not exists)。现在A因为发现了新的PhysicalAddress就会广播一条`_wire.found`事件。然后B、C收到这个事件后就会更新它们的`lastFound`,然后分别将该消息转发给A、C和A、B。这时A就会收到它自己发出的这条消息,虽然这条消息不会被转发给别的Peer或应用,但是A仍然会用这条消息更新`lastFound`。这时在A处这个PhysicalAddress就会变成Reachable and exists(可达并存在)。
|
|
@ -1,87 +0,0 @@
|
|||
---
|
||||
title: 无言集(2019)
|
||||
date: 2021-10-13 11:22:59
|
||||
tags:
|
||||
- 诗集
|
||||
categories:
|
||||
- 诗集
|
||||
---
|
||||
|
||||
> 几篇拙作,仅以备用。
|
||||
|
||||
<!-- more -->
|
||||
|
||||
## 无题 (2019年2月)
|
||||
华日丽压金黄沙,绿意已深枯枝头。
|
||||
花开艳谢惊尘起,星河大江远流归。
|
||||
|
||||
## 路远遇宾客(2019年2月)
|
||||
芳草无处寻,流水觅不回。
|
||||
庭深埋灰土,云远藏明星。
|
||||
稀罕有知春,旧鸟报新语。
|
||||
路远途难行,时日不可归。
|
||||
|
||||
## 赠高考学子(2019年2月)
|
||||
誓师未行人先行,百日将近学不尽。
|
||||
千秋挑灯战明月,万载硕果挂枝头。
|
||||
凤凰马蹄声未响,龙咆战鼓已齐鸣。
|
||||
多年热血终无恨,桃花仍恋故园间。
|
||||
|
||||
## 赠高考学子(其二) (2019年)
|
||||
夜月独挂空流明,竹影自落浓妆泥。
|
||||
鲜衣怒马青草地,清风细雨曾相依。
|
||||
昏灯摇曳行路亮,深紫暗蔽无色花。
|
||||
他日天光照大地,红袍踏叶满城哗。
|
||||
|
||||
## 观时事咏怀(2019年2月)
|
||||
马嚎鬼叫,硝烟烈火,血染凡尘。死生参半,多少离家未曾还。
|
||||
月照江流,烟霞入木,故人有念。流光未去,凭轩泪目无人归。
|
||||
|
||||
## 无题(2019年5月)
|
||||
浮沉千载,风雨万世,渡江月。复不见,风流人物怀醉风生谈笑。电光代烛,杨絮飞起,再无飘摇。春风过,清雨落,梦余无名,巷里看花。
|
||||
|
||||
## 不题(2019年5月)
|
||||
雷惊裂地无人醒,
|
||||
水光扑面不见归。
|
||||
清风翻书何识字?
|
||||
千尺红锦只能回。
|
||||
|
||||
## 论孔(2019年5月)
|
||||
桃花粉嫩终成土,
|
||||
金菊灿烂也归泥。
|
||||
试问何处恒久远,
|
||||
洞庭一度遇周公。
|
||||
|
||||
## 路遇无名(2019年5月)
|
||||
风飘飘,雨飞飞,独坐江头,伸手不见沙堆。酒醒所处,无路可行。可悲,可悲!天下忧患,吾与谁归?
|
||||
|
||||
## 我们(2019年)
|
||||
在黑暗的道路上寻找另一片阴影
|
||||
在寂静的秋风中踏着无色的叶
|
||||
在遥远的故事里唱着失去名字的歌谣
|
||||
<br />
|
||||
那个遥远的故事
|
||||
他们之中响起的哨声
|
||||
他们留下的半片泪水
|
||||
那支粘在鞋底的树枝
|
||||
尖叫着
|
||||
“只有好的才是好的”
|
||||
然后忘却了
|
||||
<br />
|
||||
那个不被在乎名字的歌谣
|
||||
唱着血
|
||||
唱着寒冷和饥饿
|
||||
唱着锤子、大棒和镰刀
|
||||
唱着那支树枝
|
||||
<br />
|
||||
唱着:
|
||||
秋风不吹
|
||||
再多叶片也落不出优美的舞姿
|
||||
道路堵住
|
||||
再多的头颅也垫不成光明的阶梯
|
||||
|
||||
## 节(2019年12月)
|
||||
爆竹声声慢,
|
||||
烟火溢天边。
|
||||
红桃念新语,
|
||||
岁岁不复还。
|
|
@ -1,81 +0,0 @@
|
|||
---
|
||||
title: 随想集(2020 & 2021)
|
||||
date: 2021-10-13 15:18:33
|
||||
tags:
|
||||
- 诗集
|
||||
categories:
|
||||
- 诗集
|
||||
---
|
||||
|
||||
> 谨以此集纪念我自己的“20年代初”。拙作几篇,仅作备查。
|
||||
|
||||
<!-- more -->
|
||||
|
||||
## 浪(2020年5月)
|
||||
桃花树站在海岸上
|
||||
前浪刚刚拍碎
|
||||
后浪还在路上
|
||||
桃花树上有着人影
|
||||
并着电喇叭
|
||||
|
||||
## 末代帝国(2020年5月)
|
||||
万岁一声酒,朽木何时朽?屋漏偏灭花烛,灯下无影踪。黄琉红璃映斜阳,山川早覆众生口。还剩鸦叫几声,花落人瘦,不敢问西东。
|
||||
|
||||
## 山川(2020年9月)
|
||||
山川
|
||||
静悄悄地绿
|
||||
沉没在不语的早餐旁边
|
||||
<br />
|
||||
旅人的手指
|
||||
唱赞歌
|
||||
伴随着星星似的闪光
|
||||
活泼地跳舞
|
||||
<br />
|
||||
千年以后
|
||||
一样地燃烧过
|
||||
一样地曾在空气中飞舞
|
||||
|
||||
## 无题(2020年11月)
|
||||
风推浮萍随身灭,冰冻三尺一日寒。
|
||||
铁马残破二十年,孤烟垂尽寻常家。
|
||||
|
||||
## 自题(2020年)
|
||||
浮萍逐波十八年,竹节伶仃醉道虚。
|
||||
莫教长歌独对月,孤灯下磨影敲门。
|
||||
|
||||
## 中国(2020年)
|
||||
还剩多少山川在唱自由的歌
|
||||
红色愤怒地指责黑色
|
||||
透明的字词传出江南
|
||||
水箱在笑
|
||||
|
||||
## 除夕(2021年2月)
|
||||
爆竹声声慢,灯火璀璨,星空从地面升到天上
|
||||
飞机正常的争吵,不同的眼睛不会说话
|
||||
也像沉积在水底的夜晚
|
||||
画着不同人的漫画
|
||||
|
||||
## 断代(2021年6月)
|
||||
一朵樱花
|
||||
流进干涸的河道
|
||||
发出铁锈的声音
|
||||
和泠冽的味道
|
||||
<br />
|
||||
我是否需要呼吸
|
||||
在极蓝的火焰里
|
||||
烧个彻底
|
||||
露出棕色的核心
|
||||
也不知道远方的幻影
|
||||
踏碎我的妄想
|
||||
剩下什么可描述之物
|
||||
<br />
|
||||
没有这样一种魔法
|
||||
把水面停下
|
||||
浮萍不再追逐波浪
|
||||
<br />
|
||||
有没有许多种可能
|
||||
自由呼吸
|
||||
毋需等上许多光阴
|
||||
|
||||
## 国(2021年9月)
|
||||
苍生难万载,灯火千年梦。曾有士人念天下,如今字里还。清风不识字?何故乱翻书。斜阳下,屋檐乱。热血终有恨,鬼怪在人间。
|
|
@ -1,107 +0,0 @@
|
|||
---
|
||||
title: 在Fedora 34上通过Howdy为sudo增加人脸识别认证
|
||||
date: 2021-07-10 22:34:29
|
||||
tags:
|
||||
- Linux记
|
||||
- 生命很短
|
||||
- logbook
|
||||
---
|
||||
|
||||
今天晚上折腾了一个小时,终于把 Howdy 折腾出来了,写篇博文作笔记以备查。
|
||||
|
||||
|
||||
[Howdy](https://github.com/boltgolt/howdy) 是一个为 Linux 提供 Windows Hello 风格验证的软件,它作为一个 PAM 模块为其它软件提供人脸验证服务。不像 Windows Hello 需要认证过的IR摄像头, Howdy 基本上只要是个摄像头都可以使用。当然,如果没有 IR ,低光环境下的识别率就很拼摄像头的素质了。
|
||||
|
||||
这篇博文主要内容是给 sudo 弄 Howdy:sudo 老是让人输密码着实令人烦躁,但是我又不想取消所有认证,正好 Howdy 的需求很低(只需要一个摄像头)。
|
||||
|
||||
<!--more-->
|
||||
|
||||
我机器上使用的发行版是 Fedora 34,64 位。
|
||||
|
||||
|
||||
## 安装Howdy
|
||||
只需要跟随 Howdy 项目的 README 里的步骤就可以安装 Howdy 了:
|
||||
|
||||
````shell
|
||||
sudo dnf copr enable luya/howdy
|
||||
sudo dnf install howdy
|
||||
````
|
||||
|
||||
然后就按照 README 上面的说法执行 `sudo howdy add` 添加面孔,然后就看见显示 `pip3 show dlib` 之类的错误信息,意思是找不到 dlib 。这个 copr 仓库里的包没有把 dlib 作为依赖,得自己安装。
|
||||
|
||||
可以用 `sudo dnf install python-dlib` 安装dlib。也可以用pip安装: `sudo pip install dlib` ,但是编译的时候肯定会卡几分钟,并且大概会占用 2-3GB 左右的内存。
|
||||
|
||||
接下来再次运行 `sudo howdy add` ,它会抱怨摄像头路径还没配置好:
|
||||
````
|
||||
[sudo] password for ***:
|
||||
Adding face model for the user ***
|
||||
Enter a label for this new model [Initial model] (max 24 characters):
|
||||
Camera path is not configured correctly, please edit the 'device_path' config value.
|
||||
Exception ignored in: <function VideoCapture.__del__ at 0x7f855eeb1b80>
|
||||
Traceback (most recent call last):
|
||||
File "/usr/lib64/security/howdy/recorders/video_capture.py", line 55, in __del__
|
||||
self.internal.release()
|
||||
AttributeError: 'VideoCapture' object has no attribute 'internal'
|
||||
````
|
||||
|
||||
运行 `sudo howdy config` 来打开配置,默认情况下会使用 GNU nano 作为编辑器。找到 `device_path = ` 这一行,我们要设置的就是这个值。但是怎么知道是哪个路径呢?我用的是VLC里面的"Open Capture Device...":将"Capture mode"设为"Video camera",在"Video device name"的下拉菜单里面的选项选择一个,点"Play"。一个一个地尝试不同的选项,哪个有画面就是哪个路径。在我的机器上,这个路径是 `/dev/video0` ,最后就是 `device_path = /dev/video0` 。
|
||||
|
||||
{% img /img/set-up-sudo-with-howdy-on-fedora-34-for-faical-authenticating/vlc-open-capture-device.png "图片里的最后一个选项:Open Capture Device...,点击之后会打开一个窗口"%}
|
||||
|
||||
配置好之后运行 `sudo howdy add`增 加面孔,这下能够顺利增加了!增加完面孔可以用 `sudo howdy test` 打开测试窗口进行测试。
|
||||
|
||||
## 配置PAM
|
||||
PAM ,或者叫 Pluggable Authentication Module ,是一个中心化的身份验证服务。[这里有一篇对PAM的介绍](https://www.redhat.com/sysadmin/pluggable-authentication-modules-pam)。
|
||||
|
||||
参考 [Arch Wiki上的Howdy词条](https://wiki.archlinux.org/title/Howdy),要配置 sudo 的验证时行为,只需要修改 `/etc/pam.d/sudo` ,在原先的第二行前面再加一行:
|
||||
````
|
||||
auth sufficient pam_python.so /lib64/security/howdy/pam.py
|
||||
````
|
||||
这里用`/lib64`替换了 Arch Wiki 词条上的`/lib`,是因为提供给 Fedora 的这个包把这些文件安装到`/lib64`而不是`/lib`,这个地方各个发行版可能都有不同的规则。
|
||||
|
||||
PAM 的配置在修改后会自动应用。现在我们可以试试使用`sudo -i`看看有没有调用人脸识别:
|
||||
````
|
||||
[sudo] password for ***:
|
||||
````
|
||||
|
||||
事情变得奇妙了起来……Howdy 并没有被调用。这说明 PAM 配置哪里出了问题,读一读跟 PAM 有关的日志:
|
||||
|
||||
````
|
||||
sudo journalctl -g pam -r
|
||||
````
|
||||
|
||||
然后发现了奇妙的错误:
|
||||
|
||||
````
|
||||
PAM adding faulty module: /lib/security/pam_python.so
|
||||
PAM unable to dlopen(/lib/security/pam_python.so): /lib/security/pam_python.so: cannot open shared object file: No such file or directory
|
||||
````
|
||||
|
||||
原来是找不到指定的 pam_python.so 这个 PAM 模块(用来调用 Howdy 的 pam.py ,Howdy 提供的 pam.py 是一个 Python 文件)。
|
||||
|
||||
还好 [luya/howdy Copr仓库](https://copr.fedorainfracloud.org/coprs/luya/howdy/) 下的评论给我找到了一个简单的出路:根据 [RPM Sphere网站] 上的流程安装了 RPM Fusion 和 RPM Sphere 这两个第三方软件源之后,可以在 RPM Sphere 这个软件源上找到`pam_python`这个包:
|
||||
|
||||
````
|
||||
sudo dnf install pam_python
|
||||
````
|
||||
|
||||
然后就可以愉悦地使用了!
|
||||
|
||||
````
|
||||
sudo -i
|
||||
````
|
||||
|
||||
## 使用pamtester测试
|
||||
[Pamtester](http://pamtester.sourceforge.net/) 可以测试 PAM 的指定验证服务,这样要检查 Howdy 是否生效或者单纯享受人脸识别(大雾)就很简单啦。
|
||||
|
||||
````
|
||||
sudo dnf install pamtester
|
||||
````
|
||||
|
||||
在这篇文章的例子里只需要(用要验证的用户的用户名代替`<用户名>`)
|
||||
|
||||
````
|
||||
pamtester sudo <用户名> authenticate
|
||||
````
|
||||
|
||||
就可以触发 PAM 的验证了。
|
|
@ -1,67 +0,0 @@
|
|||
---
|
||||
title: LightStands的第一年
|
||||
date: 2023-08-23 19:29:21
|
||||
tags:
|
||||
- LightStands
|
||||
---
|
||||
|
||||
现在的LightStands软件的第一个Commit来自2022年7月31日。不过,LightStands的想法和开发其实开始得很早,最早的版本早在2017年就开始开发了。详细的时间我并不记得,我只知道在2017年8月8日我在Git上提交了web.dart的[第一个commit](https://github.com/thislight/webart/commit/dc37ef4a44ea52d7c8515528fd68ae46ad901b62)——它是LightStands第一个版本的基础。但是,在写了几千行代码之后,这个LightStands软件就被废弃了。
|
||||
|
||||
这篇文章是我过去几年里开发LightStands软件的经历总结。
|
||||
|
||||
<!--more-->
|
||||
|
||||
## 坚实的基础
|
||||
|
||||
从2017年到2022年,我对LightStands软件的技术栈就有很多想像。最开始的版本使用了Dart语言编写,但是很快就废弃了,原因稍微有点复杂。
|
||||
|
||||
原本Dart的实现是它的一大卖点:Dart的实现可以在两个模式下运行程序,一个叫做检查模式(Checked Mode),一个是生产模式(Production Mode)。检查模式会在程序运行时进行额外的检查(包括一些类型检查),而生产模式不会。我猜这两个模式产生的原因是这样的:Dart的类型系统支持动态类型,但是检查动态类型引入了条件判断,条件判断对性能的影响非常大。
|
||||
|
||||
他们希望你可以在以生产模式运行程序,在检查模式中测试程序,这样你就拥有了the best of two worlds。通过两种模式的区别,Dart在提供动态类型的同时达到了比较高的性能。我没有做过比较正式的测试,但是生产模式比当时的CPython要快得多。
|
||||
|
||||
事情在Dart 2的时候就起了变化:Dart 2开始转型为Flutter的专用语言。Dart 2移除了生产模式和检查模式的区别,取而代之的是单独的”强模式“(Strong Mode)。
|
||||
|
||||
这只是一个好听的营销术语。我用web.dart写了一个简单的HTTP服务示例进行了大致测试,发现Dart 2.0强模式的性能与Dart 1.24.3的检查模式没有太大区别,这样的性能没有太大的亮点。并且,新的强模式对原本自动使用动态类型的变量使用了类型推断,一些情况下推断出的类型并不符合直觉,还导致Dart 2失去了向前兼容。
|
||||
|
||||
还没完呢。为了强行推广Dart 2,Pub(Dart的包管理器和包仓库)对不支持Dart 2的库降权,被降权的库就会直接放在搜索结果末尾。
|
||||
|
||||
> Google什么时候砍掉一个产品?现在或者下一秒。
|
||||
|
||||
这件事给我很深刻的教训:技术栈的基础一定要可靠,最少要确定它的维护团队不会强行推动一个尚未完成的修改。对于编程语言这件事,后来我曾考虑过Lua(还为此写了[hussar](https://gitlab.com/thislight/hussar)),最后在去年开始新开发时还是选择了Python。
|
||||
|
||||
## 减少可替换部件
|
||||
|
||||
减少Moving Parts是我在开发初期的一个大胆决定。目前LightStands有三个组件:httpgated、mangerd、cleanupd,和一个最终依赖melon。并且:
|
||||
|
||||
* 线上服务也使用SQLite3作为DBMS
|
||||
* 所有的组件都写在同一个仓库,只是暴露不同的进入点(entrypoint)
|
||||
* 运行服务只需要3个进程(分别是:HTTP API服务、拉取RSS、垃圾收集)。最开始,进程之间不需要交换信息,都是直接操作数据库
|
||||
|
||||
|
||||
这极大地简化了开发和部署。配合容器,手动部署甚至只需要5分钟就可以完成。开发的时候可以直接运行程序、使用DB Browser for SQLite直接查看数据库。而且目前来看,SQLite3并没有给性能带来太大问题。
|
||||
|
||||
Melon是一个ORM框架。最开始我将它和LightStands其它组件放在一起,这样迭代会更简单。但它是一个高内聚的框架,和LightStands其实没什么关系,这时我就想将它从LightStands中独立出来。我当时就意识到我们缺少一个私有的软件包仓库:如果最开始就有一个私有的软件包仓库,不仅可以用来整理模块,还可以简化远程服务器上的部署。我打算在今年完成这件事。
|
||||
|
||||
## 单页应用不是唯一选项
|
||||
|
||||
最开始我是想要把LightStands的登录和注册做成单页应用,在客户端上完成验证、创建访问密钥、授权的工作。但是这样做效果很一般:单页应用的无缓存访问很慢。登录和注册只有寥寥几个页面,完全无法利用单页应用渐进式下载的优势,反而被JS拖累延长了首次内容显示耗时。而且,这些工作本来就是一次性在服务器上完成更快,将他们分为几个HTTP请求确实进一步拖慢了速度。
|
||||
|
||||
我正在编写一个JavaScript库,可以简化多页应用的DOM操作。希望今年能应用在新的LightStands用户面板上。
|
||||
|
||||
## 下一站
|
||||
|
||||
LightStands是我新想法的试验田,应用了许多在很多人看来都很小众的想法。不过,激进自有其坏处:LightStands的开发相对来说很缓慢,为一个小众架构编写能够使用的库是一个巨大的挑战。编写实现这些新想法的代码需要消耗许多时间,例如LightStands使用的ORM框架melon,代码五千余行。
|
||||
|
||||
LightStands正在将旧有基于feedparser的feedloaderd迁移到全新的mangerd,mangerd不仅能够从源拉取,还支持接受推送,为ActivityPub支持做足了准备。不过在此之前,我们还面临一个重要挑战:将旧的数据结构迁移到新的数据结构。
|
||||
|
||||
新的一年,LightStands在技术上的路线图大概如下:
|
||||
|
||||
|
||||
* 将API迁移到新的数据结构
|
||||
* 支持更新通知
|
||||
* 支持ActivityPub
|
||||
|
||||
|
||||
在新的一年继续“添砖加瓦”,干杯!
|
||||
|
||||
|
|
@ -1,255 +0,0 @@
|
|||
---
|
||||
title: 进程、线程、协程
|
||||
date: 2022-03-31 08:07:45
|
||||
tags:
|
||||
- 高性能I/O
|
||||
---
|
||||
|
||||
|
||||
程序运行时,最重要的便是Program Counter(PC)和Stack。Program Counter(程序计数器)记录程序运行的位置,Stack(栈)保存当前的数据。
|
||||
|
||||
````
|
||||
+-------+ |Stack Address
|
||||
| v1 | |
|
||||
+-------+ |
|
||||
| v2 | |
|
||||
+-------+ |
|
||||
| v3 | |
|
||||
+-------+ |
|
||||
| v4 | |
|
||||
+-------+ v <-- top
|
||||
Stack 示意图
|
||||
````
|
||||
|
||||
这是一个Thread(线程)。本文主要是为了说明人们是怎么把这么简单的东西玩出各种花样的,完完全全是一篇走马观花的介绍。本文会解释关于线程的一些概念,并展示一些新的有意思的东西。
|
||||
|
||||
<!--more-->
|
||||
|
||||
## 系统线程和用户空间线程
|
||||
|
||||
操作系统管理硬件,向应用提供简单的API。大部分操作系统都包含线程管理。通常,一个系统进程包含一个或多个系统线程(OS Threads),这些线程共享进程资源——内存、file descriptors,被隔离在同一个环境中。Linux Kernel也是这么做的,比如说,可以用于限制资源访问的[cgroups](https://en.wikipedia.org/wiki/Cgroups)以进程(组)为单位管理资源。操作系统内核通常在内核里对系统线程进行排程(scheduling)。内核会跟踪线程的状态,通过算法确定下一个运行的线程。进行这个操作的的部分叫做排程器(scheduler)。
|
||||
|
||||
用户空间(userspace)是指虚拟内存(virtual memory)里内核空间以外的空间,现在也用来表示内核以外跟内核交互的代码(userland),在大部分情况下userspace和userland这两个词是混用的。用户空间线程也需要排程,有时也有排程器,但是它们都实现在用户空间里。在用户空间里实现,可以免去切换到特权模式(supervisor mode,或内核模式:kernel mode)时切换上下文(context switching)的损耗。
|
||||
|
||||
````
|
||||
Linux切换系统线程的流程示意:
|
||||
Thread0 -> [保存Thread0的上下文(PC和Stack)] -> [恢复内核的上下文] -> Linux Kernel -> [排程] -> [保存内核的上下文] -> [恢复Thread1的上下文(PC和Stack)] -> Thread1
|
||||
^进入特权模式 ^离开特权模式
|
||||
|
||||
用户空间线程的通常切换流程:
|
||||
Thread0 -> [排程] -> Thread1
|
||||
````
|
||||
|
||||
### 系统线程没有那么沉重
|
||||
通常,使用用户空间线程的理由是“系统线程很重”:需要的内存更多、切换速度更慢……但至少在Linux上,系统线程没有那么“重”。
|
||||
|
||||
首先,创建系统线程的栈空间在实际使用前并不占用内存空间。这是因为Linux默认启用过度提交(Overcommit),在虚拟内存中申请的内存并不会在实际内存中预留。你可以创建上千个2MB栈的线程,但是每个线程实际只占用8KB。
|
||||
|
||||
系统线程切换速度慢的问题并不在于我们通常认为的上下文切换,它虽然仍然消耗时间但没有我们想像的慢(在Google工程师的测试中切换来回只要<50ns)。消耗时间更多的是排程算法,排程算法是计算密集的工作,占用的时间比上下文切换多。
|
||||
|
||||
解决方法是使用计算简单甚至不需要计算的算法,这类算法经常是非公平算法。Google的工程师设计了一组叫做SwitchTo的系统调用,可以让应用告诉系统接下来切换到指定线程。这组系统调用将线程之间上下文切换的性能提升了三十倍。(尚未合并到上游)
|
||||
|
||||
<iframe width="auto" height="auto" src="https://www.youtube-nocookie.com/embed/KXuZi9aeGTw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
[User threads...with Threads slides 下载](./user_threading.pdf)
|
||||
|
||||
## 抢占式线程和协同式线程
|
||||
我们知道,我们不可能在寥寥几个CPU核之上同时运行数量多于其数量的线程,我们需要一些算法决定:
|
||||
|
||||
- 线程何时运行
|
||||
- 线程能运行多久(何时结束)
|
||||
|
||||
抢占式和协同式是两个类型,描述了算法解决后者时选择的方向。抢占式算法有可能强制暂停线程,协同式算法只有线程显式或隐式让出时才暂停线程。
|
||||
|
||||
Linux默认情况下使用抢占式算法:内核在每次线程运行时都会指定时间片,线程让出或时间片到期时内核会取回控制权,重新排程。抢占式线程很难在用户空间中实现,但并非不可能。不过抢占式线程不符合用户空间线程的普遍目的,所以用户空间线程一般是协同式线程。
|
||||
|
||||
抢占式算法保证了公平性,但对性能有负面影响;协同式线程保证了本地性,性能更好。不是所有系统默认提供的都是抢占式线程,比如FreeRTOS这类面向实时应用的操作系统提供甚至默认提供协同式线程。
|
||||
|
||||
## 无栈线程(Stackless Threads)
|
||||
|
||||
“无栈”的意思不是“没有栈”,而是“不使用栈”。其状态的大小已经确定,可以直接放在栈上而不需要使用栈。Rust和Zig的异步函数、[async.h](https://github.com/naasking/async.h)、protothread就属于这种类型。
|
||||
|
||||
前面的Rust和Zig通过编译器将代码翻译成状态机;后两者使用宏实现状态机,并且要求用户用一个固定的数据结构在让出之间保存状态。需要注意的是:状态可以直接放在栈上不意味着其运行过程不使用栈,只代表它可以不需要一个单独的栈。
|
||||
|
||||
## 在线程中同步
|
||||
无论你使用的是抢占式线程还是协同式线程,你都有可能需要在线程中进行同步。当然,协同式多线程在一些状况下不需要同步。线程安全是说在多线程环境下能够正常工作。
|
||||
|
||||
哪怕只是简单的加法,只要它涉及到多线程并且不是原子操作,你都应该仔细考虑它的副作用。在很多在指令集上,加法包含取值、加法、保存等多个操作,参照下列LLVM IR:
|
||||
|
||||
````llvm
|
||||
;;void spam() {
|
||||
;; int b = 6;
|
||||
;; int c = 4;
|
||||
;; int a = b + c;
|
||||
;;}
|
||||
define dso_local i32 @spam() #0{
|
||||
%2 = alloca i32, align 4
|
||||
%3 = alloca i32, align 4
|
||||
%4 = alloca i32, align 4
|
||||
store i32 6, i32* %2, align 4
|
||||
store i32 4, i32* %3, align 4
|
||||
%5 = load i32, i32* %2, align 4 ;; * a = b + c
|
||||
%6 = load i32, i32* %3, align 4 ;; |
|
||||
%7 = add nsw i32 %5, %6 ;; |
|
||||
store i32 %7, i32* %4, align 4 ;; *
|
||||
%8 = load i32, i32* %1, align 4
|
||||
ret void
|
||||
}
|
||||
````
|
||||
|
||||
非原子操作可以帮助CPU进行指令级并行(intrustion-level paralism),同时执行几条不相干的指令。这可以显著提高流水线性能。但是非原子操作在多线程同时访问一个值的情况下可能导致奇怪的行为。
|
||||
|
||||
考虑线程th0和th1:th0获取b=2时th1将b修改为b=4,th0获取c=3时th1将c修改为c=5,这时th0拿到的是b=2和c=3,a=b+c=5,而th1会认为a=b+c=4+5=9。你可以使用原子操作指令进行原子操作。另外,在抢占式线程的情况下,CPU的控制权随时都有可能被取回,你应该按照“在任何指令执行后线程就会被挂起”考虑你的代码。
|
||||
|
||||
现在还有一个比较重要的优化叫做非序执行(Out-of-order execution),也可以叫做代码重排(code reorder),就是当你的代码满足一定条件时,编译器或者CPU会将你的代码重新排列以满足优化要求。但是这不一定是你需要的:它会把你的代码打乱,影响到你代码的副作用。你可以使用内存围栏(memory barrier)要求特定的顺序。
|
||||
|
||||
任何同步最后都有可能成为性能瓶颈,优化你的代码架构可以帮助减少同步技术的使用范围。
|
||||
|
||||
### 同步的基本技术:Lock和Condition
|
||||
|
||||
[推荐阅读:Locking in WebKit](https://webkit.org/blog/6161/locking-in-webkit/)
|
||||
|
||||
### 其它技术
|
||||
|
||||
- 事务性内存(Transactional Memory)
|
||||
- 信号量(Semaphore)、读写锁(Read-write Lock)
|
||||
- Compare-And-Swap(CAS)、原子操作指令
|
||||
|
||||
### 无锁(Lock-less)、无死锁(Deadlock-free)和无等待(Wait-less)数据结构
|
||||
|
||||
通常,无锁数据结构在频繁操作时性能表现比使用锁的数据结构更好,常见的无锁数据结构有:
|
||||
|
||||
- Lock-less Ring Buffer
|
||||
- 无锁队列
|
||||
|
||||
无锁的意思并非是“无等待”,无锁结构的内部经常使用某种形式的自旋锁来重复执行操作直到成功。但是,这个锁的影响范围比单独的锁要小得多,对整体性能的影响更小。无锁数据结构的操作本身一般是非阻塞(non-blocking)无等待的,通过类似自旋锁的操作可以确保操作成功,但是会造成阻塞。
|
||||
|
||||
使用自旋锁的实现在大量参与者同时操作同时阻塞时会影响性能,虽然通常需要非常非常多的参与者才会影响性能:自旋锁会让这些线程保持活跃。使用混合线程(稍后在“事件驱动编程和协同式多线程”中讨论)时会使相应的协同式线程无法从活跃线程中离开,在某些情况下会造成问题。
|
||||
|
||||
无死锁数据结构保证操作数据结构的线程不会死锁。最典型的是双锁队列(Two-Lock Queue):一个头锁一个尾锁,修改相应部分时就持有相应的锁。
|
||||
|
||||
## 事件驱动编程和协同式多线程
|
||||
阻塞线程等待I/O操作完成从并发角度而言并不是什么好主意:I/O操作通常需要花费一些时间来完成。幸运的是:Linux内核内部的I/O操作其实都是异步的,线程阻塞会被看作是一次隐式让出,给其它线程一个运行的机会。但是!这个机会为什么不给我们自己的代码呢?我们只需要在完成或者错误的时候调用一下回调函数就好了,这样剩下的时间我们可以运行别的代码。
|
||||
|
||||
````lua
|
||||
-- 随便乱写的伪代码
|
||||
local uv = require "uv"
|
||||
|
||||
local file = uv.open_file("./echo.txt", "a+")
|
||||
|
||||
local run = true
|
||||
while run do -- 这写法其实不对,千万别学,只是为了展示一下回调地狱
|
||||
file:read(256, function(fail, result)
|
||||
if not fail then
|
||||
file:write(result, function(fail)
|
||||
if fail then
|
||||
print("fail:"..fail)
|
||||
run = false
|
||||
end
|
||||
end)
|
||||
else
|
||||
run = false
|
||||
end
|
||||
end) -- file:read file:write 都是非阻塞的函数,可以想象内存很快就爆炸了
|
||||
print("Going to echo 256 bytes") -- 你的stdout将会塞满这玩意,因为读和写没完成就可以来到这行了
|
||||
end
|
||||
````
|
||||
|
||||
真是糟糕的味道。所幸我们后来使用了一个叫做Promise(或者Future)的东西,它代表一个在未来完成的操作。
|
||||
|
||||
````lua
|
||||
-- 仍然是乱写的伪代码
|
||||
|
||||
local uv = require "uv"
|
||||
|
||||
local file = uv.open_file("./echo.txt", "a+")
|
||||
|
||||
local run = true
|
||||
while run do
|
||||
file:read(256)
|
||||
:on_ok(function(result)
|
||||
return file:write(result)
|
||||
end)
|
||||
:on_err(function(err)
|
||||
print(err)
|
||||
run = false
|
||||
end)
|
||||
print("Going to echo 256 bytes")
|
||||
end
|
||||
````
|
||||
|
||||
好吧,干净了点,但是现在我们还可以弄得更干净。
|
||||
|
||||
````lua
|
||||
-- 也是乱写的伪代码,不过确实可以在Lua里实现
|
||||
|
||||
local uv = require "uv"
|
||||
|
||||
local file = uv.open_file("./echo.txt", "a+")
|
||||
|
||||
while true do -- 这次逻辑上是没错的
|
||||
local status, blk = pawait(file:read(256))
|
||||
if not status then
|
||||
break
|
||||
end
|
||||
local status, err = pawait(file:write(blk))
|
||||
if not status then
|
||||
print("fail:"..err)
|
||||
break
|
||||
end
|
||||
print("Going to echo 256 bytes") -- 它不会塞满你的stdout了,因为它在上面两个操作确实完成的时候才输出
|
||||
end
|
||||
````
|
||||
|
||||
发现了吗?最后一个版本几乎和同步代码一模一样:
|
||||
````lua
|
||||
-- 也是乱写的伪代码,不过确实可以在Lua里实现
|
||||
|
||||
local file = io.open("./echo.txt", "a+")
|
||||
|
||||
while true do -- 这次逻辑上是没错的
|
||||
local status, blk = file:read(256)
|
||||
if not status then
|
||||
break
|
||||
end
|
||||
local status, err = file:write(blk)
|
||||
if not status then
|
||||
print("fail:"..err)
|
||||
break
|
||||
end
|
||||
print("Going to echo 256 bytes") -- 它不会塞满你的stdout了,因为它在上面两个操作确实完成的时候才输出
|
||||
end
|
||||
````
|
||||
|
||||
但是问题在于:它在什么地方“运行别的代码”呢?答案就是里面的`pawait(xxx)`。把整段代码看作一个协同式线程,这个线程将在`pawait`的时候让出,在里面的操作`xxx`完成之后返回值、继续运行这个线程。在线程让出的时候就可以运行别的线程。
|
||||
|
||||
进行I/O的过程可以被分为两个事件:请求I/O操作、I/O操作完成。但是事件驱动的代码并不不好写:事件带有上下文,显式处理上下文会很麻烦。通过线程,我们可以在保存上下文的同时利用这段空白时间执行别的代码。要达到这个目的只需要协同式线程,尽管使用线程会对性能带来一些负面影响,但是我相信你并不想用那么多的回调或者Promise。
|
||||
|
||||
````
|
||||
Thread0: [I/O请求] ---------阻塞-----------------> [I/O响应]
|
||||
v 排程 ^ 排程
|
||||
Thread1: [I/O请求] --------阻塞------------------> [I/O响应]
|
||||
v 排程 ^ 排程
|
||||
Thread2: [I/O请求] --------阻塞------------------> [I/O响应]
|
||||
````
|
||||
|
||||
这里的线程经常使用用户空间线程。由于一些限制,很多实现只在一个系统线程中运行所有的用户空间线程,在应对I/O密集的的环境时不会有很大影响。但我们总不希望“一核有难,N核围观”……
|
||||
|
||||
### 混合线程
|
||||
我们可以在多个系统线程中运行用户空间线程。[Go](https://go.dev)、[Rust的Tokio](https://tokio.rs)和[Kotlin的Coroutine](https://kotlinlang.org/docs/coroutines-overview.html)就采取了这种方法。这种方法让用户空间的协同式线程可以并行执行,更快地处理I/O密集之余的计算部分。
|
||||
|
||||
简单地说,这些实现会维护一个线程池——线程的数量通常根据CPU的核心数确定——来运行用户空间线程。但是需要注意,虽然现在用户空间线程可以并行运行,但它们还是协同式线程:只有在显式或隐式让出时才挂起。如果你有一个用户空间线程一直活跃,它不会挂起并且一直占用你线程池的一个线程。许多实现提供了手动让出的方法,你可以使用这些方法显式让出。
|
||||
|
||||
特别值得注意的是自旋锁——自旋锁不会让你的线程休息,你必须要确保自旋锁不会长时间卡在那。但是你可以用别的锁,而且这些实现一般都会提供合适的锁,开销会比自旋锁略微大一些。
|
||||
|
||||
因为现在你的线程可以并行运行了,你还可以考虑更多地使用基于消息传递的并发模型,比如说Actor模型:
|
||||
|
||||
{% wikipedia title:Actor_model wikiButton:true %}
|
||||
|
||||
## 扩展阅读
|
||||
- [Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue Algorithms. Michael & Scott (1996)](https://www.cs.rochester.edu/~scott/papers/1996_PODC_queues.pdf)
|
||||
- [The Linux Kernel Documentation](https://docs.kernel.org)
|
||||
- [Design Overview - libuv documentation](http://docs.libuv.org/en/v1.x/design.html)
|
||||
- [Async in depth | Tokio - An asynchronous Rust runtime](https://tokio.rs/tokio/tutorial/async)
|
||||
- [Dart asynchronous programming: Isolates and event loops](https://medium.com/dartlang/dart-asynchronous-programming-isolates-and-event-loops-bffc3e296a6a)
|
||||
- [How protothread really work](http://dunkels.com/adam/pt/expansion.html)
|
Binary file not shown.
|
@ -1,136 +0,0 @@
|
|||
---
|
||||
title: 2021年终总结:书、游戏和Kache的进展
|
||||
date: 2022-01-18
|
||||
updated: 2022-01-18
|
||||
tags:
|
||||
- 年终总结
|
||||
---
|
||||
|
||||
又过了一年。受到Mastodon时间线上的博主们启发,我想写一篇博文总结一下一年来做的事情。但是我发现我不知道该怎么组织起这样一篇博文。想了想,决定以今年看过的书、玩过的游戏和一些项目为脉络,聊聊今年的感想。减少尴尬,先以小作文经典开头问声好。
|
||||
|
||||
> 人的脆弱和坚强都超乎自己的想象。有时,我们可能脆弱得一句话就泪流满面;有时,也发现自己咬着牙走了很长的路。 —— 莫泊桑《一生》
|
||||
|
||||
<!--more-->
|
||||
|
||||
## 书
|
||||
|
||||
2021年其实可以说是我精神状态最差的一年。问题大概来源于无根浮萍的那种漂浮感——往大了说,政治、经济形势的恶化;往小了说,未来发展和学业……但这只是环境上的变化。而个人因素大概就是因为我就是那么个人吧。这一年,我读书的重点大概在于理解我的恐惧和我的社会。从端传媒两本关于香港的会员赠书、《香港第一课》到哈耶克的《个人主义与经济秩序》、《理性之谜》、两本关于美国政治体制的科普书,它们从不同角度展示社会图景。而《最好的告别》是我直面我所恐惧之一的机会:直面这从未有人回头过的旅途。
|
||||
|
||||
2021年我还看了不少书,但能让我挤出一些感想的大概也就这些。
|
||||
|
||||
### 《黑暗时代,在香港点灯的人》&《香港喺度》
|
||||
|
||||
这两本是端传媒的会员赠书。端传媒先后免费赠过几本关于香港的书,都让我印象深刻。它们为我脑海中平面的香港多画了几笔阴影,有了更多的景深和面向。最让我印象深刻的是《香港喺度》中有一篇文章讲述逃离城市到离岛上的故事,我觉得有些居于宁静、繁华不远的暧昧感觉。
|
||||
|
||||
### 《香港第一课》梁启智
|
||||
|
||||
这一本也是关于香港的书,而且是一门关于“香港问题”的书。这本书用更客观的视角(相比中国官媒和我们自己)询问了一些“香港问题”并给出一种回答。它并非香港问题的唯一答案,但是确实是了解香港问题和民主制度不能避开的小册子。
|
||||
|
||||
### 《打开一颗心》斯蒂芬·韦斯塔比
|
||||
|
||||
奇幻的现实旅程。韦斯塔比是一位英国著名的“心脏外科”医生,这本书讲述的就是他从医经历中的一些片段:“打开一颗心”。书中提到了很多奇妙的治疗方案,我印象最深刻的就是人工心脏:使用人工心脏的人没有脉搏但还是可以正常生活,“让我感到了科技的进步”。
|
||||
|
||||
### 《个人主义与经济秩序》Friedrich August von Hayek
|
||||
|
||||
哈耶克的观点可谓超越时代。有些人可能会将哈耶克的观点简单归类为“新自由主义”,然而我觉得这并不准确。我认为,哈耶克既不坚持古典自由主义,也并不提倡所谓“新自由主义”。在这本书里,我看到了他对“个人主义”研究导向的追求。
|
||||
|
||||
### 《理性之谜》雨果·梅西耶和丹·斯珀伯
|
||||
|
||||
这本书以心理学的角度综述了一个我们尚未完全理解的事实:个体理性工具的不可靠。很有趣的是:哈耶克很早就在上面的《个人主义与经济秩序》中提出了类似的问题,这就是为什么我说哈耶克的观点可以说超越时代。
|
||||
|
||||
### 《美国最高法院》(牛津通识读本)琳达•格林豪斯
|
||||
|
||||
一本一百多页的小册子,包含了美国最高法院的历史、运转甚至宪法的工作方式。给我留下了一些关于法律的问题和思考:法律究竟如何运作?为何运作?怎样运作更好?
|
||||
|
||||
### 《言论的边界:美国宪法第一修正案简史》安东尼·刘易斯
|
||||
|
||||
简介:“本书作者以理性客观的视角和深入浅出的文笔,向读者介绍了美国宪法第一修正案产生的历史背景,及其对美国社会的过去、现在和可预计的将来所产生的深刻影响。”这简介远远无法包含这本小书的内容。“言论的边界”在实践中如此复杂,我们尚未有万能的解决之道。中国当下的情况,在美国历史上早有影子。
|
||||
|
||||
### 《破云》《破云2:吞海》淮上
|
||||
|
||||
《破云》系列的故事背景是很少见的缉毒,单是题材就令人感到很新鲜。《破云》的反派描写实在是出彩,那句“六亿美金……你看,尘世的快乐就是如此值钱”实在是让我记忆犹新。《破云2:吞海》和《破云》的节奏、套路相似,但不是简单的换皮。
|
||||
把这两本书写在这里是想感慨一下这个作者的作品好几部都有一些想探索的议题,比如更早的《银河帝国之刃》甚至涉及到了民主和独裁的话题。《破云2:吞海》涉及到了自由的话题,不过我觉得作者对于自由的理解写得稍微有点浅薄,单独放在最后一章的时候其实力度有点不够。
|
||||
|
||||
### 《最好的告别:关于衰老和死亡,你必须知道的常识》Atul Gawande和彭小华
|
||||
|
||||
“如果说人生是一场盛大的史诗,那么这一定来自于既定的出生与既定的死亡。”面对与世界的告别,是所有人不得不学习却又终究觉得差一些的事情。作为仍有时间的人,我们所能做的也就是“偶尔治愈”“常常帮助”“总是安慰”,像这本书的主题一样,停下来思考:怎样让这种告别更有尊严一些?
|
||||
|
||||
若不是人们害怕那遥远的风之旅国,害怕未知的远方……我们也只是车站上的过客。伊甸园外,唯有故事隽永流芳。
|
||||
|
||||
## 文章和演讲
|
||||
今年让我印象深刻主要有演讲《User level threads.. with threads》、文章《Go Does Not Need a Java Style GC》《Locking in Webkit》。前两篇是今年我性能观念大转变的来源,后面一篇是因为这个锁的实现真的很有意思。
|
||||
|
||||
### User level threads.. with threads, Paul Turner
|
||||
|
||||
系统线程跟高性能I/O并没有冲突。这个演讲的主要内容就是:上下文切换时的开销主要来自计算下一个要排期的线程,而不是陷入ring0的开销,他们做到低切换开销的方法。在前面介绍了几种并行编程的常用模型。
|
||||
|
||||
P.S. 得益于Linux Kernel默认打开的Overcommit,创建线程的内存成本并不高。
|
||||
|
||||
[互联网博物馆的Slides存档](https://web.archive.org/web/20210212205254/http://pdxplumbers.osuosl.org/2013/ocw//system/presentations/1653/original/LPC%20-%20User%20Threading.pdf)
|
||||
|
||||
### Go Does Not Need a Java Style GC, Erik Engheim
|
||||
|
||||
Go同时拥有超高的性能和……由GC驱动的自动内存管理。但是在Java看来,这完全不可能。但现实就是如此奇妙,Go通过支持“值”加上Escape Analysis来自动确定值是否需要放在堆上,大幅降低GC的压力,使得Go不需要一个像Java那种运行得很快的GC,反而可以使用传统的标记-清除算法,这种传统算法的优势是没有“Stop The World!”修改指针的过程,可以并行运行。
|
||||
Java所需的“现代”“高速”GC反而会成为并行运行的性能瓶颈(在Android上一样适用!),为了保证locality和降低开销,Oracle JVM使用了分代标记-清除-合并算法。其它JVM实现也有些会选择类似的方法,因为这个算法可以说是GC领域里最“好”的一个。但是,这个算法的致命缺陷在于:在升代或是合并时需要移动值的位置,在移动值的过程中需要修改指针。而唯一能又快又安全地修改指针的前提条件就是让程序停止,Stop The World!
|
||||
|
||||
[Erik Engheim: Go Does Not Need a Java Style GC](https://erik-engheim.medium.com/go-does-not-need-a-java-style-gc-ac99b8d26c60)
|
||||
|
||||
### Locking in Webkit, Filip Pizlo
|
||||
|
||||
这篇文章介绍了Webkit使用的用户空间锁,相对于庞大的OS Mutex(至少64 bytes)来说,Webkit使用的这种锁大小不到1 byte。它通过一个在进程中共享的并发HashMap实现等待队列,叫做Parkinglot(停车场)。
|
||||
|
||||
Webkit将这个锁实现成自适应情况的:刚开始时表现得像SpinLock,过一段时间后用system call将自己放到系统的schedule队列末尾。开发者不用考虑到底该用SpinLock还是OS Mutex,最终性能非常漂亮:WebKit的JavaScript跑分增加了5%。
|
||||
|
||||
我写过一个在Zig里的简单实现: https://gist.github.com/thislight/e9c7335932a7139910c441d2770c33c5
|
||||
|
||||
[Webkit Blog: Locking in Webkit](https://webkit.org/blog/6161/locking-in-webkit/)
|
||||
|
||||
## 游戏
|
||||
|
||||
其实今年我玩了十多款游戏,但是让我有些想法想分享的游戏也就寥寥:将网状叙事、非线性流程、开放世界拉到极致的《辐射:新维加斯》、《地铁》游戏新作《离乡》。
|
||||
|
||||
### 《辐射:新维加斯》
|
||||
|
||||
{% steamgame 22380 %}
|
||||
|
||||
迄今为止无人超越的,最有野心也最令人感到遗憾的RPG(Role-Playing Game)作品。很多人都说GTA系列(3、4、5)或《塞尔达:旷野之息》是开放世界神作,但对我来说《辐射:新维加斯》才是真正的“开放世界”:在大多数开放世界作品中你只是体验到开放的表象,但在《辐射:新维加斯》中,**你能改变世界**。《辐射:新维加斯》的故事构建和网状叙事至今无人超越,这款游戏真正地踏入了那片没有敌手的黄沙。
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/gzF7aHxk4Y4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
跟优秀的游戏设计相对的,就是非常恐怖的bug数量,这款游戏的bug修到结束支持都没修完。我在玩之前按照[Viva New Vegas](https://vivanewvegas.github.io/)的列表打了一遍MOD,这个modlist里面已经包含了很多由玩家社区提供的修复和改进,至少bug基本都修好了。
|
||||
|
||||
愿那狂野的灵魂,在宁静的黄沙中安息。
|
||||
|
||||
### 《地铁:离乡》
|
||||
|
||||
{% steamgame 412020 %}
|
||||
|
||||
《地铁》系列与《辐射》一样,使用后末世启示录作为背景,但少几分黑色幽默,多几分认真。作为《地铁》系列游戏的第三部,《地铁:离乡》在系统和玩法上已经非常成熟,唯一改变的是从过去的封闭场景线性流程变为沙盒式世界半开放流程。我个人很喜欢这种改变,特别是这种改变也意味剧情背景的改变——从地铁隧道里走出来,走出莫斯科,火车行驶于俄罗斯大地,寻找宜居之地。大概《离乡(Exodus)》的副标题就是来自于类似的犹大神话。
|
||||
|
||||
## Kache和Rope的进展
|
||||
|
||||
过去一年我重新设计了4次Kache,终于在去年下半年正式开始实现。其网络层Rope的设计也曾改过几次,最终放弃使用ZeroMQ/libnng和Zig。前者是因为缺少对加密和UDP的支持,虽然libnng支持自定义Transport,但是我们本来大多数时候都要绕过它们定义的协议,使用专门点对点的socket来实现我们自己的链路管理。我们要取的只是它们基于消息的传输协议和跨平台的网络I/O,自定义transport显然完全丢弃了这两点。后者是因为作为一门语言,它每个版本间的改动仍然太多、太大,语言复杂使得编译器bug很多、修得慢,作为写一个要用的东西的语言不合适。最后我为Rope选择了Rust,正好Cloudflare正在维护一个Wireguard实现叫boringtun。
|
||||
|
||||
Rope在Wireguard之上运行Rope Protocol而不是Internet Protocol,RP是一个基于消息的协议,并且可以带有控制消息。相对于直接使用Internet Protocol再在它上弄一个额外的协议,我更倾向于使用一个集成的协议。
|
||||
|
||||
## 新项目:Mooncake
|
||||
|
||||

|
||||
|
||||
Go通过值类型和Escape Analysis显著降低了标记-清除系GC算法对高并发I/O的影响。Mooncake最初的主旨就是为Lisp引入值类型和Escape Analysis,降低GC对I/O的影响。个人认为CLOS这种在Lisp的括号里面实现起来太憋屈,所以我觉得Mooncake的类型系统类似Haskell和Rust会更好。
|
||||
|
||||
````lisp
|
||||
(in-package :my-hello-server)
|
||||
(@import :std/http h)
|
||||
(dconst math (@import :std/math))
|
||||
|
||||
(dfn (handler (* h::Request) h::Response) (_request)
|
||||
(if (> (math::random) 0.5)
|
||||
(h::respond "Hello, World") ;; Actually call ((std/http::ResponseSource String)::respond std/core::String std/http::Response)
|
||||
(h::respond (h::error 400 Bad Request"))))
|
||||
|
||||
(http::quick-serve 8080 handler)
|
||||
;; (http::quick-serve 8080 (\ (_request) (h::respond "Hello, Word")))
|
||||
````
|
||||
|
||||
当然,这还只是画饼而已……现在还在写完括号解析器正在写LLVM binding的状态。
|
BIN
source/_posts/yearly-report-2021/mooncake_logo.png
(Stored with Git LFS)
BIN
source/_posts/yearly-report-2021/mooncake_logo.png
(Stored with Git LFS)
Binary file not shown.
BIN
source/img/hexo-topic-feeds/site-feeds.png
(Stored with Git LFS)
BIN
source/img/hexo-topic-feeds/site-feeds.png
(Stored with Git LFS)
Binary file not shown.
BIN
source/img/hexo-topic-feeds/topic-feeds.png
(Stored with Git LFS)
BIN
source/img/hexo-topic-feeds/topic-feeds.png
(Stored with Git LFS)
Binary file not shown.
BIN
source/img/makru-tutor/2/screenshot-download-makru-langc-source.png
(Stored with Git LFS)
BIN
source/img/makru-tutor/2/screenshot-download-makru-langc-source.png
(Stored with Git LFS)
Binary file not shown.
BIN
source/img/measure-peer-reachability/multiple-transports-to-peer.png
(Stored with Git LFS)
BIN
source/img/measure-peer-reachability/multiple-transports-to-peer.png
(Stored with Git LFS)
Binary file not shown.
BIN
source/img/measure-peer-reachability/you-mudy-sound.png
(Stored with Git LFS)
BIN
source/img/measure-peer-reachability/you-mudy-sound.png
(Stored with Git LFS)
Binary file not shown.
BIN
source/img/measure-peer-reachability/you-mudy-sun.png
(Stored with Git LFS)
BIN
source/img/measure-peer-reachability/you-mudy-sun.png
(Stored with Git LFS)
Binary file not shown.
BIN
source/img/set-up-sudo-with-howdy-on-fedora-34-for-faical-authenticating/vlc-open-capture-device.png
(Stored with Git LFS)
BIN
source/img/set-up-sudo-with-howdy-on-fedora-34-for-faical-authenticating/vlc-open-capture-device.png
(Stored with Git LFS)
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue