用Python构建HTTP 2服务器

用Python构建HTTP 2服务器

2 年前 · 来自专栏 python技术文章

Python Twisted 在其Web服务器中支持HTTP 2 。HTTP2默认情况下不可用,要获取它,您需要安装 hyper-h2 (只需运行 pip install twisted[h2] )。对于整个Python生态系统而言,这确实是一个令人振奋的重大新闻,因此值得一看,它的工作方式以及设置的难度和难度。

在本文中,我将构建一些简单的Twisted网站,以通过HTTP 2提供内容,然后创建一个连接到此示例站点的客户端。HTTP 2和HTTP 1.1之间的性能是否会有很大的不同?我的演示站点可以在HTTP2中更快地工作吗?

您好HTTP2

让我们从说“你好,世界!”开始。在Python Twisted中使用HTTP 2实现。

Twisted Web服务器 已经支持Python 3,因此您可以使用3没问题。对于此博客文章,我将使用Python 3.4.3。我假设您已经安装了所有HTTP2依赖项的Twisted 16.3.0。在Python 3中解析可选依赖项时有一些小错误,因此,如果您使用的是3,则可能需要从pip手动安装“ h2”和“ priority”软件包,而不是运行 pip install twisted[h2]

我们的网站将通过HTTPS提供内容。尽管HTTP2协议本身不需要TLS,但是大多数客户端实现(尤其是主流浏览器)确实需要HTTPS。这意味着我们需要开始构建网站并获取用于本地开发的自签名证书。要生成自签名证书,您需要运行以下命令:

# generate private key
$ openssl genrsa > privkey.pem
# generate certificate that will be stored in cert.pem file
$ openssl req -new -x509 -key privkey.pem -out cert.pem -days 365 -nodes

运行上述命令后,您需要填写一些有关您的详细信息。您可以忽略其中的大多数内容或设置一些伪造的值,但是请记住,如果未将公用名设置为主机名,则某些客户端将拒绝连接。如果openssl询问您“公用名”,请记住输入“ localhost”。

现在,我们有了ssl证书,我们就可以构建通过HTTPS服务HTTP2的简单“ hello world” Twisted资源。

我们的资源将是最简单的,它看起来像这样:

class Index(Resource):
    isLeaf = True
    def render_GET(self, request):
        return b"hello world (in HTTP2)"

上面的代码创建了简单的资源,它将处理对网站根目录的所有请求。

现在,我们需要告诉Twisted侦听某些特定端口,并使用TLS在此处提供资源。要真正在使用SSL的连接上启动我们的网站,我们将使用 Twisted端点 。建议使用端点在Twisted中执行SSL。过去您可以使用Twisted DefaultSSLContextFactory,但是在将来的版本中将不推荐使用此API。Factory缺少许多SSL功能,不安全并且无法在HTTP 2下正常工作。

以下是在Twisted中正确创建https网站实例的方法:

# create instance of our web resource Index is instance of twisted.web.Resource
site = server.Site(Index())
# specify port and certificate
endpoint_spec = "ssl:port=8080:privateKey=privkey.pem:certKey=cert.pem"
# create listening endpoint
server = endpoints.serverFromString(reactor, endpoint_spec)
# start listening serving site in specified way
server.listen(site)

完整的hello world示例如下所示:

import sys
from twisted.web import server
from twisted.web.resource import Resource
from twisted.internet import reactor
from twisted.python import log
from twisted.internet import endpoints
class Index(Resource):
    isLeaf = True
    def render_GET(self, request):
        return b"hello world (in HTTP2)"
if __name__ == "__main__":
    log.startLogging(sys.stdout)
    site = server.Site(Index())
    endpoint_spec = "ssl:port=8080:privateKey=privkey.pem:certKey=cert.pem"
    server = endpoints.serverFromString(reactor, endpoint_spec)
    server.listen(site)
    reactor.run()

因此,现在我们有了Twisted服务器,该服务器具有一些所谓的HTTP 2支持,但是我们如何实际对其进行测试?显然,我们需要一些HTTP2客户端。卷曲就是这样的客户之一。不幸的是,默认情况下curl没有HTTP2支持。为了能够使用HTTP2,您需要安装可选的依赖项并从源传递标志进行编译,以告知curl2使用HTTP2支持进行编译。这是 很好的描述这里 ,或 也在这里

安装curl之后,您可以像这样测试您的网站

# remember about passing certificate to curl (--cacert option)
> curl2 --http2 https://localhost:8080 -v --cacert cert.pem
Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* TCP_NODELAY set
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x16b2bc0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.49.1
> Accept: */*

您可以看到curl报告它在连接级别使用HTTP2,但是实际的请求部分是HTTP 1.1。这是预期的。HTTP2不会更改HTTP语义,所有HTTP动词,标头等在HTTP2中均有效。HTTP2的大多数发生在TCP连接级别。

在服务器日志中,您应该看到以下消息:

> python hello.py 
2016-07-27 13:20:16+0200 [-] Log opened.
2016-07-27 13:20:16+0200 [-] Site (TLS) starting on 8080
2016-07-27 13:20:16+0200 [-] Starting factory <twisted.web.server.Site object at 0x7f263f172e80>
2016-07-27 13:20:18+0200 [-] "-" - - [27/Jul/2016:11:20:18 +0000] "GET / HTTP/2" 200 22 "-" "curl/7.49.1"

此行 "-" - - [27/Jul/2016:11:20:18 +0000] "GET / HTTP/2" 200 22 "-" "curl/7.49.1" 告诉您服务器在响应curl请求时使用了HTTP 2。

Chrome中的Hello world

为什么我使用curl而不只是使用普通的浏览器(例如Chrome)?问题是Chrome对HTTP 2的支持过于严格。Chrome需要所有连接才能使用ALPN协议协商。这 在这里 这里 详细讨论 。要支持ALPN,您的系统必须具有高于1.0.2的OpenSSL版本。在撰写本文时,绝大多数Linux系统尚未安装OpenSSL 1.0.2。只有Ubuntu 16.04随附OpenSSL 1.0.2。如果您使用的是Linux,则在整个OpenSSL系统上升级都不是一件容易的事。我不确定Mac OS或Widows或其他OS-es。我建议您自己检查自己的openssl版本,如果该版本高于1.0.2,则可以在Chrome中进行测试。否则我在 这里 创建了简单的 Dockerfile 使用Ubuntu 16.04并安装所有依赖项,这里还有关联的 makefile ,告诉您如何构建和运行docker image。

拥有所有依赖性后,还需要使Chrome接受您的伪造的自签名证书。 此处介绍了 如何完成此操作的步骤

如您所见,使HTTP2在Chrome中工作并非易事。准备就绪后,您可以通过打开开发工具来测试HTTP2支持。启用“协议”列将使您能够查看连接中使用的协议版本,例如,您的开发工具应显示以下内容:



基准HTTP2与HTTP1.1

现在,我们知道了如何使用Twisted为正常工作(和安全)的HTTP2网站提供服务,我们可以转到一些更有趣的地方,并比较HTTP1.1和HTTP2之间的区别。网站是HTTP2还是HTTP1.1真的重要吗?是否真的需要打扰HTTP2?

为了尝试,我将构建超级简单的在线书店HTTP API。我的书店将存储3000种科幻书籍,其中包括雷·布拉德伯里(Ray Bradbury)和弗兰克·赫伯特(Frank Herbert)的经典著作。我通过一些琐碎的Scrapy项目从 goodreads.com 中提取了数据。您可以 从此处下载数据 。我的书店将拥有一个初始页面,该页面列出了JSON中的所有书籍ID。然后,每本书都会有自己的页面,您可以在其中看到一些页面详细信息。客户将随机首先请求索引列表,然后它将访问每个特定页面以查看其中的内容。一个客户端将解析HTTP1.1,另一客户端将解析HTTP2。哪一个会更快?

我的API如下所示:

# server.py
import json
import sys
from twisted.web import server
from twisted.web.resource import Resource
from twisted.internet import reactor
from twisted.python import log
from twisted.internet import endpoints
def load_stock():
    # load data from JSON
    with open("books.json") as stock_file:
        return json.load(stock_file)
BOOKS = load_stock()
class Index(Resource):
    """Serve all book ids.
    def render_GET(self, request):
        return json.dumps(list(BOOKS.keys())).encode("utf8")
class Book(Resource):
    """Return detailed data about each book.
    isLeaf = True
    def render_GET(self, request):
        book_id = request.args.get(b"id")
        book = BOOKS.get(book_id[0].decode("utf8"))
        if not book:
            request.setResponseCode(404)
            return b""
        return json.dumps(book).encode("utf8")
if __name__ == "__main__":
    log.startLogging(sys.stdout)
    root = Resource()
    root.putChild(b"", Index())
    root.putChild(b"book", Book())
    site = server.Site(root)
    endpoint_spec = "ssl:port=8080:privateKey=privkey.pem:certKey=cert.pem"
    server = endpoints.serverFromString(reactor, endpoint_spec)
    server.listen(site)
    reactor.run()

如果您想与我一起启动此服务器,可以 在这里 找到 所有资料

现在,让我们看看尝试爬网我们的SF书店时HTTP1.1客户端将如何执行。客户端将是使用python-requests的普通同步脚本。它将首先访问带有所有书籍ID的初始页。获取所有图书ID后,它将请求每个图书详细信息页面并读取响应。HTTP1.1客户端将重用一个TCP连接。它将发送“连接:保持活动”标头,并且所有请求将在一个TCP连接中一个接一个地发送。

import json
import requests
s = requests.Session()
url = 'https://localhost:8080'
resp = s.get(url, verify="cert.pem")
index_data = json.loads(resp.text)
responses = []
for _id in index_data:
    book_details_path = "/book?id={}".format(_id)
    response = s.get(url + book_details_path, verify="cert.pem")
    body = json.loads(response.text)
    responses.append(body)
assert len(responses) == 3000

在我的测试服务器上的客户端之上运行会产生以下指标:

User time (seconds): 4.09
System time (seconds): 0.15
Percent of CPU this job got: 72%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:05.84

这意味着客户需要大约5秒钟来处理我们的SF网站。

现在让我们尝试HTTP2客户端。从本质上讲,它将执行与HTTP1.1客户端相同的操作,它将连接到初始索引页,获取所有书籍ID,然后再请求一本书。唯一的区别是客户端将使用 HTTP2多路复用 。这意味着,我们将一次发送多个请求,然后再获取响应,而不是一个接一个地发送请求并等待响应。HTTP 1.1允许您重用TCP连接,但是过程是:

==== start connection ==== 
send request 1 --> wait for response --> receive response 1 --> send request 2 ...
==== end connection ====

从我对HTTP2的了解来看,该过程更像

==== start connection ==== 
send request 1, send request 2, send request 3 --> wait for responses --> receive response 1, 2, 3
==== end connection ====

在HTTP1.1中,如果响应缓慢,它将阻止连接。在HTTP2中,您可以同时通过一个连接将多个请求发送到您的服务器,然后从原始来源获取响应。

为了充分利用HTTP2,我们的客户端将通过一个连接发送多个请求,然后获取响应。它将把最初的3000本书的列表拆分为100个URL的块。对于每个块,将从发送100个请求开始。在下一步中,它将遍历连接流ID和获取响应。

我将使用 python-hyper 作为底层客户端库。Twisted尚不支持HTTP2客户端,但有关支持它的工作正在进行中。

import json
from hyper import HTTPConnection
conn = HTTPConnection('localhost:8080', secure=True)
conn.request('GET', '/')
resp = conn.get_response()
# process initial page with book ids
index_data = json.loads(resp.read().decode("utf8"))
responses = []
chunk_size = 100
# split initial set of urls into chunks of 100 items
for i in range(0, len(index_data), chunk_size):
    request_ids = []
    # make requests
    for _id in index_data[i:i+chunk_size]:
        book_details_path = "/book?id={}".format(_id)
        request_id = conn.request('GET', book_details_path)
        request_ids.append(request_id)
    # get responses
    for req_id in request_ids:
        response = conn.get_response(req_id)
        body = json.loads(response.read().decode("utf8"))