入门:编写自己的HTTP/2服务器

Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发。

本文解释了如何开始使用Hyper-h2作为底层协议栈来实现编写完全成熟的HTTP/2。本文涵盖了你需要去理解的基本概念,并通过一个非常简单的HTTP/2服务器向你做出说明。

  本文假设您对编写Python已经非常熟悉,并且了解计算机网络的工作原理。若非如此,您应该先去了解这些概念然后再返回到本文,这样您理解本文会更加容易。


链接

  Hyper-h2的核心对象是H2connection。此对象是单个HTTP / 2连接的状态的抽象表示,并保存所有重要的协议状态。 当使用Hyper-h2时,您首要事情是创建这个可以完成大部分繁重工作的对象。

  这个对象的接口相对简单。发送数据时,您可以使用指明您想执行的操作的方法,来调用对象:例如,您可能想发送标记头(您就使用send_headers方法)或发送数据(您就使用send_data方法)。确定要执行的操作以后,您会从对象中获取一些字节,这些字节代表操作的HTTP/2编码,然后通过网络将其发送出去。

当您从网络接收数据时,将该数据传递给H2connection,该对象返回事件列表。这些事件,稍后会更详细的描述,定义了远程对等体在connection上的一系列操作,如刚刚传递给对象的HTTP/2编码数据。

  因此你得到一个简单的循环(可以看做是一个事件循环的更加具体的形式):

1.首先你执行一些操作

2.将通过执行这些操作创建的数据发送到网络

3.从网络中读取数据

4.把这些数据解码成事件

5.事件使您触发某些操作,返回步骤1

  当然,HTTP/2要比这些更加复杂,但在最简单的情况下,您可以使用这种循环编写一个相当有效的HTTP / 2工具。 在本文的后面,我们将这样做。

H2Connection对象的一些重要细节在Advanced Usage中有所介绍:有关详细信息,请参阅Connections:Advanced for more information。但是有一个细节应该被说明,那就是:Hyper-h2的H2connection对象不进行I/O(输入输出)。让我们简单的说一下为什么。


输入/输出

  任何有用的HTTP/2工具最终都需要做输入输出I/O。这是因为使用HTTP/2协议与其他计算机通信不是很有效,除非你的确常与这些计算机通信。

  但是,I/O并不是一件简单的事情:有很多不同的方法实现I/O,一旦你选择了一种方法,你的代码和你没有选择的方法通常就不能很好地协同工作。

  然而有许多不同的方式进行I/O,当所有HTTP / 2应用将接收的字节转换为事件,事件转换为字节发送。 因此没有理由拥有这个核心协议代码的许多不同版本:一个用于Twisted,一个用于gevent,一个用于线程,一个用于同步代码。

  这就是为什么我们在顶部说Hyper-h2是一个HTTP / 2协议栈,而不是一个完全成熟的应用。

  Hyper-h2知道如何将字节转换为事件并返回,但仅此而已。 I / O和smarts可能不同,但核心的HTTP / 2逻辑是相同的:都是Hyper-h2提供的。

  不进行I/O就使得Hyper-h2变得普通,相对简单。它有一个易于理解的性能包络,它很容易测试(因此很容易得到正确的操作方式),它的操作方式可以重复的进行。这些都是处理复杂事务的库所具有的优势。

  本文将讨论如何使用Hyper-h2构建一个相对简单的HTTP / 2应用,让您了解它如何适应您的软件。


事件

  当编写HTTP/2应用时知道远程对等体正在做什么是很重要的,如果你不注意,编写网络程序将会更容易一些。

  Hyper-h2以事件的形式对远程对等体的行为进行编码。 当您从远程对等体接收数据并将其传递到H2Connection对象(请参阅connection)时,H2Connection返回一个对象列表,每个对象表示已发生的单个事件。 每个事件指的是远程对等体采取的单个操作行为。

  一些事件是相当高级别的,这指的是比HTTP/2更具一般性的事件:例如RequestReceived事件是一个通用的HTTP概念,而不仅仅是一个HTTP/2。其他事件是极其特别的HTTP/2:PushedStreamReceived指的是服务器推送,一个非常特定的HTTP/2概念。

  这些事件存在的原因是Hyper-h2是非常通用的。 这意味着,在许多情况下,Hyper-h2不知道对事件做出什么响应。 你的代码将需要处理这些事件,并做出决定做什么。 这是在Hyper-h2之上构建的任何HTTP/2应用的主要作用。

  事件中提供了完整的事件列表。为了说明这个例子,我们将只处理一小组事件。


编写你自己的服务器

  使用刚学会的知识,我们将编写一个非常简单的HTTP/2 Web服务器。此服务器的目的是处理HTTP GET并返回客户端发送的以JSON编码的头。基本上,有点像httpbin.org/get。 没有什么奇怪,但这是一个很好的方法来了解如何应该与Hyper-h2进行交互。

  为了简单起见,我们将使用Python 3中的Python标准库来编写它。在现实中,您可能想使用某种异步框架:请参阅存储库中的examples目录以获取一些教你怎么做的示例。

  在我们开始之前,创建一个名为h2server.py的新文件:我们将使用它作为我们的工作区。 此外,您应该安装Hyper-h2:按照安装说明进行安装。 

第一步:sockets

  首先,我们需要确保我们可以监听传入的数据并将其发回。 为此,我们需要使用标准库的sockets模块。 现在我们将跳过执行TLS:如果你想从你的网络浏览器到达你的服务器,你需要添加TLS和一些其他功能。 请参考示例目录中的例子。

  让我们开始。 首先,打开h2server.py。 我们需要导入sockets模块并开始侦听connections。

  这不是sockets教程,因此我们不会深入了解这是如何工作的。 如果你想了解更多有关sockets的细节,那么你应该查阅网上很多好的教程。

  当您想要侦听传入的connections时,您需要先绑定地址。 所以请尝试设置您的文件,如下所示:

1.png

在shell窗口中,执行这个程序(python h2server.py)。 然后,打开另一个shell并运行curl http:// localhost:8080 /。 在第一个shell中,你应该看到这样:

2.png

 运行curl命令几次。 您应该会看到更多类似的行列出现。 请注意,curl命令本身将以错误退出。 这没有大碍,因为我们没有发送任何数据。

  现在继续,通过在第一个shell中按Ctrl + C停止服务器运行。 你应该看到一个KeyboardInterrupt错误使该进程关闭。

  上面的程序是什么? 好吧,首先它创建一个socket对象。 然后这个sockets绑定到一个特定的地址:('0.0.0.0',8080)。 这是一个特殊的地址:这意味着这个sockets应该监听TCP端口8080的任何流量。不要担心调用setsockopt:它只是确保你可以反复运行这个程序。

  然后我们永远循环在sockets上调用accept方法。 直到有人尝试连接到我们的TCP端口时accept方法阻塞:此时,它返回一个元组:第一个元素是一个新的sockets对象,第二个元素是新连接来自目的地址的元组。 你可以在我们的h2server.py脚本的输出中看到这一点。

  此时,我们有一个脚本可以接受入站连接。 这是一个好的开始! 让我们开始涉及HTTP / 2。

第二步:Add a H2connection

  现在我们可以监听sockets信息,准备好HTTP / 2连接对象并开始处理它的数据。 现在,让我们看看当我们提供数据时会发生什么。

  为了进行HTTP / 2连接,我们需要一个知道如何表述HTTP / 2的工具。大多数版本的curl都不行,所以让我们来安装一个Python工具。 在Python环境中,运行pip install hyper。 这将安装一个名为hyper的Python命令行HTTP / 2工具。 要确认它的工作,请尝试运行此命令,并验证输出看起来类似于下面显示的:

3.png

假设它正常工作,现在开始发送HTTP/2数据。

  回到我们的h2server.py脚本,我们将要开始处理数据。 首先我们添加一个函数,它接受从accept返回的sockets,并从中读取数据。 然后调用该函数句柄。 该函数应该创建一个H2Connection对象,然后在sockets上循环,读取数据并将其传递给connections。

  要从sockets中读取数据,我们需要调用recv。recv函数接受一个数字作为其参数,这是从单个调用返回的最大数据量(注意,recv将在任何数据可用时立即返回,即使该数量远远小于您传递给它的数量)。  为了编写这种软件,具体的数值不是非常有用,但该数值不应该过大。所以,当你不确定使用数字4096还是数字65535时,在这个例子中,我们将使用65535。

功能如下:

4.png

更新主循环,以便它将数据传递到新的数据处理函数。 你的h2server.py应该看起来像这样:

5.png

在一个shell中运行,在你的另一个shell中,你可以运行hyper --h2 GET http:// localhost:8080 /。 该shell应该处于等候状态,然后你应该从h2server.py shell看到以下输出:

6.png

然后你需要使用Ctrl + C来停止hyper和h2server.py。 随意这样做几次,看看会发生什么。

  那么,我们在这里看到了什么? 在一个循环中,当连接被打开时,我们使用recv方法从套接字中读取一些数据。 然后,我们将该数据传递给连接对象,它返回一个单一的事件对象:RemoteSettingsChanged。

  但我们并没有看到其他的一些变化。所以结果看起来像是所有的hyper改变了其设置,无其他变化。如果你看到另一个hyper窗口,你会注意到它等候了一段时间,然后最终与sockets连接超时。这是在等待什么呢?

  好,事实证明,在连接开始时,双方都需要发送一点数据,称为“HTTP / 2前导码”。在此我们不需要知道太多的细节,但基本上双方都需要发送一个HTTP / 2数据块,告诉对方它们的设置是什么。 hyper做到了,但我们没有。

下面我们将进行这样的操作。

第三步:Sending the preamble

Hyper-h2使连接设置变得很容易。你需要做的是调用initiate_connection方法,然后发送相应的数据。让我们更新句柄函数:

7.png

这有个很大的变化,既对initiate_connection的调用,但还有另一个新的方法:data_to_send。

  当对H2Connection对象进行函数调用时,通常会导致将HTTP / 2数据写入网络。但是Hyper-h2不做任何I / O,所以它本身不能进行这样的操作。相反,它将其写入内部缓冲区。 您可以使用data_to_send方法从此缓冲区检索数据。关于该方法的细节我们现在不需关心。我们需要做的是确保发出的数据是特殊数据。

您的h2server.py脚本现在应该如下所示:

8.png

进行此更改后,重新运行h2server.py脚本并使用相同的hyper命令:hyper --h2 GET http:// localhost:8080 /。 hyper命令仍然处于等候状态,但这次我们从h2server.py脚本获得更多的输出:

9.png

然后,发生了什么?

  首先要注意的是,我们现在不止一次地绕过了循环。 首先,我们收到一些触发RemoteSettingsChanged事件的数据。 然后,我们获得一些触发SettingsAcknowledged事件的数据。 最后,甚至更多的数据触发两个事件:RequestReceived和StreamEnded。

  所以,hyper告诉我们关于它的设置,接纳用户,然后给我们一个请求。 然后它结束一个流,hyper是一个HTTP / 2通信通道,它保存一个请求和响应对。

一个流不会结束,除非它被重置或双方关闭它:在这个意义上它是双向的。 所以StreamEnded事件告诉我们,hyper是关闭它的一半流:它不会给我们任何更多的数据流。 这意味着请求完成。

  那么为什么是hyper等候? 因为我们还没有发送回应:下面我们将发送回应。

第四步:Handling Events

  我们要做的是在收到请求时发送响应。 好在当收到一个请求时,我们得到一个事件,所以我们可以使用它作为我们的响应信号。

  让我们定义一个发送响应的新函数。 现在,这个响应只是一些打印“it works!”的数据。

  函数应该接受H2Connection对象,以及用信号通知请求的事件。 让我们对此进行定义。

10.png

  所以虽然这只是一个短暂的功能,但有很多处理需要解释一下。 首先,什么是流ID? 前面我们简要讨论流,说它们是一个双向通信通道,它保存一个请求和响应对。 HTTP / 2的一大特点是,可以同时发送大量流,发送和接收不同的请求和响应。 为了识别每个流,我们使用流ID。 这些在连接的整个生命周期中是唯一的,并且它们以升序排列。

  大多数H2Connection函数采用流ID:它们要求您主动说明使用哪个连接。 在这种情况下,作为一个简单的服务器,我们永远不需要自己选择一个流ID:客户端将总是为我们选择一个。 这意味着我们一直能够得到我需要事件的流ID。

接下来,我们发送一些标头。在HTTP / 2中,响应由一些头集合和可选的一些数据组成。 必须先有标头:如果你是一个客户端,那么你会发送请求头,但在我们的情况下,这些头是响应头。

  大多数情况下,这些没什么用,但你会注意到一个特殊的头在::状态中。 这是一个HTTP / 2特定的头,它用于保存HTTP状态代码,用于HTTP响应的顶部。 在这里,我们说的响应是200 OK,这是发送成功标记。

  要在Hyper-h2中发送头,请使用send_headers函数。

  接下来,我们要发送正文数据。 为此,我们使用send_data函数。 这也需要一个流ID。注意,数据是二进制的:Hyper-h2不能与unicode字符串一起使用,因此您必须将bytestrings传递到H2Connection。 一个例外的头是:Hyper-h2会自动将它们编码为UTF-8。

  最后要注意的是,在我们调用send_data时,我们将end_stream设置为True。 这告诉Hyper-h2(和远程对等体),我们完成了发送数据:响应结束。 因为当我们结束流传送时,hyper会结束它的一侧的流。

接下来:我们只需要引入这个函数。让我们再次修改我们的句柄函数:

11.png

变化结束。现在,当我们收到一些事件时,我们通过它们查看RequestReceived事件。 如果找到了该事件,则发送响应。

然后,在循环的底部,我们检查是否有任何数据要发送,如果有,我们发送它。 然后,进行循环重复。

通过这些更改,您的h2server.py文件应如下所示:

12.1.png

12.2.png

好的。 让我们运行这个,然后再次运行我们的hyper命令。

  这一次,没有什么从我们的服务器输出,并且hyper输出有效! 成功! 尝试运行它几次,我们可以看到,它不仅第一次有效,其他时候也有效!

  HTTP / 2即将实现!让我们添加最后一步:返回JSON编码的请求标头。

第五步:Returning Headers

  如果我们想要返回JSON中的请求头,首先我们要做的是找到它们。如果你检查RequestReceived的文档,你会发现除了流ID之外还有请求头。

  这意味着我们可以对send_response函数进行一个简单的更改,以获取这些头并将其编码为JSON对象。 让我们这样做:

13.png

这是一个非常简单的更改,我们需要做的是:一些额外的头和JSON转储。

第6节:Bringing it all together

  下面是我们需要的。

  把我们做的所有工程放到我们的h2server.py文件,就是:

14.1.png

14.2.png

现在,执行h2server.py,然后再次指向它。 你应该看到类似下面的输出from hyper:

15.png

在这里,您可以看到hyper发送的HTTP / 2请求的“特殊报头”。 这类似于:我们必须在我们的响应上发送的status头:它们以明确定义的方式对HTTP请求的重要部分进行编码。 如果你使用Hyper-h2编写一个客户端栈,你需要确保发送这些头文件。


恭喜!

恭喜! 你写了第一个HTTP / 2服务器! 如果您想扩展它,可以参考以下几个方向:

1.我们没有处理提出的几个事件:你可以添加一些方法来适当地处理这些事件。

2.现在我们的服务器是单线程的,所以它一次只能处理一个客户端。可以考虑使用线程重写此服务器,或使用您喜欢的异步编程框架再次编写此服务器。如果你计划使用线程,你应该知道一个H2Connection对象线程是不安全的。作为一种可能的设计模式,考虑创建线程并将accept返回的sockets传递给这些线程,然后让这些线程创建自己的H2Connection对象。

3.查看代码示例中的一些长形代码示例

4.或者,尝试在我们的存储库示例目录中使用我们的示例。这些例子更加全面,可以从您的网络浏览器到达。尝试调整他们做什么,或添加新功能给他们!

5.您可能希望通过Web浏览器访问此服务器。为此,您需要为您的服务器添加正确的TLS支持。这可能很棘手,在许多情况下,除了您安装的其他库,还需要PyOpenSSL。检查Eventlet示例以查看TLS-ify服务器需要PyOpenSSL代码。


英文原文:https://python-hyper.org/h2/en/stable/basic-usage.html
译者:qwertyl7
 

2月15日11:00到13:00网站停机维护,13:00前恢复