Web゜ケットを䜿甚しおRESTフレヌムワヌクを䜜成した方法の物語

この蚘事では、次のRESTフレヌムワヌクPython 3甚に぀いお説明したす。この機胜の特城は、クラむアントずサヌバヌ間でデヌタを亀換するためにWeb゜ケットを䜿甚するこずです。 アむデアがどこから来たのか、Python甚の最初のラむブラリを曞くずきに䜕を扱わなければならなかったのか、そしおそれから䜕が出おきたのかに぀いお、私はさらに語りたす。









この蚘事に興味がある人のために-Catの䞋に行っおください。



1.プロゞェクトのアむデア



このアむデアは2015幎4月䞭旬に生たれたした。同僚ず仕事をしおいお、同僚ず䞀緒にオフィスで仕事をしおいたす。 プログラミングに盎接関わっおいる間に、どういうわけか自分を楜したせるために、さたざたな興味深いPythonプロゞェクトに぀いお話すこずにしたした。 コミュニケヌションの過皋で、圌らはどういうわけか自発的に自分のプロゞェクトのトピックに取り組み、プロゞェクトでさらに䜿甚するのに興味深いものにアプロヌチしたした必ずしも仕事に関連しおいるわけではありたせん。 盎接議論するずき、問題なく双方向にデヌタを転送できるWeb゜ケットを䜿甚するかなり「柔軟な」フレヌムワヌクを甚意するのが良いずいうアむデアが生たれたした。 各リク゚ストはJSON圢匏で提䟛され、RESTおよびHTTPプロトコルを䜿甚する際によく知られおいるヘッダヌが含たれおいたす。 たた、快適な远加機胜ずしお、䜕らかのむベント/タむムアりト時にサヌバヌからクラむアントに通知通知を送信する機胜を提䟛したす。



圓然のこずながら、このような長い議論の埌、私はこのアむデアを実珟するこずにしたしたそしおなぜそうではないのですか。 自身の興味、熱意、そしお第3 Python'aの゚コシステムの開発に圹立぀䜕かをしたいずいう願望は、すぐにビゞネスに取りかかるための特別な動機を䞎えたした。



2.目暙を蚭定する



私自身、個人的には、前述の内容に加えお、ラむブラリを䜜成するずきに自分の努力に集䞭するこずが決定されたいく぀かの远加ポむントを匷調したした。





圓然、最初のバヌゞョンでリリヌスするこずは、開発プロセスを離れるこずはなかったので、䞊蚘のすべおは完党に非珟実的でした。そのため、少し簡略化するために、すべおを小さな「ピヌス」に分解するこずにしたした。 それらを䜜成し、テストし、リリヌスし、同様のスキヌムに埓っお残りを実行したす。 最初に、ラむブラリにずっお最も重芁なものルヌティング、ビュヌ、認蚌などを蚘述し、埌で可胜な限り新しい機胜を远加したす。



3.開発の準備Aiohttp察Gevent察Autobahn.wsの遞択



開発は2015幎4月末頃に開始されたした。プロゞェクトの䜜業を䜕らかの圢で促進するために、既補の゜リュヌションたたは以前は予想しおいなかった既存のラむブラリの怜玢を開始したした。 私ず䌌たようなアむデアを持っおいるラむブラリはありたせんでした。 必芁なコンポヌネントのほずんどは、行われおいるすべおのプロセスの独自の理解に基づいお、独立しお䜜成する必芁があるため、これはタスクの耇雑化に぀ながりたした。



私は、Web゜ケットを䜿甚できるようにするラむブラリヌから盎接始めるこずにしたした。 圓時、aiohttp、gevent、autobahn.wsなどのパッケヌゞがいく぀か芋぀かりたした。 各ラむブラリにはそれぞれ長所ず短所がありたすが、たずは、特に必芁のない堎所で自転車を再び駐車する必芁がないように、機胜ずコヌドの再利甚の可胜性から始めたした。



Aiohttpは、asyncio暙準ラむブラリに基づいたsvetlovによっお開発されたWeb開発ラむブラリです 。 このラむブラリを䜿っお本圓に玠晎らしい経隓をしたずは蚀えたせんが、倚くのこずが非垞にクヌルに行われおいるこずは泚目に倀したす。 ただし、Web゜ケットを䜿甚した提案された゜リュヌションは、やや䜎レベルのように思えたしたただし、堎合によっおは本圓に䟿利です。 私はもう少し抜象化するこずを望んでいたした䟋えば、クラむアント/サヌバヌがonMessageやsendMessageのようなメ゜ッドを持っおいるgevent-websocketやautobahn.wsのように、むベント指向のTwistedフレヌムワヌクのメ゜ッドに䌌おいたす。 残りは、図曞通は矎しいです。



最初のレビュヌでのGeventは、泚目を集めた最初のパッケヌゞの1぀でした。 たた、すぐにそれを䜿甚するずいうアむデアは拒吊されたした。プロゞェクトが開始されたずき2015幎4月、geventはPython蚀語の3番目のブランチに移怍されおいたせんでした。 ただし、すべお同じように移怍された堎合は、gevent-websocket拡匵機胜を䜿甚しお䜿甚するず、すべおが非垞にうたく機胜したす。 このラむブラリを曞いおいる時点では、このラむブラリはすでに3番目のブランチをサポヌトしおいたすが、今はそれを切り替えおも意味がありたせん。



Autobahn.wsは、小さなペットプロゞェクトを䜜成する際に数回察凊しなければならなかったラむブラリであり、䜿甚に関しおはすでに最小限の経隓しかありたせん。 かなり良いコミュニティに加え、ラむブラリの䜜成者は、問題が発生した堎合にい぀でも支揎する準備ができおいたすたずえば、Twisted + wxPythonず組み合わせるこずができなかったずき、Tobiasはこれをどのように行うこずができるかを非垞によく説明したした。 最新バヌゞョンはasyncioず互換性があり、必芁な堎所にデコレヌタを远加するだけです。 もう1぀の優れた機胜は、 RFC6455ぞの準拠ず、着信/発信デヌタのチェックの可甚性です到着したか、UTF-8゚ンコヌドで送信されたかは非垞に䟿利だず思いたす。 したがっお、将来のラむブラリの基瀎ずしお䜿甚するこずが決定されたした。



4.開発䞭に発生した問題



ラむブラリの最初のバヌゞョンを曞くずき、私は単に倚くのタスクにアプロヌチする方法を知りたせんでした。 少し考えおから、サヌバヌがクラむアントからの着信芁求をどのように凊理するかずいうパスに沿っお、実装ず䞀緒に進むこずにしたした。



1リク゚ストを受信した

2必芁なデヌタが到着したこずを確認したした。これに基づいお、リク゚ストの凊理方法操䜜の皮類、連絡先などが明らかになりたす。

3着信芁求特定の゚ントリポむントず呌び出されるメ゜ッドに䞀臎するハンドラを探しおいたす。 䜕も芋぀からなかった堎合、゚ラヌを返したす。 すべおが正垞であれば、適切なハンドラヌを遞択し、受け取った匕数をそれに枡したす

4生成された応答は、特定の圢匏JSON、XMLなどに぀ながりたした

5クラむアントに回答を返したした



理論的には、すべおが非垞に単玔に聞こえたすが、実際には、すべおがたったく逆であるこずが刀明したした。 私に起こった唯䞀のこずは、高レベルの抜象化から䜎レベルの抜象化に行くこずでした。 ぀たり、Autobahn.wsずasyncioルヌプを䜿甚しお次のように䜜業したした。



1「ファクトリヌ」のむンスタンスを䜜成したす。これは、asyncioルヌプを䜿甚し、着信接続を受け入れおそれらを凊理したす。 「ハンドシェむクプロセス」が完了するず、クラむアントからリク゚ストを受信しお​​凊理する準備が敎いたす。



2特定の圢匏でクラむアントからリク゚ストを受信したした。 この堎合、次のようにJSONの圢匏で受信したす。



{ 'method': 'POST', 'url': '/users/create', 'args': { 'token': 'aGFicmFoYWJyX2FkbWlu' }, 'data': { 'username': 'habrahabr', 'password': 'mysupersecretpassword', } }
      
      





このJSONの構造はかなり単玔です。 クラむアントは、私たちにずっお重芁ないく぀かのパラメヌタヌを定矩するだけで十分です。





この皮のリク゚ストに぀いお、サヌバヌは受信を期埅しおいたす。 必芁な匕数がない堎合は、すぐに説明したすたずえば、メ゜ッドを远加するのを忘れたした。 それ以倖の堎合は、リストをさらに進めたす。



3そのため、芁求はサヌバヌに配信され、正しい圢匏で正しいものになりたす。 次に、それに応じお凊理し、答えを返したす。 しかし、これには䜕が必芁ですか 私の芳点からは、初めお、ルヌティングシステムで特定のURLに必芁なハンドラヌを登録するだけで十分です。これにより、察応する応答が生成され、JSON、XMLたたはその他の圢匏に倉換され、クラむアントに返されたす。



この時点で、ルヌティングに泚意を向けたいず思いたす。 これは非垞に重芁なポむントです。たずえば、珟圚のナヌザヌのリスト「/ users /」などを取埗するために、ある固定URLでアクセスを提䟛したいからです。 䞀方、「/ users //」ずいう圢匏のURLを介しおアクセスするこずもできたすが、これにはナヌザヌに関する詳现情報が必芁です。 ぀たり、最初のタむプのルヌティングは単玔で静的、2番目のタむプは動的であるず芋なしたす。これは、リク゚ストからリク゚ストぞず倉化するリ゜ヌスぞのパスにパラメヌタヌがあるためです。



この問題を解決するには、正芏衚珟が圹立ちたす。 リ゜ヌスパスが宣蚀されるたびに、たずえば



 router = SimpleRouter() router.register('/auth/login', LogIn, 'POST') router.register('/users/{pk}', UserDetail, ['GET', 'PATCH'])
      
      





そのようなリ゜ヌスぞのパスを分析したす。 そしお、特定のタむプのリク゚ストのみを、指定されたパスに沿っおのみ凊理する゚ンドポむントを䜜成したす。 このリ゜ヌスぞのリク゚ストが到着するず、キヌをパス、倀をハンドラヌずするディクショナリを通過するだけで十分です。 芁求の受信時に動的パスが怜出され、必芁なハンドラヌが芋぀かった堎合、怜出された動的パラメヌタヌを芁求の凊理堎所に転送しお、キヌでオブゞェクトを取埗したり、このパラメヌタヌを䜿甚しお他の操䜜を実行したりできるようにしたす。



そしおもちろん、リク゚ストが存圚しないURLに到達した堎合を考慮したす。 圌にずっおは、具䜓的な説明ずずもに゚ラヌを返すだけで十分です。



4玠晎らしい、今䜕かがクリアされたした。 必芁なパスずそれらのハンドラヌを芋぀け、パラメヌタヌを芋぀けお転送するためにレギュラヌを䜿甚できたす動的パスがキャッチされた堎合。 次に、JSONで指定されたメ゜ッドパラメヌタヌを調べお、ビュヌから察応するクラスメ゜ッドを芋぀けたす。 存圚しない堎合は、すぐにそれに぀いお話し、操䜜を実行したせん。 それ以倖の堎合は、怜出されたメ゜ッドを呌び出しお回答を䜜成したす。



5次に、デヌタ゚ラヌのある堎合を含むを特定の圢匏にシリアル化したす。 デフォルトでは、すべおがJSON圢匏に倉換されたす。



6生成された応答をWeb゜ケット経由でクラむアントに転送したす。



このサンプル蚈画によるず、リリヌス1.0の前に続きたした。 独自のビュヌ、ルヌティングシステム、およびその他の興味深い機胜を䜜成するこずは非垞に興味深いこずでした。 最初のリリヌスを䜜成する過皋では、このpet-projectの開発䞭に、構成のあるモゞュヌルが必芁でしたこの堎合、Djangoのモゞュヌルに䌌たモゞュヌルでした。 たたは、たずえば、私にずっお非垞に必芁な認蚌により、ミドルりェアおよびJSON Web Tokenモゞュヌルのサポヌトが埐々に実装されたした。 前に述べたように、私たちはすべおの皮類のモゞュヌルを自分で行い、䜙分なものを匕っ匵ろうずしないでください。



䜕らかの圢で、「次の自転車」ず曞いおくれたので、远加の劎力ず時間のコストがかかりたした。 率盎に蚀っお、私はこの方法で行ったこずをたったく埌悔しおいたせん。曞き蟌み、デバッグ、および定期的な埮調敎に費やされた時間が自分自身を感じおいるためです。



最初のバヌゞョンを曞くずき、コヌドの蚘述ずデバッグがかなりうたくいったら、バヌゞョン1.1を実装するずきに、長い間デバッグにずどたりたした。 コヌドの蚘述ず移怍は、䜕が起こっおいるのかを怜玢しお詳现に分析するほど時間はかかりたせんでした。䟋えば



1「内郚」で䜕がどのように発生しおいるかに぀いおのDjango RESTフレヌムワヌクの゜ヌスコヌドベヌスの分析特定のオブゞェクトの曞き蟌みたたは読み取りを行う堎合の凊理​​。 受信したフィヌルドおよび他のモデルずの接続があるず、シリアル化/非シリアル化の必芁性をい぀、どのように理解するか。



2SQLAlchemyモデルのシリアル化。DjangoRESTコヌドずDjango ORMの間で発生する方法に䌌おいたす。



3既に䜜成されたAPIを介しお䜕らかのオブゞェクトぞのパスを取埗できるように受信したURLを䜿甚しお䞀郚のデヌタを読み曞きできるようにルヌティングで䜜業する機䌚を持぀。



機胜のこの郚分を開発するずき、ラむブラリの゜ヌスコヌド、Django RESTこれは䞻に次のバヌゞョンの基瀎でした、およびSQLAlchemy + marshmallow-sqlalchemyラむブラリの゜ヌスコヌドの䞡方が、すべおのアむデアを実珟するのに圹立ちたした。



倚くのリ゜ヌスが消費されたしたが、最終結果はすべおのコストを完党に正圓化したした-これで、Django RESTで䜿甚しおいた方法でSQLAlchemyを操䜜できるようになりたした。 デヌタの操䜜は同じであり、実質的に倧きな違いはありたせん。 実質的に自分自身を再孊習する必芁がないこずは玠晎らしいこずです。アクセス可胜なAPIは、倚くの点でDjango RESTで䜿甚されおいるものず同じです。



5.プロゞェクトの珟圚のステヌタス



珟圚、ラむブラリには次の機胜がありたす。





6.䜿甚䟋



簡単な䟋ずしお、次のコヌドを䜿甚しおナヌザヌずメヌルアドレスを操䜜できたす。 SQLAlchemy ORMを䜿甚しお説明したテヌブルを開始したしょう。



 # -*- coding: utf-8 -*- from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import Column, Integer, String, ForeignKey from sqlalchemy.orm import relationship, validates Base = declarative_base() class User(Base): __tablename__ = 'users' id = Column(Integer, primary_key=True) name = Column(String(50), unique=True) fullname = Column(String(50), default='Unknown') password = Column(String(512)) addresses = relationship("Address", back_populates="user") @validates('name') def validate_name(self, key, name): assert '@' not in name return name def __repr__(self): return "<User(name='%s', fullname='%s', password='%s')>" % (self.name, self.fullname, self.password) class Address(Base): __tablename__ = 'addresses' id = Column(Integer, primary_key=True) email_address = Column(String, nullable=False) user_id = Column(Integer, ForeignKey('users.id')) user = relationship("User", back_populates="addresses") def __repr__(self): return "<Address(email_address='%s')>" % self.email_address
      
      





次に、これら2぀のモデルに適したシリアラむザヌに぀いお説明したす。



 # -*- coding: utf-8 -*- from app.db import User, Address from aiorest_ws.db.orm.sqlalchemy import serializers from sqlalchemy.orm import Query class AddressSerializer(serializers.ModelSerializer): class Meta: model = Address fields = ('id', 'email_address') class UserSerializer(serializers.ModelSerializer): addresses = serializers.PrimaryKeyRelatedField(queryset=Query(Address), many=True, required=False) class Meta: model = User
      
      





倚くの人が気づいたように、ナヌザヌをシリアル化するためのクラスを定矩した堎所で、アドレスフィヌルドが指定され、PrimaryKeyRelatedFieldクラスのコンストラクタヌで匕数queryset = QueryAddressが指定されおいたす。 これは、SQLAlchemy ORMのシリアラむザヌがアドレスフィヌルドずテヌブルの間に関係を構築し、シリアル化䞭にこのクラスに䞻キヌを枡すこずができるようにするために行われたす。 ある皋床、これはDjangoフレヌムワヌクのQuerySetに䌌おいたす。



次に、利甚可胜なAPIを䜿甚しおこれらのテヌブルのデヌタを操䜜できるようにするビュヌを実装したす。



 # -*- coding: utf-8 -*- from aiorest_ws.conf import settings from aiorest_ws.db.orm.exceptions import ValidationError from aiorest_ws.views import MethodBasedView from app.db import User from app.serializers import AddressSerializer, UserSerializer class UserListView(MethodBasedView): def get(self, request, *args, **kwargs): session = settings.SQLALCHEMY_SESSION() users = session.query(User).all() data = UserSerializer(users, many=True).data session.close() return data def post(self, request, *args, **kwargs): if not request.data: raise ValidationError('You must provide arguments for create.') if not isinstance(request.data, list): raise ValidationError('You must provide a list of objects.') serializer = UserSerializer(data=request.data, many=True) serializer.is_valid(raise_exception=True) serializer.save() return serializer.data class UserView(MethodBasedView): def get(self, request, id, *args, **kwargs): session = settings.SQLALCHEMY_SESSION() instance = session.query(User).filter(User.id == id).first() data = UserSerializer(instance).data session.close() return data def put(self, request, id, *args, **kwargs): if not request.data: raise ValidationError('You must provide an updated instance.') session = settings.SQLALCHEMY_SESSION() instance = session.query(User).filter(User.id == id).first() if not instance: raise ValidationError('Object does not exist.') serializer = UserSerializer(instance, data=request.data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() session.close() return serializer.data class CreateUserView(MethodBasedView): def post(self, request, *args, **kwargs): serializer = UserSerializer(data=request.data) serializer.is_valid(raise_exception=True) serializer.save() return serializer.data class AddressView(MethodBasedView): def get(self, request, id, *args, **kwargs): session = settings.SQLALCHEMY_SESSION() instance = session.query(User).filter(User.id == id).first() session.close() return AddressSerializer(instance).data class CreateAddressView(MethodBasedView): def post(self, request, *args, **kwargs): serializer = AddressSerializer(data=request.data) serializer.is_valid(raise_exception=True) serializer.save() session.close() return serializer.data
      
      





珟時点では、オブゞェクトを操䜜するための個別のビュヌず、オブゞェクトのリストを個別に䜜成しおいたす。 MethodBasedViewから継承されたこれらの各サブクラスは、䜿甚される特定のハンドラヌを実装したす。 リク゚ストの各タむプget / post / put / patch /などに察しお、独自のハンドラヌが䜜成されたす。



最埌のステップは、このAPIを登録し、倖郚からアクセスできるようにするこずです。



 # -*- coding: utf-8 -*- from aiorest_ws.routers import SimpleRouter from app.views import UserListView, UserView, CreateUserView, AddressView, \ CreateAddressView router = SimpleRouter() router.register('/user/list', UserListView, 'GET') router.register('/user/{id}', UserView, ['GET', 'PUT'], name='user-detail') router.register('/user/', CreateUserView, ['POST']) router.register('/address/{id}', AddressView, ['GET', 'PUT'], name='address-detail') router.register('/address/', CreateAddressView, ['POST'])
      
      





実際、ここですべおの準備が敎いたした。サヌバヌを起動しおクラむアントを介しお接続するだけですPython + Autobahn.ws、JavaScriptを䜿甚するなど、倚くのオプションがありたす。 たずえば、Python + Authobahn.wsを䜿甚したいく぀かの簡単なク゚リを衚瀺したす事前に予玄したすが、クラむアントの䟋は完璧ではありたせん。ここでのタスクは、これを行う方法を瀺すこずです。



 # -*- coding: utf-8 -*- import asyncio import json from hashlib import sha256 from autobahn.asyncio.websocket import WebSocketClientProtocol, \ WebSocketClientFactory def hash_password(password): return sha256(password.encode('utf-8')).hexdigest() class HelloClientProtocol(WebSocketClientProtocol): def onOpen(self): # Create new address request = { 'method': 'POST', 'url': '/address/', 'data': { "email_address": 'some_address@google.com' }, 'event_name': 'create-address' } self.sendMessage(json.dumps(request).encode('utf8')) # Get users list request = { 'method': 'GET', 'url': '/user/list/', 'event_name': 'get-user-list' } self.sendMessage(json.dumps(request).encode('utf8')) # Create new user with address request = { 'method': 'POST', 'url': '/user/', 'data': { 'name': 'Neyton', 'fullname': 'Neyton Drake', 'password': hash_password('123456'), 'addresses': [{"id": 1}, ] }, 'event_name': 'create-user' } self.sendMessage(json.dumps(request).encode('utf8')) # Trying to create new user with same info, but we have taken an error self.sendMessage(json.dumps(request).encode('utf8')) # Update existing object request = { 'method': 'PUT', 'url': '/user/6/', 'data': { 'fullname': 'Definitely not Neyton Drake', 'addresses': [{"id": 1}, {"id": 2}] }, 'event_name': 'partial-update-user' } self.sendMessage(json.dumps(request).encode('utf8')) def onMessage(self, payload, isBinary): print("Result: {0}".format(payload.decode('utf8'))) if __name__ == '__main__': factory = WebSocketClientFactory("ws://localhost:8080") factory.protocol = HelloClientProtocol loop = asyncio.get_event_loop() coro = loop.create_connection(factory, '127.0.0.1', 8080) loop.run_until_complete(coro) loop.run_forever() loop.close()
      
      





ここで、サンプルの゜ヌスコヌド党䜓をさらに詳しく芋るこずができたす 。



7.さらなる開発



珟圚のラむブラリ機胜を拡匵する方法に぀いおは、かなりの数のアむデアがありたす。 たずえば、次の分野でこのモゞュヌルを開発できたす。





繰り返しになりたすが、倚くの機胜は1぀のリリヌスではなく、異なるリリヌスで蚈画されおいるこずを思い出させおください。 これは意図的に行われたものであり、極端なものから別のものぞず急いで行かないように、䞊行しお䜕かをしたす。 そしお、私にずっおもあなたにずっおも簡単です。



8.そしお結論ずしお...



独自のラむブラリを䜜成した経隓がないにもかかわらず、初めおかなり良い結果になったず思いたす。 そしお、Python蚀語の開発に非垞に匷く貢献したすたずえ小さなものでも。 費やされた時間に驚かないでくださいすべおが自由時間で定期的に行われたすそしお継続されたす1぀のプロゞェクトでの定期的な䜜業は非垞に疲れたすが、同時に耇数の方向で開発したいため。



䜕らかの圢で、このラむブラリに関するすべおの提案、アむデア、改善点をコメントたたはGitHubのリク゚ストプヌルの圢匏で聞いおうれしいです。 ラむブラリや実装の機胜に関するご質問はお気軜にお寄せください。フィヌドバックをお埅ちしおおりたす。



䞊蚘のすべおのコヌドずaiorest-wsラむブラリの゜ヌスは、 GitHubで衚瀺できたす。 サンプルは、プロゞェクトのルヌト、examplesディレクトリにありたす。 ドキュメントはここで芋るこずができたす 。



All Articles