前面已经看完了 Server 和 Client 的交互模型以及大概的代码结构,后面就应该细看一下一些细节的东西,首先一个我觉得比较重要的就是连接的管理,在 frp 中,作者的操作方式为使用 io.CopyBuffer
的实现方式来完成,我稍微跟了一下 Go 的标准库实现,这里会有一个比较大的性能问题,如果可以的话,我推荐使用 ZeroCopy
的方式来优化性能会有不错的表现。
此外这种实现方式还有个问题就是对于数据的统计实时度很差,在整个 frp 的结构中,对于数据统计的支持我觉得都是不足的,这可以得到更丰富的支持,其实还有另外一个问题那就是对于流量的 DUMP 也是一个硬伤,不过这些都是附加功能,对于 frp 的主要功能不影响,所以大家也就不那么在意的,但是我的意见是:如果这些功能能够上一个档次,那么对于这款软件的流行度将会是一个绝大的利好,毕竟例如我会去检查源代码的一大原因还是因为使用这款软件的环境我觉得不能被肉鸡,所以需要 review 一番源代码。
通过文档以及在前面的交互模式介绍中, 这里应该有了一个概念,那么就是用户是从 Server 端请求进来的,而请求的这个端口是 Client 让 Server 运行的 Proxy 端口,然后 frp 会将用户的流量导向 Client 端,从而完成反向代理的功能。对于 Proxy 端口是如何运作起来的,以及用户的连接请求进来是如何处理的,又是如何导向 Client 端的;这中间又做了什么操作,这些都还是未知的,所以这里我要做的事情就捋一遍这条线的代码。
将视线推移会 Server 的 Connection 管理,其实就是这一段:
这段代码以前都看过了,所以没必要了解太多了,直接看 Line 16 的 manager
就好了。
结合 Client 的代码我们知道,在 Client 端配置的 Proxy 需要将 Proxy 的内容发送给 Server 端,而这里就是收到创建 Proxy 请求的时候 Server 端是如何处理的,这里我总结了一下事情:
- 创建 Proxy 对象,并且真正得创建对应的 listener 监听
- 在 ctl 和 service 里面记录这个 Proxy 对象
- 将真正的监听地址返回给客户端,这里所谓的地址其实就是选择了哪个端口,因为我们的端口有时不是固定指定的,而是指定了一个范围,所以真正绑定的哪个是不确定的
这里有深扒的部分应该是第一部分,这个 listener 是怎么创建的,这一部分因为每种类型的处理方式是不一样的,所以我就且先来看 TCP 的
这里的逻辑也是比较简单:
- 首先得看期望监听的端口能不能搞到,不能的话肯定没办法下去
- 很简单得创建 Listener
- 将 listener 给 proxy 对象
这里还只是简单得创建 listener,那么 listener 有连接进来怎么办?
这里就是答案,这里是接收连接的地方,并不能看出啥,但是有一点就是如果一个 listener 因为各种原因被关闭了,那么这里只会将 goroutine 停掉,不会做更多的事情,那么 proxy 如何感知这个需要在后面继续;所以这里的关键还是在于 handler
这个函数是怎么实现的,这个是在 Line 656 中实现:
因为这里的代码都比较关键,所以我就都复制出来了,首先 Line 7 是要获取到和 Client 端的 connection,目的当然就是用于作为转发的目的连接啦,然后 Line 15 和 Line 22 这两段分别是做加密和压缩处理;然后关键的 Line 29 就是核心的流量互换操作了,这里是需要展开来说的。
Join
的 diamante 也是比较容易理解的,这也是我说的为啥效率低的原因了,虽然作者这里常使用了一个 Buffer Pool 尝试减少内存的分配和回收处理,但是,再往底层有硬伤,这并不能帮助太多。然后再往后就是状态数据变更了,不做过多讲解了。
那 Proxy 是如何感知 listener 的异常的呢?这里作者鸡贼得实现了一个套了一层壳的 TCP Listener,所以我刚才看到的 Listener 其实不是原始的基础库里面的 TCP Listener,而是作者自己套了一层壳的实体,这里的实现是这样的:
可以发现,真正控制 Listener 启停的是这里,能不能关核心还是 closeFlag
有没有被设置上,否则,其他情况你都得继续忙。