【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除
背景
上篇 blog
【Ubuntu】【Gitlab】拉出内网 Web 服务:Nginx 事件驱动分析(二)
分析了 Nginx 的高性能设计,多进程模型,配合事件驱动 + 非阻塞 I/O + epoll(Linux),下面继续
Python http.server 单/多线程分析
分析完了 Nginx 的高性能模型,下面再对比下之前的 Python http.server 模型
首先,在分析多线程模型之前,有个比较有意思的点,首先终端输入
bash
python3 --version
可以看到本地版本号是 v3.12.3

然后查看 http/server.py 实现如下
python
# ... 前面省略
def test(HandlerClass=BaseHTTPRequestHandler,
ServerClass=ThreadingHTTPServer,
protocol="HTTP/1.0", port=8000, bind=None):
"""Test the HTTP request handler class.
This runs an HTTP server on port 8000 (or the port argument).
"""
ServerClass.address_family, addr = _get_best_family(bind, port)
HandlerClass.protocol_version = protocol
with ServerClass(addr, HandlerClass) as httpd:
host, port = httpd.socket.getsockname()[:2]
url_host = f'[{host}]' if ':' in host else host
print(
f"Serving HTTP on {host} port {port} "
f"(http://{url_host}:{port}/) ..."
)
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\nKeyboard interrupt received, exiting.")
sys.exit(0)
if __name__ == '__main__':
import argparse
import contextlib
parser = argparse.ArgumentParser()
parser.add_argument('--cgi', action='store_true',
help='run as CGI server')
parser.add_argument('-b', '--bind', metavar='ADDRESS',
help='bind to this address '
'(default: all interfaces)')
parser.add_argument('-d', '--directory', default=os.getcwd(),
help='serve this directory '
'(default: current directory)')
parser.add_argument('-p', '--protocol', metavar='VERSION',
default='HTTP/1.0',
help='conform to this HTTP version '
'(default: %(default)s)')
parser.add_argument('port', default=8000, type=int, nargs='?',
help='bind to this port '
'(default: %(default)s)')
args = parser.parse_args()
if args.cgi:
handler_class = CGIHTTPRequestHandler
else:
handler_class = SimpleHTTPRequestHandler
# ensure dual-stack is not disabled; ref #38907
class DualStackServer(ThreadingHTTPServer):
def server_bind(self):
# suppress exception when protocol is IPv4
with contextlib.suppress(Exception):
self.socket.setsockopt(
socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
return super().server_bind()
def finish_request(self, request, client_address):
self.RequestHandlerClass(request, client_address, self,
directory=args.directory)
test(
HandlerClass=handler_class,
ServerClass=DualStackServer,
port=args.port,
bind=args.bind,
protocol=args.protocol,
)
可以看到这里默认启用的是多线程模型

这里要先解释先 http.server 的单线程和多线程模式 ,首先说单线程模式,单线程的处理类如下

可以看到,HTTPServer 是 socketserver.TCPServer 的子类,HTTPServer 继承的是 TCPServer 单线程同步处理的默认行为,其执行流程如下:
- Python
http.server启动一个单线程/进程 - 接收一个客户端连接(
accept方法阻塞接收) - 调用
handle方法处理这个请求(比如读文件,返回 HTTP 响应) - 这个请求需要完全处理完,连接关闭后,才能处理下一个请求
这里单线程执行的逻辑点在于,第二个请求必须等第一个请求结束才能被处理,无法并发 ,下面来测试验证一下这个单线程阻塞行为
首先,在 index.html 目录下新建一个 slow_server.py 启动文件,用来模拟低速处理服务器,slow_server.py 的内容如下
python
#!/usr/bin/env python3
from http.server import HTTPServer, SimpleHTTPRequestHandler
import time
class SlowHandler(SimpleHTTPRequestHandler):
def do_GET(self):
print(f"Handling {self.path} ... (will sleep 10s)")
time.sleep(10) # 模拟耗时操作
"""Serve a GET request."""
f = self.send_head()
if f:
try:
self.copyfile(f, self.wfile)
finally:
f.close()
server = HTTPServer(('localhost', 2027), SlowHandler)
server.serve_forever()
这里解释下里面几个点
- 首先是
SlowHandler是继承了http.server模块里面的静态处理类SimpleHTTPRequestHandler,SlowHandler就是准备模拟的低速服务器 - 然后这里
SlowHandler重写了下SimpleHTTPRequestHandler里面的do_Get方法,注意,这里重写的do_Get方法几乎就是拷贝的SimpleHTTPRequestHandler里面的do_Get方法,唯一不同的,就是加了两行,一行是print打印,另一行是time.sleep延时操作,相当于收到请求后,延时 10s 再处理

- 然后最后是用
HTTPServer启动该服务,该 Web 服务可以通过2027端口进行访问,注意,这里必须是HTTPServer,因为HTTPServer是单线程,现在就是要测试验证单线程的阻塞行为
OK,用 chmod 777 给 slow_server.py 赋予执行权限,然后在该目录(index.html 同级目录)目录下执行 ./slow_server.py 启动这个模拟低速 Web 服务
然后终端输入
bash
time curl http://localhost:2027/a
可以测试这个 Web 服务的连接时间
终端输入 man time,查看到该命令的描述如下

可以看到,time 命令是一个测量程序运行时资源消耗的命令(不仅仅是测量时间,虽然名字叫 time) ,比如花了多少 CPU 时间,实际耗时,内存使用等,但这个属于 GNU time,需要用 /usr/bin/time 命令(和上面的不是同一个命令,虽然都叫 time) ,关于 GNU time 就不展开分析了
直接在终端输入 time 命令的话(上面用的),只能测量时间 如下

OK,本篇先到这里,如有疑问,欢迎评论区留言讨论,祝各位功力大涨,技术更上一层楼!!!更多内容见下篇 blog