如何用python实现一个HTTP连接池


Posted in Python onJanuary 14, 2021

一. 连接池的原理

首先, HTTP连接是基于TCP连接的, 与服务器之间进行HTTP通信, 本质就是与服务器之间建立了TCP连接后, 相互收发基于HTTP协议的数据包. 因此, 如果我们需要频繁地去请求某个服务器的资源, 我们就可以一直维持与个服务器的TCP连接不断开, 然后在需要请求资源的时候, 把连接拿出来用就行了.

如何用python实现一个HTTP连接池

一个项目可能需要与服务器之间同时保持多个连接, 比如一个爬虫项目, 有的线程需要请求服务器的网页资源, 有的线程需要请求服务器的图片等资源, 而这些请求都可以建立在同一条TCP连接上.

因此, 我们使用一个管理器来对这些连接进行管理, 任何程序需要使用这些连接时, 向管理器申请就可以了, 等到用完之后再将连接返回给管理器, 以供其他程序重复使用, 这个管理器就是连接池.

如何用python实现一个HTTP连接池

二. 实现代码

1. HTTPConnectionPool类

基于上一章的分析, 连接池应该是一个收纳连接的容器, 同时对这些连接有管理能力:

class HTTPConnectionPool:

 def __init__(self, host: str, port: int = None, max_size: int = None, idle_timeout: int = None) -> None:
  """
  :param host: pass
  :param port: pass
  :param max_size: 同时存在的最大连接数, 默认None->连接数无限,没了就创建
  :param idle_timeout: 单个连接单次最长空闲时间,超时自动关闭,默认None->不限时
  """
  self.host = host
  self.port = port
  self.max_size = max_size
  self.idle_timeout = idle_timeout
  self._lock = threading.Condition()
  self._pool = []
  # 这里的conn_num指的是总连接数,包括其它线程拿出去正在使用的连接
  self.conn_num = 0
  self.is_closed = False

 def acquire(self, blocking: bool = True, timeout: int = None) -> WrapperHTTPConnection:
  ...

 def release(self, conn: WrapperHTTPConnection) -> None:
  ...

因此, 我们定义这样一个HTTPConnectionPool类, 使用一个列表来保存可用的连接. 对于外部来说, 只需要调用这个连接池对象的acquire和release方法就能取得和释放连接.

2. 线程安全地管理连接

对于线程池内部来说, 至少需要三个关于连接的操作: 从连接池中取得连接, 将连接放回连接池, 以及创建一个连接:

def _get_connection(self) -> WrapperHTTPConnection:
 # 这个方法会把连接从_idle_conn移动到_used_conn列表中,并返回这个连接
 try:
  return self._pool.pop()
 except IndexError:
  raise EmptyPoolError


def _put_connection(self, conn: WrapperHTTPConnection) -> None:
 self._pool.append(conn)


def _create_connection(self) -> WrapperHTTPConnection:
 self.conn_num += 1
 return WrapperHTTPConnection(self, HTTPConnection(self.host, self.port))

对于连接池外部来说, 主要有申请连接和释放连接这两个操作, 实际上这就是个简单的生产者消费者模型. 考虑到外部可能是多线程的环境, 我们使用threading.Condition来保证线程安全. 关于Condition的资料可以看这里.

def acquire(self, blocking: bool = True, timeout: int = None) -> WrapperHTTPConnection:
 if self.is_closed:
  raise ConnectionPoolClosed
 with self._lock:
  if self.max_size is None or not self.is_full():
   # 在还能创建新连接的情况下,如果没有空闲连接,直接创建一个就行了
   if self.is_pool_empty():
    self._put_connection(self._create_connection())
  else:
   # 不能创建新连接的情况下,如果设置了blocking=False,没连接就报错
   # 否则,就基于timeout进行阻塞,直到超时或者有可用连接为止
   if not blocking:
    if self.is_pool_empty():
     raise EmptyPoolError
   elif timeout is None:
    while self.is_pool_empty():
     self._lock.wait()
   elif timeout < 0:
    raise ValueError("'timeout' must be a non-negative number")
   else:
    end_time = time.time() + timeout
    while self.is_pool_empty():
     remaining = end_time - time.time()
     if remaining <= 0:
      raise EmptyPoolError
     self._lock.wait(remaining)
  # 走到这一步了,池子里一定有空闲连接
  return self._get_connection()


def release(self, conn: WrapperHTTPConnection) -> None:
 if self.is_closed:
  # 如果这个连接是在连接池关闭后才释放的,那就不用回连接池了,直接放生
  conn.close()
  return
 # 实际上,python列表的append操作是线程安全的,可以不加锁
 # 这里调用锁是为了通过notify方法通知其它正在wait的线程:现在有连接可用了
 with self._lock:
  if not conn.is_available:
   # 如果这个连接不可用了,就应该创建一个新连接放进去,因为可能还有其它线程在等着连接用

 conn.close()


 self.conn_num -= 1
   conn = self._create_connection()
  self._put_connection(conn)
  self._lock.notify()

我们首先看看acquire方法, 这个方法其实就是在申请到锁之后调用内部的_get_connection方法获取连接, 这样就线程安全了. 需要注意的是, 如果当前的条件无法获取连接, 就会调用条件变量的wait方法, 及时释放锁并阻塞住当前线程. 然后, 当其它线程作为生产者调用release方法释放连接时, 会触发条件变量的notify方法, 从而唤醒一个阻塞在wait阶段的线程, 即消费者. 这个消费者再从池中取出刚放回去的线程, 这样整个生产者消费者模型就运转起来了.

3. 上下文管理器

对于一个程序来说, 它使用连接池的形式是获取连接->使用连接->释放连接. 因此, 我们应该通过with语句来管理这个连接, 以免在程序的最后遗漏掉释放连接这一步骤.

基于这个原因, 我们通过一个WrapperHTTPConnection类来对HTTPConnection进行封装, 以实现上下文管理器的功能. HTTPConnection的代码可以看《用python实现一个HTTP客户端》这篇文章.

class WrapperHTTPConnection:

 def __init__(self, pool: 'HTTPConnectionPool', conn: HTTPConnection) -> None:
  self.pool = pool
  self.conn = conn
  self.response = None
  self.is_available = True

 def __enter__(self) -> 'WrapperHTTPConnection':
  return self

 def __exit__(self, *exit_info: Any) -> None:
  # 如果response没读完并且连接需要复用,就弃用这个连接
  if not self.response.will_close and not self.response.is_closed():
   self.close()
  self.pool.release(self)

 def request(self, *args: Any, **kwargs: Any) -> HTTPResponse:
  self.conn.request(*args, **kwargs)
  self.response = self.conn.get_response()
  return self.response

 def close(self) -> None:
  self.conn.close()
  self.is_available = False

同样的, 连接池可能也需要关闭, 因此我们给连接池也加上上下文管理器的功能:

class HTTPConnectionPool:
 ...

 def close(self) -> None:
  if self.is_closed:
   return
  self.is_closed = True
  pool, self._pool = self._pool, None
  for conn in pool:
   conn.close()

 def __enter__(self) -> 'HTTPConnectionPool':
  return self

 def __exit__(self, *exit_info: Any) -> None:
  self.close()

这样, 我们就可以通过with语句优雅地管理连接池了:

with HTTPConnectionPool(**kwargs) as pool:
 with pool.acquire() as conn:
  res = conn.request('GET', '/')
  ...

4. 定时清理连接

如果一个连接池的所需连接数是随时间变化的, 那么就会出现一种情况: 在高峰期, 我们创建了非常多的连接, 然后进入低谷期之后, 连接过剩, 大量的连接处于空闲状态, 浪费资源. 因此, 我们可以设置一个定时任务, 定期清理空闲时间过长的连接, 减少连接池的资源占用.

首先, 我们需要为连接对象添加一个last_time属性, 每当连接释放进入连接池后, 就修改这个属性的值为当前时间, 这样我们就能明确知道, 连接池内的每个空闲连接空闲了多久:

class WrapperHTTPConnection:
 ...

 def __init__(self, pool: 'HTTPConnectionPool', conn: HTTPConnection) -> None:
  ...
  self.last_time = None


class HTTPConnectionPool:
 ...

 def _put_connection(self, conn: WrapperHTTPConnection) -> None:
  conn.last_time = time.time()
  self._pool.append(conn)

 

然后, 我们通过threading.Timer来实现一个定时任务:

def start_clear_conn(self) -> None:
 if self.idle_timeout is None:
  # 如果空闲连接的超时时间为无限,那么就不应该清理连接
  return
 self.clear_idle_conn()
 self._clearer = threading.Timer(self.idle_timeout, self.start_clear_conn)
 self._clearer.start()


def stop_clear_conn(self) -> None:
 if self._clearer is not None:
  self._clearer.cancel()

threading.Timer只会执行一次定时任务, 因此, 我们需要在start_clear_conn中不断地把自己设置为定时任务. 这其实等同于新开了一个线程来执行start_clear_conn方法, 因此并不会出现递归过深问题. 不过需要注意的是, threading.Timer虽然不会阻塞当前线程, 但是却会阻止当前线程结束, 就算把它设置为守护线程都不行, 唯一可行的办法就是调用stop_clear_conn方法取消这个定时任务.

最后, 我们定义clear_idle_conn方法来清理闲置时间超时的连接:

def clear_idle_conn(self) -> None:
 if self.is_closed:
  raise ConnectionPoolClosed
 # 这里开一个新线程来清理空闲连接,避免了阻塞主线程导致的定时精度出错
 threading.Thread(target=self._clear_idle_conn).start()


def _clear_idle_conn(self) -> None:
 if not self._lock.acquire(timeout=self.idle_timeout):
  # 因为是每隔self.idle_timeout秒检查一次
  # 如果过了self.idle_timeout秒还没申请到锁,下一次都开始了,本次也就不用继续了
  return
 current_time = time.time()
 if self.is_pool_empty():
  pass
 elif current_time - self._pool[-1].last_time >= self.idle_timeout:
  # 这里处理下面的二分法没法处理的边界情况,即所有连接都闲置超时的情况
  self.conn_num -= len(self._pool)
  self._pool.clear()
 else:
  # 通过二分法找出从左往右第一个不超时的连接的指针
  left, right = 0, len(self._pool) - 1
  while left < right:
   mid = (left + right) // 2
   if current_time - self._pool[mid].last_time >= self.idle_timeout:
    left = mid + 1
   else:
    right = mid
  self._pool = self._pool[left:]
  self.conn_num -= left
 self._lock.release()

由于我们获取和释放连接都是从self._pool的尾部开始操作的, 因此self._pool这个容器是一个先进后出队列, 它里面放着的连接, 一定是越靠近头部的闲置时间越长, 从头到尾闲置时间依次递减. 基于这个原因, 我们使用二分法来找出列表中第一个没有闲置超时的连接, 然后把在它之前的连接一次性删除, 这样就能达到O(logN)的时间复杂度, 算是一种比较高效的方法. 需要注意的是, 如果连接池内所有的连接都是超时的, 那么这种方法是删不干净的, 需要对这种边界情况单独处理.

三. 总结

1. 完整代码及分析

这个连接池的完整代码如下:

import threading
import time
from typing import Any

from client import HTTPConnection, HTTPResponse


class WrapperHTTPConnection:

 def __init__(self, pool: 'HTTPConnectionPool', conn: HTTPConnection) -> None:
  self.pool = pool
  self.conn = conn
  self.response = None
  self.last_time = time.time()
  self.is_available = True

 def __enter__(self) -> 'WrapperHTTPConnection':
  return self

 def __exit__(self, *exit_info: Any) -> None:
  # 如果response没读完并且连接需要复用,就弃用这个连接
  if not self.response.will_close and not self.response.is_closed():
   self.close()
  self.pool.release(self)

 def request(self, *args: Any, **kwargs: Any) -> HTTPResponse:
  self.conn.request(*args, **kwargs)
  self.response = self.conn.get_response()
  return self.response

 def close(self) -> None:
  self.conn.close()
  self.is_available = False


class HTTPConnectionPool:

 def __init__(self, host: str, port: int = None, max_size: int = None, idle_timeout: int = None) -> None:
  """
  :param host: pass
  :param port: pass
  :param max_size: 同时存在的最大连接数, 默认None->连接数无限,没了就创建
  :param idle_timeout: 单个连接单次最长空闲时间,超时自动关闭,默认None->不限时
  """
  self.host = host
  self.port = port
  self.max_size = max_size
  self.idle_timeout = idle_timeout
  self._lock = threading.Condition()
  self._pool = []
  # 这里的conn_num指的是总连接数,包括其它线程拿出去正在使用的连接
  self.conn_num = 0
  self.is_closed = False
  self._clearer = None
  self.start_clear_conn()

 def acquire(self, blocking: bool = True, timeout: int = None) -> WrapperHTTPConnection:
  if self.is_closed:
   raise ConnectionPoolClosed
  with self._lock:
   if self.max_size is None or not self.is_full():
    # 在还能创建新连接的情况下,如果没有空闲连接,直接创建一个就行了
    if self.is_pool_empty():
     self._put_connection(self._create_connection())
   else:
    # 不能创建新连接的情况下,如果设置了blocking=False,没连接就报错
    # 否则,就基于timeout进行阻塞,直到超时或者有可用连接为止
    if not blocking:
     if self.is_pool_empty():
      raise EmptyPoolError
    elif timeout is None:
     while self.is_pool_empty():
      self._lock.wait()
    elif timeout < 0:
     raise ValueError("'timeout' must be a non-negative number")
    else:
     end_time = time.time() + timeout
     while self.is_pool_empty():
      remaining = end_time - time.time()
      if remaining <= 0:
       raise EmptyPoolError
      self._lock.wait(remaining)
   # 走到这一步了,池子里一定有空闲连接
   return self._get_connection()

 def release(self, conn: WrapperHTTPConnection) -> None:
  if self.is_closed:
   # 如果这个连接是在连接池关闭后才释放的,那就不用回连接池了,直接放生
   conn.close()
   return
  # 实际上,python列表的append操作是线程安全的,可以不加锁
  # 这里调用锁是为了通过notify方法通知其它正在wait的线程:现在有连接可用了
  with self._lock:
   if not conn.is_available:
    # 如果这个连接不可用了,就应该创建一个新连接放进去,因为可能还有其它线程在等着连接用
    conn.close()
    self.conn_num -= 1
    conn = self._create_connection()
   self._put_connection(conn)
   self._lock.notify()

 def _get_connection(self) -> WrapperHTTPConnection:
  # 这个方法会把连接从_idle_conn移动到_used_conn列表中,并返回这个连接
  try:
   return self._pool.pop()
  except IndexError:
   raise EmptyPoolError

 def _put_connection(self, conn: WrapperHTTPConnection) -> None:
  conn.last_time = time.time()
  self._pool.append(conn)

 def _create_connection(self) -> WrapperHTTPConnection:
  self.conn_num += 1
  return WrapperHTTPConnection(self, HTTPConnection(self.host, self.port))

 def is_pool_empty(self) -> bool:
  # 这里指的是,空闲可用的连接是否为空
  return len(self._pool) == 0

 def is_full(self) -> bool:
  if self.max_size is None:
   return False
  return self.conn_num >= self.max_size

 def close(self) -> None:
  if self.is_closed:
   return
  self.is_closed = True
  self.stop_clear_conn()
  pool, self._pool = self._pool, None
  for conn in pool:
   conn.close()

 def clear_idle_conn(self) -> None:
  if self.is_closed:
   raise ConnectionPoolClosed
  # 这里开一个新线程来清理空闲连接,避免了阻塞主线程导致的定时精度出错
  threading.Thread(target=self._clear_idle_conn).start()

 def _clear_idle_conn(self) -> None:
  if not self._lock.acquire(timeout=self.idle_timeout):
   # 因为是每隔self.idle_timeout秒检查一次
   # 如果过了self.idle_timeout秒还没申请到锁,下一次都开始了,本次也就不用继续了
   return
  current_time = time.time()
  if self.is_pool_empty():
   pass
  elif current_time - self._pool[-1].last_time >= self.idle_timeout:
   # 这里处理下面的二分法没法处理的边界情况,即所有连接都闲置超时的情况
   self.conn_num -= len(self._pool)
   self._pool.clear()
  else:
   # 通过二分法找出从左往右第一个不超时的连接的指针
   left, right = 0, len(self._pool) - 1
   while left < right:
    mid = (left + right) // 2
    if current_time - self._pool[mid].last_time >= self.idle_timeout:
     left = mid + 1
    else:
     right = mid
   self._pool = self._pool[left:]
   self.conn_num -= left
  self._lock.release()

 def start_clear_conn(self) -> None:
  if self.idle_timeout is None:
   # 如果空闲连接的超时时间为无限,那么就不应该清理连接
   return
  self.clear_idle_conn()
  self._clearer = threading.Timer(self.idle_timeout, self.start_clear_conn)
  self._clearer.start()

 def stop_clear_conn(self) -> None:
  if self._clearer is not None:
   self._clearer.cancel()

 def __enter__(self) -> 'HTTPConnectionPool':
  return self

 def __exit__(self, *exit_info: Any) -> None:
  self.close()


class EmptyPoolError(Exception):
 pass


class ConnectionPoolClosed(Exception):
 pass

首先, 这个连接池的核心就是对连接进行管理, 而这包含取出连接和释放连接两个过程. 因此这东西的本质就是一个生产者消费者模型, 取出线程时是消费者, 放入线程时是生产者, 使用threading自带的Condition对象就能完美解决线程安全问题, 使二者协同合作.

解决获取连接和释放连接这个问题之后, 其实这个连接池就已经能用了. 但是如果涉及到更多细节方面的东西, 比如判断连接是否可用, 自动释放连接, 清理闲置连接等等, 就需要对这个连接进行封装, 为它添加更多的属性和方法, 这就引入了WrapperHTTPConnection这个类. 实现它的__enter___和__exit__方法之后, 就能使用上下文管理器来自动释放连接. 至于清理闲置连接, 通过last_time属性记录每个连接的最后释放时间, 然后在连接池中添加一个定时任务就行了.

以上就是如何用python实现一个HTTP连接池的详细内容,更多关于python 实现一个HTTP连接池的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
在Python 3中实现类型检查器的简单方法
Jul 03 Python
浅析Python中的getattr(),setattr(),delattr(),hasattr()
Jun 14 Python
python实现mysql的读写分离及负载均衡
Feb 04 Python
Python拼接字符串的7种方法总结
Nov 01 Python
详解Python 定时框架 Apscheduler原理及安装过程
Jun 14 Python
Python获取好友地区分布及好友性别分布情况代码详解
Jul 10 Python
python实现通过队列完成进程间的多任务功能示例
Oct 28 Python
python add_argument()用法解析
Jan 29 Python
pycharm快捷键汇总
Feb 14 Python
django form和field具体方法和属性说明
Jul 09 Python
在Pycharm中安装Pandas库方法(简单易懂)
Feb 20 Python
如何用python反转图片,视频
Apr 24 Python
如何用python写个模板引擎
Jan 14 #Python
opencv python 对指针仪表读数识别的两种方式
Jan 14 #Python
详解如何使用Pytest进行自动化测试
Jan 14 #Python
matplotlib对象拾取事件处理的实现
Jan 14 #Python
用python查找统一局域网下ip对应的mac地址
Jan 13 #Python
python 写一个水果忍者游戏
Jan 13 #Python
python中编写函数并调用的知识点总结
Jan 13 #Python
You might like
php auth_http类库进行身份效验
2009/03/19 PHP
无法在发生错误时创建会话,请检查 PHP 或网站服务器日志,并正确配置 PHP 安装最快的解决办法
2010/08/01 PHP
ThinkPHP分页类使用详解
2014/03/05 PHP
PHP连接sql server 2005环境配置及问题解决
2014/08/08 PHP
php中实现xml与mysql数据相互转换的方法
2014/12/25 PHP
php实现四舍五入的方法小结
2015/03/03 PHP
php常量详细解析
2015/10/27 PHP
laravel实现上传图片,并且制作缩略图,按照日期存放的代码
2019/10/16 PHP
PHP常用函数之根据生日计算年龄功能示例
2019/10/21 PHP
PHP中通过getopt解析GNU C风格命令行选项
2019/11/18 PHP
PHP的图像处理实例小结【文字水印、图片水印、压缩图像等】
2019/12/20 PHP
JavaScript取得鼠标绝对位置程序代码介绍
2012/09/16 Javascript
js实现将选中值累加到文本框的方法
2015/08/12 Javascript
JS判断字符串变量是否含有某个字串的实现方法
2016/06/03 Javascript
jQuery四种选择器使用及示例
2016/06/05 Javascript
AngularJS 实现JavaScript 动画效果详解
2016/09/08 Javascript
Javascript 创建类并动态添加属性及方法的简单实现
2016/10/20 Javascript
Bootstrap表单使用方法详解
2017/02/17 Javascript
使用Vue.observable()进行状态管理的实例代码详解
2019/05/26 Javascript
使用layui+ajax实现简单的菜单权限管理及排序的方法
2019/09/10 Javascript
js实现随机抽奖
2020/03/19 Javascript
[26:40]DOTA2上海特级锦标赛A组资格赛#1 Secret VS MVP.Phx第一局
2016/02/25 DOTA
[01:00:25]2018DOTA2亚洲邀请赛3月30日 小组赛A组 VG VS Liquid
2018/03/31 DOTA
用python + hadoop streaming 分布式编程(一) -- 原理介绍,样例程序与本地调试
2014/07/14 Python
python验证码识别的示例代码
2017/09/21 Python
Bottle框架中的装饰器类和描述符应用详解
2017/10/28 Python
Python 3.6打包成EXE可执行程序的实现
2019/10/18 Python
浅谈TensorFlow之稀疏张量表示
2020/06/30 Python
浅谈如何使用python抓取网页中的动态数据实现
2020/08/17 Python
Marc O’Polo俄罗斯官方在线商店:德国高端时尚品牌
2019/12/26 全球购物
教师四风自我剖析材料
2014/09/30 职场文书
公司领导班子对照检查存在问题整改措施
2014/10/02 职场文书
个人股份合作协议书
2014/10/24 职场文书
采购内勤岗位职责
2015/04/13 职场文书
老人节主持词
2015/07/04 职场文书
财务管理制度范本
2015/08/04 职场文书