📜 ⬆️ ⬇️

Dealing with complexity in an application layer network protocol

Have you ever realized volume networking through TCP or HTTP? How much, in the case of such an experience, were you satisfied with the maintainability of the final solution? An affirmative answer to the first question (even if without the “bulkiness” of the exchange) and dissatisfaction with the flexibility of the resulting implementation allow us to recommend this article as containing one of the ways to get rid of such a misfortune.

The value of the publication, as it seems to the author, also lies in the fact that everything is illustrated not by the simplest educational example and a little reality-related example, but by a small part of the real solution from the same mobile application previously mentioned in another article .

It should be noted that Indy is used in the program code of the article, however, although it may seem strange in the material on network interaction, knowledge of this library as such is not required from the reader, because the point is to get acquainted with more abstract, high-level techniques Protocol - it is more about design.
')

Task setting on fingers


A mobile application, one of the functions of which - called synchronization - formed the basis of the article, is, generally speaking, a shopping list: the user, creating a list of goods, goes with him to the store himself, or assigns this business to another person (or group of ), but then, in the second case, it is required to transfer this list first to the server (as centralized storage), and then to the target mobile devices — it is at these moments that there is a need to use the network; It should be emphasized that synchronization is two-way, i.e., editing made by any participant (not necessarily the author) will be reflected in all others. As a vivid example, consider a hypothetical case when two ambitious amateur agronomists decided to implement one of their ideas, for which they needed to pre-purchase the necessary tools:
ActList authorSecond participant
sync
Create a list
  1. Bucket
  2. Watering can
  3. Semolina 0.5 kg
Add member
and subsequent
synchronization
  1. Bucket
  2. Watering can
  3. Semolina 0.5 kg
  1. Bucket
  2. Watering can
  3. Semolina 0.5 kg
Edit content
  1. Bucket
  2. Watering can
  3. Semolina 0.5 kg
  4. Rake
  1. Bucket 2 pcs
  2. Watering can
  3. Semolina 1 kg
Synchronization
  1. Bucket 2 pcs
  2. Watering can
  3. Semolina 1 kg
  4. Rake
  1. Bucket 2 pcs
  2. Watering can
  3. Semolina 1 kg
  4. Rake
Visually, on the device, the entire synchronization process is represented by an animated indicator and a cancel button:

The synchronization process on the device

Formalization


The table with the example above is just a sketch, the most superficial description of what should happen during synchronization, therefore, it can never be used as a serious TZ , let alone write code on such a shaky ground. A full-fledged protocol is required - a detailed, step-by-step and exhaustive description of the steps for interaction - who, what and for what sends over the network; on the OSI model, it will be at the application level (or, in other words, the application level). As an example, a small part of a real document is given, containing about 10% of all actions (the time axis is directed downwards):
CustomerDataServer
Defining lists for synchronization
...
Synchronization of goods directory
...
User Sync
...
Sync lists:
1. Adding to the server
...
2. Adding to the client
...
3. Exchange of changes
Transfer lists requiring exchange of changes; first level of hierarchy.
  1. List ID
  2. List hash
  3. Hash his descendants:
    • Of users
    • Items
Hash Analysis:
Notification of matching hashes.1. All match - the end of synchronization .
End of sync.
Requirement to transfer:
  1. List fields changed on the client - in case of a hash mismatch.
  2. Details on direct descendants - if the hash of descendants is different.

2. At least one does not match.
Transfer the requested data. The second level of the hierarchy.
  1. Modified list fields (if required)
  2. Detailing by its users (if required):
    • ID
    • Hash (if not deleted)
    • Added (for own authorship only)?
    • Removed (only for his authorship)?
  3. Detailing by its elements (if required):
    • ID
    • Hash (if not deleted)
    • Hash his descendants - chat (if not removed).
    • Removed?
...
For further understanding, it is not necessary to delve into all the nuances of the above fragment of the protocol - the main thing is to understand that there are actions on the client side (left column), there is data (middle column) obtained as a result of performing actions by one of the parties, which, strictly speaking, pass over the network, and there is a server side (right column) that performs some analysis and other work.

Solution to the forehead


Transport


Before implementing the protocol, it is necessary to decide on the transport - the protocol of the same or lower level responsible for the physical transfer of data; There are two obvious alternatives - HTTP and TCP (UDP, for an understandable reason - non-guaranteed delivery, cannot be used here). In the end, the choice fell on the second option due to two reasons: TCP, due to its binary nature, gives complete freedom over all the transmitted data and, on the same basis, has better performance, which is not the last thing in a mobile project .

The first version of the code


Having chosen a transport, we will consider the conditional implementation of the protocol on the example of the client side, taking TIdTCPClient as a basis (there will be no fundamental differences on the server - there only the component will change to TIdTCPServer ). Now and further, everything will be shown on a small part of the fragment just quoted:
...
3. Exchange of changes
Transfer lists requiring exchange of changes; first level of hierarchy.
  1. List ID
  2. List hash
  3. Hash his descendants:
    • Of users
    • Items
Hash Analysis:
Notification of matching hashes.1. All match - the end of synchronization .
End of sync.
...
Suppose there is a simple form with three components:

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

The following button handler contains strong simplifications and, as a result, some errors will not be compiled, but the essence of the initial, straightforwardly obvious approach is passed:

 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; 

It is necessary to emphasize again that the given piece is much lighter in comparison with the original code from the application - therefore, in fact, this event, for the complete protocol, will grow to many thousands of lines. If the volume is not so (up to several hundred lines), then it is quite possible to simply break it up into methods or local procedures according to the main stages and stop there, but not in this case - the scale introduces serious problems:


Further narrative will suggest ways to eliminate these shortcomings.

First approach


Protocol


The basic idea of ​​helping to deal with the volume and the complexity resulting from this is not new - it is the introduction of abstractions reflecting the “subject area”: in this case, such is network interaction, therefore such a programmatic generalization as a protocol will be introduced first; its implementation, as it is easy to guess, will be based on classes, which in turn will be grouped in modules, and the first of them will be Net.Protocol (blurry others will be added as needed):

New Net.Protocol Module

Since the term protocol has already been used, to avoid confusion, the table above will be called a protocol description . It should also be noted that while all modules are not divided into client and server modules, they are irrespective of the exchange side, common.

Initially, the protocol is described by a fairly simple code:

 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. 

The key RunExchange method RunExchange designed to start a network exchange, that is, all those steps that are present in the protocol description. The constructor, on the other hand, accepts the object that is directly responsible for the physical delivery, the very transport that, as mentioned earlier, is TCP, represented in this case by Indy components.

If we now rewrite the very first version of the code, then it will become quite compact (in it the TClientProtocol is the successor of 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; 

Such a modification, of course, does not solve any of the indicated problems yet - this will be achieved by other means.

Package


The second abstraction, which will be used already in the implementation of the protocol, is a data packet (hereinafter referred to as a packet) - it will be responsible for the manipulation of the network. If you look at the following description fragment, 2 packages correspond to it (highlighted in color; the first one is sent by the client, the second by the server):
...
3. Exchange of changes
Transfer lists requiring exchange of changes; first level of hierarchy.
  1. List ID
  2. List hash
  3. Hash his descendants:
    • Of users
    • Items
Hash Analysis:
Notification of matching hashes.1. All match - the end of synchronization .
End of sync.
...
The package code is also simple and highlighted in the new Net.Packet module:

New Net.Packet module

 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. 

The main methods of the packet are 2 methods: Send - it is used by the sender, and Receive is called by the receiving party; Transport constructor receives from the protocol. The Kind method is designed to identify specific successor packets and allows you to make sure that you have exactly expected.

After describing the abstract package, we define a couple of those that will be directly used in the above description of the protocol and contain useful data, for which we will declare a new module:

New Sync.Packets Module

 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. 

As you can see, neither these 2 packets, nor their ancestor, TPacket , contain the code that sends and receives data stored in the properties ( Hashes and HashesMatched in this case), however, showing the way to ensure this is a matter of the near future, but for now suppose that in some miraculous way everything works.

Protocol implementation


To demonstrate how the protocol uses packets, you need to enter 2 more modules - this time divisible into client and server, unlike all previous ones, is Sync.Protocol.Client and Sync.Protocol.Server :

New modules Sync.Protocol.Client and Sync.Protocol.Server

By their name, it is clear which implementation they represent.

 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. 

And the paired module:

 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. 

Final option


The previous sections only prepared the ground, created a framework for solving the problems indicated at the very beginning - it is now possible to start them, starting with access to the data.

Data


Just now, when implementing the protocols of both sides, the following code was encountered:

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

and

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

To replace the given comments with real code, it is proposed to apply such a design pattern as a facade : instead of manipulating data directly, the protocol will only have the task of calling its high-level methods that implement any arbitrarily complex and voluminous actions to communicate with the database; To do this, create the module Sync.DB :

New Sync.DB Module

 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. 

The only TDBFacade class declared here contains 3 methods necessary for all his heirs to work with transactions (with a trivial code) and a field for a physical connection to the database — there is little interesting, so let's immediately consider the implementation of the client and server facades, which are already introduced by methods specific for each of the parties. :

New modules Sync.DB.Client and Sync.DB.Server

Client facade:

 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. 

And server:

 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. 

If the reader, using the example of the client facade, it seems that the CalcListHashes method CalcListHashes quite simple and there is almost no sense in taking all the database work from the protocol into it, then it is recommended to compare the strong simplification presented here with

real code from the application.
 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; 

I would like to dwell on one thing: both facades import the Sync.Packets module and then use the packages announced in it - this creates a strong adhesion between them, which is generally undesirable, since the facade and the packages are designed to be used by the protocol and know each other about a friend they absolutely no reason. If the application were large, on which many developers would work, the coupling simply needed to be reduced, replacing the package-specific types in the facade methods with other, more general ones, such as the “abstract list of lists”, but would have to pay for all this with increased complexity; the current trade-off quite adequately distributes the risk, taking into account the small scale of the project.

The final form of the protocol


After the introduction of the facade, all protocol methods will take the final, stable form:

 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. 

Refinement package


To complete the entire erected construction, it remains to drive the last, but very important, nail - to teach the package to transmit useful information, but this task is conveniently divided into two components (using the sending example):


Packaging can be done differently: in a certain binary form, in XML, in JSON, etc. Since mobile devices do not have rich resources, it was the latter that was chosen, the JSON option, which requires less computational processing costs (compared to XML ); To implement the chosen path, add 2 methods to TPacket :

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

Their implementation is not given, because 2 ways are possible: methods are declared protected and virtual and all the packages are inherited individually, depending on the properties added to them with data, they are packaged in JSON and unpacked from it, or the second option is that the methods remain private here) and contain code for automatic conversion to JSON, which completely eliminates offspring from "logistic" concerns. The first option is valid for cases when the number of packages and their complexity are small (up to a dozen pieces, with the properties of the simplest types), but if the bill goes to large quantities, there are 32 of them in the author’s project, and the complexity is quite high, as for example

such a package
 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; 

it’s already extremely reckless to do without automation of the packaging process. , 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. 

Conclusion


? , , , , . 3 ( ):

Net.Packet,   Sync.DB.Client  Sync.DB.Server

, Indy 2 TPacket – Send Receive , FireDAC ( ) , .

, , , , . . – : , , , , ( ); – , . – , ( , ).

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


All Articles