0%

最近发现 Mac 中的磁盘剩余空间不足,使用 df -h 命令一看,确实满了。

1
2
Filesystem      Size  Used Avail Use% Mounted on
/dev/disk1s1 113G 90G 23G 20% /

然后再用 du -sh /* 看了一下,总感觉哪里不对劲,累加起来的总和只有 40+G,跟上面的统计结果相差太大了。第一时间怀疑是 sparse file 导致的,但是这种文件一般是虚拟机相关软件才可能生成和用到,可我并没有用这方面的软件,所以排除了这种可能。

然后在 “About this Mac” -> “Storage” 中查看,发现 Available 也有 71G。这说明 df 命令统计结果是存在问题的。

Google 上查了半天,很多人说跟 Time Machine 有关系,但是我明明配置的是一个外置磁盘,为什么还会占用我 Mac 本机的空间呢?
最后查了一下 Apple 的官方说明,原文如下:

1
If you enter Time Machine when your backup disk isn't available, Time Machine automatically uses local snapshots to help you restore files. Reconnect your backup disk to make even more backups available.

既然这样,那问题根源是找到了。然后就看怎么释放空间了。

查看本机中所有的 snapshots

1
sudo tmutil listlocalsnapshots /

输出如下

1
2
3
4
5
com.apple.TimeMachine.2019-02-27-224710
com.apple.TimeMachine.2019-05-05-102956
com.apple.TimeMachine.2019-05-05-113117
com.apple.TimeMachine.2019-05-05-123023
com.apple.TimeMachine.2019-05-05-133415

把不需要的 snapshots 都删除掉,我这边是全部删除

1
2
3
4
5
sudo tmutil deletelocalsnapshots 2019-02-27-224710
sudo tmutil deletelocalsnapshots 2019-05-05-102956
sudo tmutil deletelocalsnapshots 2019-05-05-113117
sudo tmutil deletelocalsnapshots 2019-05-05-123023
sudo tmutil deletelocalsnapshots 2019-05-05-133415

然后再用 df -h 看看,输出如下

1
2
Filesystem      Size  Used Avail Use% Mounted on
/dev/disk1s1 113G 40G 72G 36% /

这样看上去是正常了,问题解决了!

前言

在开发过程当中经常会碰到这样的需求,通过某一台外网的跳板机器登录到另一台的内网机器上,或者需要直接访问内网机器上的资源, 比如传输文件、访问 web 资源等。

图1

其实像这种需求有很多种方式可以实现,比如 nginx 反向代理、shadowsocks 代理、iptables 端口转发等等,在这里主要介绍使用 SSH 的端口转发实现。对于服务端的开发人员来说 SSH 简直是神器之一。

SSH 实现端口转发

SSH 共提供了三种端口转发,分别是本地转发、远程转发、动态转发。

本地转发(用-L参数)

将访问本地主机端口的请求转发到远程主机的端口上,命令形式如下:

1
ssh -NL [本地主机]:[本地端口]:[远程主机]:[远程端口] [用户名]@[跳板机]

这里的 -N 参数是指,只建立隧道,不执行远程机器的任何命令,如果没有 -N 话同时还会默认登录到主机,或者执行指定命令。

  • 场景1

    图2

    前提是主机A有权限访问 http://10.0.0.2:8888,且终端X有权限登录主机A(10.0.0.1)。
    这种情况执行以下命令,把终端X的 9999 端口请求通过主机A转发到远程主机B(10.0.0.2)的 8888 端口上,所以,在终端X的浏览器中直接访问 http://127.0.0.1:9999 即可。

    1
    2
    3
    4
    ssh -NL 0.0.0.0:9999:10.0.0.2:8888 user_a@10.0.0.1

    # 本地主机可以省略
    ssh -NL 9999:10.0.0.2:8888 user_a@10.0.0.1
  • 场景2

    图3

    虽然终端X可以直接登录主机B,但可能由于防火墙、内网服务等因素无法直接访问 http://10.0.1.2:8888 的服务。
    这种情况执行以下命令,把终端X的 9999 端口请求转发到远程主机B(10.0.1.2)的 8888 端口上即可,注意这里主机B上有两个网段的 IP 地址,可能是一个外网,一个内网。

    1
    2
    3
    4
    ssh -NL 0.0.0.0:9999:10.0.1.2:8888 user_b@10.0.0.2

    # 本地主机可以省略
    ssh -NL 9999:10.0.1.2:8888 user_b@10.0.0.2

以上两种场景都是终端X把本地主机的请求转发到远程主机,所以称为本地转发。

远程转发(用-R参数)

将远程主机端口的请求本地主机转发到内网中的服务端口上,命令形式如下:

1
ssh -NR [远程主机监听地址]:[远程主机监听端口]:[内网主机]:[内网端口] [用户名]@[远程主机]
  • 场景1

    图4

    这里终端X是无法直接访问主机A的 http://10.0.1.1:8888 服务的,也不能访问主机B,只能访问主机C。
    主机B是可以登录主机C的,而且主机B能访问主机A的 http://10.0.1.1:8888 服务。
    主机C也是无法直接访问主机B和主机A的。

    这种情况就需要在主机B上配置 SSH 的端口转发,将对主机C的 9999 端口的请求转发到主机A的 8888 端口上。
    在这里主机B的功能都是把远程主机端口的请求转发到内网中,所以这种情况称为远程转发。

    1. 这里要实现远程转发需要主机C的 sshd_config 开启了 AllowTcpForwarding 选项,否则远程转发会失败。
    2. 远程主机上的端口绑定的是127.0.0.1,如要绑定0.0.0.0的网段,主机C的 sshd_config 需要开启 GatewayPorts 选项。或者在主机C上再用一次本地转发到0.0.0.0网段也是可以的。

动态转发(用-D参数)

动态转发其实就是 socks5 代理,跟远程主机建立一条隧道,并在本地监听一个端口,所有经过这个端口的请求都统一发到远程主机上由远程主机代理发起请求。所以这样就可以访问代理主机能访问的而且本身直接访问不了的所有服务, 那样就不需要一个端口一个端口去转发了,而且安全性更高。命令格式如下:

1
ssh -ND [本地主机监听地址]:[本地主机监听端口] [用户名]@[远程主机]
  • 场景1

    图5

    这里终端X只能登录主机A,其它的主机B ~ D 都无法访问。
    这个时候如果配置一条动态转发,只要在主机A中能访问的服务,在终端X就能访问,不管是内网还是外网。

    1
    ssh -ND user_a@10.0.0.1

    这种方式如果终端A需要访问主机B ~ D的话,就需要配置走 socks5 的代理方式,比如浏览器访问网页、SSH远程登录等等。

因为这种转发完全不需要考虑具体的要访问的服务,只要能访问的都自动转发,所以称为动态转发。

通过跳板机直接登录内网主机

最简单粗暴的方式就是先从终端X登录到主机A,再从主机A登录到主机B,这样的话就非常烦琐,想访问主机B还需要两次登录操作,如果后面还有主机C、主机D,那就需要更多次登录操作了,其实 SSH 提供了更简单的方式可以做到。其实你会发现,只要了解了上面几种端口转发方式,无论是跳板机登录、拷贝文件、或者访问 web 服务,都轻而易举了。

  • 使用 -J 参数的方式

    1
    2
    3
    4
    ssh -At user_b@10.0.0.2 -J user_a@10.0.0.1

    # 中间有多台跳板机器
    ssh -At user_c@10.0.0.3 -J user_a@10.0.0.1,user_b@10.0.0.2
  • 使用 ProxyCommand 的方式

    1
    ssh -At user_b@10.0.0.2 -o ProxyCommand="ssh user_a@10.0.0.1 -W %h:%p"
  • 端口转发的方式

    1
    2
    3
    4
    5
    # 先配置动态转发
    ssh -ND localhost:8888 user_a@10.0.0.1

    # 登录主机
    ssh -At -o ProxyCommand='/usr/bin/nc -X 5 -x localhost:8888 %h %p' user_b@10.0.0.2
  • ~/.ssh/config 配置文件的方式

使用 scprsyncgit 等命令穿透跳板机器进行文件同步

  • 使用 -J 参数的方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # rsync
    rsync -e "ssh -J user_a@10.0.0.1" user_b@10.0.0.2:/path/to/file ./out/

    # git 假设 git.example.com 仅仅是一个内网域名, 外网无法访问, 而 10.0.0.1 是外网ip
    GIT_SSH_COMMAND='ssh -J user_a@10.0.0.1' git clone git@git.example.com:xx/yy.git

    # scp 不支持这种方式

    # ansible
    ansible_ssh_common_args="-J user_a@10.0.0.1"
  • 使用 ProxyCommand 的方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # scp
    scp -o "ProxyCommand ssh user_a@10.0.0.1 -W %h:%p" user_b@10.0.0.2:/path/to/file ./out/

    # rsync
    rsync -e "ssh -o 'ProxyCommand ssh user_a@10.0.0.1 -W %h:%p'" user_b@10.0.0.2:/path/to/file ./out/

    # git
    GIT_SSH_COMMAND="ssh -o 'ProxyCommand ssh user_a@10.0.0.1 -W %h:%p'" git clone git@git.example.com:xx/yy.git

    # ansible 使用需要配置以下参数
    ansible_ssh_common_args="-o ProxyCommand='ssh user_a@10.0.0.1 -W %h:%p'"

    其中 10.0.0.1 是跳板主机,10.0.0.2 是内网机器。

  • 端口转发的方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 先配置动态转发
    ssh -ND localhost:8888 user_a@10.0.0.1

    # scp
    scp -o "ProxyCommand /usr/bin/nc -X 5 -x localhost:8888 %h %p" user_b@10.0.0.2:/path/to/file ./out/

    # rsync
    rsync -e "ssh -o 'ProxyCommand /usr/bin/nc -X 5 -x localhost:8888 %h %p'" user_b@10.0.0.2:/path/to/file ./out/

    # git
    GIT_SSH_COMMAND="ssh -o 'ProxyCommand /usr/bin/nc -X 5 -x localhost:8888 %h %p'" git clone git@git.example.com:xx/yy.git

Chrome 浏览器插件 SwitchyOmega

谈到代理、端口转发,不得不让我联想到这个神器插件,可能你的电脑上配置了各种代理、转发,有时候各种冲突让人晕头转向。
有了这个插件,只需要简单配置一下,一键切换、自动路由,非常方便。

前言

在 HTTP/1.1 协议中,使用 POST 请求提交数据时常用的 Content-Type 有以下几种:

  • application/x-www-form-urlencoded 原生 Form 默认的提交方式, 最常用的一种,支持GET/POST等方法。主要把数据编码成键值对的方式, 并且把特殊字符转义成 utf-8 字符,如空格会被转义成 %20。
  • application/json 由于 JSON 格式所表示的结构化数据远比键值对复杂得多,所以使用 JSON 系列化之后的字符串进行数据交换的方式越来越受人们青睐。特别适合 RESTful 类型的接口。
  • text/xml 使用 XML-RPC(XML Remote Procedure Call) 协议进行数据传输,相比于 JSON 的方式更为臃肿。
  • multipart/form-data 使用 Form 提交小文件, 直接把文件内容放在 Body 中进行传输的方式。考虑到同时上传多个字段或文件,所以需要按照一定规则随机生成或手动指定一个 boundary 用于分割数据,然后按照一定格式、顺序进行排列构成完整的 Body 进行传输。(multipart/form-data 官方定义)

客户端发送 multipart/form-data 请求

假设现在有 ./file_1.txt./file_2.txt 两个文件,内容分别如下:

1
2
3
4
5
# cat ./file_1.txt
test file 1 content!

# cat ./file_2.txt
test file 2 content!

使用 Requests 实现

1
2
3
4
5
6
7
8
9
10
11
import requests

data = {'key_1': 'value_1', 'key_2': 'value_2'}

files = [
('file_1', open('./file_1.txt', 'rb')),
('file_2', open('./file_2.txt', 'rb')),
]

resp = requests.post('http://127.0.0.1:8000/upload', data=data, files=files)
print(resp.request.body.decode('utf-8'))

打印出来的 request body 内容是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
--bfa60c05b6631915da313e8fb696e7b2
Content-Disposition: form-data; name="key_1"

value_1
--bfa60c05b6631915da313e8fb696e7b2
Content-Disposition: form-data; name="key_2"

value_2
--bfa60c05b6631915da313e8fb696e7b2
Content-Disposition: form-data; name="file_1"; filename="file_1.txt"

test file 1 content!

--bfa60c05b6631915da313e8fb696e7b2
Content-Disposition: form-data; name="file_2"; filename="file_2.txt"

test file 2 content!

--bfa60c05b6631915da313e8fb696e7b2--

其中 bfa60c05b6631915da313e8fb696e7b2 就是上面所提到自动生成的 boundary。

值得注意的是 {'key_1': 'value_1', 'key_2': 'value_2'} 这两个本身是键值对的数据也被自动转成了 multipart/form-data 的编码方式。如果不传 files 字段时,将自动使用 application/x-www-form-urlencoded 的编码方式,所以 request body 内容应该是这样的

1
key_1=value_1&key_2=value_2

在 requests 中数据编码时,只有 data 参数为 None 时才会判断使用 json 参数,所以 datajson 两个参数同时存在时,只会编码 data 的数据;但 datafiles 是可以同时存在的,而且只要有 files 存在,其它键值对数据也会一起使用 multipart/form-data 的编码方式生成 body 数据。

1
2
3
4
5
# json 参数将会被忽略
resp = requests.post('http://127.0.0.1:8000/upload', data=data, json=xxxx)

# 这样是 OK 的
resp = requests.post('http://127.0.0.1:8000/upload', data=data, files=files)

使用 AIOHTTP 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import asyncio
import aiohttp

async def send_request():
async with aiohttp.ClientSession() as session:
data = aiohttp.FormData()
data.add_field('key_1', 'value_1')
data.add_field('key_2', 'value_2')
data.add_field('file_1', open('./file_1.txt', 'rb'), filename='file_1.txt',
content_type='multipart/form-data')
data.add_field('file_2', open('./file_2.txt', 'rb'), filename='file_2.txt',
content_type='multipart/form-data')

async with session.post('http://127.0.0.1:8000/upload', data=data) as resp:
print(await resp.text())

asyncio.run(send_request())

打印出来的 request body 如下

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
--a63b12cbef044b039c5c788b25a71336
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name="key_1"
Content-Length: 7

value_1
--a63b12cbef044b039c5c788b25a71336
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name="key_2"
Content-Length: 7

value_2
--a63b12cbef044b039c5c788b25a71336
Content-Type: multipart/form-data
Content-Disposition: form-data; name="file_1"; filename="file_1.txt"; filename*=utf-8''file_1.txt
Content-Length: 21

test file 1 content!

--a63b12cbef044b039c5c788b25a71336
Content-Type: multipart/form-data
Content-Disposition: form-data; name="file_2"; filename="file_2.txt"; filename*=utf-8''file_2.txt
Content-Length: 21

test file 2 content!

--a63b12cbef044b039c5c788b25a71336--

可以看到 aiohttp 对键值对默认使用了 Content-Type: text/plain, 即纯文本的方式,这只是不同库的默认值和实现方式有些区别而已。

服务端解析 multipart/form-data 请求

这里服务端使用 Sanic 框架接收数据请求,Sanicpython3 中性能非常好异步无阻塞的 web 框架,特别是跟 uvloop 配合着使用,性能上可以发挥到极致。用法跟Flask非常类似。项目主页: https://github.com/huge-success/sanic

1
2
3
4
5
6
7
8
9
10
11
12
from sanic import Sanic, response

app = Sanic(__name__)

@app.post('/upload')
async def upload_handler(request):
print('request.files', request.files)
print('request.form', request.form)
return response.text('ok')

if __name__ == "__main__":
app.run(host='0.0.0.0', port=8000)

输出信息如下

1
2
request.files {'file_1': [File(type='multipart/form-data', body=b'test file 1 content!\n', name='file_1.txt')], 'file_2': [File(type='multipart/form-data', body=b'test file 2 content!\n', name='file_2.txt')]}
request.form {'key_1': ['value_1'], 'key_2': ['value_2']}