把域名托管到 CloudXNS 了,完美支持 let's encrypt

lengzzz.com 这个域名我已经用了6年了,当年年少无知用了 oray 的服务,就为了一个花生壳的功能(tplink自带花生壳)。现在越发感觉 oray 的服务质量不很好。各省解析速度不一,国外就更慢了。尤其是不支持 let's encrypt 让我好生郁闷。所以赶紧换上了 CloudXNS 。

CloudXNS 的页面就看着比较顺眼。据说 DNSPod 也不错,但是我感觉网页太丑了,就没有使用它,哈哈,所以证明一句话 你的颜值决定了别人会不会去发现你的内心

QQ20160501-0@2x.png-321.5kB

注册了之后发现使用超级方便,只给你显示了一个超大的输入框,让你输入自己的域名。

QQ20160501-1@2x.png-43.7kB

输入之后,就进入了控制台节目了,好像你的域名已经托管在 CloudXNS 上了一样,当你设置好解析之后,只需要把之前域名的 NS 地址设置到 CloudXNS 上就好了。整个用户体验相当舒服。

设置好之后,控制台上会实时显示接管状态。不过5分钟,就可以用了。立即用 let’s encrypt 更新一下 ssl 证书。果然可以用了。

QQ20160501-2@2x.png-70.3kB

最后来张测速图吧。

QQ20160501-3@2x.png-138.9kB

最近读的几本书

上月做了个决定,上下班地铁上只看书不玩手机。今天回首一看,已经读了 6 本了。所以说把一件微小的事情坚持下来,它就不微小了。这几天看的书类目很杂,有技术的,有天文的,还有科普的。先把书单列一下,然后简单写个短评。

![D6D0015D8454E99C7DB8AD66A4B33974.jpg-2072.9kB][1]

最近看的书单如下,挨个写个短评吧。

  • 《微服务架构与实现》
  • 《夜观星空:天文观测实践指南》
  • 《七周七并发模型》
  • 《时间简史》
  • 《果壳中的宇宙》
  • 《通俗天文学:和大师一起与宇宙对话》

微服务架构与实现

讲的比较泛泛,不过里面讲解微服务的优点和解决的问题这部分还是比较不错的。如果可以用一个具体的实践来讲解就更好了。看完这本书之后,想去具体实践一下微服务,但是中途遇到很多细节的问题,不知道怎样做才算是最佳实践。

夜观星空:天文观测实践指南

很不错,从实践角度讲解了如何观测星座、行星、月亮、太阳。讲解了很多新手 “后院天文学家” 不知道的小技巧。利用这些小技巧可以很快入门天文学,建立对星空的兴趣与热爱。不过不足之处也很明显,太过注重实践,很多天文学的理论知识没有讲解(术语,计算方式)。导致想要深一步理解原理需要自己 wiki 。

七周七并发模型

超赞的一本书,干货很翔实。除了第一章的《线程与锁》以外,其他章讲解的内容都是我不熟悉的并发开发方式。讲解了各种并发模型解决的问题,以及实现方式。5星推荐。

时间简史

科普书籍吧,缺点很明显,没有公式。有时候讲解一个理论不是单靠图片和文字就能让人理解的。很多时候,使用比喻可能会让人理解起来更加迷茫。如果能对空间弯曲的理论进行一下数学推导就好了,真的好想了解一下黎曼数学的理论。不知道有没有其他什么比较好的科普书籍讲这部分的,最好能以通俗易懂的方式讲一下相对论的推导过程。

果壳中的宇宙

和上一本差不多吧,讲故事有些多。真正的干货看不到。毛病还是一样,不恰当的比喻让人更佳费解。

通俗天文学:和大师一起与宇宙对话

很好,首先是讲解了天文学的基础理论。天球坐标系统、天体运动的表现。之后挨个讲解太阳系的各个天体。理论和实践兼顾,不错。

在 OS X 的命令行下使用代理

相信程序员们工作中必不可少的就是一个趁手的代理。但是 Mac 客户端只提供了系统代理(供 app 调用的)和一个裸的 socks5 代理。都不方便在命令行工具中使用。下面介绍一种方法可以方便的在命令行中使用代理的方法。

首先介绍一个常识,大多数 gnu unix 程序都会访问一个环境变量 http_proxy ,比如 curl 在执行时会先访问一下 http_proxy , 如果有这个变量的话,会使用这个变量给出的地址作为 proxy 。这相当于是 gnu 给我们带来的一个福利^proxysetting

但是这个方式只支持 http、https、ftp 和 rsync 协议,不过大多数情况下也够用了,因为在命令行下大多是执行 npmgo get 才会用到代理,而这些程序打多使用 http 协议。

那么,具体思路就是使用一个转换器,把 socks5 代理转换成 http 代理,然后使用 http_proxy 环境变量。

polipo 可以做代理转换器:

1
2
# 安装polipo
brew install polipo

为了方便我们使用它,可以在 ~/.bash_profile 里加一个 alias:

1
2
3
# ~/.bash_profile

alias startPolipo='polipo socksParentProxy=localhost:1080 proxyAddress=0.0.0.0'

这样,在使用之前,需要先开一个新的 term 窗口,执行一下 startPolipo 开启一个代理转换器。可以看到 polipo 监听了 8123 端口。

之后,在需要代理的程序前面加上 http_proxy=http://localhost:8123 https_proxy=http://localhost:8123 就可以让程序使用代理了。

另外,为了更加方便,我们可以再搞一个 alise:

1
alias proxy='env http_proxy=http://localhost:8123 https_proxy=http://localhost:8123'

只需要每次在需要使用 proxy 的命令前面加上 proxy 就可以使用代理了。例如:

1
proxy curl https://google.com/

可以看见 curl 打印了 Google 首页出来

await & async 深度剖析

await 语法最早被引入到流行语言是 c# 5.0 ,微软在 c# 中添加了 await & async 关键字和一系列配套 API ,引起了开发者的一致好评。await 可以极大的简化异步编程,而使用异步编程最多的就是 javascript 了。所以开发者们也迫不及待的向 javascript 中加入 await 关键字。目前此特性已经在 babel 中比较完美的实现了。而 await 特性也被加入了 stage-3 (Candidate),不过貌似是赶不上今年的 ES2016 了,估计最晚会在 ES2017 中被正式加入 javascript 。那么本文就来深度剖析一下 await & async 的用法、好处以及实现方式。

[toc]

异步?同步?

异步编程模型对于 IO 密集型的任务具有得天独厚的优势。这里用一个例子来解释。

假如我们需要做一个爬虫,爬到的东西有两种,一种是索引页,一种是内容页。大概需要以下几步:

  • 使用 http 请求一个索引页,从这个从索引中取出所有的 URL
  • 分别请求所有的 URL
  • 如果请求到的还是 索引页 那么递归这个操作(回到步骤 1 )
  • 如果是 内容页 ,那么我们对内容页做一些处理
  • 最后,把处理后的保存到数据库

那么,分别使用原生支持异步编程的 NodeJS 和原生不支持异步编程 golang 的语言实现这个爬虫。分别使用两种语言 最常见 的实现方式。

因为发 http 请求和保存数据库还需要一些额外代码,会干扰视线,所以例子代码是递归的读一个文件夹,并把所有 html 文件做一些修改,然后保存到源文件。但是原理上和爬虫是相通的。

golang 实现爬虫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package main

import (
"io/ioutil"
"log"
"path"
"path/filepath"
"strings"
)

const extname = ".html"

var counter = 0

func handleDir(dir string) {
// 读取文件夹列表
files, err := ioutil.ReadDir(dir)
if err != nil {
log.Println("error occurs when readDir", dir, err)
return
}
// iterate 文件列表
for _, file := range files {
fullFilename := path.Join(dir, file.Name())
// 判断文件类型
if !file.IsDir() && filepath.Ext(file.Name()) == extname {
counter++
thisCount := counter
// 打开始 log
log.Println("start processing", fullFilename,
"[", thisCount, "]")

// 读取文件
fileData, err := ioutil.ReadFile(fullFilename)
if err != nil {
log.Println("error occurs when processing",
fullFilename, err)
continue
}

// 做一些处理
fileString := string(fileData)
fileString = strings.Replace(fileString,
"http://", "https://", -1)

// 保存文件
if err := ioutil.WriteFile(fullFilename,
[]byte(fileString), 0644); err != nil {
log.Println("error occurs when processing",
fullFilename, err)
continue
}

// 打结束 log
log.Println("finish processing", fullFilename,
"[", thisCount, "]")
} else if file.IsDir() {
// 文件夹的话递归
handleDir(fullFilename)
}
}
}

func main() {
handleDir("/Users/zzz/hzzz.lengzzz.com/")
}

NodeJS 实现爬虫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import fs from 'fs';
import path from 'path';

let counter = 0;
const extname = '.html';
function handleDir(dir) {
// 读取文件夹列表
fs.readdir(dir, function (err, files) {
// iterate 文件列表
files.map(file => {
let fullFilename = path.join(dir, file);
fs.stat(fullFilename, function (err, stats) {
if (err) {
console.error("error occurs when processing", file, err);
return;
}
// 判断文件类型
if (stats.isFile() && path.extname(file) == extname) {
let thisCount = counter++;

// 打开始 log
console.log('start processing', fullFilename,
'[', thisCount, ']');

// 读取文件
fs.readFile(fullFilename, 'utf-8',
function (err, fileString) {
if (err) {
console.error("error occurs when processing",
file, err);
return;
}

// 做一些处理
fileString = fileString.replace(
/http:\/\//g, 'https://');

// 写入文件
fs.writeFile(fullFilename, fileString, function (err) {
if (err) {
console.error("error occurs when processing",
file, err);
return;
}

// 打结束 log
console.log('finish processing', fullFilename,
'[', thisCount, ']');
})
})

} else if (stats.isDirectory()) {
handleDir(fullFilename);
}
})

})
});
}

function main() {
handleDir('/Users/zzz/hzzz.lengzzz.com/');
}

main();

公平起见 使用 goroutine 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package main

import (
"io/ioutil"
"log"
"path"
"path/filepath"
"strings"
)

const channelBuffer = 50
const workerCount = 30

type payload struct {
thisCount int
fullFilename string
fileString string
}

var producerToRead chan *payload
var readToReplace chan *payload
var replaceToWrite chan *payload
var writeToComplete chan *payload

func init() {
producerToRead = make(chan *payload, channelBuffer)
readToReplace = make(chan *payload, channelBuffer)
replaceToWrite = make(chan *payload, channelBuffer)
writeToComplete = make(chan *payload, channelBuffer)
}

func reader() {
for data := range producerToRead {
fileData, err := ioutil.ReadFile(data.fullFilename)
if err != nil {
log.Println("error occurs when processing",
data.fullFilename, err)
continue
}
data.fileString = string(fileData)

readToReplace <- data
}
}

func replacer() {
for data := range readToReplace {
data.fileString = strings.Replace(data.fileString,
"http://", "https://", -1)
replaceToWrite <- data
}
}

func writeer() {
for data := range replaceToWrite {

if err := ioutil.WriteFile(data.fullFilename,
[]byte(data.fileString), 0644); err != nil {
log.Println("error occurs when processing",
data.fullFilename, err)
return
}
writeToComplete <- data
}
}

func complete() {
for data := range writeToComplete {
log.Println("finish processing", data.fullFilename,
"[", data.thisCount, "]")
}
}

var counter = 0

func producer(dir string) {
const extname = ".html"

files, err := ioutil.ReadDir(dir)
if err != nil {
log.Println("error occurs when readDir", dir, err)
return
}

for _, file := range files {
fullFilename := path.Join(dir, file.Name())
if !file.IsDir() && filepath.Ext(file.Name()) == extname {
counter++
thisCount := counter
log.Println("start processing", fullFilename,
"[", thisCount, "]")

producerToRead <- &payload{
thisCount,
fullFilename,
"",
}

} else if file.IsDir() {
producer(fullFilename)
}
}
}

// 搞四个 worker
func startWorker() {
for i := 0; i < workerCount; i++ {
go reader()
go replacer()
go writeer()
go complete()
}
}

func main() {
startWorker()
producer("/Users/zzz/hzzz.lengzzz.com/")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
Title: golang worker 实现

Note over producer: 生成文件名
producer->reader: channel
Note over reader: 读文件
reader->replacer: channel
Note over replacer: 修改文件
replacer->writer: channel
Note over writer: 写文件
writer->complete: channel
Note over complete: 打 log

Note over producer,complete: 并发

性能比较

分别看一下两个程序打出的 log ,来分析一下哪个程序会运行的更快。

golang 的 log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
start processing /1001/index.html [ 1 ]
finish processing /1001/index.html [ 1 ]
start processing /1001/trackback/index.html [ 2 ]
finish processing /1001/trackback/index.html [ 2 ]
start processing /1006/index.html [ 3 ]
finish processing /1006/index.html [ 3 ]
start processing /1006/trackback/index.html [ 4 ]
finish processing /1006/trackback/index.html [ 4 ]
start processing /101/index.html [ 5 ]
finish processing /101/index.html [ 5 ]
start processing /101/trackback/index.html [ 6 ]
finish processing /101/trackback/index.html [ 6 ]
start processing /1010/index.html [ 7 ]
finish processing /1010/index.html [ 7 ]
start processing /1010/trackback/index.html [ 8 ]
finish processing /1010/trackback/index.html [ 8 ]
start processing /1027/index.html [ 9 ]
finish processing /1027/index.html [ 9 ]
start processing /1027/trackback/index.html [ 10 ]
finish processing /1027/trackback/index.html [ 10 ]

特点是挨个抓取,第一个没有抓完不开始抓第二个。

NodeJS 的 log

1
2
3
4
5
6
7
8
9
10
11
12
start processing /index.html [ 0 ]
start processing /blog/index.html [ 1 ]
start processing /blog/wp-login.html [ 2 ]
start processing /blog/1001/index.html [ 3 ]
start processing /blog/1006/index.html [ 4 ]
start processing /blog/101/index.html [ 5 ]

... 省略

start processing /blog/page/9/index.html [ 736 ]
finish processing /index.html [ 0 ]
start processing /blog/date/2013/08/index.html [ 737 ]

特点是同时抓取网页,谁先抓完先处理谁,谁先处理完谁就保存。不需要等待。

golang 第二版的 log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
start processing /blog/1001/index.html [ 1 ]
start processing /blog/1001/trackback/index.html [ 2 ]
start processing /blog/1006/index.html [ 3 ]
start processing /blog/1006/trackback/index.html [ 4 ]
start processing /blog/101/index.html [ 5 ]
start processing /blog/101/trackback/index.html [ 6 ]
start processing /blog/1010/index.html [ 7 ]
start processing /blog/1010/trackback/index.html [ 8 ]
start processing /blog/1027/index.html [ 9 ]
start processing /blog/1027/trackback/index.html [ 10 ]
start processing /blog/1030/index.html [ 11 ]
start processing /blog/1030/trackback/index.html [ 12 ]
start processing /blog/1038/index.html [ 13 ]
start processing /blog/1038/trackback/index.html [ 14 ]
finish processing /blog/101/trackback/index.html [ 6 ]
start processing /blog/1040/index.html [ 15 ]

KFC 和 麻辣烫

可以对比一下 KFC 和 麻辣烫 店的点餐方式,和上面两个程序有异曲同工之妙。

  • KFC:大家排队,前面的餐没全部做出来前不服务下一个客户
  • 麻辣烫:大家分别点餐,然后拿个号码,叫号取餐

谁更快显而易见。在 KFC 里最怕前面点个全家桶,好不容易排到第一个了,结果还不如旁边队列里最后的取餐快。

所以可见,NodeJS 只需要使用一般的写法就能自动获得 性能加成 还是很有吸引力的。

可读性比较

golang 的代码是按照先后顺序很直观的顺下来的,可以看一下几个注释,都在同一个缩进级别。

而 NodeJS 使用了大量回调函数,本身串行的逻辑看起来却像是内嵌的感觉,从代码的缩进就能明显的看出来。大家亲切的成这种代码风格叫做 冲击波

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 冲击波
{
{
{
{
{
{
{
// ============ >
}
}
}
}
}
}
}

golang 使用 goroutine 重新实现之后性能得到了提升,但是可读性也是降低了一些。如果逻辑比较复杂则 channel 不很好设计。

除此之外 NodeJS 的写法还有几个坑:

  • 对循环支持的不好,循环内部的回调函数访问同一个闭包变量(ES2015 的 let 可解决这个问题)
  • 错误处理不友好,只能( almostly )直接跳出事务,没法爽快 try catch (例子:超时重传)

同步的代码 异步的事情

那么有没有一种方法,能同时具备两种写法的优点呢?答案就是前端终极武器 await & async

首先来看一下 await 的使用方法,我给出一个同样功能(爬虫)的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import path from 'path';
import { readDir, stat, readFile, writeFile } from './api_promise';

let counter = 0;
const extname = '.html';
async function handleDir(dir) {
try {
// 读取文件夹列表
let files = await readDir(dir);
// iterate 列表
files.map(async file => {
let fullFilename = path.join(dir, file);
try {
// 检查文件类型
let stats = await stat(fullFilename);
if (stats.isFile() && path.extname(file) == extname) {
let thisCount = counter++;
// 打开始 log
console.log('start processing', fullFilename,
'[', thisCount, ']');

// 读文件
let fileString = await readFile(fullFilename, 'utf-8');

// 改文件
fileString = fileString.replace(/http:\/\//g, 'https://');

// 写文件
await writeFile(fullFilename, fileString);

// 打结束 log
console.log('finish processing', fullFilename,
'[', thisCount, ']');
} else if (stats.isDirectory()) {
handleDir(fullFilename);
}
} catch (err) {
console.error("error occurs when processing", file, err);
}
});

} catch (err) {
console.error("error occurs when readDir", dir, err);
}
}

function main() {
handleDir('/Users/zzz/hzzz.lengzzz.com/');
}

main();

代码清晰了不少,但是性能还和之前一样,异步的读文件,异步的写文件。另外我还加入了 try catch ,来演示 await 对 try catch 的支持。

此外,还需要做的事情是对原生的 API 进行一下包装。使之返回一个 Promise 以支持 await。

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import fs from "fs";

export function readDir(path) {
return new Promise(function (resolve, reject) {
fs.readdir(path, function (err, files) {
if (err) {
reject(err);
return;
}
resolve(files);
})
})
}

这里只举例一个了,这个文件 wrap 了四个 NodeJS API ,都是使用相同的方式包装的。

实现篇

其实,await 只是一个语法糖。下面,分析一下这颗糖底层是怎么实现的。

Iterator

要说 await 不得不先温习一些前置知识,比如 iterator 和协程。

在大部分语言中,要实现一个类型可以被 iterate (既让一个类型 iterable)一般需要实现一个叫 Iterable 的 interface。

1
2
3
4
5
class List implements Iterable {
public Iterator iterator() {
// ...
}
}

这个 Iterable 有方法能返回一个 Iterator 循环调用 Iterator 的方法 next() 可以得到下一个元素,调用 hasNext() 可以判断是否结束。

所以 Iterator 可以这样实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class List implements Iterable {
// nested class
class ListIterator implements Iterator {
int i = 0;
int max = 10;
public Object next() {
return i++;
}
public boolean hasNext() {
return i > max;
}
}
public Iterator iterator() {
return new ListIterator();
}
}

这样,就可以 iterate 一个 List 了:

1
2
3
4
List list = new List();
for (Object i : list) {
// ...
}

在 javascript 中也不例外,这样实现一个 Iterable :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var iterable = {
[Symbol.iterator]: function() {
var i = 0;
var iterator = {
next: function () {
var iteratorResult = {
done: i > 10,
value: i++
};
return iteratorResult;
}
};
return iterator;
}
};

for (let item of iterable) {
console.log(item);
}

协程

协程是一种抽象方式,可以让一个函数中途暂停返回一些东西,然后过一段时间后再继续执行。

1
2
3
4
5
6
7
8
9
function routine() 
local i = 0
i = i + 1
coroutine.yield(i)
i = i + 1
coroutine.yield(i)
i = i + 1
coroutine.yield(i)
end

调用三次 routine 后分别能得到 1 2 3 。原因是协程执行了一半被暂停后会保存下它自己的上下文,以便下次 resume后数据还在。

Generator

Generator 相当于 javascript 中的协程,在一个 Generator 函数中使用 yield 关键字,可以暂停函数执行,返回一个结果。

1
2
3
4
5
6
7
// 符号 * 代表 generator
function* routine() {
let i = 0;
yield i++;
yield i++;
yield i++;
}

这段代码和上面的 lua 代码等价。

使用 generator 函数可以方便的实现一个 iterator:

1
2
3
4
5
6
7
8
9
10
11
iterable = {
[Symbol.iterator]: function* () {
for (let i = 0; i < 10; ++i) {
yield i;
}
}
};

for (let item of iterable) {
console.log(item);
}

和上面的 iterable 代码等价,但是可以使用 for 循环了,是不是简洁多了?

await & async

可能大家已经想到了,async 函数就是被翻译成了 generator 函数,ya。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async function getArticle () {
var test = $('.test');
var comments = await getComments();
test.append('<p>' + JSON.stringify(comments) + '</p>');
var posts = await getPosts();
test.append('<p>' + JSON.stringify(posts) + '</p>');
}

// 翻译成=====>

function* getArticle () {
var test = $('.test');
var comments = yield getComments();
test.append('<p>' + JSON.stringify(comments) + '</p>');
var posts = yield getPosts();
test.append('<p>' + JSON.stringify(posts) + '</p>');
}

当调用一个 async 函数时,实际上是这样做的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
getArticle();

// 翻译成=====>

runner(getArticle);

function runner(getIterator) {
var iterator = getIterator();
function next(data) {
var result = iterator.next(data);
if (result.done){
return;
}
var promise = result.value;
promise.then(function (data) {
next(data);
});
}
next();
};

拓展篇

1. 在 NodeJS 中避免大规模计算

Javascript 只有一根线程,如果用 for 循环来计算 10000 个数字的和,整个 vm 都非卡死不行。

以前大家都用 setTimeout / setInterval 来把运算拆开来做:

1
2
3
4
5
6
7
8
9
10
11
let output = $('.power');
function run2() {
var i = 0, end = 10000;
var cancel = setInterval(function () {
let p = i * i;
output.append(`<p>${p}</p>`);
if (++i >= end) {
clearInterval(cancel);
}
}, 0);
}

现在可以用 await 了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getPower(x) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(x * x);
}, 0);
});
}
async function run() {
let output = $('.power');
for (let i = 0; i < 10000; i++) {
let p = await getPower(i);
output.append(`<p>${p}</p>`);
}
}

这两种都不会卡 vm 但是第二种显然直观一些。

代码

https://github.com/zwh8800/analyse-await

使用 lua 编写 socks5 代理

之前做过一个有趣的东西,把一部分 NodeJS 的 API 移植到了 lua 上。让我可以在 lua 里使用异步网络 API 了,我把做的这个小东西叫做 lua-libuv。今天去参加了一下 Gopher China 2016 中间听得无聊了,便打开电脑,基于自己的 lua-libuv 编写了一个 socks5 协议的 proxy。这篇博客,讲解一下 socks5 协议以及实现。

[toc]

socks5 协议介绍

socks5 协议可能是中国网友见得最多的协议了。很多著名的工具 都就会在本机的 1080 端口开启一个 socks5 proxy 服务,供浏览器来调用。我们今天就来聊一聊 socks5 协议。

socks5 协议不只是在中国流行,它其实是最常见的代理协议之一,几乎所有浏览器都会原生支持 socks5 协议,而在 Mac 上则是系统级别的支持,所以可见其流行程度仅次于 http 代理。由于 socks5 是工作在 tcp 层的协议,所以它又比 http 代理灵活很多。比如 http 代理只能用来看网页。但是使用 socks5 协议还可以上外网打游戏,也可以使用代理登陆 QQ 。但是 socks5 也有自己的短板,比如对 udp 支持的不好(socks5 把域名 resolve 的事都做了,就为了避免 client 调用 udp )

socks5 协议详解

socks5 协议非常简单,rfc1928 整个只有 9 页,我大概用了 10 分钟粗略看了一下就明白 socks5 的设计了。

端口

socks5 一般回使用 1080 端口来提供服务。

握手

socks5 协议使用四次应答式的握手建立一个链接。分别会

  • client:协商method
  • server:选用method
  • client:发起请求
  • server:处理请求并回复

四次握手结束之后,client 会把代理服务器当作是自己真正想通讯的服务器一样对待。而代理则会转发真正的通讯信息。

流程

1. 协商 method

当一个客户端需要建立一个 firewall 之外的连接时,首先向 socks5 服务器的 1080 端口发起一个 tcp 连接。

随后,客户端向服务器提供自己支持的 method 列表。数据的 payload 如下:

1
2
3
4
5
++++
|VER | NMETHODS | METHODS |
++++
| 1 | 1 | 1 to 255 |
++++

上面是字段名,下面是字段长度。

字段分别是:

  • 版本号,对于 socks5 来说始终为5
  • method 个数
  • method 列表,个数需要和第二个字段相同,不能超过255

method 的选项有如下几项,不过我感觉最常用的也就是第一个了。

  • 0x00: 无需授权
  • 0x01: GSSAPI
  • 0x02: 用户名/密码授权
  • 0x03 - 0x7f: IANA ASSIGNED
  • 0x80 - 0xfe: 私有 method
  • 0xff: 用于服务器向客户端返回不支持此 method 的错误代码

2. 选择 method

服务器接到请求之后,应当选用一种 method 返回给客户端:

1
2
3
4
5
+++
|VER | METHOD |
+++
| 1 | 1 |
+++

之后如果 method 是需要鉴权的,会进行相应的鉴权。这里不谈了。

3. 请求

之后客户端把自己想要连接的信息封成一个请求发给 proxy 。格式如下:

1
2
3
4
5
+++++++
|VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
+++++++
| 1 | 1 | X'00' | 1 | Variable | 2 |
+++++++

其中,

  • VER 还是代表版本,始终为 0x05
  • CMD 代表命令,表示建立连接还是监听连接
    • 0x01: Connect,连接其他服务器
    • 0x02: Bind,监听端口
    • 0x03: 建立udp连接
  • 保留
  • 地址类型
    • 0x01: IPv4
    • 0x03: DomainName
    • 0x04: IPv6
  • 目的地址
  • 目的端口

这里面,命令又会有三种情况。如上,udp因为没有建立连接这一步,所以监听和连接是等同的。

地址类型也是,当地址类型不同时,目的地址的长度会不一样

  • IPv4: 4 字节
  • DomainName: 目的地址的第一个字节代表域名长度
  • IPv6: 16 字节

所以需要按照 ATYP 来读取目的地址。

4. 回复

服务器收到请求后,需要向目的地址建立连接,如果失败了,需要告诉客户端原因。如果成功了,也需要告诉客户端代理服务器使用的地址和端口信息。

1
2
3
4
5
+++++++
|VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
+++++++
| 1 | 1 | X'00' | 1 | Variable | 2 |
+++++++
  • VER 还是代表版本,始终为 0x05
  • REP 代表返回值
    • 0x00: 成功
    • 0x01: socks 服务器错误
    • 0x02: 不允许访问
    • 0x03: Network unreachable
    • 0x04: Host unreachable
    • 0x05: Connection refused
    • 0x06: TTL expired
    • 0x07: Command not supported
    • 0x08: Address type not supported
  • 保留
  • 地址类型

例子

假如浏览器访问 https://lengzzz.com 需要如下对话

05 01 00

05 00

05 01 00 03(ATYP) 0b(LEN) 6c 65 6e 67 7a 7a 7a 2e 63 6f 6d(DOMAINNAME) 01 bb(PORT)

05 00 00 01(ATYP) 0a 00 00 11(IP) e9 c7(PORT)

Lua 实现

大概就是用了两个函数来 parse 出 client 的 payload ,分别 parse 协商 method 阶段的 payload 和请求阶段的 payload 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function parseMethodPayload(payload)
if payload:byte(1) ~= SocksVersion then
return nil, Errors.VersionError
end

local method = {
version = SocksVersion,
methods = {},
}

local methodCount = payload:byte(2)
method.methods = {payload:byte(3, 3 + methodCount - 1)}
return method
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function parseRequestPayload(payload)
if payload:byte(1) ~= SocksVersion then
return nil, Errors.VersionError
end

local request = {
version = SocksVersion,
command = CommandType.Connect,
addressType = AddressType.IPv4,
distAddress = '',
distPort = 0,
}

if payload:byte(2) > CommandType.Udp then
return nil, Errors.CommandTypeNotSupported
else
request.command = payload:byte(2)
end

local requestAddressType = payload:byte(4)
if requestAddressType ~= AddressType.IPv4 and
requestAddressType ~= AddressType.DomainName and
requestAddressType ~= AddressType.IPv6
then
return nil, Errors.AddressTypeNotSupported
else
request.addressType = requestAddressType
end

local portIndex
if request.addressType == AddressType.IPv4 then
local ipBytes = {payload:byte(5, 8)}
request.distAddress = table.concat(ipBytes, '.')
portIndex = 9
elseif request.addressType == AddressType.DomainName then
local len = payload:byte(5)
request.distAddress = payload:sub(6, 6 + len - 1)
portIndex = 5 + len + 1
elseif request.addressType == AddressType.IPv6 then
return nil, Errors.AddressTypeNotSupported
end

local portBytes = {payload:byte(portIndex, portIndex + 1) }
request.distPort = portBytes[1] * 256 + portBytes[2]

return request, nil
end

比较关键的就是这里,在会场写的,还是比较糙了。详细的代码看 Github 就好啦:

https://github.com/zwh8800/lua-libuv/blob/master/test/socksProxy.lua

迅雷远程下载 xware-docker 镜像

迅雷为广大路由器爱好者,nas爱好者,服务器爱好者提供了一个很好的平台--xware 远程下载。只需要很简单的就能部署到你的路由器、树莓派之类的闲置机器上,然后只需要通过网站 http://yuancheng.xunlei.com/ 就能远程提交下载任务,无论你是在公司还是外出。当你回到家里时,疲惫的打开电视,发现你想看的影片已经下载到服务器中了。简直是高清爱好者的福音。其实迅雷的 xware 部署起来已经很方便了,但是我为了更方便的启停服务就做了个 docker 镜像。

地址在这里: https://hub.docker.com/r/zwh8800/xware/

使用方法

首先,拉取我的xware for docker

1
docker pull zwh8800/xware

之后,启动它,需要指定一个 volume 挂在到 /data ,xware 所有下载的东西会保存到这个 volume 中。否则下载的东西会保存到容器中。

1
docker run --security-opt=seccomp:unconfined --name xware -v /var/lib/xware/:/data -d zwh8800/xware

之后,第一次运行 xware 需要绑定一下你的迅雷账号,执行

1
docker logs xware

会看到类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
initing...
try stopping xunlei service first...
killall: ETMDaemon: no process killed
killall: EmbedThunderManager: no process killed
killall: vod_httpserver: no process killed
setting xunlei runtime env...
port: 9000 is usable.

YOUR CONTROL PORT IS: 9000

starting xunlei service...
Connecting to 127.0.0.1:9000 (127.0.0.1:9000)
setting xunlei runtime env...
port: 9000 is usable.

YOUR CONTROL PORT IS: 9000

starting xunlei service...

getting xunlei service info...

THE ACTIVE CODE IS: vghqnv

go to http://yuancheng.xunlei.com, bind your device with the active code.
finished.

的内容,把 active code 复制一下,打开 http://yuancheng.xunlei.com 点击 我的下载器 旁边的 添加 把 active code 输入进去。

![添加][1]
![激活码][2]

然后,就可以使用了。

最后,最好在你的 /etc/rc.local 中添加上开机启动:

1
docker start xware

这样,down机重启后也会自动打开xware。

have fun

短链接生成算法

最早 twitter 推出短链接服务之后,各大互联网企业也都跟进推出了自己的短链接服务,就连我们公司最近也有了这个需求。短链接形如 http://t.cn/R7gyvR4 ,不仅好看而且能宏观上减少互联网的通讯量。这里记录下我做短链接的过程。

需求

首先短链接算法有两个基本需求:

  • 碰撞少

所以那种生产随机数,然后碰撞的算法肯定排除在外了,太过暴力,到后期碰撞绝对会相当严重。而 hash 算法可以算是正好能同时满足这两点需求,所以路线大概是怎么能改造一下现有的 hash 算法。

思路

常见 hash 算法有 md5 和 sha1 两种, md5 已经被证明不安全,很容易由 hash 值推出原始数据。但是我们这里只对 hash 算法的碰撞率感兴趣,和安全性关系不大,所以还是选用 md5 算法。

md5 算法的输出是固定的 16 个字节(byte),也就是 128 位(bit),大概有 $2^{128}$ 种情况。我们的需求是使用 6 位 大小写字母加数字 做短链接,大概可以表示 $62^6$ 种情况。是少于 md5 算法的,所以思路可以是取 md5 值的一部分,然后生成短链接。

好的,首先可以把62个字符列一张表。比如这样:

1
2
3
4
5
6
7
8
9
// 62个字符, 需要6bit做索引(2 ^ 6 = 64)
var charTable = [...]rune{
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6',
'7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
}

那么,如果一个网址的 md5 值是 0 可以在短链接中用 a 表示 ,同理用 b 代表 1 。

按照这个思路,我来举个例子。

比如 https://lengzzz.com 的 md5 值是 3CD4B16B4855CF2DBEB9051867665045 ,第一个字节是 0x3c ,十进制值为 60 那么在表中第 60 个是 Y ,那么用 Y 来表示 0x3c 。

但是这个思路还略有不妥,万一字节超了61就不行了。所以我们对字节使用 % 0x3d 取余运算,因为 0x3d 即为62,所以得到的数不会超出。

例如 0xd4 ,十进制为 212 显然超出了,但是 0xd4 % 0x3d 等于 26 ,第 26 个字符是 ‘0’ ,所以可以用 ‘0’ 来表示。

因此,我们可以把 md5 的输出分为 4 份,每份 4 个字节(byte)。4 个字节又可以分成 6 份,每份 5 位(bit)共 30 位(bit),这样正好是在使用一个 uint32 计算,效率较高。

算法

公司里是用 java 实现的,我再写一个 golang 的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package util

import (
"bytes"
"crypto/md5"
"encoding/binary"
)

// 62个字符, 需要6bit做索引(2 ^ 6 = 64)
var charTable = [...]rune{
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6',
'7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
}

func ShortenUrl(url string) []string {
shortUrlList := make([]string, 0, 4)

sumData := md5.Sum([]byte(url))
// 把md5sum分成4份, 每份4个字节
for i := 0; i < 4; i++ {
part := sumData[i*4 : i*4+4]
// 将4字节当作一个整数
partUint := binary.BigEndian.Uint32(part)

shortUrlBuffer := &bytes.Buffer{}
// 将30bit分成6份, 每份5bit
for j := 0; j < 6; j++ {
index := partUint % 62

shortUrlBuffer.WriteRune(charTable[index])
partUint = partUint >> 5
}
shortUrlList = append(shortUrlList, shortUrlBuffer.String())
}
return shortUrlList
}

注释比较完善,可以和文章对应着看。

键步如飞!高效bash快捷键

程序员们每天面对最多的就是自己的terminal了,所以一定要把terminal配置的高效起来,这样不但提高工作效率,而且令人身心舒畅。

[toc]

iTerm2

首先打开你的 iterm2 的 Preferences ,点击 profile 选项卡,再点击一下 key 子选项卡,把里面的 Left option acts as 设置成 +Esc 。这样按 option 键就不会出现奇奇怪怪的字符了。

下面,介绍几个用的比较爽的快捷键:

按单词移动光标

  • option+b :前移一个单词
  • option+f :后移一个单词

其中 b 和 f 分别是 backward 和 forward 的意思,这样是不是比较容易背了。

移动到行首行尾

  • fn+left :移动到行首
  • fn+right :移动到行尾
  • control+a :移动到行首
  • control+e :移动到行尾

fn+left 和 fn+right 其实是 Mac 自带的功能,分别对应大键盘上的 Home 和 End 键,此外 fn+up 和 fn+down 对应 PgUp 和 PgDown。

a 和 e 可以记忆为 第一个字母end :)

删除命令

  • control+w :删除一个单词
  • control+u :删除一行
  • control+l :清屏

w 可以认为是word

其他

使用 control+r 可以搜索最近使用的命令。另外,bash中也可以配置autojump。

1
2
brew install autojump
echo '[[ -s $(brew --prefix)/etc/profile.d/autojump.sh ]] && . $(brew --prefix)/etc/profile.d/autojump.sh' >> ~/.bash_profile

重启下 terminal 现在可以使用命令 j xxx 了。autojump使用一个数据库,记录你最常使用的目录,你只需要打目录名字的一部分就可以 jump 到目录。

在Linux下设置swap

今早起来发现博客的数据库挂了,赶紧用手机上的ConnectBot连上去把mysql启动。看了下日志大概是因为内存不够用且没设置swap,所以mysql进程申请不到内存挂了(小内存服务器桑不起)所以赶紧把swap搞上,这样至少能让服务不轻易挂掉。

这里记录一下,以备遗忘。

大概分三步

  • 生成一个空文件
  • 把文件格式化成swap格式
  • 挂载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# create swap file
sudo dd if=/dev/zero of=/swapfile bs=128M count=4

# adjust permission
sudo chmod 600 /swapfile

# format swap
sudo mkswap /swapfile

# mount swap
sudo swapon /swapfile

# see if ok
sudo swapon -s

没问题的话,最后一个命令会显示

1
2
Filename				Type		Size	Used	Priority
/swapfile file 524284 0 -1

另外,dd程序花的时间比较长,因为它确实写入东西了。但其实Linux的文件系统有个功能,它可以忽略(合并?)文件中的0,比如一个1G的文件,只有前面3个字节和后面3个字节写字了,其余都是0,那么在硬盘上只会记录几个字节,而不会把上G的0写到硬盘中,这样就能加快性能。

所以可以使用 fallocate 程序。

1
sudo fallocate -l 512M /swapfile

这个应该会很快。

最后,修改一下fstab,保证开机自动挂载:

1
2
3
4
sudo vim /etc/fstab

# write this to fstab
/swapfile none swap sw 0 0

Proudly powered by Hexo and Theme by Hacker
© 2021 wastecat