asyncioの使用例:HTTPServer?!

少し前に、多くの「グッズ」を含むPython 3.4の新しいバージョンがchangelogリリースされました。 それらの1つは、非同期ネットワークアプリケーションの作成に適したインフラストラクチャを含むasyncioモジュールです。 コルーチンコンセプトのおかげで、非同期アプリケーションコードの理解と保守が簡単です。



記事では、単純なTCP(Echo)サーバーの例を使用して、 asyncio



どのようにasyncio



かを示し、このモジュールの「致命的な欠陥」、つまり非同期HTTPサーバー実装の欠如を排除するasyncio



ます。



はじめに



直接の競争相手であり「兄弟」である竜巻のフレームワークは、 確固たる地位を確立しており、当然の人気を誇っています。 しかし、私の意見では、非同期はより単純で、より論理的で、考え抜かれたように見えます。 ただし、標準の言語ライブラリを扱っているため、これは驚くことではありません。



Pythonで以前に非同期サービスを書くことができ、そうだと言うでしょう。 しかし、これにはサードパーティのライブラリおよび/またはコールバックプログラミングスタイルの使用が必要でした。 このバージョンのPythonで完成したコルーチンコンセプトにより、標準言語ライブラリの機能のみを使用して線形非同期コードを記述できます。



Linuxでこれらすべてを書いたことをすぐに予約したいのですが、使用されるすべてのコンポーネントはクロスプラットフォームであり、Windowsでも動作するはずです。 ただし、Python 3.4が必要です。



EchoServer



Echoサーバーの例は標準ドキュメントにありますが、これは低レベルAPI「トランスポートとプロトコル」を指します 。 「毎日」の使用には、 高レベルAPI「ストリーム」が推奨されます。 サンプルのTCPサーバーコードはありませんが、低レベルAPIの例を検討し、両方のモジュールのソースコードを見ると、単純なTCPサーバーを書くことは難しくありません。



 import asyncio import logging import concurrent.futures @asyncio.coroutine def handle_connection(reader, writer): peername = writer.get_extra_info('peername') logging.info('Accepted connection from {}'.format(peername)) while True: try: data = yield from asyncio.wait_for(reader.readline(), timeout=10.0) if data: writer.write(data) else: logging.info('Connection from {} closed by peer'.format(peername)) break except concurrent.futures.TimeoutError: logging.info('Connection from {} closed by timeout'.format(peername)) break writer.close() if __name__ == '__main__': loop = asyncio.get_event_loop() logging.basicConfig(level=logging.INFO) server_gen = asyncio.start_server(handle_connection, port=2007) server = loop.run_until_complete(server_gen) logging.info('Listening established on {0}'.format(server.sockets[0].getsockname())) try: loop.run_forever() except KeyboardInterrupt: pass # Press Ctrl+C to stop finally: server.close() loop.close()
      
      





すべてが非常に明白ですが、注意を払う価値のあるいくつかのニュアンスがあります。



  server_gen = asyncio.start_server(handle_connection, port=2007) server = loop.run_until_complete(server_gen)
      
      





最初の行はサーバー自体を作成するのではなく、 asyncio



最初にサーバーにアクセスし、腸が指定されたパラメーターに従ってTCPサーバーを作成および初期化するジェネレーターを作成します。 2行目はそのようなアピールの例です。



  try: data = yield from asyncio.wait_for(reader.readline(), timeout=10.0) if data: writer.write(data) else: logging.info('Connection from {} closed by peer'.format(peername)) break except concurrent.futures.TimeoutError: logging.info('Connection from {} closed by timeout'.format(peername)) break
      
      





function- reader.readline()



は、入力ストリームからのデータの非同期読み取りを実行します。 ただし、データの読み取りを待つ時間に制限はありません。タイムアウトまでにデータを停止する必要がある場合は、 asyncio.wait_for()



でコルーチン関数呼び出しをラップする必要があります。 この場合、秒単位で指定された時間間隔が経過すると、 concurrent.futures.TimeoutError



例外が発生し、必要に応じて処理できます。

この例では、 reader.readline()



が空でない値を返すことを確認する必要があります。 それ以外の場合、接続がクライアントによって切断された後(接続はピアによってリセットされます)、空の値を読み取って返す試行は無期限に継続します。



しかし、OOPはどうですか?


OOPも大丈夫です。 @ asyncio.coroutineデコレータでコルーチン関数呼び出しを使用してメソッドをラップするだけで十分です。 APIでコルーチンとして実行される関数は明示的に指定されます。 以下は、EchoServerクラスを実装する例です。



 import asyncio import logging import concurrent.futures class EchoServer(object): """Echo server class""" def __init__(self, host, port, loop=None): self._loop = loop or asyncio.get_event_loop() self._server = asyncio.start_server(self.handle_connection, host=host, port=port) def start(self, and_loop=True): self._server = self._loop.run_until_complete(self._server) logging.info('Listening established on {0}'.format(self._server.sockets[0].getsockname())) if and_loop: self._loop.run_forever() def stop(self, and_loop=True): self._server.close() if and_loop: self._loop.close() @asyncio.coroutine def handle_connection(self, reader, writer): peername = writer.get_extra_info('peername') logging.info('Accepted connection from {}'.format(peername)) while not reader.at_eof(): try: data = yield from asyncio.wait_for(reader.readline(), timeout=10.0) writer.write(data) except concurrent.futures.TimeoutError: break writer.close() if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG) server = EchoServer('127.0.0.1', 2007) try: server.start() except KeyboardInterrupt: pass # Press Ctrl+C to stop finally: server.stop()
      
      







最初と2番目のケースでわかるように、コードは線形であり、完全に読み取り可能です。 また、2番目のケースでは、コードは自己完結型のクラスに組み込まれています。



HTTPサーバー



このすべてに対処した後、意図せずに、より実質的な何かをしたいという願望があります。 asyncio



モジュールはそのような機会を提供してくれます。 たとえば、 tornado



とは異なり、HTTPサーバーは実装されていません。 彼らが言うように、この省略を修正しようとしないことは罪です:)



HTTPRequestなどのすべてのクラスを使用してHTTPサーバー全体をゼロから作成するのはスポーツではありません。WSGIプロトコルの上で動作する既製のフレームワークがたくさんあるからです。 知識のある人は、WSGIが同期プロトコルであることを正しく認識します。 これは事実ですが、environとリクエスト本文のデータを非同期で読み取ることができます。 WSGIの結果出力はジェネレーターとして推奨されており、 asyncio



使用されるコルーチンコンセプトによく適合します。



コンテンツを返すことですべてを正しく行うフレームワークの1つはボトルです。 そのため、たとえば、ファイルの内容を完全にではなく、ジェネレータを介して部分的に提供します。 したがって、開発したWSGIサーバーをテストするために選択し、結果に満足しました。 たとえば、デモアプリケーションは、同時に複数のクライアント接続に大きなファイルを提供することができました。



私はgithubで何が起こったのかを完全に見ることができます。 テストもドキュメントもまだありませんが、ボトルフレームワークを使用したデモアプリケーションがあります。 特定のディレクトリにあるファイルのリストを表示し、サイズに関係なく、選択したファイルを非同期モードで提供します。 あなたがこの映画のカタログを投げるなら、あなたは小さなビデオホスティングを整理することができます:)



CherryPy開発チームに特別な感謝を申し上げたいと思います。彼らのコードをよく見て、「自分のバイク」を思い付かないように完全に何かを取りました。



サンプルアプリケーションを見る
 import bottle import os.path from os import listdir from bottle import route, template, static_file root = os.path.abspath(os.path.dirname(__file__)) @route('/') def index(): tmpl = """<!DOCTYPE html> <html> <head><title>Bottle of Aqua</title></head> </body> <h3>List of files:</h3> <ul> % for item in files: <li><a href="/files/{{item}}">{{item}}</a></li> % end </ul> </body> </html> """ files = [file_name for file_name in listdir(os.path.join(root, 'files')) if os.path.isfile(os.path.join(root, 'files', file_name))] return template(tmpl, files=files) @route('/files/<filename>') def server_static(filename): return static_file(filename, root=os.path.join(root,'files')) class AquaServer(bottle.ServerAdapter): """Bottle server adapter""" def run(self, handler): import asyncio import logging from aqua.wsgiserver import WSGIServer logging.basicConfig(level=logging.ERROR) loop = asyncio.get_event_loop() server = WSGIServer(handler, loop=loop) server.bind(self.host, self.port) try: loop.run_forever() except KeyboardInterrupt: pass # Press Ctrl+C to stop finally: server.unbindAll() loop.close() if __name__ == '__main__': bottle.run(server=AquaServer, port=5000)
      
      







WSGIサーバーコードを作成するとき、 asyncio



モジュールに起因する可能性のあるニュアンスに気付きませんでした。 唯一の瞬間、それはブラウザの機能(たとえば、クロム)であり、大きなファイルの受信を開始したことがわかるとリクエストをリセットします。 明らかに、これは、大きなファイルをダウンロードするためのより最適化された方法に切り替えるために行われました。その後、要求が繰り返され、ファイルが正常に受け入れられ始めるためです。 ただし、 StreamWriter.write()



関数を呼び出してファイルのアップロードが既に開始されている場合、最初に破棄された要求はConnectionResetError



例外をスローします。 このケースは、 StreamWriter.close()



で処理して閉じる必要があります。



性能



ベンチマークには、 Siegeユーティリティを選択しました。 被験者は、 bottle



、「 bottle



」とも呼ばれる非常に人気のあるウェイトレスWSGIサーバー bottle



そしてもちろんトルネードと組み合わせた「私たちの患者」(別名アクア:)でした。 アプリは可能な限り最小の挨拶でした。 次のパラメーターで実施されたテスト:100および1000の同時接続。 13バイトとキロバイトのテスト期間10秒。 テスト期間は13メガバイトで60秒。 返されるデータのサイズに関する3つのオプション、それぞれ13バイト、13キロバイト、13メガバイト。 結果は次のとおりです。

100

同時ユーザー

13 b(10秒)

13 Kb(10秒)

13 Mb(60秒)

役に立つ

トランス/秒

役に立つ

トランス/秒

役に立つ

トランス/秒

アクア+ボトル

100.0%

835.24

100.0%

804.49

99.9%

26.28

ウェイトレス+ブートル

100.0%

707.24

100.0%

642.03

100.0%

8.67

竜巻

100.0%

2282.45

100.0%

2071.27

100.0%

15.78



1000

同時ユーザー

13 b(10秒)

13 Kb(10秒)

13 Mb(60秒)

役に立つ

トランス/秒

役に立つ

トランス/秒

役に立つ

トランス/秒

アクア+ボトル

99.9%

800.41

99.9%

777.15

60.2%

26.24

ウェイトレス+ブートル

94.9%

689.23

99.9%

621.03

37.5%

8.89

竜巻

100.0%

1239.88

100.0%

978.73

55.7%

14.51



何が言えますか? 竜巻は確かに操縦しますが、「私たちの患者」は大きなファイルで先行しているようで、より多くの接続で相対的なパフォーマンスを改善しました。 さらに、彼は自信を持ってウェイトレスを回避しました(コア数に応じて4つの子プロセスがあります)。これは開発者の間では悪くありません。 私のテストが100%十分であるとは言えませんが、おそらく評価としては適合します。



更新: 13メガバイトの応答本文の奇妙な数字に気付きました 実際、10秒後にはおそらくテストが開始されなかったと思われます:)テスト期間60秒で受け取った数値を修正しました。



Siegeユーティリティの例と2番目のテーブルの最後の列の完全な結果
 $ siege -c 1000 -b -t 60S http://127.0.0.1:5000/ ** SIEGE 2.70 ** Preparing 1000 concurrent users for battle. Transactions: 1570 hits Availability: 60.18 % Elapsed time: 59.84 secs Data transferred: 20410.00 MB Response time: 5.56 secs Transaction rate: 26.24 trans/sec Throughput: 341.08 MB/sec Concurrency: 145.80 Successful transactions: 1570 Failed transactions: 1039 Longest transaction: 20.44 Shortest transaction: 0.00 $ siege -c 1000 -b -t 60S http://127.0.0.1:5001/ ** SIEGE 2.70 ** Preparing 1000 concurrent users for battle. The server is now under siege... Lifting the server siege... done. Transactions: 526 hits Availability: 37.49 % Elapsed time: 59.20 secs Data transferred: 6838.00 MB Response time: 16.05 secs Transaction rate: 8.89 trans/sec Throughput: 115.51 MB/sec Concurrency: 142.58 Successful transactions: 526 Failed transactions: 877 Longest transaction: 42.43 Shortest transaction: 0.00 $ siege -c 1000 -b -t 60S http://127.0.0.1:5002/ ** SIEGE 2.70 ** Preparing 1000 concurrent users for battle. The server is now under siege... Lifting the server siege... done. Transactions: 857 hits Availability: 55.65 % Elapsed time: 59.07 secs Data transferred: 11141.00 MB Response time: 20.14 secs Transaction rate: 14.51 trans/sec Throughput: 188.61 MB/sec Concurrency: 292.16 Successful transactions: 857 Failed transactions: 683 Longest transaction: 51.19 Shortest transaction: 3.26
      
      









アウトロ



asyncio



を使用する非同期Webサーバーには生命権があります。 深刻なプロジェクトでのこのようなサーバーの使用について話すのは時期尚早ですが、テスト後、データベースおよびキーバリューストア用のasyncio



ドライバーの登場により、実行できる可能性があります。



All Articles