像 shadowsocks 这种代码量不足一万行的小型工程,通读源码仅需几个小时,阅览顺序也没有特别的讲究。但是,当遇到代码量十万行甚至百万行的大型工程时,阅读全部代码是一件不可能完成的任务,用正确的顺序浏览就变得尤为重要。一般来说,打开一个陌生的工程,最好先定位其 main 函数。抓住了程序的入口,就抓住了逻辑的根节点,此后再分别运用广度优先搜索、深度优先搜索、回溯搜索等多种手段,逐步描绘出工程的大体轮廓。
shadowsocks 在 local.py 和 server.py 两处分别定义了 main 函数。因为这两个 main 函数属于不同的模块,这是完全合法的。在 shell.print_help 中,它们被分别赋予了 sslocal 和 ssserver 这两个名字,我们也可以称之为客户端和服务端。对比查看 local.py 和 server.py,服务端为了支持多组端口和多进程,写了不少额外的代码。如果把这两部分剥离掉,剩下的主干结构与客户端差异甚微:
在 main 函数中,shadowsocks 解析配置文件或命令行参数后,首先注册了系统守护进程,以便于在后台运行。之后,它创建了 DNS 解析器、TCP 中继器、UDP 中继器三个组件,并用事件循环将这三者统一起来。事件循环开始后,这些组件会在外部事件的触发下实现预定的行为,直至程序遭遇内部错误或因捕获外部信号而退出。
值得注意的是,虽然客户端和服务端都有 DNS 解析器,但他们承担的责任有很大的差异。客户端的 DNS 用于解析服务端的 IP 地址,如果在配置文件中使用 IP 代替域名,则客户端的解析器是一个可以移除的组件。服务端的 DNS 将用户请求中的域名翻译成目标服务器的 IP 地址,是必要的组件。考虑到服务端的部署地点,通常不会受到 DNS 污染的影响,因此可以安全地使用系统默认的 DNS 服务器。