アプリケーションレイヤーネットワークプロトコルの複雑さの解消

TCPまたはHTTPプロトコルを介してボリュームネットワーク通信を実装したことがありますか? そのような経験の場合、 付随する最終的な解決策にどの程度満足しましたか? 最初の質問に対する肯定的な回答(交換の「バルキーネス」がなくても)および結果として生じる実装の柔軟性に対する不満により、このような不幸を取り除く1つの方法を含むこの記事を推奨できます。



著者のように見えるこの出版物の価値は、すべてが最も単純な教育ではなく、現実の例と密接に関連するものではなく、 別の記事ですでに言及された、同様に実際のモバイルアプリケーションからの実際のソリューションの一部で示されているという事実にもあります。



Indyは記事のプログラムコードで使用されていることに注意してください。ただし、ネットワークの相互作用に関する資料では奇妙に思えるかもしれませんが、読者はこのライブラリをそのように認識する必要はありません。プロトコル-設計に関するものです。



指でタスクを設定する



同期と呼ばれる機能の1つであり、記事の基礎となったモバイルアプリケーションは、一般的に買い物リストです。商品のリストを作成したユーザーは、彼と一緒に店に行くか、このビジネスを別の人(またはグループ)に委ねます)、しかし、その後、2番目のケースでは、最初にこのリストをサーバー(中央ストレージとして)に転送し、次にターゲットモバイルデバイスに転送する必要があります-この時点で、ネットワークを使用する必要があります。 同期は双方向であることを強調する価値があります。つまり、参加者(必ずしも作成者ではない)によって行われた編集は、他のすべてに反映されます。 実例として、2人の野心的なアマチュア農学者が、必要なツールを事前に購入する必要があるアイデアの1つを実装することを決定したという仮想ケースを考えてみましょう。

アクション リスト作成者 二人目

同期
リスト作成
  1. バケット
  2. 水まき缶
  3. セモリナ0.5 kg
メンバーを追加

そしてその後

同期する
  1. バケット
  2. 水まき缶
  3. セモリナ0.5 kg
  1. バケット
  2. 水まき缶
  3. セモリナ0.5 kg
コンテンツ編集
  1. バケット
  2. 水まき缶
  3. セモリナ0.5 kg
  4. すくい
  1. バケット2個
  2. 水まき缶
  3. セモリナ1 kg
同期する
  1. バケット2個
  2. 水まき缶
  3. セモリナ1 kg
  4. すくい
  1. バケット2個
  2. 水まき缶
  3. セモリナ1 kg
  4. すくい
視覚的には、デバイス上では、同期プロセス全体がアニメーションインジケーターとキャンセルボタンで表されます。



デバイス同期プロセス






形式化



上記の例の表は単なるスケッチであり、同期中に起こるべきことの最も表面的な説明であるため、深刻な技術的タスクとして使用することはできません。 完全なプロトコルが必要です-相互作用のステップの詳細かつ段階的で包括的な説明-誰が、何を、なぜネットワークを介して送信するか。 OSIモデルによれば、 アプリケーションレベル (つまり、アプリケーションレベル)になります。 例として、すべてのアクションの約10%を含む実際のドキュメントの小さな部分が表示されます(時間軸は下に向けられています)。

お客様 データ サーバー
同期リストの定義
...
製品ディレクトリ同期
...
ユーザー同期
...
リストの同期:
1.サーバーへの追加
...
2.クライアントへの追加
...
3.変更の交換
変更の交換が必要なリストの転送。 階層の最初のレベル。
  1. リストID
  2. リストハッシュ
  3. 彼の子孫のハッシュ:
    • ユーザー
    • アイテム
ハッシュ分析:
ハッシュ一致通知。 1.すべての一致- 同期の終了
同期の終了。
転送の要件:

  1. クライアントで変更されたリストフィールド-ハッシュが一致しない場合。
  2. 直接の子孫の詳細-子孫のハッシュが異なる場合。


2.少なくとも1つが一致しません。
要求されたデータの送信。 階層の第2レベル。
  1. 変更されたリストフィールド(必要な場合)
  2. ユーザーによる詳細化(必要な場合):
    • ID
    • ハッシュ(削除されていない場合)
    • 追加(作成者のみ)?
    • 削除されましたか(著者のみ)?
  3. 要素ごとの詳細(必要な場合):
    • ID
    • ハッシュ(削除されていない場合)
    • その子孫のハッシュはチャットです(削除されていない場合)。
    • 削除しましたか?
...
さらに理解するために、上記のプロトコルフラグメントのすべてのニュアンスを掘り下げる必要はありません-主なことは、クライアント側(左列)にアクションがあることを理解することです。ネットワークを介して送信し、分析およびその他の作業を実行するサーバー側(右の列)があります。



額の決定



輸送



プロトコルを実装する前に、トランスポートを決定する必要があります。これは、データの物理的な転送を担当する同じレベルまたは基礎となるレベルのプロトコルです。 HTTPとTCPの2つの明白な選択肢があります(UDP、明確な理由-配信は保証されません、ここでは使用できません)。 最終的に、選択は2つの理由で2番目のオプションになりました。TCPは、そのバイナリの性質により、転送されるすべてのデータを完全に自由にし、同様に、モバイルプロジェクトの最後の場所ではない優れたパフォーマンスを備えています。



コードの最初のバージョン



トランスポートを選択しTIdTCPClient



、クライアント側の例を使用してプロトコルの条件付き実装を検討し、 TIdTCPClient



をベースとして(サーバーに基本的な違いはありません-コンポーネントのみがTIdTCPServer



変更されTIdTCPServer



)。 これから先、 フラグメントの一部のみがすべて表示されます。

...
3.変更の交換
変更の交換が必要なリストの転送。 階層の最初のレベル。
  1. リストID
  2. リストハッシュ
  3. 彼の子孫のハッシュ:
    • ユーザー
    • アイテム
ハッシュ分析:
ハッシュ一致通知。 1.すべての一致- 同期の終了
同期の終了。
...
3つのコンポーネントを持つ単純なフォームがあるとします。



 TForm1 = class(TForm) TCPClient: TIdTCPClient; ButtonSync: TButton; StoredProcLists: TFDStoredProc; procedure ButtonSyncClick(Sender: TObject); end;
      
      





以下のボタンクリックハンドラーには強力な簡略化が含まれているため、一部のエラーはコンパイルされませんが、最初の簡単で明白なアプローチの本質を伝えます。



 procedure TForm1.ButtonSyncClick(Sender: TObject); var Handler: TIdIOHandler; begin TCPClient.Connect; Handler := TCPClient.IOHandler; //     ... //    ... //   ... //  : // 1.    ... // 2.    ... // 3.   //  ,   . StoredProcLists.Open; Handler.Write(StoredProcLists.RecordCount); while not StoredProcLists.Eof do begin Handler.Write( StoredProcLists.FieldByName('ListID').AsInteger ); Handler.Write( Length(StoredProcLists.FieldByName('ListHash').AsBytes) ); Handler.Write( StoredProcLists.FieldByName('ListHash').AsBytes ); Handler.Write( Length(StoredProcLists.FieldByName('ListUsersHash').AsBytes) ); Handler.Write( StoredProcLists.FieldByName('ListUsersHash').AsBytes ); Handler.Write( Length(StoredProcLists.FieldByName('ListItemsHash').AsBytes) ); Handler.Write( StoredProcLists.FieldByName('ListItemsHash').AsBytes ); StoredProcLists.Next; end; StoredProcLists.Close; //    . if Handler.ReadByte = 1 then //  ? ... //  -  . else ... //  -   . TCPClient.Disconnect; end;
      
      





指定された部分は、アプリケーションの元のコードと比較してはるかに簡単であることを再度強調する必要があります。したがって、実際には、このイベントは、完全なプロトコルに対して、数千行になります。 ボリュームがそうでない場合(数百行まで)、メインステージでメソッドまたはローカルプロシージャに単純に分割してこれに専念することは完全に許容されますが、この場合ではありません-スケールは深刻な問題をもたらします:





さらにナレーションは、これらの欠点に対処する方法を提案します。



最初のアプローチ



プロトコル



ボリュームとこれに起因する複雑さに対処するのに役立つ主なアイデアは新しいものではありません-「対象領域」を反映する抽象化の導入です。この場合、これはネットワーク相互作用であるため、 プロトコルなどの最初のプログラムの一般化が導入されます。 ごNet.Protocol



、その実装はクラスに基づいており、クラスは順番にモジュールによってグループ化され、最初のNet.Protocol



になりNet.Protocol



(必要に応じてぼかしが追加されます)。



新しいNet.Protocolモジュール






プロトコルという用語はすでに使用されているため、混乱を避けるために、 上記の表はプロトコル記述と呼ばれます 。 また、すべてのモジュールがクライアントとサーバーに分割されているわけではないことに注意してください-それらは交換側に共通です。



プロトコルは最初、かなり単純なコードで記述されています。



 unit Net.Protocol; interface uses IdIOHandler; type TNetTransport = TIdIOHandler; TNetProtocol = class abstract protected FTransport: TNetTransport; public constructor Create(const Transport: TNetTransport); procedure RunExchange; virtual; abstract; end; implementation constructor TNetProtocol.Create(const Transport: TNetTransport); begin FTransport := Transport; end; end.
      
      





主要なRunExchange



メソッドRunExchange



、ネットワーク交換、つまりプロトコル記述に存在するすべてのステップを開始するように設計されています。 コンストラクタは、パラメータとして、物理的な配信自体を担当するオブジェクトを受け入れます。これは、前述のように、Indyコンポーネントによって表されるTCPであるまさにトランスポートです。



コードの最初のバージョンを書き直すと、非常にコンパクトになります( TClientProtocol



TClientProtocol



の後継TNetProtocol



)。



 procedure TForm1.ButtonSyncClick(Sender: TObject); var Protocol: TClientProtocol; begin TCPClient.Connect; Protocol := TClientProtocol.Create(TCPClient.IOHandler); try Protocol.RunExchange; finally Protocol.Free; end; TCPClient.Disconnect; end;
      
      





もちろん、そのような修正は、特定された問題のいずれもまだ解決していません-これは他の手段によって達成されます。



パッケージ



プロトコルの実装ですでに使用されている2番目の抽象化は、データパケット (以下、単にパケット)です-ネットワークを操作する責任があります。 説明の引用されたフラグメントを見ると、2つのパケットがそれに対応しています(強調表示。最初のパケットはクライアントによって送信され、2番目のパケットはサーバーによって送信されます)。

...
3.変更の交換
変更の交換が必要なリストの転送。 階層の最初のレベル。
  1. リストID
  2. リストハッシュ
  3. 彼の子孫のハッシュ:
    • ユーザー
    • アイテム
ハッシュ分析:
ハッシュ一致通知。 1.すべての一致- 同期の終了
同期の終了。
...
パッケージコードもシンプルで、新しいNet.Packet



モジュールに割り当てられます。



新しいNet.Packetモジュール






 unit Net.Packet; interface uses Net.Protocol; type TPacket = class abstract public type TPacketKind = UInt16; protected FTransport: TNetTransport; function Kind: TPacketKind; virtual; abstract; public constructor Create(const Transport: TNetTransport); procedure Send; procedure Receive; end; implementation constructor TPacket.Create(const Transport: TNetTransport); begin FTransport := Transport; end; procedure TPacket.Send; begin FTransport.Write(Kind); end; procedure TPacket.Receive; var ActualKind: TPacketKind; begin ActualKind := FTransport.ReadUInt16; if Kind <> ActualKind then //    . ... end; end.
      
      





パッケージには主に2つのメソッドがあります。 Send



-送信者が使用する方法と、 Receive



- Receive



者が呼び出す方法です。 コンストラクターは、プロトコルからトランスポートを受け取ります。 Kind



メソッドは、特定のパッケージ(子孫)を識別するように設計されており、期待される結果が正確に得られることを確認できます。



抽象パッケージの概要を説明した後、上記のプロトコルの説明で直接使用され、有用なデータを含むいくつかのモジュールを定義します。新しいモジュールを宣言します。



新しいSync.Packetsモジュール






 unit Sync.Packets; interface uses System.Generics.Collections, Net.Packet; type TListHashesPacket = class(TPacket) private const PacketKind = 1; public type THashes = class strict private FHash: string; FItemsHash: string; FUsersHash: string; public property Hash: string read FHash write FHash; property UsersHash: string read FUsersHash write FUsersHash; property ItemsHash: string read FItemsHash write FItemsHash; end; TListHashes = TObjectDictionary<Integer, THashes>; //   - ID . private FHashes: TListHashes; protected function Kind: TPacket.TPacketKind; override; public property Hashes: TListHashes read FHashes write FHashes; end; TListHashesResponsePacket = class(TPacket) private const PacketKind = 2; private FHashesMatched: Boolean; protected function Kind: TPacket.TPacketKind; override; public property HashesMatched: Boolean read FHashesMatched write FHashesMatched; end; //   . ... implementation function TListHashesPacket.Kind: TPacket.TPacketKind; begin Result := PacketKind; end; function TListHashesResponsePacket.Kind: TPacket.TPacketKind; begin Result := PacketKind; end; end.
      
      





ご覧のとおり、これら2つのパケットもその祖先であるTPacket



も、プロパティ(この場合はHashesMatched



HashesMatched



)に格納されたデータを送受信するコードは含まれていませんが、これが近い将来の問題であることを確認する方法を示していますが、今のところ、奇跡的な方法ですべてが機能すること。



プロトコル実装



プロトコルがパケットをどのように使用するかを示すために、さらに2つのモジュールを導入する必要があります-今回はクライアントとサーバーに分割します。以前のモジュールとは異なり、これらはSync.Protocol.Client



Sync.Protocol.Server



です。



新しいSync.Protocol.ClientおよびSync.Protocol.Serverモジュール






その名前から、どちらの側を表しているかが明らかです。



 unit Sync.Protocol.Client; interface uses Net.Protocol; type TClientProtocol = class(TNetProtocol) private procedure SendListHashes; function ListHashesMatched: Boolean; ... public procedure RunExchange; override; end; implementation uses Sync.Packets; procedure TClientProtocol.RunExchange; begin inherited; ... // 3.   SendListHashes; if ListHashesMatched then //  ? ... //  -  . else ... //  -   . end; procedure TClientProtocol.SendListHashes; var ListHashesPacket: TListHashesPacket; begin ListHashesPacket := TListHashesPacket.Create(FTransport); try //  ListHashesPacket.Hashes   . ... ListHashesPacket.Send; finally ListHashesPacket.Free; end; end; function TClientProtocol.ListHashesMatched: Boolean; var ListHashesResponsePacket: TListHashesResponsePacket; begin ListHashesResponsePacket := TListHashesResponsePacket.Create(FTransport); try ListHashesResponsePacket.Receive; Result := ListHashesResponsePacket.HashesMatched; finally ListHashesResponsePacket.Free; end; end; end.
      
      





そして、ペアのモジュール:



 unit Sync.Protocol.Server; interface uses Net.Protocol; type TServerProtocol = class(TNetProtocol) private function ListHashesMatched: Boolean; ... public procedure RunExchange; override; end; implementation uses Sync.Packets; procedure TServerProtocol.RunExchange; begin inherited; ... // 3.   if ListHashesMatched then //  ? ... //  -  . else ... //  -   . end; function TServerProtocol.ListHashesMatched: Boolean; var ClientListHashesPacket: TListHashesPacket; ListHashesResponsePacket: TListHashesResponsePacket; begin ClientListHashesPacket := TListHashesPacket.Create(FTransport); try ClientListHashesPacket.Receive; ListHashesResponsePacket := TListHashesResponsePacket.Create(FTransport); try //  ClientListHashesPacket.Hashes    , //  ListHashesResponsePacket.HashesMatched. ... ListHashesResponsePacket.Send; Result := ListHashesResponsePacket.HashesMatched; finally ListHashesResponsePacket.Free; end; finally ClientListHashesPacket.Free; end; end; end.
      
      





最終オプション



前のセクションでは、段階を設定するだけで、最初に特定された問題を解決するためのフレームワークを作成しました-データへのアクセスから開始できるようになりました。



データ



ちょうど今、両当事者のプロトコルを実装するときに、次のコードが発生しました。



 //  ListHashesPacket.Hashes   . ... ListHashesPacket.Send;
      
      





同様に



 //  ClientListHashesPacket.Hashes    , //  ListHashesResponsePacket.HashesMatched. ... ListHashesResponsePacket.Send;
      
      





上記のコメントを実際のコードに置き換えるには、このようなデザインパターンをファサードとして適用することをお勧めします。データを直接操作するのではなく、プロトコルは、データベースと通信するための任意の複雑で膨大なアクションを実装する高レベルのメソッドを呼び出すタスクになります。 これを行うには、 Sync.DB



モジュールを作成します。



新しいSync.DBモジュール






 unit Sync.DB; interface uses FireDAC.Comp.Client; type TDBFacade = class abstract protected FConnection: TFDConnection; public constructor Create; destructor Destroy; override; procedure StartTransaction; procedure CommitTransaction; procedure RollbackTransaction; end; implementation constructor TDBFacade.Create; begin FConnection := TFDConnection.Create(nil); end; destructor TDBFacade.Destroy; begin FConnection.Free; inherited; end; procedure TDBFacade.StartTransaction; begin FConnection.StartTransaction; end; procedure TDBFacade.CommitTransaction; begin FConnection.Commit; end; procedure TDBFacade.RollbackTransaction; begin FConnection.Rollback; end; end.
      
      





ここで宣言されている唯一のTDBFacade



クラスには、そのすべての子孫がトランザクション(簡単なコード)で動作するために必要な3つのメソッドが含まれています。 :



新しましたSync.DB.ClientおよびSync.DB.Serverモジュール






クライアントファサード:



 unit Sync.DB.Client; interface uses Sync.DB, Sync.Packets; type TClientDBFacade = class(TDBFacade) public procedure CalcListHashes(const Hashes: TListHashesPacket.TListHashes); ... end; implementation uses FireDAC.Comp.Client; procedure TClientDBFacade.CalcListHashes(const Hashes: TListHashesPacket.TListHashes); var StoredProcHashes: TFDStoredProc; begin StoredProcHashes := TFDStoredProc.Create(nil); try //  StoredProcHashes. ... StoredProcHashes.Open; while not StoredProcHashes.Eof do begin //  Hashes. ... StoredProcHashes.Next; end; finally StoredProcHashes.Free; end; end; end.
      
      





サーバー側:



 unit Sync.DB.Server; interface uses Sync.DB, Sync.Packets; type TServerDBFacade = class(TDBFacade) public function CompareListHashes(const ClientHashes: TListHashesPacket.TListHashes): Boolean; ... end; implementation uses FireDAC.Comp.Client; function TServerDBFacade.CompareListHashes(const ClientHashes: TListHashesPacket.TListHashes): Boolean; var StoredProcHashes: TFDStoredProc; begin Result := True; StoredProcHashes := TFDStoredProc.Create(nil); try //  StoredProcHashes. ... StoredProcHashes.Open; //   . while not StoredProcHashes.Eof do begin Result := Result and {       ClientHashes?}; StoredProcHashes.Next; end; finally StoredProcHashes.Free; end; end; end.
      
      





クライアントファサードを例として使用している読者が、 CalcListHashes



メソッドCalcListHashes



非常に単純で、データベースに関するすべての作業をその中に取り入れる意味がほとんどないと思われる場合は、ここで示した強力な単純化を比較することをお勧めします



アプリケーションからの実際のコード。
 procedure TClientSyncDBFacade.CalcListHashes(const Hashes: TListHashesPacket.THashesCollection); var Lists: TList<TLocalListID>; procedure PrepareListsToHashing; begin PrepareStoredProcedureToWork(SyncPrepareListsToHashingProcedure); FStoredProcedure.Open; while not FStoredProcedure.Eof do begin Lists.Add( FStoredProcedure['LIST_ID'] ); FStoredProcedure.Next; end; end; procedure CalcTotalChildHashes; var ListID: TLocalListID; TotalUsersHash, TotalItemsHash: TMD5Hash; begin for ListID in Lists do begin PrepareStoredProcedureToWork(SyncSelectListUsersForHashingProcedure); FStoredProcedure.ParamByName('LIST_ID').Value := ListID; TotalUsersHash := CalcTotalHashAsBytes( FStoredProcedure, ['USER_AS_STRING'] ); PrepareStoredProcedureToWork(SyncSelectListItemAndItemMessagesHashProcedure); FStoredProcedure.ParamByName('LIST_ID').Value := ListID; TotalItemsHash := CalcTotalHashAsBytes( FStoredProcedure, ['ITEM_HASH', 'ITEM_MESSAGES_HASH'] ); PrepareStoredProcedureToWork(SyncAddTotalListHashesProcedure); FStoredProcedure.ParamByName('LIST_ID').Value := ListID; FStoredProcedure.ParamByName('TOTAL_USERS_HASH').AsHash := TotalUsersHash; FStoredProcedure.ParamByName('TOTAL_ITEMS_HASH').AsHash := TotalItemsHash; FStoredProcedure.ExecProc; end; end; procedure FillHashes; var ListHashes: TListHashesPacket.THashes; begin PrepareStoredProcedureToWork(SyncSelectListHashesProcedure); FStoredProcedure.Open; while not FStoredProcedure.Eof do begin ListHashes := TListHashesPacket.THashes.Create; try ListHashes.Hash := HashToString( FStoredProcedure.FieldByName('LIST_HASH').AsHash ); ListHashes.UsersHash := HashToString( FStoredProcedure.FieldByName('LIST_USERS_HASH').AsHash ); ListHashes.ItemsHash := HashToString( FStoredProcedure.FieldByName('LIST_ITEMS_HASH').AsHash ); except ListHashes.DisposeOf; raise; end; Hashes.Add( FStoredProcedure.FieldByName('LIST_GLOBAL_ID').AsUUID, ListHashes ); FStoredProcedure.Next; end; end; begin Lists := TList<TLocalListID>.Create; try PrepareListsToHashing; CalcRecordHashes(TListHashes); CalcRecordHashes(TListItemHashes); CalcRecordHashes(TListItemMessagesHashes); CalcTotalChildHashes; FillHashes; finally Lists.DisposeOf; end; end;
      
      





両方のファサードがSync.Packets



モジュールをインポートし、その中で宣言されたパッケージを使用します-これにより、それらの間に強力な接着が作成されます。これは、ファサードとパッケージがプロトコルで使用するために設計されており、お互いを知っているためです彼らには別の理由はありません。 アプリケーションが大きく、多くの開発者が作業する場合、ファサードメソッドのパッケージ固有のタイプを他のより一般的なタイプ、たとえば「リストの抽象的なリスト」に置き換えることで、結合を減らす必要がありますが、すべての複雑さを増すためにこれを支払う必要があります。 現在の妥協案は、プロジェクトの小規模を考慮に入れてリスクを非常に適切に分散しています。



最終プロトコルビュー



ファサードの導入後、すべてのプロトコルメソッドは最終的な安定した形式になります。



 unit Sync.Protocol.Client; interface uses Net.Protocol, Sync.DB.Client; type TClientProtocol = class(TNetProtocol) private FDBFacade: TClientDBFacade; procedure SendListHashes; ... public procedure RunExchange; override; end; implementation uses Sync.Packets; procedure TClientProtocol.RunExchange; begin inherited; FDBFacade.StartTransaction; try ... // 3.   SendListHashes; if ListHashesMatched then //  ? ... //  -  . else ... //  -   . FDBFacade.CommitTransaction; except FDBFacade.RollbackTransaction; raise; end; end; procedure TClientProtocol.SendListHashes; var ListHashesPacket: TListHashesPacket; begin ListHashesPacket := TListHashesPacket.Create(FTransport); try FDBFacade.CalcListHashes(ListHashesPacket.Hashes); ListHashesPacket.Send; finally ListHashesPacket.Free; end; end; ... end.
      
      





 unit Sync.Protocol.Server; interface uses Net.Protocol, Sync.DB.Server; type TServerProtocol = class(TNetProtocol) private FDBFacade: TServerDBFacade; function ListHashesMatched: Boolean; ... public procedure RunExchange; override; end; implementation uses Sync.Packets; procedure TServerProtocol.RunExchange; begin inherited; FDBFacade.StartTransaction; try ... // 3.   if ListHashesMatched then //  ? ... //  -  . else ... //  -   . FDBFacade.CommitTransaction; except FDBFacade.RollbackTransaction; raise; end; end; function TServerProtocol.ListHashesMatched: Boolean; var ClientListHashesPacket: TListHashesPacket; ListHashesResponsePacket: TListHashesResponsePacket; begin ClientListHashesPacket := TListHashesPacket.Create(FTransport); try ClientListHashesPacket.Receive; ListHashesResponsePacket := TListHashesResponsePacket.Create(FTransport); try ListHashesResponsePacket.HashesMatched := FDBFacade.CompareListHashes(ClientListHashesPacket.Hashes); ListHashesResponsePacket.Send; Result := ListHashesResponsePacket.HashesMatched; finally ListHashesResponsePacket.Free; end; finally ClientListHashesPacket.Free; end; end; end.
      
      





パッケージ改訂



組み立てられた構造全体を完成させるために、最後の、しかし非常に重要な爪を動かします-有用な情報を送信するようパッケージに教えることですが、このタスクは2つのコンポーネントに便利に分割されます(例えば、送信):





パッケージ化はさまざまな方法で実行できます。バイナリ形式、XML、JSONなど。モバイルデバイスには豊富なリソースがないため、最後のJSONオプションが選択されました。 ); 選択したパスを実装するには、 TPacket



2つのメソッドを追加しTPacket







 unit Net.Packet; interface uses Net.Protocol, System.JSON; type TPacket = class abstract ... private function PackToJSON: TJSONObject; procedure UnpackFromJSON(const JSON: TJSONObject); ... end;
      
      





メソッドは2つの方法が可能であるため、実装は示されていません:メソッドは保護および仮想として宣言され、すべての相続人パッケージは、追加されたデータプロパティに応じて個別にJSONのパッケージ化および展開、または2番目のオプション-メソッドはプライベートのままですここに)JSONへの自動変換用のコードが含まれています。これにより、「ロジスティック」な心配から子孫が完全に排除されます。 最初のオプションは、パッケージの数とその複雑さが小さい場合(最も単純なタイプのプロパティで数十個まで)に有効ですが、法案がより高い値になる場合-作者のプロジェクトには32個あり、たとえば、非常に複雑です



そのようなパッケージ
 TListPacket = class(TStreamPacket) public type TPhoto = class(TPackableObject) strict private FSortOrder: Int16; FItemMessageID: TItemMessageID; public property ItemMessageID: TItemMessageID read FItemMessageID write FItemMessageID; property SortOrder: Int16 read FSortOrder write FSortOrder; end; TPhotos = TStandardPacket.TPackableObjectDictionary<TMessagePhotoID, TPhoto>; TMessage = class(TPackableObject) strict private FAuthor: TUserID; FAddDate: TDateTime; FText: string; FListItemID: TListItemID; public property ListItemID: TListItemID read FListItemID write FListItemID; property Author: TUserID read FAuthor write FAuthor; property AddDate: TDateTime read FAddDate write FAddDate; property Text: string read FText write FText; end; TMessages = TStandardPacket.TPackableObjectDictionary<TItemMessageID, TMessage>; TListDescendant = class(TPackableObject) strict private FListID: TListID; public property ListID: TListID read FListID write FListID; end; TItem = class(TListDescendant) strict private FAddDate: TDateTime; FAmount: TAmount; FEstimatedPrice: Currency; FExactPrice: Currency; FStandardGoods: TID; FInTrash: Boolean; FUnitOfMeasurement: TID; FStrikeoutDate: TDateTime; FCustomGoods: TGoodsID; public property StandardGoods: TID read FStandardGoods write FStandardGoods; property CustomGoods: TGoodsID read FCustomGoods write FCustomGoods; property Amount: TAmount read FAmount write FAmount; property UnitOfMeasurement: TID read FUnitOfMeasurement write FUnitOfMeasurement; property EstimatedPrice: Currency read FEstimatedPrice write FEstimatedPrice; property ExactPrice: Currency read FExactPrice write FExactPrice; property AddDate: TDateTime read FAddDate write FAddDate; property StrikeoutDate: TDateTime read FStrikeoutDate write FStrikeoutDate; property InTrash: Boolean read FInTrash write FInTrash; end; TItems = TStandardPacket.TPackableObjectDictionary<TListItemID, TItem>; TUser = class(TListDescendant) strict private FUserID: TUserID; public property UserID: TUserID read FUserID write FUserID; end; TUsers = TStandardPacket.TPackableObjectList<TUser>; TList = class(TPackableObject) strict private FName: string; FAuthor: TUserID; FAddDate: TDateTime; FDeadline: TDate; FInTrash: Boolean; public property Author: TUserID read FAuthor write FAuthor; property Name: string read FName write FName; property AddDate: TDateTime read FAddDate write FAddDate; property Deadline: TDate read FDeadline write FDeadline; property InTrash: Boolean read FInTrash write FInTrash; end; TLists = TStandardPacket.TPackableObjectDictionary<TListID, TList>; private FLists: TLists; FMessages: TMessages; FItems: TItems; FUsers: TUsers; FPhotos: TPhotos; public property Lists: TLists read FLists write FLists; property Users: TUsers read FUsers write FUsers; property Items: TItems read FItems write FItems; property Messages: TMessages read FMessages write FMessages; property Photos: TPhotos read FPhotos write SetPhotos; end;
      
      





パッケージングプロセスの自動化なしでは、すでに非常に無謀です。 特に、 RTTIを使用すると、パッケージの必要なプロパティを選択して値を操作できますが、このトピックは記事の範囲外であるため、コードは表示されません。



以前に宣言されたパッケージの可能なJSON表現を提供TListHashesPacket



して、読者が最終的に元のクラスとそのシリアル化された形式との対応を理解するのを助けると役立つようです



 { 16: { Hash: "d0860029f1400147deef86d3246d29a4", UsersHash: "77febf816dac209a22880c313ffae6ad", ItemsHash: "1679091c5a880faf6fb5e6087eb1b2dc" }, 38: { Hash: "81c8061686c10875781a2b37c398c6ab", UsersHash: "d3556bff1785e082b1508bb4e611c012", ItemsHash: "0e3a37aa85a14e359df74fa77eded3f6" } }
      
      





パッケージされたものの物理的な輸送は非常に簡単です-いくつかの基本的な方法を追加するだけですTPacket







 unit Net.Packet; interface ... implementation uses System.SysUtils, IdGlobal; ... procedure TPacket.Send; var DataLength: Integer; RawData: TBytes; JSON: TJSONObject; begin FTransport.Write(Kind); JSON := PackToJSON; try SetLength(RawData, JSON.EstimatedByteSize); DataLength := JSON.ToBytes( RawData, Low(RawData) ); FTransport.Write(DataLength); FTransport.Write( TIdBytes(RawData), DataLength ); finally JSON.Free; end; end; procedure TPacket.Receive; var ActualKind: TPacketKind; DataLength: Integer; RawData: TBytes; JSON: TJSONObject; begin ActualKind := FTransport.ReadUInt16; if Kind <> ActualKind then //    . ... DataLength := FTransport.ReadInt32; FTransport.ReadBytes( TIdBytes(RawData), DataLength, False ); JSON := TJSONObject.Create; try JSON.Parse(RawData, 0); UnpackFromJSON(JSON); finally JSON.Free; end; end; ... end.
      
      





おわりに



提案されたソリューションは、最初に提起された問題にどの程度効果的に対処しますか?ここでクライアントなどのプロトコルコードを見ると、そのメソッドはプロトコル記述の用語で動作するため、それらの間の対応を非常に明確かつ迅速に見つけることができます。ネットワークライブラリとデータへの依存はローカライズされ、3つのモジュールに配置されます(色でマークされています)。



Net.Packetモジュール、およびSync.DB.ClientおよびSync.DB.Server






このため、Indyから他の何かへの移行には2つのメソッド y のみを変更する必要がありますTPacket



- パーティの1つでFireDAC Send



Receive



置き換える(または、一般的にリポジトリとしてデータベースを放棄する)のはファサードメソッドにのみ影響し、プロトコル自体の編集はまったく必要ありません。



残念なことに、現在のソリューションは、その利点をすべて備えていても、実際の使用では他の重要なニュアンスを考慮する必要があるため、実際の使用にはまだ完全には適していません。ネットワークサブシステムにはほとんど適用されません。これは、プロトコルに違いがあるクライアントが表示されることを意味します(すべてのユーザーが定期的かつ喜んでアプリケーションを更新するわけではありません)。サーバーの応答は2つあります。古いプロトコルでクライアントにサービスを提供することを拒否するか、さまざまなバージョンでの同時操作をサポートします。この質問に対する答えは、記事の第2部で提供できます-取り上げられたトピックに関心がある場合(個人メッセージまたはコメントで表現することが提案されています)。



All Articles