因为 TCP 连接都是客户端主动发起的,也就是说需要经过三次握手才能够进行读写操作,如果客户端需要建立连接的次数较少,那么握手需要的开销倒可以忽略不计,但是如果客户端需要建立成千上万个 TCP 连接,那么就需要成千上万次握手了。如果真是这样,势必会导致系统性能低下,我们引入连接池来解决多次握手的问题。
连接池( Connection Pool )在广义上来说算是资源池,我们之前讲过的 LeakyBuffer 也算是资源池的一种,不过 LeakyBuffer 的目的是反复利用内存资源,减少内存分配的次数。而连接池则是为了减少建立连接的次数,重复利用已有的 TCP 连接。

例如在上图中,Consumer Agent 不断收到来自 Consumer 的 HTTP 请求,它解析请求的内容后,Consumer Agent 就要利用 TCP 连接给 Provider Agent 发送消息,那么如果按照传统的方法我们就需要每一个 HTTP 请求都要对应一个新的 TCP 连接,也就对应新的三次握手,这样的话系统开销会非常大。你可能会问为什么不直接用一个 TCP 连接来解决传输数据的问题,这样的话甚至不用这么多握手操作了,这问题不错,但是考虑到由于 TCP 传输的是应用层的数据,它并不了解应用层传输的信息究竟是什么意思,如果多个客户端发起的连接共用一个 TCP 连接,那么我们还需要写逻辑去区分收到的消息究竟需要属于哪个 HTTP 请求,另外还需要考虑TCP 发送和接受时读写竞争的问题,操作起来比较混乱,还不如重复利用多个 TCP 连接来发送和接受数据来的干净利落。
总的来说,连接池是一种利用空间换时间的技术。下面我们来看一下如何设计一个连接池。连接池中最核心的操作就是获取一个空闲的 TCP 连接,另外还要考虑连接池资源释放的问题,否则可能会导致内存泄漏。
1 | var ErrClosed = errors.New("pool is closed") |
在正常的 TCP 流程中,Close 会关闭本次连接,底层会进行四个挥手操作,但是我们不希望我们调用 Close 时直接关闭这个连接,而是希望能回收这个连接,所以我们对普通的 net.Conn 做了一层封装,来该改变其 Close 行为(其实这就是装饰器模式的应用),我们想要客户端调用 Close 的时候被我们的连接池所回收而非直接关闭,除非该连接被标记为不可用时 unusable (通过 MarkUnusable ), 我们才决定关闭本次连接。
1 | type PooledConn struct { |
连接池的构建需要考虑两个问题,一个就是连接创建问题,第二就是回收连接问题,创建我们用一个 Factory 工厂方法来表示,而回收连接可以利用 golang 中的通道机制( chan )来进行储存。
1 | type Factory func() (net.Conn, error) |
在初始化连接池时,我们先建立 initialCap 个连接用于刚开始时使用,具体创建可以使用 factory 产生 TCP 连接。
1 | func NewConnPool(initialCap, maxCap int, factory Factory) (Pool, error) { |
当客户端使用完一次 TCP 连接后,会主动调用 Close 来关闭此次连接,由于我们重新定义了 Close 函数,当调用 Close 时,连接不会直接关闭而是会重新放回连接池中供其他部分代码使用,从而达到重复使用的目的,如果此时连接池已满,则直接释放该连接。
1 | func (c *ConnPool) put(conn net.Conn) error { |
下面的结构体 ConnPool 实现了 Pool 接口。在 Get 方法中我们先得到具体用于存储连接的通道 conns 和用于制造连接的工厂方法 factory,然后试着去通道中拿,和 put 方法类似,如果该通道中还有空闲的 TCP 连接,则直接拿出并经过封装后( wrapConn )使用,如果没有则使用工厂方法新建一个 TCP 连接再经过封装后使用。
1 | func (c *ConnPool) Get() (net.Conn, error) { |
连接池的使用
由于具体的连接逻辑是由客户端决定的,所以 factory 应该由使用者定义。
1 | factory := func() (net.Conn, error) {return net.Dial("tcp", "localhost:8080")} |
我们通过 pool.NewConnPool 来新建一个连接池,这里我们指定连接池中初始的连接个数为5,而最大的连接个数为30。通过 p.Get 来获得可用的连接,Get 方法会根据当前池中的情况返回连接,如果池中没有可用的连接则用 factory 方法新建一个,此时系统开销较大,但是这种情况并不常见,所以基本可以忽略不计。如果池中有可用连接,则直接返回,此时开销较小。
1 | p, err := pool.NewConnPool(5, 30, factory) |
下面给出真正关闭该连接的方法,因为我们重载了 Close 方法,直接调用 Close 只会将此连接放回连接池中,所以需要在真正关闭前将此连接设为不可用( MarkUnusable )。
1 | if pc, ok := conn.(*pool.PooledConn); ok { |