现在我们有了想要连接的 Peer
,接下来要做的就是连接的建立了。在 Pingora
中,目前一共实现了三种协议:
我们也会按照这个顺序依次介绍。这一篇中我们先来看看这三种协议中最底层的:TCP(L4)
连接。
ToC
目录结构
在 Pingora
中,协议相关的内容都定义在 pingora-core::protocol
下而和协议对应的连接则定义在 pingora-core::connector
下。以 L4
连接为例,我们主要研究的文件结构就是:
- connectors/
- protocols/l4/
- ext.rs
- socket.rs
- stream.rs
Link Start!
l4
的 connect
方法接收 Peer
和 bind_to
的 SocketAddr
两个参数,最终返回 L4
的 Stream
。
connect
同时支持 Inet
和 Unix Domain Socket
。我们从 Inet
下手,来理一下整个连接的基本流程。Unix Domain Socket
部分的实现大同小异,而且整体来看比 Inet
简单,这里就不展开赘述了。
连接部分的代码如下所示:
跟随上文代码高亮的顺序,可以基本整理出 l4
连接的建立逻辑,如下:
- 首先是连接的建立本身。这里调用了
tcp_connect
,也就是 l4::ext::connect
进行了连接。
- 然后是
tcp_keepalive
。如果 Peer
需要设置这一选项,则通过 l4::ext::set_tcp_keepalive
进行设置。
- 最后是
set_nodelay
。由于 Pingora
通过 Tokio
在用户态实现了写缓存,因此这里需要将 Nagle's algorithm
禁用。
tcp_connect
l4::ext::connect
的连接过程基本和 Tokio
中的连接相似。区别是增加了 bind_to
的实现。
首先在 [1]
,Pingora
调用了 ip_bind_addr_no_port
,要求操作系统不自动分配端口。
Pingora
目前只在 Linux
上正确实现了 ip_bind_addr_no_port
,其他系统下只有 dummy 实现。对于 Linux
,Pingora
通过 setsockopt
设置了 IP_BIND_ADDRESS_NO_PORT
属性。
在确保 OS
没有自动分配 port
之后,Pingora
在 [2]
用 bind
手动绑定了 bind_to
所要求的 SocketAddr
。
Stream
在 l4::ext::connect
成功后,我们得到的是一个 tokio::net::tcp::TcpStream
,而 connect
需要返回的却是 Stream
类型。将 TcpStream
转化为 Stream
则是 protocol::l4::Stream
中定义的了。
Pingora
定义的 l4::Stream
是一个带读写缓存的 BufStream
,其中写缓存可以选择关闭。如下代码所示:
TcpStream -> Stream
Pingora
实现了 From
,因此 TcpStream
和 UnixStream
可以通过 into()
转化成 Stream
:
这里比较关键的是两个常量的选值:
BUF_READ_SIZE
: 这个值是 64k。由于 TLS record size
是 16k,因此这里通过 64k
对齐,减少 syscall
的数量。
BUF_WRITE_SIZE
: 这个值是 1460,和 MSS
匹配,通过 Tokio
在用户态的写缓存实现,替换掉内核态的 Nagle's algorithm
。
Stream::drop
Stream
的 trait Drop
实现也非常有趣。我们看看:
在 [1]
,Pingora
分别通过 nodelay
和 local_addr
尝试获取 Stream
的状态。而在 [2]
,当 Stream
仍然可写时,则调用了自身的 flush()
,并且通过 now_or_never()
尝试立即将缓存清空。
Stream::poll_write
另一个比较有趣的点是 Pingora
实现绕过写缓存的方式:
在需要 buffer_write
时,还是直接通过 self.steram
调用 poll_write
即可。但对于不希望使用缓存的场景,则是通过 get_mut()
直接拿到了 underlying I/O object
。而向 underlying stream
的写入自然就跳过 BufStream
的缓存了。
基于 HTTP 隧道的 L4 代理
在上文描述 l4::connect
的实现中,我们省略了 proxy_connect
的部分。代理连接部分代码如下所示:
在 Pingora
中,L4
的代理是通过 HTTP
隧道实现的。简单来说,就是 Proxy Server
通过 Unix Domain Socket
启动一个 HTTP 1.1
的 HTTP Proxy
,然后 Proxy Client
(也就是 Pingora
)通过 CONNECT
请求连接到指定的远端 ip:port
。完整代码如下所示:
整个过程分为基本三部分:
- 首先在
[1]
,Pingora
通过 connect_uds()
连接了指定的 Unix Domain Socket
路径。
- 然后在
[2]
,通过 raw_connect::generate_connect_header()
生成了 req_header
。
- 最后在
[3]
,通过 raw_connect::connect()
建连。
我们一步一步来看。
connect_uds
连接 Unix Domain Socket
很简单,直接调 tokio
的 connect()
方法就可以了:
接下来是生成 ReqHeader
的过程。
这里 Pingora
通过 http crate
的 Builder
构造了一个合法的 HTTP CONNECT Request
,并填充了 Pingora
在 Peer
中指定的 Proxy Header
。
raw_connect::connect
万事就绪,最后只要建立连接就可以了。
整个连接过程分三个部分。首先,在 [1]
将 request_header
通过 http_req_header_to_wire_auth_form
转换为 Buffer
:
然后,在 [2]
读取并校验 HTTP Server
的返回值、Header
是否合法:
最后在 [3]
,将 http
的 underlying_stream
返回。至此,TCP
连接已经建立,Pingora
也就可以在这条通道中继续进行 TCP
通信了。