0%

0. 写在前面

之前一直在用 Linux Mint Cinnamon 19.1,考虑到没有 Wayland 加成以及飞行堡垒显卡散热不太行,所以 Linux Mint 20 发布后上车了 Xfce,选择完全重装系统的方式,所以装完以后折腾了一下。

不过系统装了有几天了,按照我的记忆力八成是记不住什么东西的,能回忆多少是多少吧~

1. 配置软件源

你可以在应用菜单里搜索 Software Sources 或者执行:

1
mintsources

或者直接修改 /etc/apt/sources.list 更改软件源,加速一下体验。

2. 更新系统

linux mint 20 依然使用 apt,感谢这么刚的 mint 团队。

1
2
sudo apt update
sudo apt upgrade -y

3. 驱动

打开 All Settings -> Driver Manager,一顿乱点,Apply Changes

3.1 关闭独显

自带 nvidia-prime,直接切到核显:)

4. 输入法

我用 fcitx-rime,选 rime 是因为默认配置就很好用,而且上限很高:

1
2
sudo apt install -y fcitx-rime
im-config # 照着 GUI 点点鼠标

安装好以后默认繁体字,按 F4 可以切换,只需要简体拼音的话,往 ~/.config/fcitx/rime/default.custom.yaml 里写入:

1
2
3
patch:
schema_list:
- {schema: luna_pinyin_simp}

如果右下角没有键盘图标的话,注销重新登录一下。

其他 rime 配置自行解决。

5. 安装 {禁止事项}

  1. 下载 {禁止事项}
  2. 启动 {禁止事项}
  3. {禁止事项}{禁止事项}{禁止事项}{禁止事项}

6. 安装 Chrome

因为被 Google 账户绑架,所以必须安装 Chrome

  1. 通过 {禁止事项} 下载 Chrome
  2. 启动登录
  3. stackoverfloww 上说 keyring 提示输入密码留空回车
  4. 如果 keyring 设置了密码会比较麻烦
  5. 我忘了 keyring 设置密码后是怎么解决的了
  6. 第一次启动可以从控制台 HTTPS_PROXY={禁止事项} google-chrome 来使用 {禁止事项}
  7. 如果你遇到了 keyring 有密码导致 Google 账户同步后密码无法写入的问题,删除 .config/google-chrome 目录(本地数据也全丢),重启 Chrome
  8. 随手删除 Firefox

安装完浏览器,你就可以上网冲浪,不用看接下来的东西了。

7. 配置主题和窗口效果

打开 All Settings -> Appearance,一顿操作。

打开 All Settings -> Window Manager Tweaks -> Compositor,关掉 Enable display compositing,不会有人用 xfce 是为了视觉效果吧,不会吧不会吧。

8. 改系统快捷键

xfce 默认快捷键 8 太行,这点 cinnamon 玩爆,要改两个地方,入口分别是 All Settings -> Keyboard -> Application ShortcutAll Settings -> Window Manager -> Keyboard,照着 cinnamon 的习惯一顿操作。

9. 解决 pulseaudio 爆音

pulseaudio 在一段时间没播放声音后会闲置挂起,再启动时就会爆音,所以你只要音乐开着一直不关就行了(雾)。

关闭闲置挂起:

1
2
mkdir -p ~/.config/pulse/
echo -e '.include /etc/pulse/default.pa\nunload-module module-suspend-on-idle' > ~/.config/pulse/default.pa

参考: wiki.archlinux.org

10. oh-my-zsh

1
2
3
sudo apt install zsh
chsh -s /bin/zsh
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

注意,raw.githubusercontent.com 已经被 {禁止事项},要使用 {禁止事项} 才能访问。

11. 关闭启动项

打开 All Settings -> Session and Startup -> Application Autostart,一顿乱点。

12. 网易云音乐、Linux QQ

网易云音乐还没兼容,会有标题栏,无伤大雅 >> 传送门

QQ 没啥大问题 >> 传送门

13. redshift

Linux 下的护眼色门,配置在 ~/.config/redshift.conf

1
2
3
4
[redshift]
temp-day=6500
temp-night=5500
transition=1

这个鬼东西在我电脑上会闪屏,一定是我的问题 >_>

14. nvim

然后是一个简单的入教仪式:

1
2
3
4
5
6
wget https://github.com/neovim/neovim/releases/download/v0.4.3/nvim-linux64.tar.gz
tar xvzf nvim-linux64.tar.gz
sudo mv nvim-linux64 /usr/local/share/
sudo ln -sfn /usr/local/share/nvim-linux64/bin/nvim /usr/local/bin/nvim
sudo update-alternatives --install /usr/bin/editor editor /usr/local/bin/nvim 0
sudo apt purge vim vim.tiny # 直接后路堵死

15. 重新安装 gcc

莫名其妙提示找不到 stdlib.h,卸载重新安装解决了,一定也是我的问题。

1
2
sudo apt remove gcc
sudo apt install -y gcc

16. 其他设置

1
2
3
4
5
sudo echo 'vm.swappiness=0' >> /etc/sysctl.conf
sudo swapoff /swapfile
sudo cp /usr/share/systemd/tmp.mount /etc/systemd/system
sudo systemctl enable tmp.mount
sudo systemctl start tmp.mount

17. 结尾

相信看到这里,你的系统已经不能用了,换 Fedora 吧。

周末按照 CASPaxos 的论文尝试实现了一下这个算法,CASPaxos 算法本身没有什么困难的地方,Paxos + CAS,主要是算法上几个要点:

  1. prepare/accept 完全和 Paxos 一样,state+value 作为一个 Paxos 算法中的 value 传递。
  2. 对于一个空值,proposer choose value 和 Paxos 相同,对于一个非空值,proposer 认为 prepare 最终状态满足期望状态时,依然可以 choose value。
  3. 新 value 成功写入后,不能认为新 value 就是当前状态的 value,必须有 read 操作才能确定。
  4. read 也要有 accept 步骤的,否则会脏读。
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
package main

import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"strings"
"sync"
)

type ValueType struct {
Value int `json:"value"`
State int `json:"state"`
}

type Proposer struct {
mu sync.Mutex
ballotNum int
}

type Acceptor struct {
mu sync.Mutex
ballotNum int
value *ValueType
}

var self struct {
proposer Proposer
acceptor Acceptor
id int
acceptors []string
}

type Prepare struct {
BallotNum int `json:"ballot_num"`
}

type Promise struct {
OK bool `json:"ok"`
BallotNum int `json:"ballot_num"`
Value *ValueType `json:"value"`
}

func onPrepare(args *Prepare) *Promise {
acceptor := &self.acceptor
acceptor.mu.Lock()
defer acceptor.mu.Unlock()
if acceptor.ballotNum > args.BallotNum {
return &Promise{
OK: false,
}
}
acceptor.ballotNum = args.BallotNum
return &Promise{
OK: true,
BallotNum: acceptor.ballotNum,
Value: acceptor.value,
}
}

type Propose struct {
BallotNum int `json:"ballot_num"`
Value *ValueType `json:"value"`
}

type Accept struct {
OK bool `json:"ok"`
}

func onAccept(args *Propose) *Accept {
acceptor := &self.acceptor
acceptor.mu.Lock()
defer acceptor.mu.Unlock()
if acceptor.ballotNum > args.BallotNum {
return &Accept{
OK: false,
}
}
acceptor.ballotNum = args.BallotNum
acceptor.value = args.Value
return &Accept{
OK: true,
}
}

func invoke(node int, method string, args interface{}, reply interface{}) error {
data, _ := json.Marshal(args)
resp, err := http.Post(self.acceptors[node]+method, "application/json", bytes.NewReader(data))
if err != nil {
return err
}
respBody, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("%d %s", resp.StatusCode, respBody)
}
resp.Body.Close()
return json.Unmarshal(respBody, reply)
}

func nextBallotNum() int {
proposer := &self.proposer
proposer.mu.Lock()
proposer.ballotNum++
num := proposer.ballotNum
proposer.mu.Unlock()
return num*100 + self.id
}

func prepare(ballotNum int) (bool, int, *ValueType) {
replys := make(chan *Promise, len(self.acceptors))
for i := range self.acceptors {
go func(i int) {
args := &Prepare{
BallotNum: ballotNum,
}
var reply Promise
err := invoke(i, "/paxos/prepare", args, &reply)
if err != nil {
log.Printf("prepare %d: %v", i, err)
replys <- &Promise{
OK: false,
}
return
}
log.Printf("prepare %d: %+v", i, reply)
replys <- &reply
}(i)
}
var value *ValueType
maxBallotNum := ballotNum
promised := 0
n := len(self.acceptors)
for i := 0; i < n; i++ {
reply := <-replys
if !reply.OK {
continue
}
promised++
if reply.Value != nil {
if value == nil {
maxBallotNum = reply.BallotNum
value = reply.Value
} else if reply.BallotNum > maxBallotNum {
maxBallotNum = reply.BallotNum
value = reply.Value
}
}
}
if promised < len(self.acceptors)/2+1 {
return false, 0, nil
}
return true, maxBallotNum, value
}

func accept(ballotNum int, value *ValueType) bool {
replys := make(chan *Accept, len(self.acceptors))
for i := range self.acceptors {
go func(i int) {
args := &Propose{
BallotNum: ballotNum,
Value: value,
}
var reply Accept
err := invoke(i, "/paxos/accept", args, &reply)
if err != nil {
log.Printf("accept %d: %v", i, err)
replys <- &Accept{
OK: false,
}
return
}
log.Printf("accept %d: %+v", i, reply)
replys <- &reply
}(i)
}

accepted := 0
n := len(self.acceptors)
for i := 0; i < n; i++ {
reply := <-replys
if !reply.OK {
continue
}
accepted++
}
return accepted >= len(self.acceptors)/2+1
}

func apply(state int, val int) {
log.Printf("apply: %d %d", state, val)
}

func caspaxos(state int, val int) (bool, int) {
ballotNum := nextBallotNum()
ok, ballotNum, current := prepare(ballotNum)
if !ok {
return false, 0
}
var next *ValueType
if current == nil {
apply(0, val)
next = &ValueType{
Value: val,
State: 0,
}
} else if state == current.State {
apply(current.State+1, val)
next = &ValueType{
Value: val,
State: current.State + 1,
}
} else {
return false, current.State
}
ok = accept(ballotNum, next)
return ok, next.State
}

func main() {
var (
id int
listen string
nodes string
)
flag.IntVar(&id, "i", os.Getpid()%100, "id")
flag.StringVar(&listen, "l", ":8000", "listen")
flag.StringVar(&nodes, "n", "", "nodes")
flag.Parse()

self.id = id
if nodes != "" {
self.acceptors = strings.Split(nodes, ",")
}
http.HandleFunc("/paxos/prepare", func(w http.ResponseWriter, r *http.Request) {
reqData, _ := ioutil.ReadAll(r.Body)
var args Prepare
err := json.Unmarshal(reqData, &args)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
reply := onPrepare(&args)
replyData, _ := json.Marshal(reply)
w.Write(replyData)
})
http.HandleFunc("/paxos/accept", func(w http.ResponseWriter, r *http.Request) {
reqData, _ := ioutil.ReadAll(r.Body)
var args Propose
err := json.Unmarshal(reqData, &args)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
reply := onAccept(&args)
replyData, _ := json.Marshal(reply)
w.Write(replyData)
})
http.HandleFunc("/submit", func(w http.ResponseWriter, r *http.Request) {
state, _ := strconv.Atoi(r.FormValue("state"))
val, _ := strconv.Atoi(r.FormValue("val"))
ok, state := caspaxos(state, val)
fmt.Fprintf(w, "%t %d\n", ok, state)
})
log.Fatalf("http: %v", http.ListenAndServe(listen, nil))
}

问题描述

存在一个 orders 表,有 id, uid, gid 三个字段,查询同时存在 gid 为 1 和 2 的 uid

这里给出了 3 条 sql 语句和相关 EXPLAIN 结果(结果我就不排版了)。

30000 条数据 5000 用户 10 商品

插入了大约 3w 条测试数据后对几条 sql 语句做了一下测试。

1

EXPLAIN SELECT DISTINCT uid FROM orders WHERE uid IN (SELECT uid FROM orders WHERE gid = 1) AND gid = 2

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
1   SIMPLE    orders
NULL
ALL
NULL

NULL

NULL

NULL
28644 10.00 Using where; Using temporary
1 SIMPLE <subquery2>
NULL
eq_ref <auto_key> <auto_key> 4 ops.orders.uid 1 100.00 Distinct
2 MATERIALIZED orders
NULL
ALL
NULL

NULL

NULL

NULL
28644 10.00 Using where; Distinct

2

EXPLAIN SELECT DISTINCT a.uid FROM orders2 a INNER JOIN orders2 b ON a.uid = b.uid AND a.gid = 1 AND b.gid = 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1   SIMPLE    a
NULL
ALL
NULL

NULL

NULL

NULL
28644 10.00 Using where; Using temporary
1 SIMPLE b
NULL
ALL
NULL

NULL

NULL

NULL
28644 1.00 Using where; Distinct; Using join buffer (Block Nested Loop)

3

EXPLAIN SELECT uid FROM (SELECT DISTINCT uid, gid FROM orders WHERE gid = 1 OR gid = 2) a GROUP BY uid HAVING COUNT(*) = 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1   PRIMARY    <derived2>
NULL
ALL
NULL

NULL

NULL

NULL
5442 100.00 Using temporary; Using filesort
2 DERIVED orders
NULL
ALL
NULL

NULL

NULL

NULL
28644 19.00 Using where; Using temporary

加索引

uidgid 加上索引后,再次执行上面 3 条 sql 语句。

1

EXPLAIN SELECT DISTINCT uid FROM orders WHERE uid IN (SELECT uid FROM orders WHERE gid = 1) AND gid = 2

1
2
3
4
5
6
7
8
9
1   SIMPLE    orders
NULL
ref idx_uid,idx_gid idx_gid 4 const 2912 100.00 Using where; Using temporary
1 SIMPLE <subquery2>
NULL
eq_ref <auto_key> <auto_key> 4 ops.orders.uid 1 100.00 Distinct
2 MATERIALIZED orders
NULL
ref idx_uid,idx_gid idx_gid 4 const 3000 100.00 Distinct

2

EXPLAIN SELECT DISTINCT a.uid FROM orders a INNER JOIN orders b ON a.uid = b.uid AND a.gid = 1 AND b.gid = 2

1
2
3
4
5
6
1   SIMPLE    b
NULL
ref idx_uid,idx_gid idx_gid 4 const 2912 100.00 Using temporary
1 SIMPLE a
NULL
ref idx_uid,idx_gid idx_uid 4 ops.b.uid 5 10.47 Using where

3

EXPLAIN SELECT uid FROM (SELECT DISTINCT uid, gid FROM orders WHERE gid = 1 OR gid = 2) a GROUP BY uid HAVING COUNT(*) = 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1   PRIMARY    <derived2>
NULL
ALL
NULL

NULL

NULL

NULL
5912 100.00 Using temporary; Using filesort
2 DERIVED orders
NULL
ALL idx_gid
NULL

NULL

NULL
28644 20.64 Using where; Using temporary

去重

去重后,一个 uid 最多只能有一个 gid,大约 22333 条数据。

1

EXPLAIN SELECT uid FROM orders WHERE uid IN (SELECT uid FROM orders WHERE gid = 1) AND gid = 2

1
2
3
4
5
6
7
8
9
10
11
12
1   SIMPLE    orders
NULL
ref idx_uid,idx_gid idx_gid 4 const 2220 100.00 Using where
1 SIMPLE <subquery2>
NULL
eq_ref <auto_key> <auto_key> 4 ops.orders.uid 1 100.00
NULL

2 MATERIALIZED orders
NULL
ref idx_uid,idx_gid idx_gid 4 const 2233 100.00
NULL

2

EXPLAIN SELECT a.uid FROM orders a INNER JOIN orders b ON a.uid = b.uid AND a.gid = 1 AND b.gid = 2

1
2
3
4
5
6
7
8
1   SIMPLE    b
NULL
ref idx_uid,idx_gid idx_gid 4 const 2220 100.00
NULL

1 SIMPLE a
NULL
ref idx_uid,idx_gid idx_uid 4 ops.b.uid 4 10.00 Using where

3

EXPLAIN SELECT uid FROM orders WHERE gid = 1 OR gid = 2 GROUP BY uid HAVING COUNT(*) = 2

1
2
3
4
5
1   SIMPLE    orders
NULL
index idx_uid,idx_gid idx_uid 4
NULL
22333 19.94 Using where

总结

碰到这个问题时,想过 1 和 2,因为对 MySQL 方面基础不扎实,主观上都否定了,回来测了一下大致才有了比较清晰的认识。

  • 1 这个解法当时讨论了一下,脑子一热忘记后面的 gid 也能用上索引,认为 1 只能用到一次索引,写出这个 sql 语句后就发现问题了。。。
  • 2 在加了索引后是性能上最好的,哪怕去重后性能上几乎不差 3,也是主观上被最快否决的答案。
  • 3 是 dave 给的答案,非常巧妙。在 3w 数据+去重的情况下,性能也是最好的,不过没去重情况下,需要一个 distinct 的子查询时会全表扫描,表现不是很好。

PS: 一开始打算在阿里云上起一个 rds 实例测试,结果发现需要用户余额 >= 100 才配使用,腾讯云就没有这个限制。然后腾讯云 MySQL 管理使用 PMA,emmmmmm

拓展题

找出有 gid 1 但没有 gid 2 的 uid,这题目前想到两个解法

SELECT uid FROM orders WHERE uid NOT IN (SELECT uid FROM orders WHERE gid = 2) AND gid = 1

另一个是

SELECT a.uid FROM orders2 a LEFT JOIN orders2 b ON a.uid = b.uid AND a.gid = 1 AND b.gid = 2 WHERE a.gid = 1 AND b.id IS NULL

传送门: edit-distance

这题是比较简单的 DP 题, 状态 D[i][j] 为子串 word1[..=i] 到 word2[..=j] 的最小编辑距离, 状态转移为

1
D[i][j] = min(D[i-1][j], D[i][j-1], D[i-1][j-1]) + 1

解释一下, 从 D[i][j-1] 到 D[i][j] 相当于插入操作, D[i-1][j] 到 D[i][j] 相当于删除操作, D[i-1][j-1] 到 D[i][j] 就相当于替换操作, 考虑到 i 和 j 位置的字符相同时不需要替换, 所以最后的状态转移为

1
D[i][j] = min(D[i-1][j]+1, D[i][j-1]+1, D[i-1][j-1] + word1[i] != word2[j])

然后是我的实现, 加了辅助空串(PS: word1 到 word2 的相互编辑距离是相等的, 所以遍历方向任意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
impl Solution {
pub fn min_distance(word1: String, word2: String) -> i32 {
let n = word1.len() + 1;
let m = word2.len() + 1;
let mut d = Vec::new();
d.resize(n * m, 0);
for i in 0..n {
d[i * m] = i as i32;
}
for j in 0..m {
d[j] = j as i32;
}
let word1 = word1.as_bytes();
let word2 = word2.as_bytes();
for i in 1..n {
for j in 1..m {
d[i * m + j] = (d[i * m + j - 1] + 1)
.min(d[(i - 1) * m + j] + 1)
.min(d[(i - 1) * m + j - 1] + (word1[i - 1] != word2[j - 1]) as i32);
}
}
d[n * m - 1]
}
}

起于一个悲伤的故事

前段时间放公司用的 ducky 2108s 突然坏了, 这可是我的第一块机械键盘, 贼鸡儿难受. 难受完了折腾了块 GH60 HHKB 配列放公司, 这不是第一次折腾了, 所以总体还是比较顺利的.

准备

先编辑键盘配列, 在 http://www.keyboard-layout-editor.com 以 60% 键盘为基础编辑键盘配列.

这里分享一下我基于网上 HHKB 配列改的 HHKB + SW1345 配列: http://www.keyboard-layout-editor.com/#/gists/a9e05ed5642ca481adfd3b6eb384fc3e.

UPDATE: 还有一个我觉得更丰富好用的 HHKB 62 键盘方案, 也就是我配图中键盘最后的方案.

然后查看 Summary, 你可以知道你需要多少键帽和具体规格.

这里直接列一下我的物料列表:

  • GH60 Satan PCB * 1
  • Cherry 红轴 * 60
  • 通用亚克力外壳(含定位板) * 1
  • 键帽
    • 1x * 51(R4 * 15, R3 * 12, R2 * 24)
    • 1.5x * 4
    • 1.75x * 2
    • 2.25x * 2
    • 7x * 1
  • 卫星轴
    • 7x * 1
    • 2x * 2
  • MiniB USB 线
  • 黄花 907 电烙铁+友邦锡丝+高温海绵
  • 3mm 厚键盘jio贴 * 6

简单谈一下这些东西.

首先 PCB 用了 GH60 Satan, 国产板, 能支持 LED 灯, 没有蓝牙. 蓝牙有一个方案叫 BLE60, RGB 灯的话方案也多, 看个人需求. 某宝上店家可以帮你刷配列, 如果你自己做好了配列可以让老板帮你刷, 自己刷我后面会聊.

轴的话我其实买了 RGB 轴, 如果没有用 RGB 灯可以不买 RGB 轴, 我是为了 RGB 轴的透明.

外壳我用了通用亚克力外壳, 也是为了追求透明, 但是其实这个壳有问题, 有好几个键轴卡不住, 非常容易把轴焊歪, 建议不要轻易尝试, 网上有 GH60 HHKB 专用壳, 还能挡尘, 通用壳没有挡板, 很容易进灰.

键帽自然也是透明键帽, 混合了一些其他键帽, 7x 的空格是真的难买, 又买了点彩色增补键, 卫星轴也要 7x.

友邦锡丝是真的好用, 其他什么架子都不需要, 电烙铁调温 300 度, 高温海绵打湿但不滴水, 焊的时候擦焊头.

其实还有 LED 灯, 轴间纸啊之类的东西, 都看个人需求了.

刷固件

使用 https://tkg.io, KeyboardGH60 RevCHN, Layer ModeSimple, Composite Layer 填入从 keyboard-layout-editor 复制的 Raw Data, 如果没有其他冲突和需求, 直接 Download .eep file.

然后从 https://github.com/kairyu/tkg-toolkit 下载刷配列工具 https://github.com/kairyu/tkg-toolkit/archive/master.zip 解压, 以 Windows 为例:

  1. PCB 板连接电脑后
  2. 按一下 PCB 上的按钮
  3. 打开 zadig*.exe 安装/替换一下驱动.
  4. 执行 setup.bat, 选 GH60 RevCHN, 剩下的一路回车
  5. 把 eep 文件拖到 reflash.bat 上

焊轴

我也是交够学费的人, 所以总体还是比较顺利的(doge), 没手拍照, 口头谈谈一些要注意的地方.

首先我习惯是大键先不装, 把字母和数字的轴先装到定位板上, 然后再把 PCB 安上, 焊四个角固定一下位置. 之前我是直接焊完所有小键再安装大键的, 如果是通用定位板, 不建议这样, 考虑到大键容易焊歪, 先焊大键再焊数字字母, 歪了也能止损.

拿我的壳和配列来说, 正常键位无脑焊, 重点是右上角两个键, 回车键, 右 shift, 和右 fn, 这几个键定位板完全没卡住. 我的焊法是, 焊笔沾非常少量锡, 固定轴位置, 然后确认没歪后再焊死.

如果真的焊歪了, 据说能救的只有刀头同时加热两个焊点, 反正我吸焊是没成功过.

反正组装就那么回事, 我这么手残的人也完全吃得消, 帕金森点锡法了解一下.

最后

左右下角其实是没按键的, 也懒得刷具体按键了, 其实可以根据 filco minila 的配列一样, 加一个左下角 caps lock 和右 ctrl.

gh60-hhkb-sw1345-kbd.jpg

不用采集卡在 PS4 上直播

开始之前

我在 Ubuntu 16.04 和 Raspbian Stretch 上都成功实现了劫持 PS4 自带的 twitch 推流并转推到 bilibili.

主要参考了

这里我以树莓派举例, 我的是 Raspberry 2 Model B. 主要思路是通过劫持 DNS 来欺骗 PS4 推流.

1. 安装 Raspbian Stretch 到闪存卡

如果不是使用树莓派, 这一步可以跳过.

1
2
3
wget --trust-server-names https://downloads.raspberrypi.org/raspbian_lite_latest
unzip 2018-04-18-raspbian-stretch-lite.zip
sudo dd bs=4M if=2018-04-18-raspbian-stretch-lite.img of=/dev/mmcblk0 status=progress conv=fsync

具体安装可以参考: https://www.raspberrypi.org/documentation/installation/installing-images/linux.md

安装完成后闪存卡上会出现两个分区, 在 boot 分区执行命令启用 ssh:

1
sudo touch ssh

默认账号: pi; 默认密码: raspberry

2. 安装 nginx 和 nginx-rtmp-module

  1. 下载解压源码
1
2
3
4
5
6
7
8
cd $HOME
mkdir live
cd live
wget https://nginx.org/download/nginx-1.14.0.tar.gz
tar
wget https://github.com/arut/nginx-rtmp-module/archive/v1.2.1.tar.gz
tar xzvf nginx-1.14.0.tar.gz
tar xzvf v1.2.1.tar.gz

可能还要更新系统和使用 sudo raspi-config 配置一下, 这里不再展开.

  1. 编译
1
2
3
4
5
sudo apt install libssl-dev libpcre3-dev
cd nginx-1.14.0
./configure --add-module=$HOME/live/nginx-rtmp-module-1.2.1
make
sudo make install
  1. nginx.service

创建编辑 /lib/systemd/system/nginx.service 文件(内容来自 nginx.com):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Unit]
Description=The NGINX HTTP and reverse proxy server
After=syslog.target network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/usr/local/nginx/logs/nginx.pid
ExecStartPre=/usr/local/nginx/sbin/nginx -t
ExecStart=/usr/local/nginx/sbin/nginx
ExecReload=/usr/local/nginx/sbin/nginx -s reload
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true

[Install]
WantedBy=multi-user.target

但我们暂时不启动它.

3. 配置静态 IP

这一步是可选的, 但是有一定必要.

Raspbian 默认使用 dhcpcd, 编辑 /etc/dhcpcd.conf 添加以下内容:

1
2
3
4
interface eth0
static ip_address=192.168.1.101/24
static routers=192.168.1.1
static domain_name_servers=192.168.1.1

为 eth0 配置了 IP 为 192.168.1.101, 网关和 DNS 使用路由器地址 192.168.1.1.

4. 配置 nginx

编辑 /usr/local/nginx/conf/nginx.conf

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
#user  nobody;
worker_processes 1;

...

# ====== modify =========

error_log /dev/null crit;

rtmp {
server {
listen 1935;
chunk_size 65536;
application app {
live on;
record off;
meta copy;
push rtmp://<转推地址>+<转推key>;
}
}
}

# ====== modify =========

http {
...

server {
...
location / {
root html;
index index.html index.htm;
}

# ====== modify =========
access_log off;
location /stat {
rtmp_stat all;
rtmp_stat_stylesheet stat.xsl;
}

location /stat.xsl {
root /home/pi/nginx-rtmp-module-1.2.1;
}
# ====== modify =========

...
}

...
}

测试配置:

1
sudo /usr/local/nginx/sbin/nginx -t

到这里, 如果使用 ffmepg 推流, 理论上是可以在 http://192.168.1.101/stat 上看到流的信息.

5. DNS 劫持

介绍操作之前, 先聊聊主要思路. 主要思路无非两种, 一种是利用网关 iptables NAT(不管是在路由器上还是 Linux), 还有一种就是修改 DNS. 网关的方式配置的点比较多, 所以只介绍 DNS 的方式.

  1. 安装 dnsmasq
  2. 配置 /etc/dnsmasq.conf, 添加几个域名到我们的地址
1
2
3
address=/live-tpe.twitch.tv/192.168.1.101
address=/live.twitch.tv/192.168.1.101
address=/live-sjc.twitch.tv/192.168.1.101

关于定位具体域名, 需要用一些奇怪的方式确认, 不具体讨论.

  1. 重启 dnsmasq

6. 配置 PS

  1. 在 PS 上配置 twitch 直播
  2. 自订 PS 的网络, 在 DNS 那一步, Primary 指定 192.168.1.101, Second DNS 指定网关地址

7. 最后

  1. 在 bilibili 开启直播
  2. 复制地址和流密钥到 nginx 中, 重新加载 nginx.
  3. PS 中开始推流

8. 后续

对于不同地区可能存在不同的域名, 理想方式是让所有 live-.*.twitch.com 的域名都指向树莓派 IP, 所以我写了一个支持正则的 dns 代理代替 dnsmasq: https://github.com/vizee/dnsproxy.

还有一个更集成的方案是

dnsproxy + rtmpproxy 直接实现直播,具体查看 rtmpproxy

go compile 支持 _ 作为零值

起因

很久以前就有类似的 proposal, 并且官方也有回复, 以下是链接:

个人认为官方还是太过于保守了, 至少目前 go1 的语法下, 在 return 表达式中引入 _ 用于返回错误上还是有一些编写上的方便的(小声 bb).


DIY compile

我的 go 版本 1.10.2, 修改 $GOROOT/src/cmd/compile/internal/gc 下 3 个文件:

  • 增加 hackgc.go
  • 修改 subr.go
  • 修改 typecheck.go

1. 增加 hackgc.go:

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
package gc

import (
"cmd/compile/internal/types"
"os"
)

var isnilable [NTYPE]bool

func init() {
isnilable[TPTR32] = true
isnilable[TPTR64] = true
isnilable[TINTER] = true
isnilable[TMAP] = true
isnilable[TCHAN] = true
isnilable[TFUNC] = true
isnilable[TSLICE] = true
isnilable[TUNSAFEPTR] = true
}

var (
hackgcblank = os.Getenv("HACKBLANK") != ""
)

func convblanklit(n *Node, t *types.Type) *Node {
if t == nil {
return n
}

nn := *n
n = &nn

var zero interface{}

et := t.Etype
switch {
case isInt[et]:
p := &Mpint{}
p.Rune = (t == types.Runetype || t.Orig == types.Runetype)
zero = p
case isnilable[et]:
zero = (*NilVal)(nil)
case et == TSTRING:
zero = ""
case et == TBOOL:
zero = false
case isFloat[et]:
zero = newMpflt()
case isComplex[et]:
zero = newMpcmplx()
case et == TARRAY:
n.Op = OARRAYLIT
n.Type = t
return n
case et == TSTRUCT:
n.Op = OSTRUCTLIT
n.Type = t
return n
default:
goto bad
}

n.Op = OLITERAL
n.Type = t
n.SetVal(Val{zero})
bad:
return n
}

func hackblank(n *Node, t *types.Type) *Node {
lno := setlineno(n)
n = convblanklit(n, t)
lineno = lno
return n
}

这一步主要是定义了一个由环境变量 HACKBLANK 提供的开关 hackgcblank; 和一个函数 hackblank, 用于把 _ 转化为一个零值 literal.


2. 修改 subr.go

subr.go:958

1
n = defaultlit(n, t)

修改为:

1
2
3
4
5
if hackgcblank && isblank(n) {
n = hackblank(n, t)
} else {
n = defaultlit(n, t)
}

这一步的 assignconvfn 函数提供了一个赋值时检查和转换右值 nType 到左值类型 t 的功能, 所以这里对 _ 做一个特殊处理, 如果一个右值 n_, 那么将它转为左值类型 t 的零值 literal.


3. 修改 typecheck.go

typecheck.go:310

1
if isblank(n) {

修改为

1
if !hackgcblank && isblank(n) {

这一步是检查右值类型是发生的, 我们希望在开关打开时忽略这个检查.

验证

替换编译器:

1
2
3
cd $GOROOT/src/cmd/compile
go clean -cache && go build -i -v
mv compile ../../../pkg/tool/linux_amd64/compile

使用一段代码验证我们的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"errors"
"fmt"
)

type S struct {
A int
B bool
}

func foo() (int, bool, float64, complex128, string, map[int]int, [2]bool, interface{}, S, error) {
return _, _, _, _, _, _, _, _, _, errors.New("oops")
}

func main() {
fmt.Println(foo())
}

It works!

是不是引入了新的问题

看一段代码

1
2
3
4
5
6
7
8
package main

func main() {
n := _
a, b := _
_ = n
_, _ = a, b
}

这段代码编译器会 crash, 因为我们的做法是将左值的类型赋予右值的 _, 但这里左值的类型未知, 我的做法是禁止右值 _ 用于赋值语句.

修复代码

修改 typecheck.go:3265

1
n.Right = typecheck(n.Right, Erv)

修改为

1
2
3
4
5
n.Right = typecheck(n.Right, Erv)
if hackgcblank && isblank(n.Right) && n.Right.Op == ONAME {
n.Right.Type = nil
yyerror("cannot assign _ to left value")
}

修改 typecheck.go:3317

1
typecheckslice(n.Rlist.Slice(), Erv)

修改为

1
2
3
4
5
6
7
8
9
10
typecheckslice(n.Rlist.Slice(), Erv)
if hackgcblank {
for _, rv := range n.Rlist.Slice() {
if isblank(rv) && rv.Op == ONAME {
rv.Type = nil
yyerror("cannot assign _ to left value")
break
}
}
}

检查赋值语句中是否存在 _, 如果存在则认为是编译错误.


结束

到此修改完成, 示例中的代码理论可以运行, 具体还带来哪些编译影响未知, 并可能存在代码导致编译器 crash, 这里只是做了一个简单的修改, 真的对这个功能有需求, 还是应该通过 issue/proposal 向官方提议.

grpc 一致性 hash 负载均衡

go-grpc 库只提供了一个默认的轮询负载均衡器 grpc.RoundRobin,通过实现 grpc.Balancer 可以实现自定义规则的 Balancer,所以实现一个一致性 hash 的 grpc.Balancer。

Implement

这篇文章的基础上,增加 grpclb 包实现一致性 hash 负载均衡器。

1
2
3
4
5
6
7
8
9
10
11
12
13
├── example
│   ├── helloclient
│   │   ├── helloclient
│   │   └── main.go
│   ├── helloproto
│   │   ├── hello.pb.go
│   │   └── hello.proto
│   └── helloserver
│   └── main.go
├── grpclb
│   └── consistentlb.go
└── grpcresolver
└── consul.go

grpclb/consistentlb.go

使用 github.com/vizee/consistent 的 ketama 实现 consistent hashing ring

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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
package grpclb

import (
"context"
"errors"
"strings"
"sync"
"sync/atomic"

"github.com/vizee/consistent"
"google.golang.org/grpc"
"google.golang.org/grpc/naming"
)

const (
ketameNodeNum = 64
)

var (
ErrBalancerClosed = errors.New("balancer closed")
ErrMissingHashKey = errors.New("missing hash key")
ErrNoNode = errors.New("no node")
ErrUnavailable = errors.New("unavailable")
)

var (
HashKey interface{} = 0
)

type lbaddr struct {
ok int32
addr string
}

type consistentlb struct {
mu sync.RWMutex
closed bool
addrs map[string]*lbaddr
ketama *consistent.Ketama
wait chan struct{}
notify chan []grpc.Address
w naming.Watcher
r naming.Resolver
}

func (b *consistentlb) update(updates []*naming.Update) {
for _, u := range updates {
switch u.Op {
case naming.Add:
b.addrs[u.Addr] = &lbaddr{
addr: u.Addr,
}
case naming.Delete:
delete(b.addrs, u.Addr)
}
}
// 每次地址更新时重建 ketama ring
b.ketama.Reset(len(b.addrs) * ketameNodeNum)
open := make([]grpc.Address, len(b.addrs))
i := 0
for addr := range b.addrs {
open[i] = grpc.Address{
Addr: addr,
}
b.ketama.Add(addr, ketameNodeNum)
i++
}
b.ketama.Build()

b.notify <- open
}

func (b *consistentlb) watch(w naming.Watcher) {
done := false
for !done {
updates, err := w.Next()
if err != nil {
return
}
b.mu.Lock()
if b.w == w {
b.update(updates)
} else {
// 如果不一致, 说明当前 w 被替换
done = true
}
b.mu.Unlock()
}
}

func (b *consistentlb) Start(target string, config grpc.BalancerConfig) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.closed {
return ErrBalancerClosed
}
if b.r == nil {
// 像 grpc.RoundRobin 一样允许直接指定地址列表
addrs := strings.Split(target, ";")
updates := make([]*naming.Update, len(addrs))
for i, addr := range addrs {
updates[i] = &naming.Update{
Op: naming.Add,
Addr: addr,
}
}
b.update(updates)
return nil
}
// 只能存在一个 watcher
w, err := b.r.Resolve(target)
if err != nil {
return err
}
if b.w != nil {
b.w.Close()
}
// 更新 watcher 需要放弃之前所有的地址
b.w = w
b.addrs = make(map[string]*lbaddr)
b.ketama.Reset(0)

go b.watch(w)
return nil
}

func (b *consistentlb) Up(addr grpc.Address) (down func(error)) {
b.mu.RLock()
defer b.mu.RUnlock()
a := b.addrs[addr.Addr]
// 标记地址为 ok(connected)
if a == nil || !atomic.CompareAndSwapInt32(&a.ok, 0, 1) {
return nil
}
return func(error) {
// 只对地址标记处理
atomic.StoreInt32(&a.ok, 0)
}
}

func (b *consistentlb) Get(ctx context.Context, opts grpc.BalancerGetOptions) (addr grpc.Address, put func(), err error) {
key, ok := ctx.Value(HashKey).(uint32)
if !ok {
err = ErrMissingHashKey
return
}
retry:
b.mu.RLock()
if b.closed {
b.mu.RUnlock()
err = ErrBalancerClosed
return
}
node, ok := b.ketama.Get32(key)
if !ok {
b.mu.RUnlock()
if opts.BlockingWait {
b.mu.Lock()
wait := b.wait
if wait == nil {
wait = make(chan struct{})
b.wait = wait
}
b.mu.Unlock()
select {
case <-ctx.Done():
err = ctx.Err()
case <-wait:
goto retry
}
} else {
err = ErrNoNode
}
return
}
// 与轮询不同, 为了保证 key 总能对应到固定 node, 允许拿到未连接的地址
a := b.addrs[node]
if atomic.LoadInt32(&a.ok) != 0 {
addr = grpc.Address{
Addr: a.addr,
}
} else {
err = ErrUnavailable
}
b.mu.RUnlock()
return
}

func (b *consistentlb) Notify() <-chan []grpc.Address {
return b.notify
}

func (b *consistentlb) Close() error {
b.mu.Lock()
defer b.mu.Unlock()
if b.closed {
return ErrBalancerClosed
}
if b.w != nil {
b.w.Close()
b.w = nil
}
if b.wait == nil {
b.wait = make(chan struct{})
}
// 唤醒所有的 wait
close(b.wait)
close(b.notify)
b.ketama = nil
b.closed = true
return nil
}

func UseConsistent(r naming.Resolver) grpc.Balancer {
return &consistentlb{
r: r,
addrs: make(map[string]*lbaddr),
notify: make(chan []grpc.Address, 1),
ketama: &consistent.Ketama{},
}
}

修改 example/helloclient/main.go

替换 grpc.RoundRobin

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
package main

import (
"context"
"log"
"playground/gogrpc/example/helloproto"
"playground/gogrpc/grpclb"
"playground/gogrpc/grpcresolver"
"time"

"github.com/hashicorp/consul/api"
"google.golang.org/grpc"
)

func main() {
registry, err := api.NewClient(api.DefaultConfig())
if err != nil {
log.Fatalln(err)
}
lb := grpclb.UseConsistent(grpcresolver.ForConsul(registry))
cc, err := grpc.Dial("helloserver", grpc.WithInsecure(), grpc.WithBalancer(lb))
if err != nil {
log.Fatalln(err)
}
client := helloproto.NewHelloClient(cc)
for range time.Tick(time.Second) {
resp, err := client.Say(context.Background(), &helloproto.HelloReq{
Text: "hello: " + time.Now().String(),
})
if err != nil {
log.Println("say failed", err)
continue
}
log.Println("server reply", resp)
}
}

grpc 使用 consul 服务发现

grpc 可以自定义 Balancer,而在 Balancer 基础上可以通过实现自定义的 naming.Resolver 来达到使用 consul 等服务发现组件来发现服务的功能。

大概流程如下:

  1. grpc 在 Dial 的时候通过 WithBalancer 传入 Balancer
  2. Balancer 会通过 naming.Resolver 去解析(Resolve) Dial 传入的 target 得到一个 naming.Watcher
  3. naming.Watcher 持续监视 target 解析到地址列表的变更并通过 Next 返回给 Balancer

implement

提供一个简单实现,目录结构如下:

1
2
3
4
5
6
7
8
9
├── example
│   ├── helloclient
│   │   └── main.go
│   ├── helloproto
│   │   └── hello.proto
│   └── helloserver
│   └── main.go
└── grpcresolver
└── consul.go

grpcresolver/consul.go

实现 consulResolver 把依赖的服务名作为 Watch 的 target

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
package grpcresolver

import (
"net"
"strconv"
"sync"
"sync/atomic"

"github.com/hashicorp/consul/api"
"google.golang.org/grpc/naming"
)

type watchEntry struct {
addr string
modi uint64
last uint64
}

type consulWatcher struct {
down int32
c *api.Client
service string
mu sync.Mutex
watched map[string]*watchEntry
lastIndex uint64
}

func (w *consulWatcher) Close() {
atomic.StoreInt32(&w.down, 1)
}

func (w *consulWatcher) Next() ([]*naming.Update, error) {
w.mu.Lock()
defer w.mu.Unlock()
watched := w.watched
lastIndex := w.lastIndex
retry:
services, meta, err := w.c.Catalog().Service(w.service, "", &api.QueryOptions{
WaitIndex: lastIndex,
})
if err != nil {
return nil, err
}
if lastIndex == meta.LastIndex {
if atomic.LoadInt32(&w.down) != 0 {
return nil, nil
}
goto retry
}
lastIndex = meta.LastIndex
var updating []*naming.Update
for _, s := range services {
ws := watched[s.ServiceID]
if ws == nil {
ws = &watchEntry{
addr: net.JoinHostPort(s.ServiceAddress, strconv.Itoa(s.ServicePort)),
modi: s.ModifyIndex,
}
watched[s.ServiceID] = ws
updating = append(updating, &naming.Update{
Op: naming.Add,
Addr: ws.addr,
})
} else if ws.modi != s.ModifyIndex {
updating = append(updating, &naming.Update{
Op: naming.Delete,
Addr: ws.addr,
})
ws.addr = net.JoinHostPort(s.ServiceAddress, strconv.Itoa(s.ServicePort))
ws.modi = s.ModifyIndex
updating = append(updating, &naming.Update{
Op: naming.Add,
Addr: ws.addr,
})
}
ws.last = lastIndex
}
for id, ws := range watched {
if ws.last != lastIndex {
delete(watched, id)
updating = append(updating, &naming.Update{
Op: naming.Delete,
Addr: ws.addr,
})
}
}
w.watched = watched
w.lastIndex = lastIndex
return updating, nil
}

type consulResolver api.Client

func (r *consulResolver) Resolve(target string) (naming.Watcher, error) {
return &consulWatcher{
c: (*api.Client)(r),
service: target,
watched: make(map[string]*watchEntry),
}, nil
}

func ForConsul(reg *api.Client) naming.Resolver {
return (*consulResolver)(reg)
}

example/helloproto/hello.proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
syntax = "proto3";

package helloproto;

service Hello {
rpc Say(HelloReq) returns (HelloResp);
}

message HelloReq {
string text = 1;
}

message HelloResp {
string text = 1;
}

生成 hello.pb.go

protoc –go_out=plugins=grpc:. hello.proto

example/helloserver/main.go

服务端注册流程和大多数 consul 服务注册类似

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
package main

import (
"context"
"crypto/rand"
"encoding/hex"
"flag"
"fmt"
"log"
"net"
"playground/gogrpc/example/helloproto"
"strconv"
"time"

"github.com/hashicorp/consul/api"
grpc "google.golang.org/grpc"
)

type helloserver struct {
id string
}

func (s *helloserver) Say(ctx context.Context, req *helloproto.HelloReq) (*helloproto.HelloResp, error) {
log.Printf("[%s] reply %s", s.id, req.Text)
return &helloproto.HelloResp{
Text: fmt.Sprintf("[%s] %s", s.id, req.Text),
}, nil
}

func main() {
const ttl = 30 * time.Second
host := flag.String("h", "127.0.0.1", "host")
port := flag.Int("p", 9876, "port")
flag.Parse()
l, err := net.Listen("tcp", net.JoinHostPort(*host, strconv.Itoa(*port)))
if err != nil {
log.Fatalln(err)
}
registry, err := api.NewClient(api.DefaultConfig())
if err != nil {
log.Fatalln(err)
}
var h [16]byte
rand.Read(h[:])
id := fmt.Sprintf("helloserver-%s", hex.EncodeToString(h[:]))
err = registry.Agent().ServiceRegister(&api.AgentServiceRegistration{
ID: id,
Name: "helloserver",
Port: *port,
Address: *host,
Check: &api.AgentServiceCheck{
TTL: (ttl + time.Second).String(),
Timeout: time.Minute.String(),
},
})
if err != nil {
log.Fatalln(err)
}
go func() {
checkid := "service:" + id
for range time.Tick(ttl) {
err := registry.Agent().PassTTL(checkid, "")
if err != nil {
log.Fatalln(err)
}
}
}()
svr := grpc.NewServer()
helloproto.RegisterHelloServer(svr, &helloserver{
id: id,
})
log.Println("serving", id, l.Addr())
err = svr.Serve(l)
if err != nil {
log.Fatalln(err)
}
}

example/helloclient/main.go

客户端使用自定义 resolver 和 grpc.RoundRobin 创建 Balancer

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
package main

import (
"context"
"log"
"playground/gogrpc/example/helloproto"
"playground/gogrpc/grpcresolver"
"time"

"github.com/hashicorp/consul/api"
"google.golang.org/grpc"
)

func main() {
registry, err := api.NewClient(api.DefaultConfig())
if err != nil {
log.Fatalln(err)
}
lbrr := grpc.RoundRobin(grpcresolver.ForConsul(registry))
cc, err := grpc.Dial("helloserver", grpc.WithInsecure(), grpc.WithBalancer(lbrr))
if err != nil {
log.Fatalln(err)
}
client := helloproto.NewHelloClient(cc)
for range time.Tick(time.Second) {
resp, err := client.Say(context.Background(), &helloproto.HelloReq{
Text: "hello: " + time.Now().String(),
})
if err != nil {
log.Println("say failed", err)
continue
}
log.Println("server reply", resp)
}
}

go get 代理

代理服务端

要把 go get 请求转发别的 repo 上,需要利用 go get 的 discovery 特性去动态查找 repo。

首先要知道 go get 做了什么,定位到 repoRootForImportDynamic 函数(源文件 go/src/cmd/go/internal/get/vcs.go),可以知道命令将 ImportPath 作为 url 发起了一次请求(web.GetMaybeInsecure),并且将获取的到结果使用 parseMetaGoImports 函数(源文件 go/src/cmd/go/internal/get/discovery.go)解析。
根据 parseMetaGoImports 函数可以得知,结果作为 xml 解析,并且在查找所有 meta 元素是否存在 name 属性值为 go-import,如果存在则解析 content 属性值,拆分为 PrefixVCSRepoRoot

那我们可以明确知道了,我们需要返回一个 xml,内容格式可能是这样的

1
2
3
4
5
<html>
<head>
<meta name="go-import" content="PREFIX VCS REPOROOT" />
</head>
</html>

然后一个简单的可配置的 https go-get 服务端完成了

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
package main

import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"path"
"regexp"
"text/template"
)

type Meta struct {
re *regexp.Regexp
Pattern string `json:"pattern"`
Pkg string `json:"pkg"`
VCS string `json:"vcs"`
Repo string `json:"repo"`
Source string `json:"source"`
SourceDir string `json:"sourcedir"`
SourceLine string `json:"sourceline"`
Doc string `json:"doc"`
Body string `json:"body"`
}

var metatpl = template.Must(template.New("meta").Parse(`<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="go-import" content="{{.Pkg}} {{.VCS}} {{.Repo}}"/>
{{- if .Source}}
<meta name="go-source" content="{{.Pkg}} {{.Source}} {{.SourceDir}} {{.SourceLine}}"/>
{{- end}}
{{- if .Doc}}
<meta http-equiv="refresh" content="0; url={{.Doc}}"/>
{{- end}}
</head>
<body>
{{- if .Body}}
{{.Body}}
{{- end}}
</body>
</html>
`))

var pkgs map[string]*Meta

func getpkg(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
if r.FormValue("go-get") != "1" {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
pkg := path.Join(r.Host, r.URL.Path)

log.Printf("get %s", pkg)

meta := pkgs[pkg]
if meta == nil {
for _, m := range pkgs {
if m.re != nil && m.re.MatchString(pkg) {
meta = m
break
}
}
}
if meta == nil {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
metatpl.Execute(w, meta)
}

func loadmeta(filename string) error {
data, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
var allmeta []*Meta
err = json.Unmarshal(data, &allmeta)
if err != nil {
return err
}
pkgs = make(map[string]*Meta, len(allmeta))
for _, m := range allmeta {
if m.Pattern != "" {
m.re = regexp.MustCompile(m.Pattern)
}
pkgs[m.Pkg] = m
}
return nil
}

func main() {
err := loadmeta("./meta.json")
if err != nil {
log.Fatalln(err)
}
err = http.ListenAndServeTLS(":443", "./cart.pem", "./key.pri", http.HandlerFunc(getpkg))
if err != nil {
log.Fatalln(err)
}
}

具体的输出内容,我是参考 curl 从 golang.org 拉取的结果调整的,问题不大,go https 服务端需要的证书和私钥文件生成是可以参考 Tony Bai 的文章

实际应用

我用这个服务代理所有对 golang.org/x/tools 的 get 请求,配置如下

1
2
3
4
5
6
7
8
9
10
11
[
{
"pattern": "golang.org/x/tools/.*",
"pkg": "golang.org/x/tools",
"vcs": "git",
"repo": "https://github.com/golang/tools",
"source": "https://github.com/golang/tools/",
"sourcedir": "https://github.com/golang/tools/tree/master{/dir}",
"sourceline": "https://github.com/golang/tools/blob/master{/dir}/{file}#L{line}"
}
]

过程中遇到了几个问题,也是 Tony Bai 文章中提到的。

  1. bad certificate

    这个问题比较好解决,生成的证书 Common Name 不匹配,直接修改。

  2. unknown certificate authority

    这个比较难解决,好在 go get 提供了一个 -insecure 的命令允许 tls 建立连接过程跳过对证书的 CA 的校验。但这个做法只能在手动操作下比较好操作,如果走集成的命令,比如 vim-go 的 GoUpdateBinaries 命令的话,就要去改 vim-go 的 plugin 源码了。其实还有一种方式,如果只是我们私人使用的话,直接让系统 CA 信任我们自签的证书就完全没毛病了。(或者找黑心证书商买,大雾

  3. 多域名
    网上文章好好找找,问题不大。