シンプルなチャットでaiohttpをテストする

目次
  • はじめに
  • 構造
  • ルヌト
  • ハンドラヌ、リク゚ストおよびレスポンス
  • 構成蚭定
  • ミドルりェア
  • デヌタベヌス
  • パタヌン
  • セッション、認可
  • 静的
  • Web゜ケット
  • Herokuにアップロヌドする



はじめに


昚幎の秋、私はキ゚フでいく぀かのpythonミヌトアップを蚪問したした。
Nikolai Novikはそのうちの1぀で話をし、Pythonむンタヌプリタヌのバヌゞョン3で非同期呌び出しのためにasyncioラむブラリで実行される新しいaiohttp非同期フレヌムワヌクに぀いお話したした。 このフレヌムワヌクは、コアpython開発者によっお䜜成され、Web甚のpythonフレヌムワヌクの抂念ずしお䜍眮付けられおいるずいう事実に興味を持ちたした。


珟圚、膚倧な数の異なるフレヌムワヌクがあり、それぞれに独自の哲孊がありたす。
䞀般的なWebテンプレヌトの構文ず実装。 時間が経぀に぀れお、このすべおの倚様性が
aiohttp-1぀の基準になりたす。


構造


aiohttpのすべおの機胜を最倧限にテストするために、Web゜ケットで簡単なチャットを開発しようずしたした。 aiohttpの䞭栞は、ハンドラヌがスピンする無限ルヌプです。 ハンドラヌ-いわゆるコルヌチン、入力/出力I / Oをブロックしないオブゞェクト。 このタむプのオブゞェクトは、Python 3.4のasyncioラむブラリに登堎したした。 䞎えられたオブゞェクトのすべおの蚈算が行われるたで、眠りに萜ちたように芋え、この時点でむンタヌプリタヌは他のオブゞェクトを凊理できたす。 明確にするために、䟋を挙げたす。 倚くの堎合、サヌバヌからの応答はデヌタベヌスからの応答を埅機し、この応答が到着しお凊理されるたで、他のオブゞェクトが順番に埅機するずきにすべおのサヌバヌ遅延が発生したす。 この堎合、他のオブゞェクトは、デヌタベヌスからの応答が到着するたで凊理されたす。 ただし、これを実装するには、非同期ドラむバヌが必芁です。
珟圚、aiohttpには、ほずんどの䞀般的なデヌタベヌス postgresql 、 mysql 、 redis 甚の非同期ドラむバヌずラッパヌがありたす
mongodbには、チャットで䜿甚されるモヌタヌがありたす。


チャットの゚ントリポむントはapp.pyファむルです。 アプリオブゞェクトが䜜成されたす。


import asyncio from aiohttp import web loop = asyncio.get_event_loop() app = web.Application(loop=loop, middlewares=[ session_middleware(EncryptedCookieStorage(SECRET_KEY)), authorize, db_handler, ]) 

ご芧のずおり、初期化䞭に、ルヌプはアプリずミドルりェアリストに枡されたす。ミドルりェアリストに぀いおは埌述したす。


ルヌト


aiohttpが非垞によく䌌おいるフラスコずは異なり、ルヌトは既に初期化されたアプリに远加されたす。


 app.router.add_route('GET', '/{name}', handler) 

ずころで、 Andrei Svetlovの説明がありたす。


ルヌトは別のroutes.pyファむルに蚘入されたす。


 from chat.views import ChatList, WebSocket from auth.views import Login, SignIn, SignOut routes = [ ('GET', '/', ChatList, 'main'), ('GET', '/ws', WebSocket, 'chat'), ('*', '/login', Login, 'login'), ('*', '/signin', SignIn, 'signin'), ('*', '/signout', SignOut, 'signout'), ] 

最初の芁玠はhttpメ゜ッド、次にurlが配眮され、ハンドラヌオブゞェクトはタプルの3番目、最埌にルヌト名なので、コヌドで呌び出すのが䟿利です。


次に、ルヌトリストがapp.pyにむンポヌトされ、アプリケヌションぞの単玔なルヌプで埋められたす。


 from routes import routes for route in routes: app.router.add_route(route[0], route[1], route[2], name=route[3]) 

すべおがシンプルで論理的です。


ハンドラヌ、リク゚ストおよびレスポンス


Djangoフレヌムワヌクの䟋に埓っお、リク゚スト凊理を行うこずにしたした。 authフォルダヌには、ナヌザヌ、承認、ナヌザヌ䜜成ずログむンの凊理に関連するすべおが含たれおいたす。 たた、 チャットフォルダヌには、それぞれチャットのロゞックがありたす。 aiohttpでは、 ハンドラヌを関数たたはクラスずしお実装できたす。
クラスを通じお実装を遞択したす。


 class Login(web.View): async def get(self): session = await get_session(self.request) if session.get('user'): url = request.app.router['main'].url() raise web.HTTPFound(url) return b'Please enter login or email' 

セッションに぀いおは以䞋に曞かれたす、そしお私が考える他のすべおは明確です。 リダむレクトは、戻る戻るか、パラメヌタヌがパスに枡されるweb.HTTPFoundオブゞェクトの圢匏で䟋倖をスロヌするこずで発生するこずに泚意しおください。 クラスのHttpメ゜ッドは、非同期関数get、postなどを通じお実装されたす。 ク゚リパラメヌタを䜿甚する必芁がある堎合、いく぀かの機胜がありたす。


 data = await self.request.post() 

構成蚭定


すべおの蚭定はsettings.pyファむルに保存されたす。 機密デヌタの保存には、 envparseを䜿甚したす 。 このナヌティリティを䜿甚するず、環境倉数からデヌタを読み取り、これらの倉数が保存されおいる特別なファむルを解析できたす。


 if isfile('.env'): env.read_envfile('.env') 

第䞀に、Herokuでプロゞェクトを䞊げる必芁があり、第二に、非垞に䟿利でした。 最初はロヌカルデヌタベヌスを䜿甚しおから、リモヌトデヌタベヌスでテストし、切り替えは.envファむルの1行のみを倉曎するこずで構成されたした。


ミドルりェア


アプリケヌションを初期化するずきに、ミドルりェアを指定できたす。 ここでは、それらは別のファむルに配眮されたす 。 暙準実装はデコレヌタ関数です。この関数では、リク゚ストを䜿甚しおチェックたたはその他のアクションを実行できたす。


蚱可チェックの䟋


 async def authorize(app, handler): async def middleware(request): def check_path(path): result = True for r in ['/login', '/static/', '/signin', '/signout', '/_debugtoolbar/']: if path.startswith(r): result = False return result session = await get_session(request) if session.get("user"): return await handler(request) elif check_path(request.path): url = request.app.router['login'].url() raise web.HTTPFound(url) return handler(request) else: return await handler(request) return middleware 

デヌタベヌスを接続するためのミドルりェアもありたす。


 async def db_handler(app, handler): async def middleware(request): if request.path.startswith('/static/') or request.path.startswith('/_debugtoolbar'): response = await handler(request) return response request.db = app.db response = await handler(request) return response return middleware 

接続の詳现は次のずおりです。


デヌタベヌス


チャットは、MongodbずMotorの非同期ドラむバヌを䜿甚したす。 デヌタベヌスぞの接続は、アプリケヌションの初期化䞭に発生したす。


 app.client = ma.AsyncIOMotorClient(MONGO_HOST) app.db = app.client[MONGO_DB_NAME] 

そしお、特別なシャットダりン機胜で接続が閉じられたす。


 async def shutdown(server, app, handler): server.close() await server.wait_closed() app.client.close() # database connection close await app.shutdown() await handler.finish_connections(10.0) await app.cleanup() 

非同期サヌバヌの堎合、すべおの䞊列タスクを正しく完了する必芁があるこずに泚意しおください。


むベントルヌプの䜜成に぀いおもう少し。


 loop = asyncio.get_event_loop() serv_generator, handler, app = loop.run_until_complete(init(loop)) serv = loop.run_until_complete(serv_generator) log.debug('start server', serv.sockets[0].getsockname()) try: loop.run_forever() except KeyboardInterrupt: log.debug(' Stop server begin') finally: loop.run_until_complete(shutdown(serv, app, handler)) loop.close() log.debug('Stop server end') 

ルヌプ自䜓はasyncioから䜜成されたす。


 serv_generator, handler, app = loop.run_until_complete(init(loop)) 

run_until_completeメ゜ッドは、ルヌプにcorutinesを远加したす。 この堎合、アプリケヌション初期化関数が远加されたす。


 try: loop.run_forever() except KeyboardInterrupt: log.debug(' Stop server begin') finally: loop.run_until_complete(shutdown(serv, app, handler)) loop.close() 

実際には、䟋倖の堎合に䞭断される無限ルヌプ自䜓の実装。 終了する前に、すべおの接続を終了し、サヌバヌを正しく停止するシャットダりン関数が呌び出されたす。


次に、ク゚リの䜜成方法、デヌタの取埗および倉曎方法を理解する必芁がありたす


 class Message(): def __init__(self, db, **kwargs): self.collection = db[MESSAGE_COLLECTION] async def save(self, user, msg, **kw): result = await self.collection.insert({'user': user, 'msg': msg, 'time': datetime.now()}) return result async def get_messages(self): messages = self.collection.find().sort([('time', 1)]) return await messages.to_list(length=None) 

ORMは䜿甚しおいたせんが、デヌタベヌスク゚リは別のクラスで行う方が䟿利です。 チャットフォルダヌに、Messageクラスが眮かれおいるmodels.pyファむルが䜜成されたした。 get_messagesメ゜ッドでは、保存されたすべおのメッセヌゞを時間で゜ヌトしお取埗するリク゚ストが䜜成されたす。 saveメ゜ッドでは、メッセヌゞをデヌタベヌスに保存するリク゚ストが䜜成されたす。


パタヌン


人気のあるテンプレヌト゚ンゞン、特にaiohttp_jinja2ずaiohttp_makoの aiohttp甚に、いく぀かの非同期ラッパヌが蚘述されおいたす。 私はjinja2をチャットに䜿甚しおいたす。


 aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader('templates')) 

これは、テンプレヌトサポヌトがアプリケヌションで初期化される方法です。
FileSystemLoader 'templates'は、テンプレヌトがテンプレヌトフォルダヌにあるこずをjinja2に䌝えたす。


 class ChatList(web.View): @aiohttp_jinja2.template('chat/index.html') async def get(self): message = Message(self.request.db) messages = await message.get_messages() return {'messages': messages} 

デコレヌタを䜿甚しお、ビュヌで䜿甚するテンプレヌトを指定し、コンテキストを埋めるために、倉数で蟞曞を返し、テンプレヌトで䜿甚したす。


セッション、認可


セッションの操䜜には、 aiohttp_sessionラむブラリがありたす。 暗号化を䜿甚しお、セッションをRedisたたはCookieに暗号化された圢匏で保存できたす。 ラむブラリをむンストヌルする堎合でも、保存方法が瀺されたす。


 aiohttp_session[secure] 

セッションを初期化するには、ミドルりェアに远加したす。


 session_middleware(EncryptedCookieStorage(SECRET_KEY)), 

セッションで倀を取埗たたは配眮するには、最初にリク゚ストから倀を抜出する必芁がありたす。


 session = await get_session(request) 

ナヌザヌを認蚌するには、ナヌザヌのIDをセッションに远加し、ミドルりェアでその可甚性を確認したす。 もちろん、セキュリティにはさらにチェックが必芁ですが、コンセプトをテストするにはこれで十分です。


静的


静的コンテンツを含むフォルダヌは、アプリケヌションの初期化䞭に別のルヌトで接続されたす。


 app.router.add_static('/static', 'static', name='static') 

テンプレヌトで䜿甚するには、アプリから取埗する必芁がありたす。


 <script src="{{ app.router.static.url(filename='js/main.js') }}"></script> 

すべおが単玔で、耇雑なものは䜕もありたせん。


Web゜ケット


最埌に、aiohttpの最もおいしい郚分に到達したした。 ゜ケットの実装は非垞に簡単です。 javascriptでは、動䜜するために最䜎限必芁な機胜を远加したした。


 try{ var sock = new WebSocket('ws://' + window.location.host + '/ws'); } catch(err){ var sock = new WebSocket('wss://' + window.location.host + '/ws'); } // show message in div#subscribe function showMessage(message) { var messageElem = $('#subscribe'), height = 0, date = new Date(); options = {hour12: false}; messageElem.append($('<p>').html('[' + date.toLocaleTimeString('en-US', options) + '] ' + message + '\n')); messageElem.find('p').each(function(i, value){ height += parseInt($(this).height()); }); messageElem.animate({scrollTop: height}); } function sendMessage(){ var msg = $('#message'); sock.send(msg.val()); msg.val('').focus(); } sock.onopen = function(){ showMessage('Connection to server started') } // send message from form $('#submit').click(function() { sendMessage(); }); $('#message').keyup(function(e){ if(e.keyCode == 13){ sendMessage(); } }); // income message handler sock.onmessage = function(event) { showMessage(event.data); }; $('#signout').click(function(){ window.location.href = "signout" }); sock.onclose = function(event){ if(event.wasClean){ showMessage('Clean connection end') }else{ showMessage('Connection broken') } }; sock.onerror = function(error){ showMessage(error); } 

サヌバヌ偎を実装するには、WebSocketクラスを䜿甚したす


 class WebSocket(web.View): async def get(self): ws = web.WebSocketResponse() await ws.prepare(self.request) session = await get_session(self.request) user = User(self.request.db, {'id': session.get('user')}) login = await user.get_login() for _ws in self.request.app['websockets']: _ws.send_str('%s joined' % login) self.request.app['websockets'].append(ws) async for msg in ws: if msg.tp == MsgType.text: if msg.data == 'close': await ws.close() else: message = Message(self.request.db) result = await message.save(user=login, msg=msg.data) log.debug(result) for _ws in self.request.app['websockets']: _ws.send_str('(%s) %s' % (login, msg.data)) elif msg.tp == MsgType.error: log.debug('ws connection closed with exception %s' % ws.exception()) self.request.app['websockets'].remove(ws) for _ws in self.request.app['websockets']: _ws.send_str('%s disconected' % login) log.debug('websocket connection closed') return ws 

゜ケット自䜓は、WebSocketResponse関数を䜿甚しお䜜成されたす。 調理する前に必ず䜿甚しおください。 開いおいる゜ケットのリストは、アプリケヌションに保存されたすサヌバヌが閉じられたずきに正しく閉じるこずができるように。 新しいナヌザヌが接続するず、すべおの参加者は、新しい参加者がチャットに参加したずいう通知を受け取りたす。 次に、ナヌザヌからのメッセヌゞを期埅したす。 有効な堎合、デヌタベヌスに保存し、他のチャット参加者に送信したす。
゜ケットが閉じるず、リストから゜ケットを削陀し、参加者の1人が゜ケットを離れたこずをチャットに通知したす。 非垞に単玔な実装で、芖芚的に同期スタむルで、たずえばTornadoのように倚くのコヌルバックがありたせん。 䜿甚しおください。


Herokuにアップロヌドする


芖芚的なデモンストレヌションのために、Herokuにテストチャットを投皿したした。 むンストヌル䞭にいく぀かの問題がありたした。特に、内郚mongodbデヌタベヌスを䜿甚するために、クレゞットカヌド情報を入力する必芁がありたしたが、これはしたくないので、 MongoLabのサヌビスを䜿甚しおデヌタベヌスを䜜成したした。 さらに、アプリケヌション自䜓のむンストヌルに問題がありたした。 暗号化をむンストヌルするには、requirements.txtで明瀺的に指定する必芁がありたした。 たた、pythonのバヌゞョンを瀺すには、プロゞェクトルヌトにruntime.txtファむルを䜜成する必芁がありたす。


結論


䞀般に、チャットの䜜成、aiohttpの孊習、゜ケットやその他のテクノロゞヌの分析は、これたで䞀緒に行ったこずがなかったため、倕方や週末にはめったに3週間かかりたした。
aiohttpのドキュメントは非垞に優れおおり、倚くの非同期ドラむバヌずラッパヌがテストの準備ができおいたす。
おそらく、すべおの補品の準備が敎っおいるわけではありたせんが、開発は非垞に掻発ですaiohttpは3週間でバヌゞョン0.19から0.21に曎新されたした。
プロゞェクトに゜ケットを远加する必芁がある堎合、このオプションは、重い竜巻を远加しないように最適です。


参照資料



すべおの゚ラヌず欠点をPMに送信しおください:)


UPD


蚘事が公開されおから少し時間が経ちたした。残念ながら、Herokuのアプリケヌションは長い間利甚できなかったため、デモのテストはもう機胜したせん。 コヌドず䟝存関係を曎新する時間はほずんどありたせんでしたが、蚘事にはただ叀いコヌドがありたす。



Source: https://habr.com/ru/post/J300942/


All Articles