基于 Mihomo + Docker 实现旁路由

背景

之前尝试过基于 OpenWrt + OpenClash 搭建旁路由,但 OpenWrt 系统本身较为臃肿,且 OpenClash 配置复杂,稳定性一般。后来也关注过 Sing-box,虽然功能新颖但变动较快,稳定性有待验证。因此,决定基于 OpenClash 的核心 Mihomo,手动搭建一套精简、高效的旁路由方案。

特别是在 Vibe Coding 等 AI 辅助开发模式下,AI 需要自动全局代理,以避免手动解决 npm 包安装、Docker 镜像拉取以及调用大语言模型 API 时的连通性问题。手动开发时可能偶尔需要代理,但 AI 自动开发时对网络连通性要求极高,一个稳定可靠的旁路由方案能极大提升开发效率。

采用 Docker + Macvlan + Mihomo + TUN 的组合,相较于在 OpenWrt 上再搭建服务,具有以下优势:

  • 更轻量:无需完整的 OpenWrt 系统,仅需 Docker 容器运行 Mihomo。
  • 启动更快:容器化部署,启动和重启速度显著提升。
  • 更稳定:Mihomo 作为 Clash 核心,经过多年发展,稳定性有保障。

架构图

flowchart LR
    subgraph Terminal[终端设备]
        T1[电脑/手机]
    end

    subgraph Host[宿主机]
        subgraph Docker[Docker]
            M[Mihomo容器]
        end
    end

    subgraph Network[网络出口]
        Direct[直连]
        Proxy[代理]
    end

    T1 -->|设置网关为 192.168.1.11| M
    M --> Rule[规则引擎]
    Rule -->|中国域名/IP| Direct
    Rule -->|其他| Proxy
    Direct --> Internet[互联网]
    Proxy --> Internet

准备工作

  1. 一台支持 Docker 的 Linux 主机(物理机或虚拟机均可)
  2. 确保系统支持 macvlan 网络驱动(通常 Linux 内核 3.9+ 都支持)
  3. 准备配置文件 config.yaml(下文提供模板)

创建 Macvlan 网络

首先,需要创建一个 macvlan 网络,让容器能够获得独立的局域网 IP。假设你的主机网卡为 eth0,网段为 192.168.1.0/24,网关为 192.168.1.1

执行以下命令创建 macvlan 网络:

1
2
3
4
5
docker network create -d macvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
-o parent=eth0 \
macnet

注意:请根据你的实际网络环境修改 --subnet--gateway-o parent 参数。

macvlan 网络限制(可选):默认情况下,宿主机与 macvlan 容器之间无法直接通信。如果宿主机也需要使用旁路由代理,需要额外创建一个 macvlan shim 网络:

1
2
3
4
5
# 创建宿主机使用的 macvlan 接口
ip link add macvlan-shim link eth0 type macvlan mode bridge
ip addr add 192.168.1.10/24 dev macvlan-shim
ip link set macvlan-shim up
ip route add 192.168.1.11/32 dev macvlan-shim

这样宿主机就可以通过 192.168.1.10 访问容器的 192.168.1.11 了。如果宿主机不需要代理,可以跳过此步骤。

准备配置文件

创建一个目录用于存放配置文件,例如 /root/mihomo。在该目录下创建 config.yaml 文件。以下是一个精简的配置模板(示例配置),包含 proxy-providers 示例,请替换为自己的配置信息。

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
# Mihomo Minimal Transparent Proxy Config
# Suitable for Docker macvlan + TUN model

# -----------------------------
# 代理配置(示例)
# -----------------------------
proxies:
- name: "SS1"
type: ss
server: aaa.mangege.com # 替换为你的代理服务器地址
port: 443 # 替换为你的代理端口
cipher: chacha20-ietf-poly1305
password: "aaa" # 替换为你的代理密码
udp: true
udp-over-tcp: true
udp-over-tcp-version: 2
ip-version: ipv4

# -----------------------------
# 代理提供者(示例)
# -----------------------------
proxy-providers:
my-provider:
type: http
path: ./providers/my-provider.yaml
url: https://example.com/provider.yaml # 替换为你的订阅链接
interval: 3600
health-check:
enable: true
url: http://www.gstatic.com/generate_204
interval: 300

# -----------------------------
# 基础配置
# -----------------------------
mixed-port: 7890
redir-port: 7892 # TCP 透明代理
tproxy-port: 7893 # UDP 透明代理
ipv6: true
allow-lan: true
unified-delay: false
tcp-concurrent: true

# -----------------------------
# 外部控制
# -----------------------------
external-controller: 127.0.0.1:9090
external-ui: ui
external-ui-url: "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip"

# -----------------------------
# Geo 数据
# -----------------------------
geodata-mode: true
geox-url:
geoip: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip-lite.dat"
geosite: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
mmdb: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country-lite.mmdb"
asn: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb"

# -----------------------------
# 其他设置
# -----------------------------
client-fingerprint: chrome

profile:
store-selected: true
store-fake-ip: true

sniffer:
enable: true
sniff:
HTTP:
ports: [80, 8080-8880]
override-destination: true
TLS:
ports: [443, 8443]
QUIC:
ports: [443, 8443]
skip-domain:
- "Mijia Cloud"
- "+.push.apple.com"

# -----------------------------
# TUN 设置(关键)
# -----------------------------
tun:
enable: true
stack: gvisor
mtu: 1500
dns-hijack:
- "any:53"
- "tcp://any:53"
auto-route: true
auto-redirect: true
auto-detect-interface: true
fake-ip-range: 198.18.0.1/16

# -----------------------------
# DNS 设置
# -----------------------------
dns:
enable: true
ipv6: true
enhanced-mode: fake-ip
fake-ip-filter:
- "*"
- "+.lan"
- "+.local"
- "+.market.xiaomi.com"
default-nameserver:
- tls://223.5.5.5
- tls://223.6.6.6
fallback:
- tls://1.1.1.1
- tls://8.8.8.8
nameserver:
- https://doh.pub/dns-query
- https://dns.alidns.com/dns-query

# -----------------------------
# 代理组
# -----------------------------
proxy-groups:
- name: "DEFAULT"
type: select
proxies: [SS1]

# -----------------------------
# 规则
# -----------------------------
rules:
# 局域网IP直连
- GEOIP,lan,DIRECT,no-resolve
# 中国域名直连(通过GeoSite规则)
- GEOSITE,CN,DIRECT
# 中国IP直连(通过GeoIP规则)
- GEOIP,CN,DIRECT
# Tailscale相关域名直连(可选)
- DOMAIN-SUFFIX,ts.net,DIRECT
- DOMAIN-SUFFIX,tailscale.io,DIRECT
- DOMAIN-SUFFIX,tailscale.com,DIRECT
# 其他所有流量走代理组DEFAULT(即使用SS1代理)
- MATCH,DEFAULT

重要提示

  • 将上述配置中的代理信息(serverportpassword 等)替换为你自己的。
  • 如果使用 proxy-providers,请确保 url 指向有效的订阅链接。
  • 根据需要调整 tundnsrules 等配置。
  • 更多配置选项请参考 Mihomo 官方文档:

启动容器

使用以下命令启动 Mihomo 容器:

1
2
3
4
5
6
7
docker run -d \
--name mihomo \
--restart always \
--network macnet \
--ip 192.168.1.11 \
-v /root/mihomo/config.yaml:/root/.config/mihomo/config.yaml \
metacubex/mihomo:latest

注意

  • --ip 参数指定容器的 IP 地址,请确保该 IP 在你的局域网网段内且未被占用。
  • -v 参数将主机上的配置文件映射到容器内,请确保路径正确。
  • --network macnet 使用之前创建的 macvlan 网络。

配置说明

  • Macvlan 网络:使容器获得独立 IP,如同物理设备接入局域网。
  • TUN 模式:启用 TUN 接口实现透明代理,对应用透明。
  • DNS 劫持:通过 dns-hijack 劫持 DNS 查询,防止 DNS 泄露。
  • 规则分流:采用“中国站点与中国IP走直连,否则走代理”的策略。具体规则:局域网IP直连、中国域名直连(GeoSite)、中国IP直连(GeoIP)、Tailscale域名直连,其他所有流量走代理组。
  • 代理组:配置一个名为 DEFAULT 的 select 代理组,包含 SS1 代理。规则中的 MATCH,DEFAULT 将所有未匹配的流量导向此代理组,实现代理功能。
  • 客户端配置:需要在要使用旁路由的机器上手动配置静态IP和DNS服务器地址,将它们都设置为 Mihomo 容器的IP地址(如 192.168.1.11),以确保流量正确通过旁路由。
    • Windows:网络设置 → 更改适配器选项 → 右键以太网/WiFi → 属性 → IPv4 → 手动设置IP和DNS
    • macOS:系统偏好设置 → 网络 → 高级 → TCP/IP → 手动配置 → DNS → 添加 192.168.1.11
    • Linuxnmcli 或编辑 /etc/resolv.conf,添加 nameserver 192.168.1.11
  • 安全性建议
    1. 配置文件权限:chmod 600 /root/mihomo/config.yaml
    2. 防火墙设置:确保 Mihomo 端口(7890、7892、7893、9090)不被外部访问
    3. 定期更新:定期更新镜像以获取安全补丁
    4. 日志监控:定期检查日志,发现异常访问
  • 性能调优
    1. MTU 调整:在 config.yaml 的 tun 部分调整 MTU 值(通常 1500 或 1400)
    2. 连接数限制:根据服务器性能调整 max-concurrent 参数
    3. 缓存优化:启用 store-selectedstore-fake-ip 以提升性能
    4. DNS 缓存:调整 DNS 缓存时间以减少查询次数

验证配置

配置完成后,需要验证旁路由是否工作正常。

基础测试

  1. 检查容器状态

    1
    docker ps | grep mihomo
  2. 测试 DNS 解析

    1
    nslookup google.com 192.168.1.11
  3. 测试网络连通性

    1
    2
    3
    4
    5
    # 从客户端 ping 网关
    ping 192.168.1.11

    # 从客户端测试代理
    curl -x socks5://192.168.1.11:7890 https://httpbin.org/ip

流量测试

  1. 国内网站直连测试

    1
    curl -I https://www.baidu.com
  2. 国外网站代理测试

    1
    curl -I https://www.google.com
  3. 查看 Mihomo 日志确认分流

    1
    docker logs mihomo -f | grep -E "DIRECT|REJECT|匹配"

性能测试

  1. 速度测试

    1
    2
    3
    # 使用 speedtest-cli 测试
    pip install speedtest-cli
    speedtest-cli --server 12345 # 替换为实际服务器ID
  2. 延迟测试

    1
    ping -c 10 192.168.1.11

故障排查

常见问题

  1. 容器无法启动

    • 检查配置文件语法:docker logs mihomo
    • 确认 macvlan 网络已创建:docker network ls
    • 检查 IP 地址冲突:ping 192.168.1.11
  2. 客户端无法上网

    • 确认客户端网关和 DNS 都设置为 192.168.1.11
    • 检查 Mihomo 日志:docker logs mihomo -f
    • 测试 DNS 解析:nslookup google.com 192.168.1.11
  3. 速度慢或不稳定

    • 检查代理服务器状态
    • 调整 MTU 值(在 config.yaml 的 tun 部分修改)
    • 查看容器资源使用:docker stats mihomo

日志查看

1
2
3
4
5
6
7
8
# 查看实时日志
docker logs mihomo -f

# 查看最近100行日志
docker logs mihomo --tail 100

# 查看特定时间后的日志
docker logs mihomo --since 2026-03-25T21:00:00

配置验证

1
2
3
4
5
6
7
8
# 检查容器状态
docker ps | grep mihomo

# 检查网络连接
docker exec mihomo ping -c 3 192.168.1.1

# 检查 DNS 服务
docker exec mihomo nslookup google.com

维护

容器管理

  1. 重启容器

    1
    docker restart mihomo
  2. 停止容器

    1
    docker stop mihomo
  3. 删除容器

    1
    docker rm -f mihomo

配置更新

  1. 修改配置文件

    1
    2
    3
    4
    5
    # 编辑配置文件
    vim /root/mihomo/config.yaml

    # 重启容器使配置生效
    docker restart mihomo
  2. 配置热重载(如果支持):

    1
    2
    # 通过 API 重载配置
    curl -X PUT http://127.0.0.1:9090/configs -d '{"path":"/root/.config/mihomo/config.yaml"}'

镜像更新

  1. 拉取最新镜像

    1
    docker pull metacubex/mihomo:latest
  2. 重建容器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 停止并删除旧容器
    docker stop mihomo
    docker rm mihomo

    # 使用新镜像重新创建容器
    docker run -d \
    --name mihomo \
    --restart always \
    --network macnet \
    --ip 192.168.1.11 \
    -v /root/mihomo/config.yaml:/root/.config/mihomo/config.yaml \
    metacubex/mihomo:latest

备份与恢复

  1. 备份配置

    1
    2
    3
    4
    5
    # 备份配置文件
    cp /root/mihomo/config.yaml /root/mihomo/config.yaml.backup

    # 备份容器
    docker export mihomo > mihomo-backup.tar
  2. 恢复配置

    1
    2
    3
    4
    5
    # 恢复配置文件
    cp /root/mihomo/config.yaml.backup /root/mihomo/config.yaml

    # 重启容器
    docker restart mihomo

总结

通过 Docker + Macvlan + Mihomo + TUN 的组合,我们搭建了一个轻量、稳定且高效的旁路由方案。相比传统的 OpenWrt 方案,它更加精简,启动更快,维护也更简单。只需将终端设备的网关指向旁路由的 IP(如 192.168.1.11),即可享受全局代理服务。

Node.js 使用 FFI 调用 win32 API

FFI 全称 Foreign Function Interface .
主要解决在 Node.js 里用 JS 调用 C/C++ 写的动态库的问题.
https://www.npmjs.com/package/ffi-napi

  1. 在安装 ffi 之前,请先安装好 node-gyp 相关的依赖,具体请看官方安装说明 https://github.com/nodejs/node-gyp .
  2. Node.js 12 及以上的版本,请安装 ffi-napi 包,而不是 ffi 包. 原理请看 N-API 介绍: https://xcoder.in/2017/07/01/nodejs-addon-history/
  3. 请安装 ref-napi 包,而不是 ref 包,参数是指针类型时需要. 其它像 struct, union, array 请找对应的 napi 的包装. https://github.com/node-ffi-napi

先看 FFI官方给的示例:

1
2
3
4
5
6
var ffi = require('ffi-napi');

var libm = ffi.Library('libm', {
'ceil': [ 'double', [ 'double' ] ]
});
libm.ceil(1.5); // 2

示例是调用 C 的 math 相关的库的 ceil 函数. 在 Mac 或 Linux 下,我们可以通过 man ceil 看函数的 C 签名.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NAME
ceil -- round to smallest integral value not less than x

SYNOPSIS
#include <math.h>

double
ceil(double x);

long double
ceill(long double x);

float
ceilf(float x);

DESCRIPTION
The ceil() functions return the smallest integral value greater than or
equal to x.

可以看到 ceil 函数返回一个 double 值,且需要一个 double 值.

https://github.com/node-ffi/node-ffi/wiki/Node-FFI-Tutorial 手册描述 ffi.Library 的函数签名如下:

ffi.Library(libraryFile, { functionSymbol: [ returnType, [ arg1Type, arg2Type, ... ], ... ]);

所以示例代码调用 libm 的 ceil 意思就很明白.

之所以需要 ref 包,是因为在调用指针等 JS 里没有的类型时,需要用它来构建参数的值.

FFI 手册 里的代码来举例

sqlite3_open 等函数的签名请看 https://www.sqlite.org/capi3ref.html#sqlite3_open

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var ref = require('ref');
var ffi = require('ffi');

// typedef
var sqlite3 = ref.types.void; // we don't know what the layout of "sqlite3" looks like
var sqlite3Ptr = ref.refType(sqlite3);
var sqlite3PtrPtr = ref.refType(sqlite3Ptr);
var stringPtr = ref.refType(ref.types.CString);

// binding to a few "libsqlite3" functions...
var libsqlite3 = ffi.Library('libsqlite3', {
'sqlite3_open': [ 'int', [ 'string', sqlite3PtrPtr ] ],
'sqlite3_close': [ 'int', [ sqlite3Ptr ] ],
'sqlite3_exec': [ 'int', [ sqlite3Ptr, 'string', 'pointer', 'pointer', stringPtr ] ],
'sqlite3_changes': [ 'int', [ sqlite3Ptr ]]
});

// now use them:
var dbPtrPtr = ref.alloc(sqlite3PtrPtr);
libsqlite3.sqlite3_open("test.sqlite3", dbPtrPtr);
var dbHandle = dbPtrPtr.deref();

函数签名我们可以用 ref.types 来构建,也可以直接写成 string 字符,对于复杂的指针类型,我们其实可以直接用 'pointer' 就行了.但是使用 ref.alloc 创建指针等复杂参数值时,就必须得 ref.types 方式来构建了.

ref.alloc 函数返回的是指针,如果是需要传值,需要调用再调用 deref 方法. 从手册里的下文示例代码可以看出 alloc 返回的是指针类型.

1
2
3
4
5
6
7
8
9
var intPtr = ref.refType('int');

var libmylibrary = ffi.Library('libmylibrary', { ...,
'manipulate_number': [ 'void', [ intPtr ] ]
});

var outNumber = ref.alloc('int'); // allocate a 4-byte (32-bit) chunk for the output data
libmylibrary.manipulate_number(outNumber);
var actualNumber = outNumber.deref();

异步调用函数示例代码如下:

https://github.com/node-ffi/node-ffi/wiki/Node-FFI-Tutorial#async-library-calls

1
2
3
4
5
var libmylibrary = ffi.Library('libmylibrary', {
'mycall': [ 'int', [ 'int' ] ]
});

libmylibrary.mycall.async(1234, function (err, res) {});

开始主题,调用 win32 api . 以 SystemParametersInfo 函数为例.

首先找到 SystemParametersInfo 的函数原型:

https://github.com/tpn/winsdk-10/blob/master/Include/10.0.10240.0/um/WinUser.h

在上文件搜索,没有 SystemParametersInfo 函数,只有 SystemParametersInfo 常量,根据是否为 UNICODE 模式,决定是调用 SystemParametersInfoA(ANSI) 还是 SystemParametersInfoW (WideChar) .因为我们在 Node.js 里调用没有引入头文件编译,所以我们任意选择一个即可,本文选择用 SystemParametersInfoA 来做示例.

SystemParametersInfoA 函数的文档请看: https://docs.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-systemparametersinfoa

1
2
3
4
5
6
BOOL SystemParametersInfoA(
UINT uiAction,
UINT uiParam,
PVOID pvParam,
UINT fWinIni
);

1,2,4 参数为数字类型, 3 参数为指针.

示例是调用此方法检测系统是否进入屏保状态, 我们定位到 SPI_GETSCREENSAVERRUNNING 关键的那一行,告诉我们参数3需要为一个 bool 的指针,如果检测屏保已经启动,会把指针的值设为 true .

1
Determines whether a screen saver is currently running on the window station of the calling process. The pvParam parameter must point to a BOOL variable that receives TRUE if a screen saver is currently running, or FALSE otherwise. Note that only the interactive window station, WinSta0, can have a screen saver running.

uiParam,fWinIni 看文档的说明,在调用 SPI_GETSCREENSAVERRUNNING 时,我们传 0 即可.

所以最终的 js 调用代码如下.

1
2
3
4
5
6
7
8
9
10
const ffi = require('ffi-napi')
const ref = require('ref-napi')

const user32 = ffi.Library('user32.dll', {
SystemParametersInfoA: ['bool', ['uint', 'uint', 'pointer', 'uint']]
})

// 0x0072 SPI_GETSCREENSAVERRUNNING, 从头文件或文档里查,因不是 C++ 项目,所以只能以直接写值,否则可以写 SPI_GETSCREENSAVERRUNNING ,然后 C++ 的预处理器会帮忙替换.
let checkResult = user32.SystemParametersInfoA(0x0072, 0, isEnable, 0)
console.log(`checkResult ${checkResult} isEnable ${isEnable.deref()}`)

使用 ffi 调用 win32 api 示例即完成了,如需调用其它函数,只需要找到相关的头文件和文档说明按照上代码的方式编写即可.

使用 Docker 搭建 MySQL 从库备份 VPS 数据库

瞎折腾建站为了节省成本,只能买些小品牌服务商的 VPS. 小品牌不放心,数据得自己好好备份.

MySQL 备份方式有很多,比如直接 Dump ,但小 VPS 空间少,带宽少,拉到本地速度慢.除非是数据库数据比较小,不然不推荐用 Dump 来备份.
增量备份还可以考虑 XtraBackup ,但维护起来比较麻烦,而且也占用本地空间.

个人看来,使用从库应该是备份 VPS 的 MySQL 的最佳方案.

  • 占用空间小(binlog 需要占用一些空间)
  • 增量同步,比较实时(只要网络与IO跟的上,从库基本上没有延迟.个人测试,国外主库,国内从库,有1小时的延迟的样子)
  • 稳定,搭建后维护成本低.

为什么选择 Docker 来搭建从库,而不是直接安装 MySQL 到主机上?
有时机器上面已经安装了 MySQL ,再搭建一个 MySQL 做从库,需要改 MySQL 配置文件,添加开机自启脚本,麻烦,容易出错.而 Docker 可以完美的解决这些问题.

使用 Docker ,你可以在两台不同的 VPS 互相做主从备份,或在家里的电脑,自己的工作电脑建立从库(不是 24 小时开机的话, 主库的 binlog 保存久一点), 而不影响主机上原有的 MySQL 的使用.

本教程基于 Percona 编译的 MySQL 版本, 主从库版本请一致.
本文对于 Docker 和 MySQL 的主从配置写的不是很详细,需要自己有一定的基础.

参考链接:

1 安装 Docker

请参考官网链接 https://docs.docker.com/install/

2 拉镜像

请根据自己的版本需求修改 tag .

docker pull percona:8.0

3 备份主库数据库

使用 XtraBackup 备份主库数据(不用停机), 然后把备份文件打包,下载到从库机器上面 (速度慢就套层 CF CDN).

xtrabackup --backup --target-dir=/data/backups/

具体教程 https://www.percona.com/doc/percona-xtrabackup/LATEST/backup_scenarios/full_backup.html

4 准备从库数据库文件

在从库机器上面解压数据库备份文件,然后执行准备命令. 执行了 prepare ,这个时候 MySQL 就可以直接基于此目录的数据文件启动了.

xtrabackup --prepare --target-dir=/data/backups/

6 创建从库数据库配置文件

1
2
3
4
vi /data/confs/slavedb.cnf
# Config Settings:
[mysqld]
server-id=2

注意主库也要配置一个不同的 server-id , 并开启 binlog .

5 修改数据库文件用户 ID

1
2
sudo chown -R 1001:1001 /data/backups/
sudo chown -R 1001:1001 /data/confs/

之所以改成 1001 ,是因为 Percona 的 MySQL Docker 镜像启动 MySQL 用的用户 mysql 的 id 是 1001 . 而 archlinux 的 mysql user id 是 89 .所以只能直接改 user id .

其它 MySQL 镜像的 user id 也许不一样, 可以先启动一个 MySQL Docker 实例,然后再使用 docker exec -it mysql-slave bash 启动一个 shell, 再在 shell 执行 id 命令,即可找到正确的 user id .

如果文件权限不改对,启动 MySQL 会报错的. 可以使用 docker logs mysql-slave 查看报错信息.

6 创建主库 MySQL 用户

1
2
CREATE USER 'repl'@'%' IDENTIFIED BY 'mysqlpass';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';

把密码修改成你的复杂密码,在 mysql 8, 这密码的复杂度是创建不了用户的.

7 启动从库

1
2
3
docker run --name mysql-slave -d -v /data/confs:/etc/mysql/conf.d -v /data/backups:/var/lib/mysql -e MYSQL_ROOT_PASSWORD='your_master_root_password' -d percona:8.0
docker logs mysql-slave # 查看是否有报错,有报错解决报错
docker exec -it mysql-slave bash # 启动 shell, 之前输入 mysql -uroot -pyour_master_root_password 连接 mysql 进行管理.

不同的 Docker 镜像, -v 参数有可能不同,请以自己的镜像文档为准. https://hub.docker.com/_/percona

8 配置从库

在上个步骤已经启动一个 MySQL shell ,现在我们需要配置从库连接到主库即可.

1
2
3
4
5
change master to master_host="mysql master ip",master_port=3306,master_user="repl",master_password="mysqlpass",master_log_file="mysql-bin.000007",master_log_pos=155;

start slave;

show slave status;

master_log_file 与 master_log_pos 查看数据库备份文件夹的 xtrabackup_info 的 binlog_pos 即可.

到这一步,整个从库已经配置成功,可以使用 show slave status 查看从库状态. 在主库上面使用 show master status 与从库对比,查看同步延迟.

最后,记得设置 docker 服务为开机自启.

在Onda v820w 平板安装三系统(Linux, Win, Android)

Onda v820w 平板使用的是 Intel Z3735F CPU,本质上就是一台普通 PC .
只是麻烦的地方有两点,一是只支持 EFI 启动, 二是 EFI 只支持 32 位.

准备工具:

  1. otg 线
  2. usb hub
  3. usb 键盘与鼠标
  4. U 盘 多个

平板本身就支持 Android 和 Win 双启动.添加 Linux 系统有两种安装方式,一种是安装到 SD 卡. 一种是把 Win 分区删除掉重新分区,分 8G 左右给 Linux . Win 用 12 G.

本文主要介绍把 Win 重新分区后再安装 Archlinux . 从安装的简易程度来说, Debian 应该是优选的发行版本,是唯数不多的还支持 32 bit CPU 的发行版本.

Archlinux 已经不支持 32 bit CPU, 平板的CPU是 64 bit,但其 UEFI 只支持 32 bit .所以 archlinux 的 usb 启动盘只能手动创建,而使用 cp archlinux.iso /dev/sdb 的创建的启动盘在普通电脑能启动,但这平板是不行的.

制作 archlinux 启动盘

  1. 使用 fdisk 等你熟悉的磁盘分区工具进行分区, gpt 分区表, 一个 Microsoft basic data 分区,分区格式为 fat32.

    使用 mkfs.vfat -F 32 -n ARCH /dev/sdb1 命令进行格式化时,把分区 Label 设置为 ARCH ,后面 grub 配置文件是根据 LABEL 查找根目录的.注意要把命令的设备路径改成你的.

  2. 提取 iso 文件到 U盘,参考 https://wiki.archlinux.org/index.php/USB_flash_installation_media#Using_manual_formatting

1
2
3
4
5
6
# mkdir -p /mnt/{iso,usb}
# mount -o loop archlinux-version-x86_64.iso /mnt/iso
# mount /dev/sdXn /mnt/usb
# cp -a /mnt/iso/* /mnt/usb
# sync
# umount /mnt/iso
  1. 安装 32 bit UEFI 启动文件, 参考 https://wiki.archlinux.org/index.php/ASUS_x205ta#Creating_bootia32.efi https://wiki.archlinux.org/index.php/GRUB/Tips_and_tricks#GRUB_standalone
1
2
# 把 /run/media/outman/ARCH/ 路径替换你的u盘的挂载路径.
grub-mkstandalone -d /usr/lib/grub/i386-efi/ -O i386-efi --modules="part_gpt part_msdos" --locales="en@quot" --themes="" -o "/run/media/outman/ARCH/EFI/boot/bootia32.efi" "boot/grub/grub.cfg=./grub.cfg" -v

创建 /run/media/outman/ARCH/EFI/boot/grub.cfg 文件,记得路径是你的路径.
<FS-LABEL> 替换成 ARCH (之前的 U 盘的分区 Label)

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
insmod part_gpt
insmod part_msdos
insmod fat
insmod efi_gop
insmod efi_uga
insmod video_bochs
insmod video_cirrus
insmod font

if loadfont "${prefix}/fonts/unicode.pf2" ; then
insmod gfxterm
set gfxmode="1024x768x32;auto"
terminal_input console
terminal_output gfxterm
fi

menuentry "Arch Linux archiso x86_64" {
set gfxpayload=keep
search --no-floppy --set=root --label <FS-LABEL>
linux /arch/boot/x86_64/vmlinuz archisobasedir=arch archisolabel=<FS-LABEL> add_efi_memmap
initrd /arch/boot/x86_64/archiso.img
}

menuentry "UEFI Shell x86_64 v2" {
search --no-floppy --set=root --label <FS-LABEL>
chainloader /EFI/shellx64_v2.efi
}

menuentry "UEFI Shell x86_64 v1" {
search --no-floppy --set=root --label <FS-LABEL>
chainloader /EFI/shellx64_v1.efi
}

这样, archlinux 的启动盘已经制作完成.

调整分区,重装 Win

安装 archlinux 系统

  1. 接入 otg , usb hub ,键盘 .
  2. 重启或开机不停按 ESC , 之后进 BOOT MANAGE ,选择你的 U 盘启动.
  3. 如果没有看到 U 盘,可以尝试使用 BOOT FROM FILE,选择 EFI/boot/bootia32.efi 文件,这样会进入一个 grub shell,之后可以使用 configfile (hd0,gpt1)/EFI/boot/grub.cfg(带自动补全,自己补对目录) 来启动自己写的 grub 配置文件,高手可以直接在 grub shell 里敲命令启动.
  4. 稍等几十秒启动完成后, 使用 wifi-menu 连接 wifi ,自带无线驱动,连上就可以用.
  5. 之后就是普通的 archlinux 安装流程,我这就不细写了,只写一些需要注意的地方.

Linux 分区分两个,一个 boot 分区(同时是EFI分区), 一个根目录分区.

下面是我的分区表信息,供参考
/dev/mmcblk1p18 是 boot 分区
/dev/mmcblk1p19 是 linux 根目录分区

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
Disk /dev/mmcblk1: 28.9 GiB, 31037849600 bytes, 60620800 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt

Device Start End Sectors Size Type
/dev/mmcblk1p1 40 131111 131072 64M EFI System
/dev/mmcblk1p2 131112 262183 131072 64M Microsoft basic data
/dev/mmcblk1p3 262184 294951 32768 16M Microsoft basic data
/dev/mmcblk1p4 294952 327719 32768 16M Microsoft basic data
/dev/mmcblk1p5 327720 360487 32768 16M Microsoft basic data
/dev/mmcblk1p6 360488 393255 32768 16M Microsoft basic data
/dev/mmcblk1p7 393256 524327 131072 64M Microsoft basic data
/dev/mmcblk1p8 524328 589863 65536 32M Microsoft basic data
/dev/mmcblk1p9 589864 622631 32768 16M Microsoft basic data
/dev/mmcblk1p10 622632 655399 32768 16M Microsoft basic data
/dev/mmcblk1p11 655400 688167 32768 16M Microsoft basic data
/dev/mmcblk1p12 688168 2785319 2097152 1G Microsoft basic data
/dev/mmcblk1p13 2785320 3309607 524288 256M Microsoft basic data
/dev/mmcblk1p14 3309608 5406759 2097152 1G Microsoft basic data
/dev/mmcblk1p15 5406760 20086823 14680064 7G Microsoft basic data
/dev/mmcblk1p16 20088832 20121599 32768 16M Microsoft reserved
/dev/mmcblk1p17 20121600 45254655 25133056 12G Microsoft basic data
/dev/mmcblk1p18 45254656 45778943 524288 256M EFI System
/dev/mmcblk1p19 45778944 60620766 14841823 7.1G Linux filesystem
1
2
3
# 格式化分区
mkfs.vfat -F 32 -n ARCHBOOT /dev/mmcblk1p18
mkfs.ext4 -E lazy_itable_init /dev/mmcblk1p19
1
2
3
4
挂载分区
mount /dev/mmcblk1p19 /mnt
mkdir /mnt/boot
mount /dev/mmcblk1p18 /mnt/boot

arch-root 后安装完后,执行下面命令.

1
2
3
4
5
6
7
# wifi 用
pacman -S wpa_supplicant dialog # wifi-menu
# 启动用
pacman -S grub efibootmgr
# 安装启动 /boot/ 目录是挂载的 EFI 分区
grub-install --target=i386-efi --efi-directory=/boot/ --bootloader-id=GRUB
grub-mkconfig -o /boot/grub/grub.cfg

之后就是重启,然后不停按 ESC 键,之后在 EFI 启动项里,可以看到 GRUB 启动项,选择启动即可,三系统安装完成.

一些坑

1. 重新刷 Android 进不了 DNX Mode ?

按 ESC 选 SCU 进 BIOS 设置,进 BOOT 的 TAB , 关闭 Quick boot .

然后官方的文档是说等显示 DNX Mode 才释放三个按键.事实上,同时按 音量上下加开机键 后,出现字就释放开机键,还保持音量上下键不放,等进入 DNX Mode 再释放.

2. Linux 终端下旋转屏幕 (无X)

https://askubuntu.com/questions/237963/how-do-i-rotate-my-display-when-not-using-an-x-server

echo 1 > /sys/class/graphics/fbcon/rotate

1
2
3
4
0 - Normal rotation
1 - Rotate clockwise
2 - Rotate upside down
3 - Rotate counter-clockwise

X 环境下使用 xrandr

3. 触屏驱动

https://github.com/onitake/gsl-firmware

4. 只安装 Win 或 linux

其实就是普通的电脑一样,删除掉所有分区重新分区即可.

http://www.ondaforum.com/topic/3544-guide-remove-android-and-install-only-windows-8-or-10-v820w/

5. 不错的参考

https://hhuysqt.github.io/ubuntu-tablet/

解析邮件碰到的那些坑

本文主要讲解使用 mail 库解析邮件所碰到的坑.

邮件格式本身的解析由 mail 库.由于邮件格式标准过多且过于复杂,鉴于个人能力有限,所以就不讲解邮件相关的标准的.需要自己先阅读相关资料.比如 RFC 822, multipart 等方面的资料.

坑主要分两大类:

  1. 编码 (修炼的必经之路)
  2. 邮件非常见格式解析 (主要是苹果设备发出来的邮件)
  • 正文只有图片(只包含附件 part, 无 text part 或 html part)
  • 正文有多个文本段(multi text part)
  • multipart 再包含 multipart

mail 基础技巧

  1. 查看 mail 官方文档
  2. Mail.new(str) 的 str 变量,需要为 RFC 822 标准格式
  3. gmail 邮件详情页的 “显示原始邮件” ,下载下来的 original_msg.txt 文件,是 RFC 822 标准,调试时可以直接下载此文件来调试.
  4. iamp 抓取时, imap.uid_fetch(uid, ['RFC822'])[0] 这样可以拿到 RFC 822 格式的内容. 参考来源

编码

编码这个坑与编程语言无关,它是我们修炼必经的路.

  • 世界上有多种字符,比如英文,简体中文,繁体中文.
  • 一种字符有可能有多种编码,比如简体中文有 GB2312, GBK, GB18030 . 参考来源
  • 一种编码有可能有多种实现,比如 Unicode 编码有 UTF-8, UTF-16, UTF-32 多种实现. 参考来源

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# "中" 字不同编码的十六进制值. http://blog.bigbinary.com/2011/07/20/ruby-pack-unpack.html
puts "中".encode('UTF-8').unpack('H*') # e4b8ad
puts "中".encode('UTF-32').unpack('H*') # 0000feff00004e2d
puts "中".encode('GBK').unpack('H*') # d6d0

# 字节数组
puts "中".encode('UTF-8').bytes.inspect # [228, 184, 173]
puts "中".encode('UTF-32').bytes.inspect # [0, 0, 254, 255, 0, 0, 78, 45]
puts "中".encode('GBK').bytes.inspect # [214, 208]

# Base64
require "base64"
puts Base64.encode64('中'.encode('GBK')) # 1tA=
puts Base64.encode64('中'.encode('UTF-8')) # 5Lit

从上示例可以看出,同一个字符,用不同编码时,其二进制数据值有可能不一样.

那么编码的主要问题是什么? 请看代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
require "base64"
str = Base64.decode64('1tA=') # 解码 "中" 的 GBK 编码的 base64 值.
puts str.encoding # ASCII-8BIT, 相当于是一个字节数组(byte array, 1byte = 8bit)
puts str.bytes.inspect # [214, 208] , 等于上示例的 "中".encode('GBK').bytes.inspect .也就是说变量的在内存里的二进制值还是 GBK 编码.
puts str # 打印出乱码. 因为终端一般设置的编码为 UTF-8 ,如果想要此语句不显示成乱码,把终端编码改成 GBK 即可.记得改回 UTF-8.

puts str.force_encoding('GBK').bytes.inspect # [214, 208], 字节还是 GBK 未变
puts str.force_encoding('GBK').encode('UTF-8') # 中 force_encoding 只是改变变量的编码元信息. encode 把变量的字节从 GBK 变成 UTF-8 . 这样打印就不乱码了.

puts str.force_encoding('BIG5').bytes.inspect # [214, 208], 字节还是 GBK 未变
puts str.force_encoding('BIG5').encode('UTF-8') # 笢 同样的字节数据,在繁体 BIG5 编码里有效且是另外一个字符.

str.encode('UTF-8') # 报错.因为不知道字节的编码信息,有可能默认编码转换映射是从 ASCII to UTF-8(待考证)

把一个字符转成对应编码的字节不难,把一个已知编码信息的字节转成对应字符也不难.

难在把一个不知道编码信息的二进制数据,转成对应的字符. [214, 208] 是 GBK 编码里的 “中” 字,同时也是 BIG5 编码里面的 “笢” 字.

上示例就是演示了邮件里碰到的编码问题,当正文经过 base64 编码后.收到邮件后, base64 解码出来的二进制数据,到底是 GBK, 还是 BIG5 ,还是其它编码?

所幸的是,大部分情况我们都不需要靠猜, 一般邮件正文 part 都有这样一段 header 信息. 其 Content-Type 的 charset 就告诉了我们这段 base64 解码后的二进制是什么编码.

1
2
3
MIME-Version: 1.0
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: base64

代码示例:

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
require 'mail' # 需要先安装 mail gem
# 构建 RFC822 标准的邮件字符串
mail = Mail.new do
from '[email protected]'
to '[email protected]'
subject 'This is a test email'
content_type 'text/plain; charset=GBK'
body "中".encode('GBK')
end

original_msg = mail.to_s
=begin
Date: Sat, 19 Aug 2017 13:39:10 +0800
From: mikel@test.lindsaar.net
To: you@test.lindsaar.net
Message-ID: <5997cefe17bb6_5ded1e74693bc8972ac@hparch.mail>
Subject: This is a test email
Mime-Version: 1.0
Content-Type: text/plain;
charset=GBK
Content-Transfer-Encoding: base64

1tA=
=end
puts original_msg

# 开始解析
puts "*" * 42
mail = Mail.new(original_msg)
body = mail.body.decoded
puts body # 乱码
puts body.encoding # ASCII-8BIT
puts body.force_encoding(mail.charset).encode('UTF-8') # 中
# 需要检查 charset 是否存在,通过 Encoding.find 方法

理想的世界, content_type 是带了 charset .但现实与理想总是存在差距.有些没有带 charset ,有些甚至连 content_type 整行都没有.

这个时候编码就要靠猜了, charlock_holmes 就是干这事的.

但这个只有在正文很多的时候才会有可能猜的准.

1
2
3
4
5
6
7
8
puts CharlockHolmes::EncodingDetector.detect('中文测试很长的文字'.encode('GBK').force_encoding('ASCII-8BIT'))
# {:type=>:text, :encoding=>"UTF-16BE", :ruby_encoding=>"UTF-16BE", :confidence=>10}

puts CharlockHolmes::EncodingDetector.detect('遍身罗绮者 不是养蚕人'.encode('GBK').force_encoding('ASCII-8BIT'))
# {:type=>:text, :encoding=>"ISO-8859-6", :ruby_encoding=>"ISO-8859-6", :confidence=>16, :language=>"ar"}

puts CharlockHolmes::EncodingDetector.detect('中文测试,工要在地一上是中国;'.encode('GBK').force_encoding('ASCII-8BIT'))
# {:type=>:text, :encoding=>"GB18030", :ruby_encoding=>"GB18030", :confidence=>100, :language=>"zh"}

所以,优先以 content_type 的 charset 去解码, charlock_holmes 只是最后方案.

事实上 encode 方法有个坑,就是有可能 encode 碰到无效字节,会导致报错. 推荐加上 invalid 和 undef 参数. replace 默认是替换成问号.还可以直接删除无效字节,推荐使用替换.

1
"abc".encode('UTF-8', invalid: :replace, undef: :replace)

邮件非常见格式解析

1. 正文只有图片

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
require 'mail' # 需要先安装 mail gem
require 'open-uri'
# 构建 RFC822 标准的邮件字符串
mail = Mail.new do
from '[email protected]'
to '[email protected]'
subject 'This is a test email'
content_type 'image/png; filename=One_black_Pixel.png'
body open('https://upload.wikimedia.org/wikipedia/en/4/45/One_black_Pixel.png').read
end

original_msg = mail.to_s
=begin
Date: Sat, 19 Aug 2017 14:25:48 +0800
From: mikel@test.lindsaar.net
To: you@test.lindsaar.net
Message-ID: <5997d9ec98315_695f2a4c229bd097632@hparch.mail>
Subject: This is a test email
Mime-Version: 1.0
Content-Type: image/png;
filename=One_black_Pixel.png
Content-Transfer-Encoding: base64

iVBORwoaCgAAAApJSERSAAAAAQAAAAEIAgAAAJB3U94AAAABc1JHQgCuzhzp
AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwwAADsMBx2+oZAAAAAxJREFU
GFdjYGBgAAAABAABXM3/aQAAAABJRU5ErkJggg==
=end
puts original_msg

# 开始解析
puts "*" * 42
mail = Mail.new(original_msg)
puts mail.body # 正文是乱码
puts mail.attachment? # true

对于这种只有图片的邮件,我们先用 attachment? 方法判断是不是附件,是附件的话,按附件的逻辑处理,比如保存到本地.

2. 正文有多个文本段

此非常见格式的邮件一般是苹果设备自带的邮件客户端发出来的.

苹果邮件客户端这么做是为了实现在纯文本格式邮件插入图片的上下环绕效果.

主流的做法纯文本不插入图片,图片只作为普通附件存在.要插入图片,使用 html 格式,通过 html img 标签来实现,img src 填图片附件的 cid .

这样的格式,在 gmail 显示不出环绕效果,只作为普通附件显示. 在苹果的客户端可以显示.

MailCatcher 这个接收测试工具,和我开始一样,以为一个邮件只有一个 text part,所以导致这种邮件只会显示部分文本.

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
require 'mail' # 需要先安装 mail gem
require 'open-uri'
# 构建 RFC822 标准的邮件字符串
mail = Mail.new do
from '[email protected]'
to '[email protected]'
subject 'This is a test email'
part :content_type => "multipart/alternative", :content_disposition => "inline" do |p|
p.part body: "abc"
p.part content_type: 'image/png; filename=One_black_Pixel.png', body: open('https://upload.wikimedia.org/wikipedia/en/4/45/One_black_Pixel.png').read
p.part body: "def"
end
end

original_msg = mail.to_s
=begin
Date: Sat, 19 Aug 2017 14:58:30 +0800
From: mikel@test.lindsaar.net
To: you@test.lindsaar.net
Message-ID: <5997e19678f0f_70774d198d1bc8306db@hparch.mail>
Subject: This is a test email
Mime-Version: 1.0
Content-Type: multipart/mixed;
boundary="--==_mimepart_5997e19675b41_70774d198d1bc8305cd";
charset=UTF-8
Content-Transfer-Encoding: 7bit


----==_mimepart_5997e19675b41_70774d198d1bc8305cd
Content-Type: multipart/alternative;
boundary="--==_mimepart_5997e1959f3c7_70774d198d1bc830458";
charset=UTF-8
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
Content-ID: <5997e1967a6a3_70774d198d1bc830787@hparch.mail>


----==_mimepart_5997e1959f3c7_70774d198d1bc830458
Content-Type: text/plain;
charset=UTF-8
Content-Transfer-Encoding: 7bit

abc
----==_mimepart_5997e1959f3c7_70774d198d1bc830458
Content-Type: text/plain;
charset=UTF-8
Content-Transfer-Encoding: 7bit

def
----==_mimepart_5997e1959f3c7_70774d198d1bc830458
Content-Type: image/png;
filename=One_black_Pixel.png
Content-Transfer-Encoding: base64

iVBORwoaCgAAAApJSERSAAAAAQAAAAEIAgAAAJB3U94AAAABc1JHQgCuzhzp
AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwwAADsMBx2+oZAAAAAxJREFU
GFdjYGBgAAAABAABXM3/aQAAAABJRU5ErkJggg==

----==_mimepart_5997e1959f3c7_70774d198d1bc830458--

----==_mimepart_5997e19675b41_70774d198d1bc8305cd--
=end
puts original_msg

# 开始解析
puts "*" * 42
mail = Mail.new(original_msg)
puts mail.multipart?
puts mail.text_part.body # 默认只取了第一个 text part

puts "*" * 42
# 通过 all_parts 拿到所有 part, 包含本身.
mail.all_parts.each do |part|
# 需要排除 multipart , attachment, 生产代码还需要区分 text 还是 html. text 和 text 加在一起, html 和 html 加在一起.
# 这里还有一个大坑,就是多 text part 字符拼接时,一定要先把编码转成 utf-8 .因为苹果设备如果刚好那部分只有英文,那么编码为 ASCII, 如果有中文,编码为 GBK .
# 有兴趣的朋友可以用苹果邮件客户端自己测试一下
puts part.body if !part.multipart? && !part.attachment?
end

multipart 再包含 multipart

这种情景主要出现在苹果邮件客户端同时发送 text 和 html 格式的. html 是一个 sub multipart.

处理方法同上, all_parts 会自动遍历 sub multipart . 我们只要排除 multipart? 和 attachment? 即可.

使用 Meta Generator 打造你的 Rails Admin

按个人理解, Admin Interfaces 的主要作用是减少后台管理界面的 CRUD 开发的重复工作量,并提供登录注销等常见功能的实现.

Admin Interfaces 实现主要分两大块.

  1. 基于继承和配置.
  • 代表: Django Admin(最著名的), ActiveAdmin.
  • 优点: 代码量少.
  • 缺点: 定制难度高.
  1. 基于代码生成.
  • 代表: Rails Scaffold(自带的太简单了).
  • 优点: 代码生成在项目里,定制只要直接修改代码即可,非常灵活.
  • 缺点: 写自定义代码生成器有点难度.
  1. 同时使用代码生成和继承方式.
  • 代表: ActiveScaffold
  • 优点: 在减少代码的同时也保证了定制的灵活性.
  • 缺点: 同上

作为有一个有追求的人,虽然已经 ActiveAdmin 和 ActiveScaffold 这样不错 Rails Admin Interfaces.但为了追求定制的灵活性的最大化,必须得自己造个轮子出来,哪怕是方的轮子.

什么是 Meta Generator?

就是写个 Generator A, Generator A 生成一个 Generator B 到你的项目里, 平常你主要运行 Generator B 生成 CRUD 相关代码. 这里的 Generator A 就是 Meta Generator.

Meta Generator 示例

首先,请认真读完 Creating and Customizing Rails Generators & Templates .

文中的 bin/rails generate generator initializer 的 generator 即是一个 Meta Generator,利用 generator 产生的代码存放在你的 Rails 项目的 lib/generators ,这样我们就可以很方便的修改. 我们只要参考 generator ,写一个类似的 Gem ,把代码生成到 lib/generators 目录即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
cd your_workspace # 修改成你自己的目录
bundle exec rails plugin new meta_generator_demo # 创建一个 Rails Engine 的 Gem 项目
cd meta_generator_demo
vi meta_generator_demo.gemspec # 把里面的带 TODO 的都改成你准备填写的信息
mkdir -p lib/generators


cd your_rails_project
bundle exec rails g generator meta_generator_demo # 创建一个生成器
mv lib/generators/meta_generator_demo your_workspace/meta_generator_demo/lib/generators/meta_generator_demo # 修改成你自己的目录

cd your_workspace/meta_generator_demo # 修改成你自己的目录
vi lib/generators/meta_generator_demo/meta_generator_demo_generator.rb # 文件内容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# lib/generators/meta_generator_demo/meta_generator_demo_generator.rb
class MetaGeneratorDemoGenerator < Rails::Generators::NamedBase
source_root File.expand_path('../templates', __FILE__)

def create_demo_file
create_file 'lib/generators/demo_generator.rb', <<-EOS
class DemoGenerator < Rails::Generators::Base
desc "This generator creates an model file at app/models"
def create_demo_file
create_file "app/models/demo.rb", "class Demo; end"
end
end
EOS
end
end

Meta Generator 已经编写完成,可以测试了.

1
2
3
4
5
6
7
8
cd your_rails_project
vi Gemfile # 添加 "gem 'meta_generator_demo', path: 'your_workspace/meta_generator_demo'" 到 Gemfile 里面,不包含 " 符号.
bundle install
bundle exec rails g # 是不是可以看到 meta_generator_demo 这个选项了?
bundle exec rails g meta_generator_demo Demo
bundle exec rails g # 是不是可以看到 demo 这个选项了?
bundle exec rails g demo # 是不是可以看到 demo 这个选项了?
cat app/models/demo.rb # 最终生成的文件, 如果需要改 demo.rb 生成的内容,只需要改 Rails 项目里的 lib/generators/demo_generator.rb 文件即可.

总结

  1. Gem 的 lib/generators/meta_generator_demo/meta_generator_demo_generator.rb 文件有个 create_demo_file 方法, 此方法在你的 Rails 项目生成 lib/generators/demo_generator.rb 文件.
  2. 生成的 lib/generators/demo_generator.rb 文件有个 create_demo_file 方法,此方法创建了 app/models.demo.rb 文件.

Rails Admin Generator 示例

示例项目: https://github.com/adminonrails/aor

此项目已经弃坑,作为示例提供参考. 抛砖引玉,希望高手们能定制出更加适合自己的方案

挖了这个坑,后面因为做的项目都是前后端分离,就没有再填了. 另外觉得这个项目也比较鸡肋,对新手不友好,上手和定制有点难度.对高手来说,有自己的一套方式,一般是自己挖个坑.

大部分功能都有写单元测试,项目有在生产环境使用过.

项目介绍:

  • bootstrap: bootstrap 的静态资源,无 sass 依赖.
  • authentication: 提供登录验证的一些辅助方法,源码就一个文件. 主要提供 logged_in?, current_user 等方法. 参考老版 publify .
  • authorization: 基于 cancancan 提供后台权限验证的一些辅助方法. 主要基于 controller_name 和 action_name 来限制. 参考 spree 的后台验证逻辑.
  • theme: 代码生成器

使用请参考 test 和 dummy 目录测试代码.

theme 详解:

https://github.com/adminonrails/aor/blob/master/theme/Rakefile

上文件 DummyGenerator 部分,是一个新 rails 项目使用 aor 的主要流程, 这里是用来每次运行 dummy rails 测试项目,先生成最新的 aor 代码.

  1. 生成 kaminari 的 bootstrap3 模板.
  2. 添加 cancan 的 AdmAbility 文件.
  3. 运行 aor:theme
  4. 生成 admin user model.

https://github.com/adminonrails/aor/blob/master/theme/lib/generators/aor/theme/theme_generator.rb

上文件主要在安装了 aor-theme gem,运行 aor:theme 命令的代码.

  1. 复制 admin js 和 css 文件.
  2. 添加公共的头部,侧边,表单验证错误提示文件.
  3. 添加 base admin controller 和 helper 文件.
  4. 把 admin.js 和 admin.css 添加到 assets 里,这样编译 js 和 css 会单独生成 admin 文件.
  5. 生成表单验证错误提示的 bootstrap 样式.
    6 复制项目的 admin generator 到当前 rails 的 lib/generators 目录.

https://github.com/adminonrails/aor/tree/master/theme/lib/generators/aor/theme/templates/generator

此目录的文件,主要增强 rails 自带的 scaffold, scaffold_controller . 我们不覆盖 rails scaffold,只是添加一个自己的 admin:scaffold . 使用时运行 rails g admin:scaffold .

里面的 rb 文件逻辑,主要是修改 scaffold 的 source_paths 路径,优先使用我们的 controller 和 views 模板.

子目录 erb 和 rails 即是模板.

总结

此项目混合两种方式,一种是通过代码继承,子类通过重写父类方法来实现自定义.一种是生成代码,再修改生成的生成器代码,来实现自定义.

使用多个路由器有线桥接实现无线漫游

虽然 WDS 是平价的无线漫游实现最优先考虑的方案.但 WDS 几个路由之间的通信还是无线,无线没有有线稳定这是不争的事实.

使用有线桥接来实现无线漫游,各个路由之间的通信通过有线,相对来说理论上要稳定些.

示例:

三个路由器,一主二从.

两个也行,我这只是演示,表示接两个三个从路由都是没问题的.

1. 主路由设置

主路由和普通路由设置没有差别, Wan 口接光猫, Lan 接电脑和从路由器.

主路由为 openwrt 系统,其 Lan IP 为 192.168.2.1 .

我的主路由设置截图:

master router

2. 从路由 1 设置

所有 从路由要关闭 DHCP 服务器 ,并 Lan IP 要设置为与主路由的 Lan IP 同网段.

从路由的 Wan 无需要接网线, 主路由接根网线直接插到从路由的 Lan 上 .

从路由 1 为 openwrt 系统, 其 Lan IP 为 192.168.2.5 .

从路由器 Lan 口设置截图:

slave router 1

  • 圈1 为 Lan 的 IP ,通过此 IP 可以访问到你的从路由器.
  • 圈2 和 圈3 设置从路由器的网关和DNS,不设置从路由器不能上网,但接入从路由器的设备能上网.
  • 圈4 为关闭 Lan 口的 DHCP 服务器.

3. 从路由 2 设置

从路由 2 设置和 从路由 1 设置没什么差别,我这边只是给了一个 TP-LINK 的路由来演示,表示路由器不一定要用 openwrt 系统.

Wan 不接网线, 主路由过来的网线接在 Lan 口上. 设置 Lan 口 IP 和关闭 Lan DHCP 服务器.

slave router 2

4. 结尾

主要设置已经完成,之后配置 Wifi 为相同的 SSID 和 密码就行了.注意信道不要一样.推荐使用 1 6 11信道.比如我的主路由的信道是1 ,从路由 1 的信道是 6 ,从路由 2 的信道是 11 .

这样就低成本的实现高质量的无线漫游了.

原理

主要把从路由的 Lan 区当交换机用,只是比普通的交换机多了支持无线的接入,而无线接入进来还是在 Lan 的网段.

为什么要关闭 DHCP ? 因为从路由器都是当交换机,这样所有的 IP 都是从主路由的 DHCP 获取,这样获取的 IP 都是一样的,并且网关还是主路由器的 LAN IP .

网上有说有些路由器不支持把 LAN 口当交换机使用,感觉现在的路由应该都支持这么基础的功能吧.

参考资料

https://wiki.openwrt.org/doc/recipes/bridgedclient

上文提到要关闭防火墙,个人测试不关也没关系.

https://wiki.openwrt.org/doc/howto/wide.area.wifi

上文提到多个路由要用不同信道.

Linux 软件阵列与低端硬件阵列卡性能对比

本文主要测试 linux 软件阵列卡与 LSI SAS 9211-8i 硬件阵列卡做raid 0 的性能.

测试不太严谨,结果仅供参考.

测试结果:

result

从结果可以看出,低端的硬件阵列卡性能还不如软件阵列.

软件阵列顺序写 Block 的时候,大概比硬件阵列要快 4M/s 的样子. Rewrite 都要快 38M/s . 顺序读 Block 要快 168M/s .

但是软件阵列确实比硬件阵列消耗 CPU 些. 很多测试的结果软件阵列大概比硬件阵列多用了 5%~10% 的 CPU.

测试环境:

CPU: Pentium E5400
内存: 4G (可用 3782 M)
系统: Centos 7.2.1511
硬盘: 4个同型号的 2TB 的硬盘,两个用来组硬件阵列,两个用来组软阵列.

LSI SAS 9211-8i 之所以称为低端阵列卡,因为阵列卡本身无缓存.

测试工具: bonnie++

相关信息:

列出所有硬盘:

[root@host220 ~]# fdisk -l
Disk /dev/sdb: 3998.0 GB, 3997997989888 bytes, 7808589824 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes
Disk label type: dos
Disk identifier: 0x00000000

   Device Boot      Start         End      Blocks   Id  System
/dev/sdb1               1  4294967295  2147483647+  ee  GPT
Partition 1 does not start on physical sector boundary.

Disk /dev/sdc: 2000.4 GB, 2000398934016 bytes, 3907029168 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes


Disk /dev/sdd: 2000.4 GB, 2000398934016 bytes, 3907029168 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes

sdb 为由硬件阵列卡组的 raid0 ,两个 2TB,所以 sdb 大小 将近有 4TB.
sdc sdd 也是插在阵列卡上,但是直通的,所以直接显示 2TB 一个.

查看软阵列信息:

[root@host220 ~]# cat /proc/mdstat 
Personalities : [raid0] 
md0 : active raid0 sdd[1] sdc[0]
      3906766848 blocks super 1.2 512k chunks
      
unused devices: <none>

可以看到由 sdc sdd 两个硬盘组建成 md0 这个软 raid0 阵列.

查看挂载信息:

[root@host220 ~]# df -h | grep -E 'mnt|Mount'
Filesystem               Size  Used Avail Use% Mounted on
/dev/md0p1               3.6T   89M  3.4T   1% /mnt/raidtest/soft
/dev/sdb1                3.6T   89M  3.4T   1% /mnt/raidtest/hw

soft 文件夹挂载的是软阵列.hw 文件夹挂载的是硬阵列.

硬件阵列卡测试结果:

[root@host220 ~]# bonnie++ -u root -d /mnt/raidtest/hw/
Using uid:0, gid:0.
Writing a byte at a time...done
Writing intelligently...done
Rewriting...done
Reading a byte at a time...done
Reading intelligently...done
start 'em...done...done...done...done...done...
Create files in sequential order...done.
Stat files in sequential order...done.
Delete files in sequential order...done.
Create files in random order...done.
Stat files in random order...done.
Delete files in random order...done.
Version  1.96       ------Sequential Output------ --Sequential Input- --Random-
Concurrency   1     -Per Chr- --Block-- -Rewrite- -Per Chr- --Block-- --Seeks--
Machine        Size K/sec %CP K/sec %CP K/sec %CP K/sec %CP K/sec %CP  /sec %CP
host220.cs.lo 7560M   283  99 298715  53 102106  21  2699  98 190370  16 388.6   8
Latency             30693us     253ms     758ms   17693us     246ms     372ms
Version  1.96       ------Sequential Create------ --------Random Create--------
host220.cs.local    -Create-- --Read--- -Delete-- -Create-- --Read--- -Delete--
              files  /sec %CP  /sec %CP  /sec %CP  /sec %CP  /sec %CP  /sec %CP
                 16 28154  91 +++++ +++ +++++ +++ 31745  90 +++++ +++ +++++ +++
Latency               169us     534us     554us      99us      29us      61us
1.96,1.96,host220.cs.local,1,1476076832,7560M,,283,99,298715,53,102106,21,2699,98,190370,16,388.6,8,16,,,,,28154,91,+++++,+++,+++++,+++,31745,90,+++++,+++,+++++,+++,30693us,253ms,758ms,17693us,246ms,372ms,169us,534us,554us,99us,29us,61us

防作弊截图

软件阵列卡测试结果:

[root@host220 ~]# bonnie++ -u root -d /mnt/raidtest/soft/
Using uid:0, gid:0.
Writing a byte at a time...done
Writing intelligently...done
Rewriting...done
Reading a byte at a time...done
Reading intelligently...done
start 'em...done...done...done...done...done...
Create files in sequential order...done.
Stat files in sequential order...done.
Delete files in sequential order...done.
Create files in random order...done.
Stat files in random order...done.
Delete files in random order...done.
Version  1.96       ------Sequential Output------ --Sequential Input- --Random-
Concurrency   1     -Per Chr- --Block-- -Rewrite- -Per Chr- --Block-- --Seeks--
Machine        Size K/sec %CP K/sec %CP K/sec %CP K/sec %CP K/sec %CP  /sec %CP
host220.cs.lo 7560M   590  99 302015  50 140381  26  2738  96 359120  26 631.2  12
Latency             29796us     444ms     302ms   21224us     225ms     572ms
Version  1.96       ------Sequential Create------ --------Random Create--------
host220.cs.local    -Create-- --Read--- -Delete-- -Create-- --Read--- -Delete--
              files  /sec %CP  /sec %CP  /sec %CP  /sec %CP  /sec %CP  /sec %CP
                 16 27982  89 +++++ +++ +++++ +++ 31718  88 +++++ +++ +++++ +++
Latency               164us     432us     468us      94us      30us      59us
1.96,1.96,host220.cs.local,1,1476076562,7560M,,590,99,302015,50,140381,26,2738,96,359120,26,631.2,12,16,,,,,27982,89,+++++,+++,+++++,+++,31718,88,+++++,+++,+++++,+++,29796us,444ms,302ms,21224us,225ms,572ms,164us,432us,468us,94us,30us,59us

防作弊截图

结果整理

bonnie++ 的命令输出的最后一行是 csv 的行,我们把它们都写到 /tmp/a.csv 文件里,再调用 cat a.csv | bon_csv2html .这样就把结果通过 html 表格来展示了.

bonnie++ 的结果解析请看参考此两篇文章.

Sequential Output部分表示写文件的相关信息
Sequential Input部分表示读文件的相关信息
Per Chr表示以字符为单位读写文件
Block表示以block为单位读写文件
Rewrite表示修改并重写已经存在的文件的每一个block
K/sec表示每秒读或写文件的速率,以K为单位
%CP表示在某阶段执行操作时平均消耗的CPU

Sequential Create和Radom Create 这两大类测试均是用创建,读取,删除大量的小文件来测试磁盘效率。

使用 Swagger 为你的 HTTP API 写文档

尝试过用 Wiki 和 Swagger 等工具写基于 HTTP 协议的 API 的 文档,虽然有提供 curl 示例,但接口调用者使用起来还是觉得不方便,毕竟不是所有人都习惯命令行.

直到了解到 Swagger, 简直发现了写 HTTP API 文档的神器啊. 现已捐赠给 Open API Initiative (OAI) , 和 OpenAPI 2.0 Specification 合并了.

Swagger 简介

详情介绍可以直接看官网.

按个人的理解. Swagger 提供一种简单的方式为 HTTP API 写文档,同时又方便 API 调用者测试.

Swagger 本身是由标准与工具组成的.

Swagger 标准

现已和 OpenAPI 2.0 Specification 是同一个标准了.

这个标准,有点像是 XML 和 XML Schema 的关系.

XML 是非结构化数据,根本就不清楚 A 接点下面包含的是 B 还是 C 接口. 而 XML Schema 就是用来告诉我们, A 接点下面包含什么接点,同时还支持数据验证等功能.

我们写出来的 HTTP API 接口,如果没有文档,调用者根本就知道要传什么参数,返回什么数据. 而 OpenAPI Specification 就是这样一种标准,告诉我们应该怎样描述我们的接口,描述接口要传什么参数,返回什么数据.

OpenAPI Specification 最终是 JSON 或 YAML 数据格式表示, Specification 本身是告诉我们应该生成怎样的 JSON 或 YAML 数据.

主要描述请求的主机是什么,路径是什么,请求是 GET 还是 POST 等; 传参是 QUERY STRING 还是 BODY 等, 需要传什么头,返回什么头. 返回的数据是什么格式.

Swagger 工具

  • Swagger UI: 把 Swagger 标准的 JSON 数据,显示成友好可操作的 HTML 文档,方便调用者查看与调试接口.
  • Swagger Editor: 一个在线 YAML 编辑器,方便编写 Swagger 标准的接口描述数据,并能生成JSON格式的数据,同时能生成本地客户端,方便文档分发.
  • Sdk Generators: 根据 Swagger 标准的数据生成接口代码.

在 Rails 里使用 Swagger

上面三个工具,只用到 Swagger UI, 用它把写的接口描述JSON数据显示成友好的 HTML 界面.

ruby 中,如果用 grape 写 HTTP API,那配合 grape-swagger ,可以同步生成好文档,非常方便.

但个人习惯 Rails 了,觉得用 grape 要自己管理数据迁移脚本之类的,太麻烦了.

Ruby 里面的 Swagger 库我选 Swagger::Blocks ,纯 Ruby 实现,代码只有一个文件,700多行,简单. 其本身只是一个生成 Swagger 标准的 JSON 数据的 DSL . 调用 Swagger::Blocks.build_root_json 方法,最终生成的只是 json 字符串而已.与 Web 框架无关,只要把此 json 数据做为 response 数据返回即可.

下面创建一个 Rails 项目,对 Swagger 主要点进行演示,算是个人踩过坑后的一点心得.

1. 创建 Rails 演示项目

参考 Getting Started with Rails 创建一个 Rails 项目,并带有简单的 CURD 接口.

gem install rails 
rails new -B swagger_demo
cd swagger_demo
vi Gemfile # 编辑 Gemfile 文件,把第一行的 https://rubygems.org 替换成 https://gems.ruby-china.org
bundle install
bundle exec rails g scaffold Article title:string text:text # 生成 Article 的 model 和 controller
bundle exec rake db:create db:migrate # 创建数据库,运行迁移

代码: https://github.com/mangege/swagger_demo/tree/step1

2. 建立 swagger 初始文件

vi Gemfile # 添加 gem 'swagger-blocks' 到最后一行
bundle install
bundle exec rails g controller Apidocs index
vi app/controllers/apidocs_controller.rb # 复制此段内容 https://github.com/fotinakis/swagger-blocks#docs-controller ,还需要再编辑此文件内容,最终请看仓库代码
vi config/routes.rb # 删除掉 get 'apidocs/index' ,添加 resources :apidocs, only: [:index]
cd /tmp; git clone https://github.com/swagger-api/swagger-ui.git
cp -R /tmp/swagger-ui/dist ~/workspace/swagger_demo/public/ # 复制 swagger 的静态文件到 rails 项目的 public 目录下.
cd ~/workspace/swagger_demo/public/; mv dist swagger-ui # 重命名 dist 文件夹为 swagger-ui
vi public/swagger-ui/index.html # 替换 http://petstore.swagger.io/v2/swagger.json 为 /apidocs.json

打开浏览器,访问 http://localhost:3000/swagger-ui/ 即可.

代码: https://github.com/mangege/swagger_demo/tree/step2

3. 为 Article 接口添加文档

按照 Swagger::Blocks 的示例,一般是在 model 或 controller 文件里写文档.但这样有可能导致 model 或 controller 文件行数过长.

通过分析源码了解,我们随便建一个类也可以.所以我们在 app 目录下建立专门的 swagger 目录.

mkdir app/swagger
vi config/application.rb # 添加 config.autoload_paths << Rails.root.join('app/swagger') ,改了此文件记得重启 rails server

vi app/swagger/app/swagger/article_swagger.rb

添加以下内容

class ArticleSwagger
  include Swagger::Blocks

  swagger_schema :Article do
    key :required, [:id]
    property :id do
      key :type, :integer
    end
    property :title do
      key :type, :string
      key :description, '标题'
    end
    property :text do
      key :type, :string
      key :description, '正文'
    end
  end
end

vi app/controllers/apidocs_controller.rb # 在 SWAGGERED_CLASSES 添加 ArticleSwagger

vi app/swagger/articles_controller_swagger.rb

添加以下内容

class ArticlesControllerSwagger
  include Swagger::Blocks

  swagger_path '/articles' do
	operation :get do
	  key :description, 'article list'
	  key :operationId, 'articleIndex'
	  key :tags, [
		'article'
	  ]
	  response 200 do
		key :description, 'article response'
		schema do
		  key :type, :array
		  items do
			key :'$ref', :Article
		  end
		end
	  end
	  response :default do
		key :description, 'unexpected error'
		schema do
		  key :'$ref', :ErrorModel
		end
	  end
	end
	operation :post do
	  key :description, 'create article'
	  key :operationId, 'articleCreate'
	  key :tags, [
		'article'
	  ]
	  parameter do
		key :name, :article
		key :in, :body
		key :required, true
		schema do
		  key :'$ref', :Article
		end
	  end
	  response 200 do
		key :description, 'article response'
		schema do
		  key :'$ref', :Article
		end
	  end
	  response :default do
		key :description, 'unexpected error'
		schema do
		  key :'$ref', :ErrorModel
		end
	  end
	end
  end

  swagger_path '/articles/{id}' do
	operation :get do
	  key :description, 'article show'
	  key :operationId, 'articleShow'
	  key :tags, [
		'article'
	  ]
      parameter do
        key :name, :id
        key :in, :path
        key :required, true
        key :type, :integer
      end
	  response 200 do
		key :description, 'article response'
		schema do
          key :'$ref', :Article
		end
	  end
	  response :default do
		key :description, 'unexpected error'
		schema do
		  key :'$ref', :ErrorModel
		end
	  end
	end
	operation :patch do
	  key :description, 'article update'
	  key :operationId, 'articleUpdate'
	  key :tags, [
		'article'
	  ]
      parameter do
        key :name, :id
        key :in, :path
        key :required, true
        key :type, :integer
      end
	  parameter do
		key :name, :article
		key :in, :body
		key :required, true
		schema do
		  key :'$ref', :Article
		end
	  end
	  response 200 do
		key :description, 'article response'
		schema do
          key :'$ref', :Article
		end
	  end
	  response :default do
		key :description, 'unexpected error'
		schema do
		  key :'$ref', :ErrorModel
		end
	  end
	end
	operation :delete do
	  key :description, 'article destroy'
	  key :operationId, 'articleDestroy'
	  key :tags, [
		'article'
	  ]
      parameter do
        key :name, :id
        key :in, :path
        key :required, true
        key :type, :integer
      end
	  response 204 do
		schema do
		  key :type, :string
		end
	  end
	  response :default do
		key :description, 'unexpected error'
		schema do
		  key :'$ref', :ErrorModel
		end
	  end
	end
  end
end

vi app/swagger/error_model_swagger.rb

添加以下内容

module ErrorModelSwagger
  include Swagger::Blocks

  swagger_schema :ErrorModel do
    key :description, '错误定义'
    key :required, [:code, :message]
    property :code do
      key :type, :integer
      key :description, '错误代码. 401 没有登录, 403 没有权限, 422 表单数据有误'
    end
    property :message do
      key :type, :string
      key :description, '错误消息'
    end
    property :errors do
      key :type, :object
      key :description, '错误详情. 键为出错的属性名.值为出错信息,值是字符串数组.'
    end
  end

end

vi app/controllers/apidocs_controller.rb # 在 SWAGGERED_CLASSES 添加 ErrorModelSwagger 和 ArticlesControllerSwagger

vi app/controllers/application_controller.rb # 替换 exception 为 null_session ,注意,如果项目还有普通的web页面,不要把此改成 null_session ,而是新建一个 api_controller.rb 文件,在新建的文件里设置为 null_session ,然后所有的 api controller 都继承与它.

好了, Article 的 CRUD 操作的接口文档都已经编写完成,现在我们打开浏览器,访问 http://localhost:3000/swagger-ui/ ,即可通过 swagger ui 来阅读文档,并测试接口了.

代码: https://github.com/mangege/swagger_demo/tree/step3

前端第三方登录,后端Token验证备忘(Google 与 Facebook)

现在前后端分离比较流行,如果项目集成第三方登录,一般集成也是由前端完成.

前端把 user_id 与 token 相关信息传给后端,后端需要验证 user_id 与 token 的有效性,否则伪造新用户的成本非常低.

本文主要讲 google 与 facebook 的登录的 token 验证. 如果需要看国内第三方登录的,可以直接略过此文.

Google 篇

前端集成可以看此文档 https://developers.google.com/identity/sign-in/web/sign-in .

本文已经相关的 html 复制保存到 gist https://gist.github.com/mangege/ff9a41ff2898cf19f88070e2945519c7#file-g-html ,把页面上的 google-signin-client_id 值改成你的即可.

之后用 python2 -m SimpleHTTPServer 运行一个 web server , 访问 http://localhost:8000/g.html .

登录后可以在浏览器的控制台看到 id_token 的值, 复制出来备用.

后端验证 Google 的文档,找的好辛苦. https://developers.google.com/identity/sign-in/web/backend-auth

验证很简单, 替换此连接的 XYZ123 为之前复制的 id_token https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=XYZ123 ,直接到浏览器访问即可. 或使用 curl 命令.

Example

我们主要要验证结果的 aud 与 sub , aud 保证此 token 为你的站点生成, sub 保证 user_id 的正确性.

Facebook 篇

前端集成可以看此文档 https://developers.facebook.com/docs/facebook-login/web .

gist https://gist.github.com/mangege/ff9a41ff2898cf19f88070e2945519c7#file-fb-html

后端验证 Facebook 的文档没找到直接的,是翻到 debug_token 这个接口觉得符合需求,就用它了.

Facebook PHP SDK 有提到与 JS 结合 https://developers.facebook.com/docs/php/howto/example_access_token_from_javascript .

看了一下 PHP SDK 的实现, 主要通过读取 cookies 里面 fbsr_ 的值来进行验证 https://github.com/facebook/facebook-php-sdk-v4/blob/master/src/Facebook/Helpers/FacebookSignedRequestFromInputHelper.php#L158 . 太复杂,想找简单的接口实现.

最初的想法是直接拿到前端的 access token, 再后端去调用 /me 接口,成功则有效. 后来仔细考虑过.这也会导致有安全漏洞.比如某站长拿自己网站的用户 access_token 来登录,这样是有效的,因为 /me 接口无法验证 access_token 的来源.

后来在这里看到 debug_token https://developers.facebook.com/docs/facebook-login/access-tokens/debugging-and-error-handling ,觉得返回很适合用来做验证.

对应的 graph api 文档 https://developers.facebook.com/docs/graph-api/reference/v2.6/debug_token

因为我们把 用户的 access_token 作为 input_token 传给了 /debug_token 接口,所以调用 /debug_token 接口我们得传应用的 access_token .
应用的 access_token 有两种方式生成,一种是通过接口生成,另外一种是把 app_id 与 app_secret 拼接在一起作为 access_token 传给接口.
拼接的这种方式简单,我们就是拼接. https://developers.facebook.com/docs/facebook-login/access-tokens#apptokens

最终我们只要把页面拿到的 access_token 和拼接出来的 access_token 传给 /debug_token 接口即可.

Example

最终 Ruby 代码

gist : https://gist.github.com/mangege/ff9a41ff2898cf19f88070e2945519c7#file-auth_token_util-rb

这样就不用安装 google 或 facebook 的 sdk.