Κριτική στο πρωτόκολλο και τις οργανωτικές προσεγγίσεις του Telegram. Μέρος 1, τεχνικό: εμπειρία συγγραφής πελάτη από την αρχή - TL, MT

Πρόσφατα, δημοσιεύσεις σχετικά με το πόσο καλό είναι το Telegram, πόσο λαμπροί και έμπειροι είναι οι αδερφοί Durov στην κατασκευή συστημάτων δικτύου κ.λπ. έχουν αρχίσει να εμφανίζονται πιο συχνά στο Habré. Ταυτόχρονα, πολύ λίγοι άνθρωποι έχουν πραγματικά βυθιστεί στην τεχνική συσκευή - το πολύ, χρησιμοποιούν ένα αρκετά απλό (και αρκετά διαφορετικό από το MTProto) Bot API που βασίζεται σε JSON και συνήθως απλώς αποδέχονται στην πίστη όλους τους επαίνους και τα PR που περιστρέφονται γύρω από τον αγγελιοφόρο. Σχεδόν πριν από ενάμιση χρόνο, ο συνάδελφός μου στη ΜΚΟ Eshelon Vasily (δυστυχώς, ο λογαριασμός του στο Habré διαγράφηκε μαζί με το προσχέδιο) άρχισε να γράφει από την αρχή τον δικό του πελάτη Telegram στο Perl και αργότερα έγινε μέλος του συγγραφέα αυτών των γραμμών. Γιατί Perl, θα ρωτήσουν κάποιοι αμέσως; Επειδή τέτοια έργα υπάρχουν ήδη σε άλλες γλώσσες. Στην πραγματικότητα, δεν είναι αυτό το θέμα, θα μπορούσε να υπάρχει οποιαδήποτε άλλη γλώσσα όπου δεν υπάρχει έτοιμη βιβλιοθήκη, και κατά συνέπεια ο συγγραφέας πρέπει να πάει μέχρι τέλους από το μηδέν. Επιπλέον, η κρυπτογραφία είναι θέμα εμπιστοσύνης, αλλά επαληθεύστε. Με ένα προϊόν που στοχεύει στην ασφάλεια, δεν μπορείτε απλά να βασίζεστε σε μια έτοιμη βιβλιοθήκη από τον κατασκευαστή και να την εμπιστεύεστε τυφλά (ωστόσο, αυτό είναι ένα θέμα για το δεύτερο μέρος). Προς το παρόν, η βιβλιοθήκη λειτουργεί αρκετά καλά στο «μέσο» επίπεδο (σας επιτρέπει να κάνετε οποιαδήποτε αιτήματα API).

Ωστόσο, δεν θα υπάρχει πολλή κρυπτογραφία ή μαθηματικά σε αυτήν τη σειρά αναρτήσεων. Θα υπάρχουν όμως πολλές άλλες τεχνικές λεπτομέρειες και αρχιτεκτονικά δεκανίκια (επίσης χρήσιμα για όσους δεν θα γράφουν από την αρχή, αλλά θα χρησιμοποιούν τη βιβλιοθήκη σε οποιαδήποτε γλώσσα). Έτσι, ο κύριος στόχος ήταν να προσπαθήσουμε να εφαρμόσουμε τον πελάτη από την αρχή σύμφωνα με την επίσημη τεκμηρίωση. Δηλαδή, ας υποθέσουμε ότι ο πηγαίος κώδικας των επίσημων πελατών είναι κλειστός (και πάλι, στο δεύτερο μέρος θα καλύψουμε λεπτομερέστερα το θέμα του γεγονότος ότι αυτό ισχύει αυτό συμβαίνει έτσι), αλλά, όπως παλιά, για παράδειγμα, υπάρχει ένα πρότυπο όπως το RFC - είναι δυνατόν να γραφτεί ένας πελάτης μόνο σύμφωνα με τις προδιαγραφές, "χωρίς να κοιτάξουμε" τον πηγαίο κώδικα, είτε είναι επίσημος (Telegram Desktop, κινητό), ή ανεπίσημο Telethon;

Πίνακας περιεχομένων:

Τεκμηρίωση... υπάρχει, σωστά; Είναι αλήθεια?..

Θραύσματα σημειώσεων για αυτό το άρθρο άρχισαν να συλλέγονται το περασμένο καλοκαίρι. Όλο αυτό το διάστημα στην επίσημη ιστοσελίδα https://core.telegram.org Η τεκμηρίωση ήταν από το Επίπεδο 23, δηλ. κόλλησε κάπου το 2014 (θυμάστε, δεν υπήρχαν καν κανάλια τότε;). Φυσικά, θεωρητικά, αυτό θα έπρεπε να μας είχε επιτρέψει να εφαρμόσουμε έναν πελάτη με λειτουργικότητα εκείνη την εποχή το 2014. Αλλά ακόμη και σε αυτήν την κατάσταση, η τεκμηρίωση ήταν, πρώτον, ελλιπής και, δεύτερον, σε σημεία που έρχεται σε αντίθεση με τον εαυτό της. Μόλις πριν από ένα μήνα, τον Σεπτέμβριο του 2019, ήταν τυχαία Ανακαλύφθηκε ότι υπήρχε μια μεγάλη ενημέρωση της τεκμηρίωσης στον ιστότοπο, για το εντελώς πρόσφατο Layer 105, με μια σημείωση ότι τώρα όλα πρέπει να ξαναδιαβαστούν. Πράγματι, πολλά άρθρα αναθεωρήθηκαν, αλλά πολλά παρέμειναν αμετάβλητα. Επομένως, όταν διαβάζετε την παρακάτω κριτική σχετικά με την τεκμηρίωση, θα πρέπει να έχετε κατά νου ότι ορισμένα από αυτά τα πράγματα δεν είναι πλέον σχετικά, αλλά μερικά είναι ακόμα αρκετά. Άλλωστε, τα 5 χρόνια στον σύγχρονο κόσμο δεν είναι απλά πολλά, αλλά πολύ πολλά απο. Από τότε (ειδικά αν δεν ληφθούν υπόψη οι απορριφθέντες και ανανεωμένοι ιστότοποι geochat από τότε), ο αριθμός των μεθόδων API στο σχήμα έχει αυξηθεί από εκατό σε περισσότερες από διακόσιες πενήντα!

Από πού να ξεκινήσετε ως νέος συγγραφέας;

Δεν έχει σημασία αν γράφετε από την αρχή ή χρησιμοποιείτε, για παράδειγμα, έτοιμες βιβλιοθήκες όπως Telethon για Python ή Madeline για PHP, σε κάθε περίπτωση, θα χρειαστείτε πρώτα καταχωρήστε την αίτησή σας - λάβετε παραμέτρους api_id и api_hash (όσοι έχουν εργαστεί με το VKontakte API καταλαβαίνουν αμέσως) με το οποίο ο διακομιστής θα αναγνωρίσει την εφαρμογή. Αυτό πρέπει να κάντε το για νομικούς λόγους, αλλά θα μιλήσουμε περισσότερο για το γιατί οι συγγραφείς της βιβλιοθήκης δεν μπορούν να το δημοσιεύσουν στο δεύτερο μέρος. Μπορεί να είστε ικανοποιημένοι με τις τιμές δοκιμής, αν και είναι πολύ περιορισμένες - γεγονός είναι ότι τώρα μπορείτε να εγγραφείτε μόνο ένα εφαρμογή, οπότε μην βιαστείτε να το κάνετε.

Τώρα, από τεχνικής άποψης, θα πρέπει να μας ενδιαφέρει το γεγονός ότι μετά την εγγραφή θα πρέπει να λαμβάνουμε ειδοποιήσεις από το Telegram σχετικά με ενημερώσεις τεκμηρίωσης, πρωτοκόλλου κ.λπ. Δηλαδή, θα μπορούσε κανείς να υποθέσει ότι ο ιστότοπος με τις αποβάθρες απλώς εγκαταλείφθηκε και συνέχισε να λειτουργεί ειδικά με αυτούς που άρχισαν να κάνουν πελάτες, επειδή είναι ευκολότερο. Αλλά όχι, δεν παρατηρήθηκε κάτι τέτοιο, δεν ήρθε καμία πληροφορία.

Και αν γράφετε από την αρχή, τότε η χρήση των παραμέτρων που έχετε αποκτήσει είναι στην πραγματικότητα πολύ μακριά. Αν και https://core.telegram.org/ και μιλάει για αυτά στο Ξεκινώντας πρώτα από όλα, στην πραγματικότητα, θα πρέπει πρώτα να εφαρμόσετε Πρωτόκολλο MTProto - αλλά αν πίστευες διάταξη σύμφωνα με το μοντέλο OSI στο τέλος της σελίδας για μια γενική περιγραφή του πρωτοκόλλου, τότε είναι εντελώς μάταιο.

Στην πραγματικότητα, τόσο πριν όσο και μετά το MTProto, σε πολλά επίπεδα ταυτόχρονα (όπως λένε οι ξένοι δικτυακοί που εργάζονται στον πυρήνα του λειτουργικού συστήματος, παραβίαση επιπέδου), ένα μεγάλο, επίπονο και τρομερό θέμα θα μπει εμπόδιο...

Δυαδική σειριοποίηση: TL (Type Language) και το σχήμα του, και τα επίπεδα, και πολλές άλλες τρομακτικές λέξεις

Αυτό το θέμα, στην πραγματικότητα, είναι το κλειδί για τα προβλήματα του Telegram. Και θα υπάρξουν πολλές τρομερές λέξεις αν προσπαθήσετε να εμβαθύνετε σε αυτό.

Λοιπόν, εδώ είναι το διάγραμμα. Αν σου έρθει αυτή η λέξη στο μυαλό, πες: Σχήμα JSON, σωστά σκέφτηκες. Ο στόχος είναι ο ίδιος: κάποια γλώσσα για να περιγράψει ένα πιθανό σύνολο μεταδιδόμενων δεδομένων. Εδώ τελειώνουν οι ομοιότητες. Αν από τη σελίδα Πρωτόκολλο MTProto, ή από το δέντρο προέλευσης του επίσημου πελάτη, θα προσπαθήσουμε να ανοίξουμε κάποιο σχήμα, θα δούμε κάτι σαν:

int ? = Int;
long ? = Long;
double ? = Double;
string ? = String;

vector#1cb5c415 {t:Type} # [ t ] = Vector t;

rpc_error#2144ca19 error_code:int error_message:string = RpcError;

rpc_answer_unknown#5e2ad36e = RpcDropAnswer;
rpc_answer_dropped_running#cd78e586 = RpcDropAnswer;
rpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer;

msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;

---functions---

set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:bytes = Set_client_DH_params_answer;

ping#7abe77ec ping_id:long = Pong;
ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong;

invokeAfterMsg#cb9f372d msg_id:long query:!X = X;
invokeAfterMsgs#3dc4b4f0 msg_ids:Vector<long> query:!X = X;

account.updateProfile#78515775 flags:# first_name:flags.0?string last_name:flags.1?string about:flags.2?string = User;
account.sendChangePhoneCode#8e57deb flags:# allow_flashcall:flags.0?true phone_number:string current_number:flags.0?Bool = auth.SentCode;

Ένα άτομο που το βλέπει αυτό για πρώτη φορά θα είναι διαισθητικά σε θέση να αναγνωρίσει μόνο ένα μέρος από αυτά που γράφονται - καλά, αυτές είναι προφανώς δομές (αν και πού είναι το όνομα, στα αριστερά ή στα δεξιά;), υπάρχουν πεδία σε αυτές, μετά από την οποία ακολουθεί ένας τύπος μετά από άνω τελεία... μάλλον. Εδώ σε αγκύλες υπάρχουν πιθανώς πρότυπα όπως στη C++ (στην πραγματικότητα, ΟΧΙ ακριβως). Και τι σημαίνουν όλα τα άλλα σύμβολα, ερωτηματικά, θαυμαστικά, ποσοστά, σημάδια κατακερματισμού (και προφανώς σημαίνουν διαφορετικά πράγματα σε διαφορετικά μέρη), άλλοτε παρόντες και άλλοτε όχι, δεκαεξαδικοί αριθμοί - και το πιο σημαντικό, πώς να βγείτε από αυτό κανονικά (που δεν θα απορριφθεί από τον διακομιστή) ροή byte; Θα πρέπει να διαβάσετε την τεκμηρίωση (ναι, υπάρχουν σύνδεσμοι προς το σχήμα στην έκδοση JSON κοντά - αλλά αυτό δεν το καθιστά πιο σαφές).

Ανοίξτε τη σελίδα Δυαδική σειριοποίηση δεδομένων και βουτήξτε στον μαγικό κόσμο των μανιταριών και των διακριτών μαθηματικών, κάτι παρόμοιο με το ματάν στο 4ο έτος. Αλφάβητο, τύπος, τιμή, συνδυασμός, λειτουργικός συνδυασμός, κανονική μορφή, σύνθετος τύπος, πολυμορφικός τύπος... και όλα αυτά είναι μόνο η πρώτη σελίδα! Το επόμενο σε περιμένει Γλώσσα TL, το οποίο, αν και περιέχει ήδη ένα παράδειγμα τετριμμένου αιτήματος και απάντησης, δεν δίνει καθόλου απάντηση σε πιο τυπικές περιπτώσεις, πράγμα που σημαίνει ότι θα πρέπει να επαναλάβετε την αφήγηση των μαθηματικών μεταφρασμένων από τα ρωσικά στα αγγλικά σε άλλες οκτώ ενσωματωμένες σελίδες!

Οι αναγνώστες που είναι εξοικειωμένοι με τις λειτουργικές γλώσσες και την αυτόματη εξαγωγή συμπερασμάτων τύπων θα βλέπουν, φυσικά, τη γλώσσα περιγραφής σε αυτήν τη γλώσσα, ακόμη και από το παράδειγμα, ως πολύ πιο οικεία, και μπορούν να πουν ότι αυτό στην πραγματικότητα δεν είναι κακό. Οι ενστάσεις επ' αυτού είναι:

  • ναι γκολ ακούγεται καλό, αλλά δυστυχώς, αυτή δεν επιτεύχθηκε
  • Η εκπαίδευση στα ρωσικά πανεπιστήμια ποικίλλει ακόμη και μεταξύ των ειδικοτήτων πληροφορικής - δεν έχουν παρακολουθήσει όλοι το αντίστοιχο μάθημα
  • Τέλος, όπως θα δούμε, στην πράξη είναι δεν απαιτείται, αφού χρησιμοποιείται μόνο ένα περιορισμένο υποσύνολο ακόμη και του TL που περιγράφηκε

Όπως είπε LeoNerd στο κανάλι #perl στο δίκτυο FreeNode IRC, ο οποίος προσπάθησε να εφαρμόσει μια πύλη από το Telegram στο Matrix (η μετάφραση του αποσπάσματος είναι ανακριβής από τη μνήμη):

Φαίνεται ότι κάποιος εισήχθη στη θεωρία τύπων για πρώτη φορά, ενθουσιάστηκε και άρχισε να προσπαθεί να παίξει μαζί της, χωρίς να ενδιαφέρεται πραγματικά αν χρειαζόταν στην πράξη.

Δείτε μόνοι σας, αν η ανάγκη για γυμνούς τύπους (int, long, κ.λπ.) ως κάτι στοιχειώδες δεν εγείρει ερωτήματα - τελικά πρέπει να υλοποιηθούν χειροκίνητα - για παράδειγμα, ας προσπαθήσουμε να αντλήσουμε από αυτούς διάνυσμα. Δηλαδή, στην πραγματικότητα, μια συστοιχία, αν ονομάσετε τα πράγματα που προκύπτουν με το όνομά τους.

Αλλά πριν

Μια σύντομη περιγραφή ενός υποσυνόλου της σύνταξης TL για όσους δεν διαβάζουν την επίσημη τεκμηρίωση

constructor = Type;
myVec ids:Vector<long> = Type;

fixed#abcdef34 id:int = Type2;

fixedVec set:Vector<Type2> = FixedVec;

constructorOne#crc32 field1:int = PolymorType;
constructorTwo#2crc32 field_a:long field_b:Type3 field_c:int = PolymorType;
constructorThree#deadcrc bit_flags_of_what_really_present:# optional_field4:bit_flags_of_what_really_present.1?Type = PolymorType;

an_id#12abcd34 id:int = Type3;
a_null#6789cdef = Type3;

Ο ορισμός ξεκινά πάντα σχεδιαστής, μετά την οποία προαιρετικά (στην πράξη - πάντα) μέσω του συμβόλου # πρέπει να είναι CRC32 από την κανονικοποιημένη συμβολοσειρά περιγραφής αυτού του τύπου. Στη συνέχεια ακολουθεί μια περιγραφή των πεδίων· εάν υπάρχουν, ο τύπος μπορεί να είναι κενός. Όλα αυτά τελειώνουν με ένα σύμβολο ίσου, το όνομα του τύπου στον οποίο ανήκει αυτός ο κατασκευαστής - στην πραγματικότητα, ο υποτύπος. Ο τύπος στα δεξιά του ίσον είναι πολυμορφικό - δηλαδή, μπορούν να αντιστοιχούν αρκετοί συγκεκριμένοι τύποι.

Εάν ο ορισμός εμφανίζεται μετά τη γραμμή ---functions---, τότε η σύνταξη θα παραμείνει η ίδια, αλλά το νόημα θα είναι διαφορετικό: ο κατασκευαστής θα γίνει το όνομα της συνάρτησης RPC, τα πεδία θα γίνουν παράμετροι (καλά, δηλαδή, θα παραμείνει ακριβώς η ίδια δομή, όπως περιγράφεται παρακάτω , αυτό θα είναι απλώς το εκχωρημένο νόημα) και ο "πολυμορφικός τύπος " - ο τύπος του επιστρεφόμενου αποτελέσματος. Είναι αλήθεια ότι θα παραμείνει πολύμορφο - μόλις ορίστηκε στην ενότητα ---types---, αλλά αυτός ο κατασκευαστής "δεν θα ληφθεί υπόψη". Υπερφόρτωση των τύπων των καλούμενων συναρτήσεων από τα ορίσματά τους, π.χ. Για κάποιο λόγο, πολλές λειτουργίες με το ίδιο όνομα αλλά διαφορετικές υπογραφές, όπως στη C++, δεν προβλέπονται στο TL.

Γιατί "κατασκευαστής" και "πολυμορφικός" αν δεν είναι OOP; Λοιπόν, στην πραγματικότητα, θα είναι ευκολότερο για κάποιον να το σκεφτεί αυτό με όρους OOP - ένας πολυμορφικός τύπος ως αφηρημένη κλάση, και οι κατασκευαστές είναι οι άμεσες απόγονες κλάσεις του, και final στην ορολογία ορισμένων γλωσσών. Στην πραγματικότητα, φυσικά, μόνο εδώ ομοιότητα με πραγματικές υπερφορτωμένες μεθόδους κατασκευής σε γλώσσες προγραμματισμού OO. Επειδή εδώ υπάρχουν απλώς δομές δεδομένων, δεν υπάρχουν μέθοδοι (αν και η περιγραφή των συναρτήσεων και των μεθόδων περαιτέρω είναι αρκετά ικανή να δημιουργήσει σύγχυση στο κεφάλι ότι υπάρχουν, αλλά αυτό είναι διαφορετικό) - μπορείτε να σκεφτείτε έναν κατασκευαστή ως τιμή από οι οποίες κατασκευάζεται πληκτρολογήστε κατά την ανάγνωση μιας ροής byte.

Πώς συμβαίνει αυτό; Ο αποσειριοποιητής, ο οποίος διαβάζει πάντα 4 byte, βλέπει την τιμή 0xcrc32 - και καταλαβαίνει τι θα συμβεί στη συνέχεια field1 με τύπο int, δηλ. διαβάζει ακριβώς 4 byte, σε αυτό το υπερκείμενο πεδίο με τον τύπο PolymorType ανάγνωση. Βλέπει 0x2crc32 και καταλαβαίνει ότι υπάρχουν δύο πεδία παραπέρα, πρώτον long, που σημαίνει ότι διαβάζουμε 8 byte. Και μετά πάλι ένας σύνθετος τύπος, ο οποίος αποσειροποιείται με τον ίδιο τρόπο. Για παράδειγμα, Type3 θα μπορούσε να δηλωθεί στο κύκλωμα μόλις δύο κατασκευαστές, αντίστοιχα, τότε πρέπει να συναντηθούν 0x12abcd34, μετά από το οποίο πρέπει να διαβάσετε 4 επιπλέον byte intΉ 0x6789cdef, μετά από το οποίο δεν θα υπάρχει τίποτα. Οτιδήποτε άλλο - πρέπει να κάνετε μια εξαίρεση. Τέλος πάντων, μετά από αυτό επιστρέφουμε στην ανάγνωση 4 bytes int περιθώρια field_c в constructorTwo και με αυτό τελειώνουμε την ανάγνωση μας PolymorType.

Τέλος, αν σε πιάσουν 0xdeadcrc για constructorThree, τότε όλα γίνονται πιο περίπλοκα. Το πρώτο μας πεδίο είναι bit_flags_of_what_really_present με τύπο # - στην πραγματικότητα, αυτό είναι απλώς ένα ψευδώνυμο για τον τύπο nat, που σημαίνει «φυσικός αριθμός». Αυτό είναι, στην πραγματικότητα, το ανυπόγραφο int είναι, παρεμπιπτόντως, η μόνη περίπτωση που οι ανυπόγραφοι αριθμοί εμφανίζονται σε πραγματικά κυκλώματα. Έτσι, ακολουθεί μια κατασκευή με ερωτηματικό, που σημαίνει ότι αυτό το πεδίο - θα υπάρχει στο καλώδιο μόνο εάν το αντίστοιχο bit έχει οριστεί στο πεδίο που αναφέρεται (περίπου σαν τριαδικός τελεστής). Λοιπόν, ας υποθέσουμε ότι αυτό το bit έχει οριστεί, πράγμα που σημαίνει ότι πρέπει να διαβάσουμε περαιτέρω ένα πεδίο όπως Type, που στο παράδειγμά μας έχει 2 κατασκευαστές. Το ένα είναι κενό (αποτελείται μόνο από το αναγνωριστικό), το άλλο έχει ένα πεδίο ids με τύπο ids:Vector<long>.

Ίσως πιστεύετε ότι τόσο τα πρότυπα όσο και τα γενικά είναι στα πλεονεκτήματα ή στην Java. Αλλά όχι. Σχεδόν. Αυτό μονό περίπτωση χρήσης γωνιακών βραχιόνων σε πραγματικά κυκλώματα, και χρησιμοποιείται ΜΟΝΟ για Vector. Σε μια ροή byte, αυτά θα είναι 4 CRC32 byte για τον ίδιο τον τύπο Vector, πάντα τα ίδια, μετά 4 byte - ο αριθμός των στοιχείων του πίνακα και μετά αυτά τα ίδια τα στοιχεία.

Προσθέστε σε αυτό το γεγονός ότι η σειριοποίηση γίνεται πάντα σε λέξεις των 4 byte, όλοι οι τύποι είναι πολλαπλάσιοι - περιγράφονται επίσης οι ενσωματωμένοι τύποι bytes и string με χειροκίνητη σειριοποίηση του μήκους και αυτή την ευθυγράμμιση κατά 4 - καλά, φαίνεται να ακούγεται φυσιολογικό και μάλιστα σχετικά αποτελεσματικό; Παρόλο που το TL υποστηρίζεται ότι είναι μια αποτελεσματική δυαδική σειριοποίηση, στο διάολο, με την επέκταση σχεδόν οτιδήποτε, ακόμα και των τιμών Boolean και των συμβολοσειρών ενός χαρακτήρα σε 4 byte, το JSON θα είναι ακόμα πολύ πιο παχύ; Κοιτάξτε, ακόμη και τα περιττά πεδία μπορούν να παραλειφθούν με σημαίες bit, όλα είναι αρκετά καλά, ακόμη και επεκτάσιμα για το μέλλον, οπότε γιατί να μην προσθέσετε νέα προαιρετικά πεδία στον κατασκευαστή αργότερα;...

Αλλά όχι, αν δεν διαβάσετε τη σύντομη περιγραφή μου, αλλά την πλήρη τεκμηρίωση και σκεφτείτε την υλοποίηση. Πρώτον, το CRC32 του κατασκευαστή υπολογίζεται σύμφωνα με την κανονικοποιημένη γραμμή της περιγραφής κειμένου του σχήματος (αφαίρεση επιπλέον κενού κενού κ.λπ.) - επομένως, εάν προστεθεί ένα νέο πεδίο, η γραμμή περιγραφής τύπου θα αλλάξει και ως εκ τούτου το CRC32 και , κατά συνέπεια, σειριοποίηση. Και τι θα έκανε ο παλιός πελάτης αν λάμβανε ένα πεδίο με ορισμένες νέες σημαίες και δεν ξέρει τι να τις κάνει στη συνέχεια;..

Δεύτερον, ας θυμηθούμε CRC32, που χρησιμοποιείται εδώ ουσιαστικά ως λειτουργίες κατακερματισμού για να προσδιοριστεί με μοναδικό τρόπο ποιος τύπος (απο)σειριοποιείται. Εδώ βρισκόμαστε αντιμέτωποι με το πρόβλημα των συγκρούσεων - και όχι, η πιθανότητα δεν είναι μία στις 232, αλλά πολύ μεγαλύτερη. Ποιος θυμήθηκε ότι το CRC32 έχει σχεδιαστεί για να ανιχνεύει (και να διορθώνει) σφάλματα στο κανάλι επικοινωνίας και, κατά συνέπεια, βελτιώνει αυτές τις ιδιότητες εις βάρος άλλων; Για παράδειγμα, δεν ενδιαφέρεται για την αναδιάταξη των byte: αν υπολογίσετε το CRC32 από δύο γραμμές, στη δεύτερη αλλάζετε τα πρώτα 4 byte με τα επόμενα 4 byte - θα είναι το ίδιο. Όταν η εισαγωγή μας είναι συμβολοσειρές κειμένου από το λατινικό αλφάβητο (και λίγα σημεία στίξης) και αυτά τα ονόματα δεν είναι ιδιαίτερα τυχαία, η πιθανότητα μιας τέτοιας αναδιάταξης αυξάνεται πολύ.

Με την ευκαιρία, ποιος έλεγξε τι υπήρχε; πραγματικά CRC32; Ένας από τους πρώιμους πηγαίους κώδικες (ακόμη και πριν από τον Waltman) είχε μια συνάρτηση κατακερματισμού που πολλαπλασίαζε κάθε χαρακτήρα με τον αριθμό 239, τόσο αγαπητό σε αυτούς τους ανθρώπους, χα χα!

Τελικά, εντάξει, καταλάβαμε ότι οι κατασκευαστές με τύπο πεδίου Vector<int> и Vector<PolymorType> θα έχει διαφορετικό CRC32. Τι γίνεται με την on-line απόδοση; Και από θεωρητικής σκοπιάς, γίνεται αυτό μέρος του τύπου? Ας υποθέσουμε ότι περνάμε έναν πίνακα δέκα χιλιάδων αριθμών, καλά με Vector<int> όλα είναι ξεκάθαρα, το μήκος και άλλα 40000 byte. Τι κι αν αυτό Vector<Type2>, το οποίο αποτελείται από ένα μόνο πεδίο int και είναι μόνο στον τύπο - χρειάζεται να επαναλάβουμε το 10000xabcdef0 34 φορές και μετά 4 byte int, ή η γλώσσα μπορεί να την ΑΝΕΞΑΡΤΗΣΕΙ για εμάς από τον κατασκευαστή fixedVec και αντί για 80000 byte να μεταφέρω πάλι μόνο 40000;

Αυτή δεν είναι καθόλου μια αδρανής θεωρητική ερώτηση - φανταστείτε ότι λαμβάνετε μια λίστα χρηστών ομάδας, καθένας από τους οποίους έχει αναγνωριστικό, όνομα, επώνυμο - η διαφορά στην ποσότητα των δεδομένων που μεταφέρονται μέσω μιας σύνδεσης κινητής τηλεφωνίας μπορεί να είναι σημαντική. Είναι ακριβώς η αποτελεσματικότητα της σειριοποίησης του Telegram που μας διαφημίζεται.

Ετσι…

Vector, που δεν κυκλοφόρησε ποτέ

Εάν προσπαθήσετε να περιηγηθείτε στις σελίδες περιγραφής των συνδυαστικών και ούτω καθεξής, θα δείτε ότι ένα διάνυσμα (ακόμα και ένας πίνακας) προσπαθεί επίσημα να βγει μέσα από πλειάδες πολλών φύλλων. Αλλά στο τέλος ξεχνούν, το τελευταίο βήμα παραλείπεται και απλώς δίνεται ένας ορισμός ενός διανύσματος, ο οποίος δεν είναι ακόμη συνδεδεμένος με έναν τύπο. Τι συμβαίνει? Σε γλώσσες προγραμματισμός, ειδικά στις λειτουργικές, είναι αρκετά τυπικό να περιγράφουμε τη δομή αναδρομικά - ο μεταγλωττιστής με την τεμπέλικη αξιολόγησή του θα καταλάβει και θα κάνει τα πάντα μόνος του. Στη γλώσσα σειριοποίηση δεδομένων αυτό που χρειάζεται είναι η ΑΠΟΤΕΛΕΣΜΑΤΙΚΟΤΗΤΑ: αρκεί να περιγράψουμε απλά λίστα, δηλ. δομή δύο στοιχείων - το πρώτο είναι στοιχείο δεδομένων, το δεύτερο είναι η ίδια δομή ή ένας κενός χώρος για την ουρά (πακέτο (cons) στο Lisp). Αλλά αυτό προφανώς θα απαιτήσει του καθενός Το στοιχείο ξοδεύει επιπλέον 4 byte (CRC32 στην περίπτωση σε TL) για να περιγράψει τον τύπο του. Ένας πίνακας μπορεί επίσης να περιγραφεί εύκολα σταθερό μέγεθος, αλλά στην περίπτωση ενός πίνακα άγνωστου μήκους εκ των προτέρων, αποκόπτουμε.

Επομένως, καθώς το TL δεν επιτρέπει την έξοδο ενός διανύσματος, έπρεπε να προστεθεί στο πλάι. Τελικά η τεκμηρίωση λέει:

Η σειριοποίηση χρησιμοποιεί πάντα τον ίδιο κατασκευαστή "διάνυσμα" (const 0x1cb5c415 = crc32 ("διάνυσμα t:Τύπος # [ t ] = Διάνυσμα t") που δεν εξαρτάται από τη συγκεκριμένη τιμή της μεταβλητής τύπου t.

Η τιμή της προαιρετικής παραμέτρου t δεν εμπλέκεται στη σειριοποίηση, καθώς προέρχεται από τον τύπο αποτελέσματος (γνωστός πάντα πριν από την αποσειριοποίηση).

Κοίτα καλύτερα: vector {t:Type} # [ t ] = Vector t - αλλά πουθενά Αυτός ο ίδιος ο ορισμός δεν λέει ότι ο πρώτος αριθμός πρέπει να είναι ίσος με το μήκος του διανύσματος! Και δεν έρχεται από πουθενά. Αυτό είναι δεδομένο που πρέπει να το έχετε κατά νου και να το εφαρμόσετε με τα χέρια σας. Σε άλλο σημείο, η τεκμηρίωση μάλιστα αναφέρει ειλικρινά ότι ο τύπος δεν είναι πραγματικός:

Ο πολυμορφικός ψευδότυπος Vector t είναι ένας «τύπος» του οποίου η τιμή είναι μια ακολουθία τιμών οποιουδήποτε τύπου t, είτε σε πλαίσιο είτε γυμνό.

... αλλά δεν εστιάζει σε αυτό. Όταν, κουρασμένος από το τέντωμα των μαθηματικών (ίσως και γνωστός σε σένα από πανεπιστημιακό μάθημα), αποφασίζεις να τα παρατήσεις και να δεις πώς να τα δουλέψεις στην πράξη, η εντύπωση που σου μένει στο μυαλό είναι ότι αυτό είναι σοβαρό. Τα μαθηματικά στον πυρήνα, τα εφευρέθηκαν ξεκάθαρα οι Cool People (δύο μαθηματικοί - νικητής του ACM), και όχι ο καθένας. Ο στόχος - η επίδειξη - επετεύχθη.

Παρεμπιπτόντως, για τον αριθμό. Να σας το υπενθυμίσουμε # είναι συνώνυμο nat, φυσικός αριθμός:

Υπάρχουν εκφράσεις τύπου (τύπος-expr) και αριθμητικές εκφράσεις (nat-expr). Ωστόσο, ορίζονται με τον ίδιο τρόπο.

type-expr ::= expr
nat-expr ::= expr

αλλά στη γραμματική περιγράφονται με τον ίδιο τρόπο, δηλ. Αυτή η διαφορά πρέπει και πάλι να θυμόμαστε και να τεθεί σε εφαρμογή με το χέρι.

Λοιπόν, ναι, τύποι προτύπων (vector<int>, vector<User>) έχουν ένα κοινό αναγνωριστικό (#1cb5c415), δηλ. αν γνωρίζετε ότι η κλήση ανακοινώνεται ως

users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;

τότε δεν περιμένετε πλέον μόνο ένα διάνυσμα, αλλά ένα διάνυσμα χρηστών. Ακριβέστερα, θα πρέπει να περιμένετε - σε πραγματικό κώδικα, κάθε στοιχείο, αν όχι γυμνός τύπος, θα έχει έναν κατασκευαστή, και με καλό τρόπο στην υλοποίηση θα ήταν απαραίτητο να ελέγξουμε - αλλά μας εστάλη ακριβώς σε κάθε στοιχείο αυτού του διανύσματος αυτός ο τύπος? Τι θα γινόταν αν ήταν κάποιο είδος PHP, στο οποίο ένας πίνακας μπορεί να περιέχει διαφορετικούς τύπους σε διαφορετικά στοιχεία;

Σε αυτό το σημείο αρχίζετε να σκέφτεστε - είναι απαραίτητο ένα τέτοιο TL; Ίσως για το καλάθι θα ήταν δυνατή η χρήση ενός ανθρώπινου σειριοποιητή, το ίδιο protobuf που υπήρχε ήδη τότε; Αυτή ήταν η θεωρία, ας δούμε την πράξη.

Υπάρχουσες υλοποιήσεις TL σε κώδικα

Ο TL γεννήθηκε στα βάθη του VKontakte ακόμη και πριν από τα διάσημα γεγονότα με την πώληση της μετοχής του Durov και (ασφαλώς), πριν ακόμη ξεκινήσει η ανάπτυξη του Telegram. Και σε ανοιχτό κώδικα πηγαίος κώδικας της πρώτης υλοποίησης μπορείτε να βρείτε πολλά αστεία δεκανίκια. Και η ίδια η γλώσσα εφαρμόστηκε εκεί πληρέστερα από ό,τι είναι τώρα στο Telegram. Για παράδειγμα, οι κατακερματισμοί δεν χρησιμοποιούνται καθόλου στο σχήμα (που σημαίνει έναν ενσωματωμένο ψευδότυπο (όπως ένα διάνυσμα) με αποκλίνουσα συμπεριφορά). Ή

Templates are not used now. Instead, the same universal constructors (for example, vector {t:Type} [t] = Vector t) are used w

αλλά ας εξετάσουμε, για λόγους πληρότητας, να εντοπίσουμε, ας πούμε, την εξέλιξη του Γίγαντα της Σκέψης.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Ή αυτό το όμορφο:

    static const char *reserved_words_polymorhic[] = {

      "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", NULL

      };

Αυτό το απόσπασμα αφορά πρότυπα όπως:

intHash {alpha:Type} vector<coupleInt<alpha>> = IntHash<alpha>;

Αυτός είναι ο ορισμός ενός τύπου προτύπου hashmap ως διάνυσμα ζευγών int - Type. Στην C++ θα μοιάζει κάπως έτσι:

    template <T> class IntHash {
      vector<pair<int,T>> _map;
    }

Έτσι, alpha - λέξη-κλειδί! Αλλά μόνο στη C++ μπορείς να γράψεις T, αλλά να γράψεις άλφα, βήτα... Αλλά όχι περισσότερες από 8 παραμέτρους, εκεί τελειώνει η φαντασία. Φαίνεται ότι κάποτε στην Αγία Πετρούπολη έγιναν κάποιοι διάλογοι σαν αυτόν:

-- Надо сделать в TL шаблоны
-- Бл... Ну пусть параметры зовут альфа, бета,... Какие там ещё буквы есть... О, тэта!
-- Грамматика? Ну потом напишем

-- Смотрите, какой я синтаксис придумал для шаблонов и вектора!
-- Ты долбанулся, как мы это парсить будем?
-- Да не ссыте, он там один в схеме, захаркодить -- и ок

Αλλά αυτό αφορούσε την πρώτη δημοσιευμένη εφαρμογή του TL «γενικά». Ας προχωρήσουμε στην εξέταση των υλοποιήσεων στους ίδιους τους πελάτες του Telegram.

Λέξη στον Βασίλη:

Vasily, [09.10.18 17:07] Πάνω απ' όλα, ο γάιδαρος είναι καυτός επειδή δημιούργησαν ένα σωρό αφαιρέσεις, και μετά σφυρήλωσαν πάνω τους ένα μπουλόνι και κάλυψαν τη γεννήτρια κωδικών με πατερίτσες
Ως αποτέλεσμα, πρώτα από το dock pilot.jpg
Στη συνέχεια από τον κωδικό dzhekichan.webp

Φυσικά, από ανθρώπους που είναι εξοικειωμένοι με τους αλγόριθμους και τα μαθηματικά, μπορούμε να περιμένουμε ότι έχουν διαβάσει Aho, Ullmann και είναι εξοικειωμένοι με τα εργαλεία που έχουν γίνει de facto πρότυπα στη βιομηχανία κατά τη διάρκεια των δεκαετιών για τη σύνταξη των μεταγλωττιστών DSL τους, σωστά;..

Συγγραφέας telegram-cli είναι ο Vitaly Valtman, όπως μπορεί να γίνει κατανοητό από την εμφάνιση της μορφής TLO εκτός των (cli) ορίων της, μέλος της ομάδας - τώρα έχει διατεθεί μια βιβλιοθήκη για ανάλυση TL ξεχωριστά, ποια είναι η εντύπωση της TL αναλυτής; ..

16.12 04:18 Vasily: Νομίζω ότι κάποιος δεν γνώρισε το lex+yacc
16.12 04:18 Vasily: Δεν μπορώ να το εξηγήσω διαφορετικά
16.12 04:18 Vasily: Λοιπόν, ή πληρώθηκαν για τον αριθμό των γραμμών στο VK
16.12 04:19 Vasily: 3k+ γραμμές κ.λπ.<censored> αντί για αναλυτή

Ίσως μια εξαίρεση; Ας δούμε πώς делает Αυτός είναι ο ΕΠΙΣΗΜΟΣ πελάτης - Telegram Desktop:

    nametype = re.match(r'([a-zA-Z.0-9_]+)(#[0-9a-f]+)?([^=]*)=s*([a-zA-Z.<>0-9_]+);', line);
    if (not nametype):
      if (not re.match(r'vector#1cb5c415 {t:Type} # [ t ] = Vector t;', line)):
         print('Bad line found: ' + line);

1100+ γραμμές στην Python, μερικές κανονικές εκφράσεις + ειδικές περιπτώσεις όπως ένα διάνυσμα, το οποίο, φυσικά, δηλώνεται στο σχήμα όπως θα έπρεπε σύμφωνα με τη σύνταξη TL, αλλά βασίστηκαν σε αυτήν τη σύνταξη για να το αναλύσουν... Γεννιέται το ερώτημα, γιατί ήταν όλα θαύμα;иΕίναι πιο πολυεπίπεδο αν κανείς δεν πρόκειται να το αναλύσει σύμφωνα με την τεκμηρίωση ούτως ή άλλως;!

Παρεμπιπτόντως... Θυμάστε ότι μιλήσαμε για τον έλεγχο του CRC32; Έτσι, στη γεννήτρια κώδικα Telegram Desktop υπάρχει μια λίστα εξαιρέσεων για εκείνους τους τύπους στους οποίους το υπολογισμένο CRC32 δεν ταιριάζει με αυτό που φαίνεται στο διάγραμμα!

Vasily, [18.12/22 49:XNUMX] και εδώ θα σκεφτόμουν αν χρειάζεται ένα τέτοιο TL
αν ήθελα να μπλέξω με εναλλακτικές υλοποιήσεις, θα άρχιζα να εισάγω αλλαγές γραμμής, οι μισοί από τους αναλυτές θα σπάσουν σε ορισμούς πολλών γραμμών
tdesktop, όμως, επίσης

Θυμηθείτε το σημείο για το one-liner, θα επιστρέψουμε σε αυτό λίγο αργότερα.

Εντάξει, το telegram-cli είναι ανεπίσημο, το Telegram Desktop είναι επίσημο, αλλά τι γίνεται με τα άλλα; Ποιος ξέρει;... Στον κώδικα πελάτη Android δεν υπήρχε καθόλου αναλυτής σχήματος (που εγείρει ερωτήματα σχετικά με τον ανοιχτό κώδικα, αλλά αυτό είναι για το δεύτερο μέρος), αλλά υπήρχαν πολλά άλλα αστεία κομμάτια κώδικα, αλλά περισσότερα για αυτά στο υποενότητα παρακάτω.

Ποια άλλα ερωτήματα εγείρει στην πράξη η σειριοποίηση; Για παράδειγμα, έκαναν πολλά πράγματα, φυσικά, με πεδία bit και πεδία υπό όρους:

Βασίλι: flags.0? true
σημαίνει ότι το πεδίο είναι παρόν και ισούται με αληθές εάν έχει οριστεί η σημαία

Βασίλι: flags.1? int
σημαίνει ότι το πεδίο είναι παρόν και πρέπει να απελευθερωθεί

Βασίλι: Γαϊδούρι, μην ανησυχείς για αυτό που κάνεις!
Vasily: Υπάρχει μια αναφορά κάπου στο έγγραφο ότι η αλήθεια είναι ένας γυμνός τύπος μηδενικού μήκους, αλλά είναι αδύνατο να συναρμολογηθεί οτιδήποτε από το έγγραφό τους
Vasily: Στις υλοποιήσεις ανοιχτού κώδικα δεν ισχύει ούτε αυτό, αλλά υπάρχουν ένα σωρό πατερίτσες και στηρίγματα

Τι γίνεται με τον Telethon; Κοιτάζοντας μπροστά στο θέμα του MTProto, ένα παράδειγμα - στην τεκμηρίωση υπάρχουν τέτοια κομμάτια, αλλά το σημάδι % περιγράφεται μόνο ως «αντιστοιχεί σε δεδομένο γυμνό τύπο», δηλ. στα παρακάτω παραδείγματα υπάρχει είτε σφάλμα είτε κάτι μη τεκμηριωμένο:

Vasily, [22.06.18 18:38] Σε ένα μέρος:

msg_container#73f1f8dc messages:vector message = MessageContainer;

Σε ένα διαφορετικό:

msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;

Και αυτές είναι δύο μεγάλες διαφορές, στην πραγματική ζωή έρχεται κάποιο είδος γυμνού φορέα

Δεν έχω δει έναν ορισμό γυμνού φορέα και δεν έχω συναντήσει

Η ανάλυση γράφεται με το χέρι στον τηλεμαραθώνιο

Στο διάγραμμά του σχολιάζεται ο ορισμός msg_container

Και πάλι, το ερώτημα παραμένει σχετικά με το %. Δεν περιγράφεται.

Vadim Goncharov, [22.06.18 19:22] και στο tdesktop;

Vasily, [22.06.18 19:23] Αλλά ο αναλυτής TL τους σε κανονικούς κινητήρες πιθανότατα δεν θα το φάει ούτε αυτό

// parsed manually

Το TL είναι μια όμορφη αφαίρεση, κανείς δεν την εφαρμόζει πλήρως

Και το % δεν βρίσκεται στην έκδοση του συστήματος

Αλλά εδώ η τεκμηρίωση έρχεται σε αντίθεση με τον εαυτό της, οπότε idk

Βρέθηκε στη γραμματική, θα μπορούσαν απλά να είχαν ξεχάσει να περιγράψουν τη σημασιολογία

Είδατε το έγγραφο στο TL, δεν μπορείτε να το καταλάβετε χωρίς μισό λίτρο

«Λοιπόν, ας πούμε», θα πει ένας άλλος αναγνώστης, «επικρίνεις κάτι, οπότε δείξε μου πώς πρέπει να γίνει».

Ο Βασίλι απαντά: «Όσο για τον αναλυτή, μου αρέσουν πράγματα όπως

    args: /* empty */ { $$ = NULL; }
        | args arg { $$ = g_list_append( $1, $2 ); }
        ;

    arg: LC_ID ':' type-term { $$ = tl_arg_new( $1, $3 ); }
            | LC_ID ':' condition '?' type-term { $$ = tl_arg_new_cond( $1, $5, $3 ); free($3); }
            | UC_ID ':' type-term { $$ = tl_arg_new( $1, $3 ); }
            | type-term { $$ = tl_arg_new( "", $1 ); }
            | '[' LC_ID ']' { $$ = tl_arg_new_mult( "", tl_type_new( $2, TYPE_MOD_NONE ) ); }
            ;

κάπως μου αρέσει καλύτερα από

struct tree *parse_args4 (void) {
  PARSE_INIT (type_args4);
  struct parse so = save_parse ();
  PARSE_TRY (parse_optional_arg_def);
  if (S) {
    tree_add_child (T, S);
  } else {
    load_parse (so);
  }
  if (LEX_CHAR ('!')) {
    PARSE_ADD (type_exclam);
    EXPECT ("!");
  }
  PARSE_TRY_PES (parse_type_term);
  PARSE_OK;
}

ή

        # Regex to match the whole line
        match = re.match(r'''
            ^                  # We want to match from the beginning to the end
            ([w.]+)           # The .tl object can contain alpha_name or namespace.alpha_name
            (?:
                #             # After the name, comes the ID of the object
                ([0-9a-f]+)    # The constructor ID is in hexadecimal form
            )?                 # If no constructor ID was given, CRC32 the 'tl' to determine it

            (?:s              # After that, we want to match its arguments (name:type)
                {?             # For handling the start of the '{X:Type}' case
                w+            # The argument name will always be an alpha-only name
                :              # Then comes the separator between name:type
                [wd<>#.?!]+  # The type is slightly more complex, since it's alphanumeric and it can
                               # also have Vector<type>, flags:# and flags.0?default, plus :!X as type
                }?             # For handling the end of the '{X:Type}' case
            )*                 # Match 0 or more arguments
            s                 # Leave a space between the arguments and the equal
            =
            s                 # Leave another space between the equal and the result
            ([wd<>#.?]+)     # The result can again be as complex as any argument type
            ;$                 # Finally, the line should always end with ;
            ''', tl, re.IGNORECASE | re.VERBOSE)

αυτό είναι ΟΛΟΚΛΗΡΟ το λεξιλόγιο:

    ---functions---         return FUNCTIONS;
    ---types---             return TYPES;
    [a-z][a-zA-Z0-9_]*      yylval.string = strdup(yytext); return LC_ID;
    [A-Z][a-zA-Z0-9_]*      yylval.string = strdup(yytext); return UC_ID;
    [0-9]+                  yylval.number = atoi(yytext); return NUM;
    #[0-9a-fA-F]{1,8}       yylval.number = strtol(yytext+1, NULL, 16); return ID_HASH;

    n                      /* skip new line */
    [ t]+                  /* skip spaces */
    //.*$                 /* skip comments */
    /*.**/              /* skip comments */
    .                       return (int)yytext[0];

εκείνοι. πιο απλό είναι να το θέσω ήπια».

Σε γενικές γραμμές, ως αποτέλεσμα, ο αναλυτής και η γεννήτρια κώδικα για το υποσύνολο TL που χρησιμοποιείται στην πραγματικότητα χωρούν σε περίπου 100 γραμμές γραμματικής και ~300 γραμμές της γεννήτριας (μετρώντας όλες printτου παραγόμενου κώδικα), συμπεριλαμβανομένων των συλλεκτών πληροφοριών τύπου για ενδοσκόπηση σε κάθε τάξη. Κάθε πολυμορφικός τύπος μετατρέπεται σε μια άδεια αφηρημένη βασική κλάση και οι κατασκευαστές κληρονομούν από αυτήν και έχουν μεθόδους για σειριοποίηση και αποσειριοποίηση.

Έλλειψη τύπων στη γλώσσα τύπου

Η δυνατή πληκτρολόγηση είναι καλό, σωστά; Όχι, αυτό δεν είναι ένα holivar (αν και προτιμώ τις δυναμικές γλώσσες), αλλά ένα αξίωμα στο πλαίσιο του TL. Με βάση αυτό, η γλώσσα θα πρέπει να παρέχει κάθε είδους επιταγές για εμάς. Λοιπόν, εντάξει, ίσως όχι ο ίδιος, αλλά η υλοποίηση, αλλά θα έπρεπε τουλάχιστον να τα περιγράψει. Και τι είδους ευκαιρίες θέλουμε;

Πρώτα απ 'όλα, περιορισμοί. Εδώ βλέπουμε στην τεκμηρίωση για τη μεταφόρτωση αρχείων:

Στη συνέχεια, το δυαδικό περιεχόμενο του αρχείου χωρίζεται σε μέρη. Όλα τα μέρη πρέπει να έχουν το ίδιο μέγεθος ( ανταλλακτικό μέγεθος ) και πρέπει να πληρούνται οι ακόλουθες προϋποθέσεις:

  • part_size % 1024 = 0 (διαιρείται με 1KB)
  • 524288 % part_size = 0 (τα 512 KB πρέπει να διαιρούνται ομοιόμορφα με το part_size)

Το τελευταίο μέρος δεν χρειάζεται να πληροί αυτές τις προϋποθέσεις, με την προϋπόθεση ότι το μέγεθός του είναι μικρότερο από το part_size.

Κάθε μέρος πρέπει να έχει έναν αριθμό σειράς, file_part, με τιμή που κυμαίνεται από 0 έως 2,999.

Μετά την κατάτμηση του αρχείου, πρέπει να επιλέξετε μια μέθοδο για την αποθήκευσή του στον διακομιστή. Χρήση upload.saveBigFilePart σε περίπτωση που το πλήρες μέγεθος του αρχείου είναι μεγαλύτερο από 10 MB και upload.saveFilePart για μικρότερα αρχεία.
[…] μπορεί να επιστραφεί ένα από τα ακόλουθα σφάλματα εισαγωγής δεδομένων:

  • FILE_PARTS_INVALID — Μη έγκυρος αριθμός εξαρτημάτων. Η τιμή δεν είναι μεταξύ 1..3000

Υπάρχει κάτι από αυτά στο διάγραμμα; Είναι αυτό κατά κάποιο τρόπο εκφραστικό χρησιμοποιώντας TL; Οχι. Αλλά με συγχωρείτε, ακόμη και ο Turbo Pascal του παππού ήταν σε θέση να περιγράψει τους τύπους που καθορίστηκαν σειρές. Και ήξερε κάτι ακόμα, πλέον γνωστό ως enum - ένας τύπος που αποτελείται από μια απαρίθμηση ενός σταθερού (μικρού) αριθμού τιμών. Σε γλώσσες όπως η C - αριθμητική, σημειώστε ότι μέχρι στιγμής έχουμε μιλήσει μόνο για τύπους αριθμοί. Υπάρχουν όμως και πίνακες, συμβολοσειρές... για παράδειγμα, θα ήταν ωραίο να περιγράψουμε ότι αυτή η συμβολοσειρά μπορεί να περιέχει μόνο έναν αριθμό τηλεφώνου, σωστά;

Τίποτα από αυτά δεν υπάρχει στο TL. Αλλά υπάρχει, για παράδειγμα, στο σχήμα JSON. Και αν κάποιος άλλος μπορεί να διαφωνήσει σχετικά με τη διαιρετότητα των 512 KB, ότι αυτό πρέπει ακόμα να ελεγχθεί σε κώδικα, τότε βεβαιωθείτε ότι ο πελάτης απλώς δεν μπορούσε στείλτε έναν αριθμό εκτός εύρους 1..3000 (και δεν θα μπορούσε να έχει προκύψει το αντίστοιχο λάθος) θα ήταν δυνατό, σωστά;..

Παρεμπιπτόντως, σχετικά με τα σφάλματα και τις τιμές επιστροφής. Ακόμη και εκείνοι που έχουν εργαστεί με το TL θολώνουν τα μάτια τους - δεν μας το ξημέρωσε αμέσως αυτό ο καθένας μια συνάρτηση στο TL μπορεί πραγματικά να επιστρέψει όχι μόνο τον περιγραφόμενο τύπο επιστροφής, αλλά και ένα σφάλμα. Αλλά αυτό δεν μπορεί να συναχθεί με κανέναν τρόπο χρησιμοποιώντας το ίδιο το TL. Φυσικά, είναι ήδη ξεκάθαρο και δεν χρειάζεται τίποτα στην πράξη (αν και στην πραγματικότητα, το RPC μπορεί να γίνει με διαφορετικούς τρόπους, θα επανέλθουμε σε αυτό αργότερα) - αλλά τι γίνεται με την καθαρότητα των εννοιών των μαθηματικών αφηρημένων τύπων από τον ουράνιο κόσμο;.. Σήκωσα το ρυμουλκό - ταιριάξτε το λοιπόν.

Και τέλος, τι γίνεται με την αναγνωσιμότητα; Λοιπόν, εκεί, γενικά, θα ήθελα περιγραφή το έχετε σωστά στο σχήμα (στο σχήμα JSON, πάλι, είναι), αλλά αν είστε ήδη κουρασμένοι με αυτό, τότε τι γίνεται με την πρακτική πλευρά - τουλάχιστον επιπόλαια εξέταση των διαφορών κατά τις ενημερώσεις; Δείτε μόνοι σας στο πραγματικά παραδείγματα:

-channelFull#76af5481 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int = ChatFull;
+channelFull#1c87a71a flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_view_stats:flags.12?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int = ChatFull;

ή

-message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long = Message;
+message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long = Message;

Εξαρτάται από όλους, αλλά το GitHub, για παράδειγμα, αρνείται να επισημάνει τις αλλαγές μέσα σε τέτοιες μεγάλες ουρές. Το παιχνίδι «βρες 10 διαφορές» και αυτό που βλέπει αμέσως ο εγκέφαλος είναι ότι η αρχή και το τέλος και στα δύο παραδείγματα είναι ίδιες, πρέπει να διαβάσεις κουραστικά κάπου στη μέση... Κατά τη γνώμη μου, αυτό δεν είναι μόνο στη θεωρία, αλλά καθαρά οπτικά βρώμικο και ατημέλητο.

Παρεμπιπτόντως, για την καθαρότητα της θεωρίας. Γιατί χρειαζόμαστε πεδία bit; Δεν φαίνεται ότι αυτοί μυρωδιά κακό από την άποψη της θεωρίας τύπων; Η εξήγηση φαίνεται σε προηγούμενες εκδόσεις του διαγράμματος. Στην αρχή, ναι, έτσι ήταν, για κάθε φτάρνισμα δημιουργήθηκε ένας νέος τύπος. Αυτά τα βασικά στοιχεία εξακολουθούν να υπάρχουν σε αυτή τη μορφή, για παράδειγμα:

storage.fileUnknown#aa963b05 = storage.FileType;
storage.filePartial#40bc6f52 = storage.FileType;
storage.fileJpeg#7efe0e = storage.FileType;
storage.fileGif#cae1aadf = storage.FileType;
storage.filePng#a4f63c0 = storage.FileType;
storage.filePdf#ae1e508d = storage.FileType;
storage.fileMp3#528a0677 = storage.FileType;
storage.fileMov#4b09ebbc = storage.FileType;
storage.fileMp4#b3cea0e4 = storage.FileType;
storage.fileWebp#1081464c = storage.FileType;

Αλλά φανταστείτε τώρα, εάν έχετε 5 προαιρετικά πεδία στη δομή σας, τότε θα χρειαστείτε 32 τύπους για όλες τις πιθανές επιλογές. Συνδυαστική έκρηξη. Έτσι, η κρυσταλλική καθαρότητα της θεωρίας του TL συντρίβεται για άλλη μια φορά ενάντια στο χυτοσίδηρο της σκληρής πραγματικότητας της σειριοποίησης.

Επιπλέον, σε ορισμένα σημεία αυτά τα παιδιά παραβιάζουν τη δική τους τυπολογία. Για παράδειγμα, στο MTProto (επόμενο κεφάλαιο) η απόκριση μπορεί να συμπιεστεί από το Gzip, όλα είναι καλά - εκτός από το ότι τα επίπεδα και το κύκλωμα παραβιάζονται. Για άλλη μια φορά, δεν καρπώθηκε το ίδιο το RpcResult, αλλά το περιεχόμενό του. Λοιπόν, γιατί να το κάνω αυτό;.. Έπρεπε να κόψω σε ένα δεκανίκι για να λειτουργήσει η συμπίεση οπουδήποτε.

Ή ένα άλλο παράδειγμα, κάποτε ανακαλύψαμε ένα σφάλμα - στάλθηκε InputPeerUser αντί για InputUser. Ή αντιστρόφως. Αλλά λειτούργησε! Δηλαδή, ο διακομιστής δεν ενδιαφερόταν για τον τύπο. Πώς μπορεί αυτό να είναι? Η απάντηση μπορεί να μας δοθεί από κομμάτια κώδικα από το telegram-cli:

  if (tgl_get_peer_type (E->id) != TGL_PEER_CHANNEL || (C && (C->flags & TGLCHF_MEGAGROUP))) {
    out_int (CODE_messages_get_history);
    out_peer_id (TLS, E->id);
  } else {    
    out_int (CODE_channels_get_important_history);

    out_int (CODE_input_channel);
    out_int (tgl_get_peer_id (E->id));
    out_long (E->id.access_hash);
  }
  out_int (E->max_id);
  out_int (E->offset);
  out_int (E->limit);
  out_int (0);
  out_int (0);

Με άλλα λόγια, εδώ γίνεται η σειριοποίηση ΧΕΙΡΟΚΙΝΗΤΑ, δεν δημιουργήθηκε κώδικας! Ίσως ο διακομιστής υλοποιείται με παρόμοιο τρόπο;.. Κατ' αρχήν, αυτό θα λειτουργήσει αν γίνει μία φορά, αλλά πώς μπορεί να υποστηριχθεί αργότερα κατά τη διάρκεια των ενημερώσεων; Γι' αυτό εφευρέθηκε το σχέδιο; Και εδώ περνάμε στην επόμενη ερώτηση.

Εκδόσεις. Επίπεδα

Το γιατί οι σχηματικές εκδόσεις ονομάζονται στρώματα μπορεί να υποτεθεί μόνο με βάση το ιστορικό δημοσιευμένων σχηματικών. Προφανώς, στην αρχή οι συγγραφείς θεώρησαν ότι βασικά πράγματα μπορούσαν να γίνουν χρησιμοποιώντας το αμετάβλητο σχήμα και μόνο όπου είναι απαραίτητο, για συγκεκριμένα αιτήματα, υποδεικνύουν ότι γίνονταν χρησιμοποιώντας διαφορετική έκδοση. Κατ 'αρχήν, ακόμη και μια καλή ιδέα - και το νέο θα είναι, όπως ήταν, "αναμεμιγμένο", στρωμένο πάνω από το παλιό. Ας δούμε όμως πώς έγινε. Είναι αλήθεια ότι δεν μπόρεσα να το δω από την αρχή - είναι αστείο, αλλά το διάγραμμα του βασικού στρώματος απλά δεν υπάρχει. Τα επίπεδα ξεκίνησαν με 2. Η τεκμηρίωση μας λέει για μια ειδική δυνατότητα TL:

Εάν ένας πελάτης υποστηρίζει το επίπεδο 2, τότε πρέπει να χρησιμοποιηθεί ο ακόλουθος κατασκευαστής:

invokeWithLayer2#289dd1f6 {X:Type} query:!X = X;

Στην πράξη, αυτό σημαίνει ότι πριν από κάθε κλήση API, ένα int με την τιμή 0x289dd1f6 πρέπει να προστεθεί πριν από τον αριθμό της μεθόδου.

Ακούγεται φυσιολογικό. Τι έγινε όμως μετά; Μετά εμφανίστηκε

invokeWithLayer3#b7475268 query:!X = X;

Τι ακολουθεί λοιπόν; Όπως μπορείτε να μαντέψετε,

invokeWithLayer4#dea0d430 query:!X = X;

Αστείος? Όχι, είναι πολύ νωρίς για γέλια, σκεφτείτε το γεγονός αυτό κάθε ένα αίτημα από άλλο στρώμα πρέπει να τυλιχτεί σε έναν τόσο ειδικό τύπο - αν είναι όλα διαφορετικά για εσάς, πώς αλλιώς μπορείτε να τα διακρίνετε; Και η προσθήκη μόλις 4 byte μπροστά είναι μια αρκετά αποτελεσματική μέθοδος. Ετσι,

invokeWithLayer5#417a57ae query:!X = X;

Αλλά είναι προφανές ότι μετά από λίγο αυτό θα γίνει κάποιο είδος βακκαναλίας. Και ήρθε η λύση:

Ενημέρωση: Ξεκινώντας από το Layer 9, βοηθητικές μεθόδους invokeWithLayerN μπορεί να χρησιμοποιηθεί μόνο μαζί με initConnection

Ζήτω! Μετά από 9 εκδόσεις, καταλήξαμε τελικά σε αυτό που γινόταν στα πρωτόκολλα Διαδικτύου στη δεκαετία του '80 - συμφωνώντας για την έκδοση μία φορά στην αρχή της σύνδεσης!

Τι ακολουθεί λοιπόν;..

invokeWithLayer10#39620c41 query:!X = X;
...
invokeWithLayer18#1c900537 query:!X = X;

Αλλά τώρα μπορείτε ακόμα να γελάτε. Μόνο μετά από άλλα 9 επίπεδα, προστέθηκε τελικά ένας γενικός κατασκευαστής με αριθμό έκδοσης, ο οποίος πρέπει να κληθεί μόνο μία φορά στην αρχή της σύνδεσης και η έννοια των επιπέδων φαινόταν να έχει εξαφανιστεί, τώρα είναι απλώς μια υπό όρους έκδοση, όπως οπουδήποτε αλλού. Το πρόβλημα λύθηκε.

Ακριβώς?..

Vasily, [16.07.18 14:01] Ακόμα και την Παρασκευή σκέφτηκα:
Ο τηλεδιακομιστής στέλνει συμβάντα χωρίς αίτημα. Τα αιτήματα πρέπει να είναι τυλιγμένα στο InvokeWithLayer. Ο διακομιστής δεν αναδιπλώνει ενημερώσεις· δεν υπάρχει δομή για την αναδίπλωση απαντήσεων και ενημερώσεων.

Εκείνοι. ο πελάτης δεν μπορεί να καθορίσει το επίπεδο στο οποίο θέλει ενημερώσεις

Vadim Goncharov, [16.07.18 14:02] δεν είναι το InvokeWithLayer καταρχήν δεκανίκι;

Vasily, [16.07.18 14:02] Αυτός είναι ο μόνος τρόπος

Vadim Goncharov, [16.07.18 14:02] που ουσιαστικά θα πρέπει να σημαίνει συμφωνία για το επίπεδο στην αρχή της συνεδρίας

Παρεμπιπτόντως, προκύπτει ότι δεν παρέχεται υποβάθμιση πελάτη

Ενημερώσεις, δηλ. τύπος Updates στο σχήμα, αυτό στέλνει ο διακομιστής στον πελάτη όχι ως απόκριση σε ένα αίτημα API, αλλά ανεξάρτητα όταν συμβαίνει ένα συμβάν. Αυτό είναι ένα περίπλοκο θέμα που θα συζητηθεί σε άλλη ανάρτηση, αλλά προς το παρόν είναι σημαντικό να γνωρίζετε ότι ο διακομιστής αποθηκεύει Ενημερώσεις ακόμα και όταν ο πελάτης είναι εκτός σύνδεσης.

Έτσι, εάν αρνηθείτε να τυλίξετε του καθενός πακέτο για να υποδείξει την έκδοσή του, αυτό λογικά οδηγεί στα ακόλουθα πιθανά προβλήματα:

  • ο διακομιστής στέλνει ενημερώσεις στον πελάτη ακόμη και πριν ο πελάτης ενημερώσει ποια έκδοση υποστηρίζει
  • τι πρέπει να κάνω μετά την αναβάθμιση του πελάτη;
  • που εγγυήσειςότι η γνώμη του διακομιστή σχετικά με τον αριθμό επιπέδου δεν θα αλλάξει κατά τη διάρκεια της διαδικασίας;

Πιστεύετε ότι πρόκειται για καθαρά θεωρητική εικασία, και στην πράξη αυτό δεν μπορεί να συμβεί, επειδή ο διακομιστής είναι γραμμένος σωστά (τουλάχιστον, είναι καλά ελεγμένος); Χα! Όπως και να είναι!

Αυτό ακριβώς συναντήσαμε τον Αύγουστο. Στις 14 Αυγούστου, υπήρχαν μηνύματα ότι κάτι ενημερώνεται στους διακομιστές του Telegram... και μετά στα αρχεία καταγραφής:

2019-08-15 09:28:35.880640 MSK warn  main: ANON:87: unknown object type: 0x80d182d1 at TL/Object.pm line 213.
2019-08-15 09:28:35.751899 MSK warn  main: ANON:87: unknown object type: 0xb5223b0f at TL/Object.pm line 213.

και στη συνέχεια αρκετά megabyte ίχνη στοίβας (καλά, την ίδια στιγμή διορθώθηκε η καταγραφή). Εξάλλου, αν κάτι δεν αναγνωρίζεται στο TL σας, είναι δυαδικό με υπογραφή, πιο κάτω ΟΛΑ πάει, η αποκωδικοποίηση θα γίνει αδύνατη. Τι πρέπει να κάνετε σε μια τέτοια κατάσταση;

Λοιπόν, το πρώτο πράγμα που έρχεται στο μυαλό οποιουδήποτε είναι να αποσυνδεθεί και να προσπαθήσει ξανά. Δεν βοήθησε. Ψάχνουμε στο google το CRC32 - αποδείχθηκε ότι ήταν αντικείμενα από το σχήμα 73, αν και δουλέψαμε στο 82. Εξετάζουμε προσεκτικά τα αρχεία καταγραφής - υπάρχουν αναγνωριστικά από δύο διαφορετικά σχήματα!

Μήπως το πρόβλημα είναι καθαρά στον ανεπίσημο πελάτη μας; Όχι, ξεκινάμε το Telegram Desktop 1.2.17 (έκδοση που παρέχεται σε πολλές διανομές Linux), γράφει στο αρχείο καταγραφής εξαίρεσης: MTP Μη αναμενόμενο αναγνωριστικό τύπου #b5223b0f που διαβάζεται στο MTPMessageMedia…

Κριτική στο πρωτόκολλο και τις οργανωτικές προσεγγίσεις του Telegram. Μέρος 1, τεχνικό: εμπειρία συγγραφής πελάτη από την αρχή - TL, MT

Η Google έδειξε ότι ένα παρόμοιο πρόβλημα είχε ήδη συμβεί σε έναν από τους ανεπίσημους πελάτες, αλλά στη συνέχεια οι αριθμοί έκδοσης και, κατά συνέπεια, οι υποθέσεις ήταν διαφορετικές...

Οπότε τι θα έπρεπε να κάνουμε? Ο Vasily και εγώ χωρίσαμε: προσπάθησε να ενημερώσει το κύκλωμα στο 91, αποφάσισα να περιμένω μερικές μέρες και να δοκιμάσω το 73. Και οι δύο μέθοδοι λειτούργησαν, αλλά επειδή είναι εμπειρικές, δεν υπάρχει κατανόηση για το πόσες εκδόσεις πάνω ή κάτω χρειάζεστε να πηδήξετε ή πόσο καιρό πρέπει να περιμένετε .

Αργότερα μπόρεσα να αναπαράγω την κατάσταση: εκκινούμε τον πελάτη, τον απενεργοποιούμε, μεταγλωττίζουμε ξανά το κύκλωμα σε άλλο επίπεδο, κάνουμε επανεκκίνηση, αντιμετωπίζουμε ξανά το πρόβλημα, επιστρέφουμε στο προηγούμενο - ωχ, καμία αλλαγή κυκλώματος και ο πελάτης επανεκκινείται για λίγο λίγα λεπτά θα βοηθήσουν. Θα λάβετε ένα μείγμα δομών δεδομένων από διαφορετικά επίπεδα.

Εξήγηση? Όπως μπορείτε να μαντέψετε από διάφορα έμμεσα συμπτώματα, ο διακομιστής αποτελείται από πολλές διεργασίες διαφορετικών τύπων σε διαφορετικά μηχανήματα. Πιθανότατα, ο διακομιστής που είναι υπεύθυνος για το "buffering" έβαλε στην ουρά ό,τι του έδωσαν οι ανώτεροί του και το έδωσαν στο σχήμα που υπήρχε τη στιγμή της δημιουργίας. Και έως ότου αυτή η ουρά «σάπιε», δεν θα μπορούσε να γίνει τίποτα γι 'αυτό.

Ίσως... αλλά αυτό είναι ένα τρομερό δεκανίκι;!.. Όχι, πριν σκεφτούμε τρελές ιδέες, ας δούμε τον κώδικα των επίσημων πελατών. Στην έκδοση Android δεν βρίσκουμε κανένα πρόγραμμα ανάλυσης TL, αλλά βρίσκουμε ένα μεγάλο αρχείο (το GitHub αρνείται να το αγγίξει) με (απο)σειριοποίηση. Ακολουθούν τα αποσπάσματα κώδικα:

public static class TL_message_layer68 extends TL_message {
    public static int constructor = 0xc09be45f;
//...
//еще пачка подобных
//...
    public static class TL_message_layer47 extends TL_message {
        public static int constructor = 0xc992e15c;
        public static Message TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) {
            Message result = null;
            switch (constructor) {
                case 0x1d86f70e:
                    result = new TL_messageService_old2();
                    break;
                case 0xa7ab1991:
                    result = new TL_message_old3();
                    break;
                case 0xc3060325:
                    result = new TL_message_old4();
                    break;
                case 0x555555fa:
                    result = new TL_message_secret();
                    break;
                case 0x555555f9:
                    result = new TL_message_secret_layer72();
                    break;
                case 0x90dddc11:
                    result = new TL_message_layer72();
                    break;
                case 0xc09be45f:
                    result = new TL_message_layer68();
                    break;
                case 0xc992e15c:
                    result = new TL_message_layer47();
                    break;
                case 0x5ba66c13:
                    result = new TL_message_old7();
                    break;
                case 0xc06b9607:
                    result = new TL_messageService_layer48();
                    break;
                case 0x83e5de54:
                    result = new TL_messageEmpty();
                    break;
                case 0x2bebfa86:
                    result = new TL_message_old6();
                    break;
                case 0x44f9b43d:
                    result = new TL_message_layer104();
                    break;
                case 0x1c9b1027:
                    result = new TL_message_layer104_2();
                    break;
                case 0xa367e716:
                    result = new TL_messageForwarded_old2(); //custom
                    break;
                case 0x5f46804:
                    result = new TL_messageForwarded_old(); //custom
                    break;
                case 0x567699b3:
                    result = new TL_message_old2(); //custom
                    break;
                case 0x9f8d60bb:
                    result = new TL_messageService_old(); //custom
                    break;
                case 0x22eb6aba:
                    result = new TL_message_old(); //custom
                    break;
                case 0x555555F8:
                    result = new TL_message_secret_old(); //custom
                    break;
                case 0x9789dac4:
                    result = new TL_message_layer104_3();
                    break;

ή

    boolean fixCaption = !TextUtils.isEmpty(message) &&
    (media instanceof TLRPC.TL_messageMediaPhoto_old ||
     media instanceof TLRPC.TL_messageMediaPhoto_layer68 ||
     media instanceof TLRPC.TL_messageMediaPhoto_layer74 ||
     media instanceof TLRPC.TL_messageMediaDocument_old ||
     media instanceof TLRPC.TL_messageMediaDocument_layer68 ||
     media instanceof TLRPC.TL_messageMediaDocument_layer74)
    && message.startsWith("-1");

Χμ... φαίνεται άγριο. Αλλά, μάλλον, αυτός δημιουργείται κώδικας, τότε εντάξει;.. Αλλά σίγουρα υποστηρίζει όλες τις εκδόσεις! Είναι αλήθεια ότι δεν είναι ξεκάθαρο γιατί όλα αναμειγνύονται μεταξύ τους, μυστικές συνομιλίες και όλα τα είδη _old7 κάπως δεν μοιάζεις με τη γενιά μηχανών... Ωστόσο, περισσότερο από όλα με εξέπληξε

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Παιδιά, δεν μπορείτε καν να αποφασίσετε τι υπάρχει μέσα σε ένα στρώμα; Λοιπόν, εντάξει, ας πούμε ότι "δύο" κυκλοφόρησαν με ένα σφάλμα, καλά, συμβαίνει, αλλά ΤΡΕΙΣ;.. Αμέσως, πάλι η ίδια γκανιότα; Τι είδους πορνογραφία είναι αυτή, συγγνώμη;...

Στον πηγαίο κώδικα του Telegram Desktop, παρεμπιπτόντως, συμβαίνει κάτι παρόμοιο - αν ναι, πολλές δεσμεύσεις στη σειρά στο σχήμα δεν αλλάζουν τον αριθμό του επιπέδου του, αλλά διορθώνουν κάτι. Σε συνθήκες όπου δεν υπάρχει επίσημη πηγή δεδομένων για το σχήμα, από πού μπορούν να ληφθούν, εκτός από τον πηγαίο κώδικα του επίσημου πελάτη; Και αν το πάρετε από εκεί, δεν μπορείτε να είστε σίγουροι ότι το σχέδιο είναι απολύτως σωστό μέχρι να δοκιμάσετε όλες τις μεθόδους.

Πώς μπορεί να δοκιμαστεί αυτό; Ελπίζω οι λάτρεις των μονάδων, των λειτουργικών και άλλων δοκιμών να μοιραστούν στα σχόλια.

Εντάξει, ας δούμε ένα άλλο κομμάτι κώδικα:

public static class TL_folders_deleteFolder extends TLObject {
    public static int constructor = 0x1c295881;

    public int folder_id;

    public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) {
        return Updates.TLdeserialize(stream, constructor, exception);
    }

    public void serializeToStream(AbstractSerializedData stream) {
        stream.writeInt32(constructor);
        stream.writeInt32(folder_id);
    }
}

//manually created

//RichText start
public static abstract class RichText extends TLObject {
    public String url;
    public long webpage_id;
    public String email;
    public ArrayList<RichText> texts = new ArrayList<>();
    public RichText parentRichText;

    public static RichText TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) {
        RichText result = null;
        switch (constructor) {
            case 0x1ccb966a:
                result = new TL_textPhone();
                break;
            case 0xc7fb5e01:
                result = new TL_textSuperscript();
                break;

Αυτό το σχόλιο "δημιουργήθηκε με μη αυτόματο τρόπο" υποδηλώνει ότι μόνο ένα μέρος αυτού του αρχείου γράφτηκε χειροκίνητα (μπορείτε να φανταστείτε ολόκληρο τον εφιάλτη συντήρησης;) και το υπόλοιπο δημιουργήθηκε από μηχανή. Ωστόσο, τότε τίθεται ένα άλλο ερώτημα - ότι οι πηγές είναι διαθέσιμες όχι τελείως (α la GPL blobs στον πυρήνα του Linux), αλλά αυτό είναι ήδη ένα θέμα για το δεύτερο μέρος.

Αρκετά όμως. Ας περάσουμε στο πρωτόκολλο πάνω από το οποίο τρέχει όλη αυτή η σειριοποίηση.

ΜΤΠρώτο

Λοιπόν, ας ανοίξουμε γενική περιγραφή и λεπτομερής περιγραφή του πρωτοκόλλου και το πρώτο πράγμα που σκοντάφτουμε είναι η ορολογία. Και με την αφθονία των πάντων. Σε γενικές γραμμές, αυτό φαίνεται να είναι ένα αποκλειστικό χαρακτηριστικό του Telegram - καλώντας τα πράγματα διαφορετικά σε διαφορετικά μέρη ή διαφορετικά πράγματα με μια λέξη ή αντίστροφα (για παράδειγμα, σε ένα API υψηλού επιπέδου, αν δείτε ένα πακέτο αυτοκόλλητων, δεν είναι τι νόμισες).

Για παράδειγμα, το "μήνυμα" και η "συνεδρία" σημαίνουν κάτι διαφορετικό εδώ από ό,τι στη συνηθισμένη διεπαφή πελάτη του Telegram. Λοιπόν, όλα είναι ξεκάθαρα με το μήνυμα, θα μπορούσε να ερμηνευθεί με όρους OOP ή απλά να ονομαστεί η λέξη "πακέτο" - αυτό είναι ένα χαμηλό επίπεδο μεταφοράς, δεν υπάρχουν τα ίδια μηνύματα όπως στη διεπαφή, υπάρχουν πολλά μηνύματα υπηρεσίας . Αλλά η συνεδρία... αλλά πρώτα πράγματα.

στρώμα μεταφοράς

Το πρώτο πράγμα είναι η μεταφορά. Θα μας πουν για 5 επιλογές:

  • TCP
  • Ιστοσελίδα
  • Υποδοχή Ιστού μέσω HTTPS
  • HTTP
  • HTTPS

Vasily, [15.06.18 15:04] Υπάρχει επίσης μεταφορά UDP, αλλά δεν είναι τεκμηριωμένη

Και TCP σε τρεις παραλλαγές

Το πρώτο είναι παρόμοιο με το UDP μέσω TCP, κάθε πακέτο περιλαμβάνει έναν αριθμό ακολουθίας και ένα crc
Γιατί είναι τόσο επίπονη η ανάγνωση εγγράφων σε ένα καρότσι;

Λοιπόν, εκεί είναι τώρα TCP ήδη σε 4 παραλλαγές:

  • συντομευμένη
  • Ενδιάμεσος
  • Ενδιάμεσο με επένδυση
  • Πλήρης

Λοιπόν, εντάξει, Ενδιάμεσο με επένδυση για MTProxy, αυτό προστέθηκε αργότερα λόγω γνωστών γεγονότων. Γιατί όμως δύο ακόμη εκδόσεις (τρεις συνολικά) όταν μπορούσες να τα βγάλεις πέρα ​​με μία; Και τα τέσσερα διαφέρουν ουσιαστικά μόνο ως προς τον τρόπο ρύθμισης του μήκους και του ωφέλιμου φορτίου του κύριου MTProto, το οποίο θα συζητηθεί περαιτέρω:

  • στο Bridged είναι 1 ή 4 byte, αλλά όχι 0xef, μετά το σώμα
  • στο Intermediate αυτό είναι μήκους 4 byte και ένα πεδίο και η πρώτη φορά που πρέπει να στείλει ο πελάτης 0xeeeeeeee για να δηλώσετε ότι είναι Ενδιάμεσο
  • στο Full το πιο εθιστικό, από τη σκοπιά ενός δικτυωτή: μήκος, αριθμός σειράς και ΟΧΙ ΑΥΤΟ που είναι κυρίως MTProto, body, CRC32. Ναι, όλα αυτά είναι πάνω από το TCP. Αυτό μας παρέχει αξιόπιστη μεταφορά με τη μορφή διαδοχικής ροής byte· δεν χρειάζονται ακολουθίες, ειδικά αθροίσματα ελέγχου. Εντάξει, τώρα κάποιος θα μου αντιταχθεί ότι το TCP έχει άθροισμα ελέγχου 16-bit, οπότε συμβαίνει καταστροφή δεδομένων. Υπέροχο, αλλά στην πραγματικότητα έχουμε ένα πρωτόκολλο κρυπτογράφησης με κατακερματισμούς μεγαλύτερους από 16 byte, όλα αυτά τα σφάλματα - και ακόμη περισσότερα - θα εντοπιστούν από μια αναντιστοιχία SHA σε υψηλότερο επίπεδο. Δεν υπάρχει κανένα σημείο στο CRC32 πάνω από αυτό.

Ας συγκρίνουμε το Bridged, στο οποίο είναι δυνατό ένα byte μήκους, με το Intermediate, το οποίο δικαιολογεί το "Σε περίπτωση που απαιτείται ευθυγράμμιση δεδομένων 4 byte", το οποίο είναι αρκετά ανόητο. Τι, πιστεύεται ότι οι προγραμματιστές του Telegram είναι τόσο ανίκανοι που δεν μπορούν να διαβάσουν δεδομένα από μια υποδοχή σε ένα ευθυγραμμισμένο buffer; Πρέπει ακόμα να το κάνετε αυτό, γιατί η ανάγνωση μπορεί να σας επιστρέψει οποιοδήποτε αριθμό byte (και υπάρχουν επίσης διακομιστές μεσολάβησης, για παράδειγμα...). Ή, από την άλλη πλευρά, γιατί να αποκλείσουμε το Σύντομο εάν θα έχουμε ακόμα βαριά padding πάνω από 16 byte - εξοικονομήστε 3 byte μερικές φορές ?

Έχει κανείς την εντύπωση ότι στον Nikolai Durov αρέσει πολύ να ανακαλύπτει ξανά τροχούς, συμπεριλαμβανομένων των πρωτοκόλλων δικτύου, χωρίς καμία πραγματική πρακτική ανάγκη.

Άλλες επιλογές μεταφοράς, συμ. Web και MTProxy, δεν θα το εξετάσουμε τώρα, ίσως σε άλλη ανάρτηση, αν υπάρχει αίτημα. Σχετικά με αυτό το ίδιο MTProxy, ας θυμηθούμε μόνο τώρα ότι λίγο μετά την κυκλοφορία του το 2018, οι πάροχοι έμαθαν γρήγορα να το μπλοκάρουν, που προορίζεται για φραγή παράκαμψηςΜε μέγεθος πακέτου! Και επίσης το γεγονός ότι ο διακομιστής MTProxy γραμμένος (και πάλι από τον Waltman) σε C ήταν υπερβολικά συνδεδεμένος με τις λεπτομέρειες του Linux, αν και αυτό δεν απαιτούνταν καθόλου (θα επιβεβαιώσει ο Phil Kulin) και ότι ένας παρόμοιος διακομιστής είτε στο Go είτε στο Node.js θα χωράει σε λιγότερες από εκατό γραμμές.

Αλλά θα βγάλουμε συμπεράσματα για τον τεχνικό γραμματισμό αυτών των ανθρώπων στο τέλος της ενότητας, αφού εξετάσουμε άλλα θέματα. Προς το παρόν, ας περάσουμε στο επίπεδο 5 του OSI, session - στο οποίο τοποθέτησαν τη συνεδρία MTProto.

Κλειδιά, μηνύματα, συνεδρίες, Diffie-Hellman

Το τοποθέτησαν εκεί όχι εντελώς σωστά... Μια συνεδρία δεν είναι η ίδια περίοδος λειτουργίας που είναι ορατή στη διεπαφή στην ενότητα Ενεργές συνεδρίες. Αλλά με τη σειρά.

Κριτική στο πρωτόκολλο και τις οργανωτικές προσεγγίσεις του Telegram. Μέρος 1, τεχνικό: εμπειρία συγγραφής πελάτη από την αρχή - TL, MT

Λάβαμε λοιπόν μια συμβολοσειρά byte γνωστού μήκους από το επίπεδο μεταφοράς. Αυτό είναι είτε κρυπτογραφημένο μήνυμα είτε απλό κείμενο - εάν είμαστε ακόμα στο στάδιο της βασικής συμφωνίας και το κάνουμε στην πραγματικότητα. Για ποια από τις πολλές έννοιες που ονομάζονται «κλειδί» μιλάμε; Ας διευκρινίσουμε αυτό το θέμα για την ίδια την ομάδα του Telegram (Ζητώ συγγνώμη που μετέφρασα τη δική μου τεκμηρίωση από τα αγγλικά με κουρασμένο μυαλό στις 4 π.μ., ήταν πιο εύκολο να αφήσω κάποιες φράσεις ως έχουν):

Υπάρχουν δύο οντότητες που ονομάζονται Συνεδρίαση - ένα στη διεπαφή χρήστη των επίσημων πελατών στις «τρέχουσες συνεδρίες», όπου κάθε περίοδος σύνδεσης αντιστοιχεί σε μια ολόκληρη συσκευή / λειτουργικό σύστημα.
Το δεύτερο είναι Συνεδρία MTProto, το οποίο έχει τον αύξοντα αριθμό του μηνύματος (με έννοια χαμηλού επιπέδου) και το οποίο μπορεί να διαρκέσει μεταξύ διαφορετικών συνδέσεων TCP. Πολλές συνεδρίες MTProto μπορούν να εγκατασταθούν ταυτόχρονα, για παράδειγμα, για να επιταχυνθεί η λήψη αρχείων.

Ανάμεσα σε αυτά τα δύο συνεδρίες υπάρχει μια έννοια εξουσιοδότηση. Στην εκφυλισμένη περίπτωση μπορούμε να το πούμε αυτό Συνεδρία διεπαφής χρήστη είναι το ίδιο με εξουσιοδότηση, αλλά δυστυχώς, όλα είναι περίπλοκα. Ας κοιτάξουμε:

  • Ο χρήστης στη νέα συσκευή δημιουργεί πρώτα auth_key και το δεσμεύει σε λογαριασμό, για παράδειγμα μέσω SMS - γι' αυτό εξουσιοδότηση
  • Συνέβη μέσα στο πρώτο Συνεδρία MTProto, που έχει session_id μέσα σου.
  • Σε αυτό το βήμα, ο συνδυασμός εξουσιοδότηση и session_id θα μπορούσε να κληθεί παράδειγμα - αυτή η λέξη εμφανίζεται στην τεκμηρίωση και τον κωδικό ορισμένων πελατών
  • Στη συνέχεια, ο πελάτης μπορεί να ανοίξει μερικοί Συνεδρίες MTProto κάτω από το ίδιο auth_key - στο ίδιο DC.
  • Στη συνέχεια, μια μέρα ο πελάτης θα χρειαστεί να ζητήσει το αρχείο από άλλο DC - και για αυτό το DC θα δημιουργηθεί ένα νέο auth_key !
  • Να ενημερώσει το σύστημα ότι δεν εγγράφεται νέος χρήστης, αλλά ο ίδιος εξουσιοδότηση (Συνεδρία διεπαφής χρήστη), ο πελάτης χρησιμοποιεί κλήσεις API auth.exportAuthorization στο σπίτι DC auth.importAuthorization στο νέο DC.
  • Όλα είναι ίδια, μπορεί να είναι αρκετά ανοιχτά Συνεδρίες MTProto (το καθένα με το δικό του session_id) σε αυτό το νέο DC, κάτω από του auth_key.
  • Τέλος, ο πελάτης μπορεί να θέλει Perfect Forward Secrecy. Κάθε auth_key ήταν μόνιμος κλειδί - ανά DC - και ο πελάτης μπορεί να καλέσει auth.bindTempAuthKey για χρήση προσωρινή auth_key - και πάλι, μόνο ένα temp_auth_key ανά DC, κοινό για όλους Συνεδρίες MTProto σε αυτό το DC.

Σημειώστε ότι αλάτι (και μελλοντικά άλατα) είναι επίσης ένα για auth_key εκείνοι. μοιράζονται μεταξύ όλων Συνεδρίες MTProto στο ίδιο DC.

Τι σημαίνει "μεταξύ διαφορετικών συνδέσεων TCP"; Αυτό σημαίνει λοιπόν κάτι όπως cookie εξουσιοδότησης σε έναν ιστότοπο - διατηρεί (επιβιώνει) πολλές συνδέσεις TCP σε έναν δεδομένο διακομιστή, αλλά μια μέρα χαλάει. Μόνο σε αντίθεση με το HTTP, στο MTProto τα μηνύματα μέσα σε μια περίοδο λειτουργίας αριθμούνται και επιβεβαιώνονται διαδοχικά. Εάν εισέρχονταν στο τούνελ, η σύνδεση διακόπηκε - μετά τη δημιουργία μιας νέας σύνδεσης, ο διακομιστής θα στείλει ευγενικά ό,τι δεν παρέδωσε σε αυτήν τη συνεδρία στην προηγούμενη Σύνδεση TCP.

Ωστόσο, οι παραπάνω πληροφορίες συνοψίζονται μετά από πολύμηνη έρευνα. Στο μεταξύ, υλοποιούμε τον πελάτη μας από την αρχή; - ας επιστρέψουμε στην αρχή.

Ας δημιουργήσουμε λοιπόν auth_key επί Εκδόσεις Diffie-Hellman από το Telegram. Ας προσπαθήσουμε να κατανοήσουμε την τεκμηρίωση...

Vasily, [19.06.18 20:05] data_with_hash := SHA1(data) + data + (οποιαδήποτε τυχαία byte); έτσι ώστε το μήκος ίσο με 255 byte.
encrypted_data := RSA(data_with_hash, server_public_key); ένας αριθμός μήκους 255 byte (μεγάλο endian) αυξάνεται στην απαιτούμενη ισχύ πάνω από τον απαιτούμενο συντελεστή και το αποτέλεσμα αποθηκεύεται ως αριθμός 256 byte.

Έχουν λίγο ναρκωτικά DH

Δεν μοιάζει με DH ενός υγιούς ατόμου
Δεν υπάρχουν δύο δημόσια κλειδιά στο dx

Λοιπόν, στο τέλος αυτό διευθετήθηκε, αλλά έμεινε ένα υπόλειμμα - η απόδειξη της δουλειάς γίνεται από τον πελάτη ότι ήταν σε θέση να συνυπολογίσει τον αριθμό. Τύπος προστασίας από επιθέσεις DoS. Και το κλειδί RSA χρησιμοποιείται μόνο μία φορά προς μία κατεύθυνση, ουσιαστικά για κρυπτογράφηση new_nonce. Όμως, ενώ αυτή η φαινομενικά απλή επέμβαση θα πετύχει, τι θα έχετε να αντιμετωπίσετε;

Vasily, [20.06.18/00/26 XNUMX:XNUMX] Δεν έχω φτάσει ακόμα στο appid αίτημα

Έστειλα αυτό το αίτημα στο DH

Και, στην αποβάθρα μεταφοράς λέει ότι μπορεί να απαντήσει με 4 byte ενός κωδικού σφάλματος. Αυτό είναι όλο

Λοιπόν, μου είπε -404, και τι;

Έτσι του είπα: «Πιάσε τις μαλακίες σου κρυπτογραφημένες με κλειδί διακομιστή με δακτυλικό αποτύπωμα όπως αυτό, θέλω DH» και μου απάντησε με ένα ηλίθιο 404

Τι θα σκεφτόσασταν για αυτήν την απάντηση διακομιστή; Τι να κάνω? Δεν υπάρχει κανείς να ρωτήσει (αλλά περισσότερα για αυτό στο δεύτερο μέρος).

Εδώ όλο το ενδιαφέρον γίνεται στην αποβάθρα

Δεν έχω τίποτα άλλο να κάνω, απλά ονειρευόμουν να μετατρέπω αριθμούς μπρος-πίσω

Δύο αριθμοί 32 bit. Τα μάζεψα όπως όλους

Αλλά όχι, αυτά τα δύο πρέπει να προστεθούν πρώτα στη γραμμή ως BE

Vadim Goncharov, [20.06.18 15:49] και εξαιτίας αυτού 404;

Vasily, [20.06.18 15:49] ΝΑΙ!

Vadim Goncharov, [20.06.18 15:50] οπότε δεν καταλαβαίνω τι μπορεί να "δεν βρήκε"

Vasily, [20.06.18 15:50] περίπου

Δεν μπόρεσα να βρω μια τέτοια αποσύνθεση σε πρώτους παράγοντες%)

Δεν διαχειριστήκαμε καν την αναφορά σφαλμάτων

Vasily, [20.06.18 20:18] Α, υπάρχει και το MD5. Ήδη τρία διαφορετικά hashes

Το δακτυλικό αποτύπωμα κλειδιού υπολογίζεται ως εξής:

digest = md5(key + iv)
fingerprint = substr(digest, 0, 4) XOR substr(digest, 4, 4)

SHA1 και sha2

Ας το βάλουμε λοιπόν auth_key λάβαμε 2048 bit σε μέγεθος χρησιμοποιώντας το Diffie-Hellman. Τι έπεται? Στη συνέχεια, ανακαλύπτουμε ότι τα χαμηλότερα 1024 bit αυτού του κλειδιού δεν χρησιμοποιούνται με κανέναν τρόπο... αλλά ας το σκεφτούμε προς το παρόν. Σε αυτό το βήμα, έχουμε ένα κοινό μυστικό με τον διακομιστή. Έχει καθιερωθεί ένα ανάλογο της συνεδρίας TLS, η οποία είναι μια πολύ δαπανηρή διαδικασία. Αλλά ο διακομιστής εξακολουθεί να μην ξέρει τίποτα για το ποιοι είμαστε! Όχι ακόμα, στην πραγματικότητα. εξουσιοδότηση. Εκείνοι. αν σκεφτήκατε με όρους "login-password", όπως κάνατε κάποτε στο ICQ, ή τουλάχιστον "login-key", όπως στο SSH (για παράδειγμα, σε κάποιο gitlab/github). Λάβαμε ένα ανώνυμο. Τι γίνεται αν ο διακομιστής μας πει "αυτοί οι αριθμοί τηλεφώνου εξυπηρετούνται από άλλο DC"; Ή ακόμα και "ο αριθμός τηλεφώνου σας είναι απαγορευμένος"; Το καλύτερο που μπορούμε να κάνουμε είναι να κρατήσουμε το κλειδί με την ελπίδα ότι θα είναι χρήσιμο και δεν θα χαλάσει μέχρι τότε.

Παρεμπιπτόντως, το «παραλάβαμε» με επιφύλαξη. Για παράδειγμα, εμπιστευόμαστε τον διακομιστή; Κι αν είναι ψεύτικο; Θα χρειαστούν κρυπτογραφικοί έλεγχοι:

Vasily, [21.06.18 17:53] Προσφέρουν στους πελάτες κινητής τηλεφωνίας να ελέγξουν έναν αριθμό 2 kbit για πρωταρχικό χαρακτήρα%)

Αλλά δεν είναι καθόλου ξεκάθαρο, nafeijoa

Vasily, [21.06.18 18:02] Το έγγραφο δεν λέει τι να κάνετε αν αποδειχθεί ότι δεν είναι απλό

Δεν ειπώθηκε. Ας δούμε τι κάνει ο επίσημος πελάτης Android σε αυτήν την περίπτωση; ΕΝΑ αυτό είναι ό, τι (και ναι, όλο το αρχείο είναι ενδιαφέρον) - όπως λένε, θα το αφήσω εδώ:

278     static const char *goodPrime = "c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c3720fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f642477fe96bb2a941d5bcd1d4ac8cc49880708fa9b378e3c4f3a9060bee67cf9a4a4a695811051907e162753b56b0f6b410dba74d8a84b2a14b3144e0ef1284754fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9ca3928fef5b9ae4e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851f0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b";
279   if (!strcasecmp(prime, goodPrime)) {

Όχι, φυσικά είναι ακόμα εκεί μερικοί Υπάρχουν τεστ για την πρωταρχικότητα ενός αριθμού, αλλά προσωπικά δεν έχω πλέον επαρκείς γνώσεις μαθηματικών.

Εντάξει, έχουμε το κύριο κλειδί. Για να συνδεθείτε, δηλ. αποστολή αιτημάτων, πρέπει να εκτελέσετε περαιτέρω κρυπτογράφηση, χρησιμοποιώντας AES.

Το κλειδί μηνύματος ορίζεται ως τα 128 μεσαία bit του SHA256 του σώματος του μηνύματος (συμπεριλαμβανομένης της περιόδου λειτουργίας, του αναγνωριστικού μηνύματος, κ.λπ.), συμπεριλαμβανομένων των byte συμπλήρωσης, που προβάλλονται από 32 byte που λαμβάνονται από το κλειδί εξουσιοδότησης.

Vasily, [22.06.18 14:08] Μέσος όρος, σκύλα, bits

Ελήφθη auth_key. Ολα. Πέρα από αυτά... δεν είναι ξεκάθαρο από το έγγραφο. Μη διστάσετε να μελετήσετε τον ανοιχτό κώδικα.

Σημειώστε ότι το MTProto 2.0 απαιτεί από 12 έως 1024 byte συμπλήρωσης, υπό την προϋπόθεση ότι το μήκος του μηνύματος που προκύπτει θα διαιρείται με 16 byte.

Λοιπόν πόση γέμιση πρέπει να προσθέσετε;

Και ναι, υπάρχει και 404 σε περίπτωση λάθους

Αν κάποιος μελετούσε προσεκτικά το διάγραμμα και το κείμενο της τεκμηρίωσης, παρατήρησε ότι δεν υπάρχει MAC εκεί. Και ότι το AES χρησιμοποιείται σε μια συγκεκριμένη λειτουργία IGE που δεν χρησιμοποιείται πουθενά αλλού. Φυσικά, γράφουν για αυτό στις συχνές ερωτήσεις τους... Εδώ, όπως, το ίδιο το κλειδί μηνύματος είναι επίσης ο κατακερματισμός SHA των αποκρυπτογραφημένων δεδομένων, που χρησιμοποιείται για τον έλεγχο της ακεραιότητας - και σε περίπτωση αναντιστοιχίας, της τεκμηρίωσης για κάποιο λόγο συνιστά να τα αγνοούμε σιωπηλά (αλλά τι γίνεται με την ασφάλεια, τι γίνεται αν μας σπάσουν;).

Δεν είμαι κρυπτογράφος, ίσως δεν υπάρχει τίποτα κακό με αυτό το mode σε αυτή την περίπτωση από θεωρητική άποψη. Μπορώ όμως ξεκάθαρα να αναφέρω ένα πρακτικό πρόβλημα, χρησιμοποιώντας το Telegram Desktop ως παράδειγμα. Κρυπτογραφεί την τοπική προσωρινή μνήμη (όλα αυτά τα D877F783D5D3EF8C) με τον ίδιο τρόπο όπως τα μηνύματα στο MTProto (μόνο σε αυτήν την περίπτωση έκδοση 1.0), π.χ. πρώτα το κλειδί μηνύματος, μετά τα ίδια τα δεδομένα (και κάπου στην άκρη το κύριο μεγάλο auth_key 256 byte, χωρίς τα οποία msg_key άχρηστος). Έτσι, το πρόβλημα γίνεται αντιληπτό σε μεγάλα αρχεία. Δηλαδή, πρέπει να διατηρήσετε δύο αντίγραφα των δεδομένων - κρυπτογραφημένα και αποκρυπτογραφημένα. Και αν υπάρχουν megabyte, ή ροή βίντεο, για παράδειγμα;.. Κλασικά σχήματα με MAC μετά το κρυπτογραφημένο κείμενο σας επιτρέπουν να το διαβάσετε σε ροή, μεταδίδοντάς το αμέσως. Αλλά με το MTProto θα πρέπει αρχικά κρυπτογραφήστε ή αποκρυπτογραφήστε ολόκληρο το μήνυμα, μόνο στη συνέχεια μεταφέρετέ το στο δίκτυο ή στο δίσκο. Επομένως, στις πιο πρόσφατες εκδόσεις του Telegram Desktop στην προσωρινή μνήμη user_data Χρησιμοποιείται επίσης μια άλλη μορφή - με AES σε λειτουργία CTR.

Vasily, [21.06.18 01:27] Ω, ανακάλυψα τι είναι το IGE: Το IGE ήταν η πρώτη προσπάθεια σε μια «λειτουργία κρυπτογράφησης ελέγχου ταυτότητας», αρχικά για το Kerberos. Ήταν μια αποτυχημένη προσπάθεια (δεν παρέχει προστασία ακεραιότητας) και έπρεπε να αφαιρεθεί. Αυτή ήταν η αρχή μιας 20ετούς αναζήτησης για μια λειτουργία κρυπτογράφησης ελέγχου ταυτότητας που λειτουργεί, η οποία κορυφώθηκε πρόσφατα σε λειτουργίες όπως το OCB και το GCM.

Και τώρα τα επιχειρήματα από την πλευρά του καλαθιού:

Η ομάδα πίσω από το Telegram, με επικεφαλής τον Nikolai Durov, αποτελείται από έξι πρωταθλητές ACM, οι μισοί από τους οποίους είναι διδακτορικοί στα μαθηματικά. Τους πήρε περίπου δύο χρόνια για να παρουσιάσουν την τρέχουσα έκδοση του MTProto.

Αυτό είναι αστείο. Δύο χρόνια στο κατώτερο επίπεδο

Ή θα μπορούσατε απλώς να πάρετε tls

Εντάξει, ας πούμε ότι έχουμε κάνει την κρυπτογράφηση και άλλες αποχρώσεις. Είναι τελικά δυνατό να αποστέλλονται αιτήματα σειριακά σε TL και να αποσυντονίζονται οι απαντήσεις; Λοιπόν, τι και πώς πρέπει να στείλετε; Εδώ, ας πούμε, η μέθοδος initConnection, ίσως είναι αυτό;

Vasily, [25.06.18 18:46] Αρχικοποιεί τη σύνδεση και αποθηκεύει πληροφορίες στη συσκευή και την εφαρμογή του χρήστη.

Δέχεται app_id, device_model, system_version, app_version και lang_code.

Και κάποια απορία

Τεκμηρίωση όπως πάντα. Μη διστάσετε να μελετήσετε τον ανοιχτό κώδικα

Αν όλα ήταν περίπου ξεκάθαρα με το invokeWithLayer, τότε τι φταίει εδώ; Αποδεικνύεται, ας πούμε ότι έχουμε - ο πελάτης είχε ήδη κάτι να ρωτήσει τον διακομιστή - υπάρχει ένα αίτημα που θέλαμε να στείλουμε:

Vasily, [25.06.18 19:13] Κρίνοντας από τον κωδικό, η πρώτη κλήση είναι τυλιγμένη σε αυτό το χάλι, και το ίδιο το χάλι είναι τυλιγμένο σε invokewithlayer

Γιατί το initConnection δεν θα μπορούσε να είναι ξεχωριστή κλήση, αλλά πρέπει να είναι περιτύλιγμα; Ναι, όπως αποδείχθηκε, πρέπει να γίνεται κάθε φορά στην αρχή κάθε συνεδρίας, και όχι μία φορά, όπως με το κύριο κλειδί. Αλλά! Δεν μπορεί να κληθεί από μη εξουσιοδοτημένο χρήστη! Τώρα έχουμε φτάσει στο στάδιο που εφαρμόζεται Αυτό σελίδα τεκμηρίωσης - και μας λέει ότι...

Μόνο ένα μικρό μέρος των μεθόδων API είναι διαθέσιμο σε μη εξουσιοδοτημένους χρήστες:

  • auth.sendCode
  • auth.resendCode
  • account.getPassword
  • auth.checkPassword
  • auth.checkPhone
  • auth.signUp
  • auth.signIn
  • auth.importΕξουσιοδότηση
  • help.getConfig
  • help.getNearestDc
  • help.getAppUpdate
  • help.getCdnConfig
  • langpack.getLangPack
  • langpack.getStrings
  • langpack.getDifference
  • langpack.getLanguages
  • langpack.getLanguage

Το πρώτο από αυτά, auth.sendCode, και υπάρχει αυτό το αγαπημένο πρώτο αίτημα στο οποίο στέλνουμε τα api_id και api_hash και μετά λαμβάνουμε ένα SMS με έναν κωδικό. Και αν βρισκόμαστε σε λάθος DC (οι τηλεφωνικοί αριθμοί σε αυτήν τη χώρα εξυπηρετούνται από άλλη, για παράδειγμα), τότε θα λάβουμε ένα σφάλμα με τον αριθμό του επιθυμητού DC. Για να μάθετε σε ποια διεύθυνση IP ανά αριθμό DC πρέπει να συνδεθείτε, βοηθήστε μας help.getConfig. Κάποτε υπήρχαν μόνο 5 συμμετοχές, αλλά μετά τα περίφημα γεγονότα του 2018, ο αριθμός έχει αυξηθεί σημαντικά.

Τώρα ας θυμηθούμε ότι φτάσαμε σε αυτό το στάδιο στον διακομιστή ανώνυμα. Δεν είναι πολύ ακριβό να αποκτήσετε απλώς μια διεύθυνση IP; Γιατί να μην το κάνετε αυτό και άλλες λειτουργίες στο μη κρυπτογραφημένο τμήμα του MTProto; Ακούω την ένσταση: "πώς μπορούμε να βεβαιωθούμε ότι δεν είναι η RKN που θα απαντήσει με ψευδείς διευθύνσεις;" Σε αυτό θυμόμαστε ότι, σε γενικές γραμμές, οι επίσημοι πελάτες Τα κλειδιά RSA είναι ενσωματωμένα, δηλ. μπορείς απλά σημάδι αυτή η πληροφορία. Στην πραγματικότητα, αυτό γίνεται ήδη για πληροφορίες σχετικά με την παράκαμψη αποκλεισμού που λαμβάνουν οι πελάτες μέσω άλλων καναλιών (λογικά, αυτό δεν μπορεί να γίνει στο ίδιο το MTProto, πρέπει επίσης να γνωρίζετε πού να συνδεθείτε).

ΕΝΤΑΞΕΙ. Σε αυτό το στάδιο της εξουσιοδότησης πελάτη, δεν είμαστε ακόμη εξουσιοδοτημένοι και δεν έχουμε καταχωρίσει την αίτησή μας. Θέλουμε απλώς να δούμε προς το παρόν τι ανταποκρίνεται ο διακομιστής στις μεθόδους που είναι διαθέσιμες σε έναν μη εξουσιοδοτημένο χρήστη. Και εδώ…

Vasily, [10.07.18 14:45] https://core.telegram.org/method/help.getConfig

config#7dae33e0 [...] = Config;
help.getConfig#c4f9186b = Config;

https://core.telegram.org/api/datacenter

config#232d5905 [...] = Config;
help.getConfig#c4f9186b = Config;

Στο σχήμα, το πρώτο έρχεται δεύτερο

Στο σχήμα tdesktop η τρίτη τιμή είναι

Ναι, από τότε, φυσικά, η τεκμηρίωση έχει ενημερωθεί. Αν και σύντομα μπορεί να γίνει ξανά άσχετο. Πώς πρέπει να γνωρίζει ένας αρχάριος προγραμματιστής; Μήπως αν καταχωρήσεις την αίτησή σου να σε ενημερώσουν; Ο Βασίλι το έκανε αυτό, αλλά δυστυχώς, δεν του έστειλαν τίποτα (και πάλι, θα μιλήσουμε για αυτό στο δεύτερο μέρος).

...Παρατηρήσατε ότι έχουμε ήδη μεταφερθεί με κάποιο τρόπο στο API, δηλ. στο επόμενο επίπεδο, και χάσατε κάτι στο θέμα MTProto; Χωρίς έκπληξη:

Vasily, [28.06.18 02:04] Μμ, ψαχουλεύουν μερικούς από τους αλγόριθμους στο e2e

Το Mtproto ορίζει αλγόριθμους και κλειδιά κρυπτογράφησης και για τους δύο τομείς, καθώς και μια δομή περιτυλίγματος

Αλλά αναμιγνύουν συνεχώς διαφορετικά επίπεδα της στοίβας, επομένως δεν είναι πάντα σαφές πού τελείωσε το mtproto και πού ξεκίνησε το επόμενο επίπεδο

Πώς ανακατεύονται; Λοιπόν, εδώ είναι το ίδιο προσωρινό κλειδί για το PFS, για παράδειγμα (παρεμπιπτόντως, το Telegram Desktop δεν μπορεί να το κάνει). Εκτελείται από ένα αίτημα API auth.bindTempAuthKey, δηλ. από το ανώτερο επίπεδο. Αλλά ταυτόχρονα παρεμβαίνει στην κρυπτογράφηση στο χαμηλότερο επίπεδο - μετά από αυτό, για παράδειγμα, πρέπει να το κάνετε ξανά initConnection κ.λπ., αυτό δεν είναι просто κανονικό αίτημα. Αυτό που είναι επίσης ιδιαίτερο είναι ότι μπορείτε να έχετε μόνο ΕΝΑ προσωρινό κλειδί ανά DC, αν και το πεδίο auth_key_id σε κάθε μήνυμα σάς επιτρέπει να αλλάζετε το κλειδί τουλάχιστον κάθε μήνυμα και ότι ο διακομιστής έχει το δικαίωμα να "ξεχάσει" το προσωρινό κλειδί ανά πάσα στιγμή - η τεκμηρίωση δεν λέει τι πρέπει να κάνετε σε αυτήν την περίπτωση... καλά, γιατί θα μπορούσε Δεν έχετε πολλά κλειδιά, όπως με ένα σετ μελλοντικών αλάτων, και...

Υπάρχουν μερικά άλλα πράγματα που αξίζει να σημειωθούν σχετικά με το θέμα MTProto.

Μηνύματα μηνυμάτων, msg_id, msg_seqno, επιβεβαιώσεις, ping προς τη λάθος κατεύθυνση και άλλες ιδιοσυγκρασίες

Γιατί πρέπει να γνωρίζετε για αυτά; Επειδή «διαρρέουν» σε υψηλότερο επίπεδο και πρέπει να τα γνωρίζετε όταν εργάζεστε με το API. Ας υποθέσουμε ότι δεν μας ενδιαφέρει το msg_key· το κατώτερο επίπεδο έχει αποκρυπτογραφήσει τα πάντα για εμάς. Αλλά μέσα στα αποκρυπτογραφημένα δεδομένα έχουμε τα ακόλουθα πεδία (επίσης το μήκος των δεδομένων, ώστε να ξέρουμε πού βρίσκεται το padding, αλλά αυτό δεν είναι σημαντικό):

  • αλάτι - int64
  • session_id - int64
  • message_id — int64
  • seq_no - int32

Να σας υπενθυμίσουμε ότι υπάρχει μόνο ένα αλάτι για ολόκληρο το DC. Γιατί να ξέρεις για αυτήν; Όχι μόνο επειδή υπάρχει αίτημα get_future_salts, που σας λέει ποια διαστήματα θα ισχύουν, αλλά και γιατί αν το αλάτι σας είναι «σάπιο», τότε απλά θα χαθεί το μήνυμα (αίτημα). Ο διακομιστής, φυσικά, θα αναφέρει το νέο αλάτι με έκδοση new_session_created - αλλά με το παλιό θα πρέπει να το ξαναστείλεις κάπως π.χ. Και αυτό το ζήτημα επηρεάζει την αρχιτεκτονική της εφαρμογής.

Ο διακομιστής επιτρέπεται να απορρίψει εντελώς τις συνεδρίες και να απαντήσει με αυτόν τον τρόπο για πολλούς λόγους. Στην πραγματικότητα, τι είναι μια συνεδρία MTProto από την πλευρά του πελάτη; Αυτοί είναι δύο αριθμοί session_id и seq_no μηνύματα εντός αυτής της συνεδρίας. Λοιπόν, και η υποκείμενη σύνδεση TCP, φυσικά. Ας πούμε ότι ο πελάτης μας εξακολουθεί να μην ξέρει πώς να κάνει πολλά πράγματα, αποσυνδέθηκε, επανασυνδέθηκε. Εάν αυτό συνέβη γρήγορα - η παλιά συνεδρία συνεχίστηκε στη νέα σύνδεση TCP, αυξήστε seq_no περαιτέρω. Αν πάρει πολύ χρόνο, ο διακομιστής θα μπορούσε να το διαγράψει, γιατί από την πλευρά του είναι και μια ουρά, όπως διαπιστώσαμε.

Τι πρέπει να είναι seq_no? Ω, αυτή είναι μια δύσκολη ερώτηση. Προσπαθήστε να καταλάβετε ειλικρινά τι εννοούσε:

Μήνυμα σχετικό με το περιεχόμενο

Ένα μήνυμα που απαιτεί ρητή επιβεβαίωση. Αυτά περιλαμβάνουν όλα τα μηνύματα χρήστη και πολλά μηνύματα υπηρεσίας, σχεδόν όλα με εξαίρεση τα κοντέινερ και τις επιβεβαιώσεις.

Αριθμός ακολουθίας μηνυμάτων (msg_seqno)

Ένας αριθμός 32 bit ίσος με το διπλάσιο του αριθμού των μηνυμάτων «σχετιζόμενων με το περιεχόμενο» (αυτών που απαιτούν επιβεβαίωση, και ειδικότερα εκείνων που δεν είναι κοντέινερ) που δημιουργήθηκαν από τον αποστολέα πριν από αυτό το μήνυμα και στη συνέχεια αυξήθηκαν κατά ένα εάν το τρέχον μήνυμα είναι μήνυμα σχετικό με το περιεχόμενο. Ένα δοχείο δημιουργείται πάντα μετά από ολόκληρο το περιεχόμενό του. Επομένως, ο αύξων αριθμός του είναι μεγαλύτερος ή ίσος με τους αριθμούς σειράς των μηνυμάτων που περιέχονται σε αυτόν.

Τι είδους τσίρκο είναι αυτό με αύξηση κατά 1 και μετά άλλο κατά 2;.. Υποψιάζομαι ότι αρχικά εννοούσαν "το λιγότερο σημαντικό κομμάτι για το ACK, το υπόλοιπο είναι ένας αριθμός", αλλά το αποτέλεσμα δεν είναι το ίδιο - συγκεκριμένα, βγαίνει, μπορεί να σταλεί μερικοί επιβεβαιώσεις έχουν το ίδιο seq_no! Πως? Λοιπόν, για παράδειγμα, ο διακομιστής μας στέλνει κάτι, το στέλνει και εμείς οι ίδιοι παραμένουμε σιωπηλοί, απαντώντας μόνο με μηνύματα υπηρεσίας που επιβεβαιώνουν τη λήψη των μηνυμάτων του. Σε αυτήν την περίπτωση, οι εξερχόμενες επιβεβαιώσεις μας θα έχουν τον ίδιο εξερχόμενο αριθμό. Εάν είστε εξοικειωμένοι με το TCP και πιστεύετε ότι αυτό ακούγεται κάπως άγριο, αλλά δεν φαίνεται πολύ άγριο, επειδή στο TCP seq_no δεν αλλάζει, αλλά η επιβεβαίωση πηγαίνει στο seq_no από την άλλη θα σπεύσω να σε στεναχωρήσω. Οι επιβεβαιώσεις παρέχονται στο MTProto ΔΕΝ επί seq_no, όπως στο TCP, αλλά από msg_id !

Τι είναι αυτό msg_id, το πιο σημαντικό από αυτά τα πεδία; Ένα μοναδικό αναγνωριστικό μηνύματος, όπως υποδηλώνει το όνομα. Ορίζεται ως ένας αριθμός 64-bit, τα χαμηλότερα bit του οποίου έχουν και πάλι τη μαγεία «διακομιστής-όχι-διακομιστής» και τα υπόλοιπα είναι μια χρονική σήμανση Unix, συμπεριλαμβανομένου του κλασματικού τμήματος, μετατοπισμένο κατά 32 bit προς τα αριστερά. Εκείνοι. timestamp per se (και μηνύματα με χρόνους που διαφέρουν πολύ θα απορρίπτονται από τον διακομιστή). Από αυτό προκύπτει ότι γενικά αυτό είναι ένα αναγνωριστικό που είναι καθολικό για τον πελάτη. Με δεδομένο αυτό - ας θυμηθούμε session_id - είμαστε εγγυημένοι: Σε καμία περίπτωση δεν μπορεί ένα μήνυμα που προορίζεται για μία περίοδο λειτουργίας να σταλεί σε διαφορετική περίοδο λειτουργίας. Δηλαδή, αποδεικνύεται ότι υπάρχει ήδη τρία επίπεδο - περίοδος λειτουργίας, αριθμός συνεδρίας, αναγνωριστικό μηνύματος. Γιατί τέτοια υπερεπιπλοκή, αυτό το μυστήριο είναι πολύ μεγάλο.

Ετσι, msg_id χρειάζεται για...

RPC: αιτήματα, απαντήσεις, σφάλματα. Επιβεβαιώσεις.

Όπως ίσως έχετε παρατηρήσει, δεν υπάρχει κανένας ειδικός τύπος ή λειτουργία "make an RPC request" πουθενά στο διάγραμμα, αν και υπάρχουν απαντήσεις. Άλλωστε, έχουμε μηνύματα που σχετίζονται με το περιεχόμενο! Αυτό είναι, όποιος το μήνυμα μπορεί να είναι αίτημα! Ή να μην είναι. Παρά όλα αυτά, του καθενός υπάρχει msg_id. Υπάρχουν όμως απαντήσεις:

rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult;

Εδώ υποδεικνύεται σε ποιο μήνυμα είναι απάντηση. Επομένως, στο ανώτατο επίπεδο του API, θα πρέπει να θυμάστε ποιος ήταν ο αριθμός του αιτήματός σας - νομίζω ότι δεν χρειάζεται να εξηγήσετε ότι η εργασία είναι ασύγχρονη και μπορεί να υπάρχουν πολλά αιτήματα σε εξέλιξη ταυτόχρονα, οι απαντήσεις στις οποίες μπορούν να επιστραφούν με οποιαδήποτε σειρά; Κατ 'αρχήν, από αυτό και τα μηνύματα σφάλματος όπως κανένας εργαζόμενος, μπορεί να εντοπιστεί η αρχιτεκτονική πίσω από αυτό: ο διακομιστής που διατηρεί μια σύνδεση TCP μαζί σας είναι ένας εξισορροπητής front-end, προωθεί αιτήματα στα backends και τα συλλέγει πίσω μέσω message_id. Φαίνεται ότι όλα εδώ είναι ξεκάθαρα, λογικά και καλά.

Ναι;.. Και αν το σκεφτείς; Άλλωστε, η ίδια η απόκριση RPC έχει επίσης ένα πεδίο msg_id! Πρέπει να φωνάξουμε στον διακομιστή "δεν απαντάς στην απάντησή μου!"; Και ναι, τι υπήρχε με τις επιβεβαιώσεις; Σχετικά με τη σελίδα μηνύματα σχετικά με μηνύματα μας λέει τι είναι

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

και πρέπει να γίνει από κάθε πλευρά. Αλλά όχι πάντα! Εάν λάβατε ένα RpcResult, το ίδιο χρησιμεύει ως επιβεβαίωση. Δηλαδή, ο διακομιστής μπορεί να απαντήσει στο αίτημά σας με το MsgsAck - όπως "το έλαβα". Το RpcResult μπορεί να ανταποκριθεί αμέσως. Θα μπορούσε να είναι και τα δύο.

Και ναι, πρέπει ακόμα να απαντήσετε στην απάντηση! Επιβεβαίωση. Διαφορετικά, ο διακομιστής θα το θεωρήσει μη παραδοτέο και θα σας το στείλει ξανά. Ακόμα και μετά την επανασύνδεση. Εδώ όμως τίθεται φυσικά το θέμα των τάιμ άουτ. Ας τα δούμε λίγο αργότερα.

Στο μεταξύ, ας δούμε πιθανά σφάλματα εκτέλεσης ερωτήματος.

rpc_error#2144ca19 error_code:int error_message:string = RpcError;

Ω, κάποιος θα αναφωνήσει, εδώ είναι μια πιο ανθρώπινη μορφή - υπάρχει μια γραμμή! Με την ησυχία σου. Εδώ λίστα σφαλμάτων, αλλά φυσικά όχι πλήρης. Από αυτό μαθαίνουμε ότι ο κώδικας είναι κάτι όπως Σφάλματα HTTP (καλά, φυσικά, η σημασιολογία των απαντήσεων δεν τηρείται, σε ορισμένα σημεία κατανέμονται τυχαία μεταξύ των κωδικών) και η γραμμή μοιάζει με CAPITAL_LETTERS_AND_NUMBERS. Για παράδειγμα, PHONE_NUMBER_OCCUPIED ή FILE_PART_Х_MISSING. Λοιπόν, δηλαδή, θα χρειαστείτε ακόμα αυτή τη γραμμή αναλύω λέξη. Για παράδειγμα FLOOD_WAIT_3600 θα σημαίνει ότι πρέπει να περιμένετε μια ώρα, και PHONE_MIGRATE_5, ότι ένας αριθμός τηλεφώνου με αυτό το πρόθεμα πρέπει να είναι καταχωρημένος στο 5ο Δ.Σ. Έχουμε μια γλώσσα τύπου, σωστά; Δεν χρειαζόμαστε όρισμα από μια συμβολοσειρά, τα κανονικά θα κάνουν, εντάξει.

Και πάλι, αυτό δεν βρίσκεται στη σελίδα μηνυμάτων υπηρεσίας, αλλά, όπως είναι ήδη συνηθισμένο με αυτό το έργο, οι πληροφορίες μπορούν να βρεθούν σε άλλη σελίδα τεκμηρίωσης. Or ρίχνει υποψίες. Πρώτον, κοιτάξτε, παραβίαση πληκτρολόγησης/στρώματος - RpcError μπορεί να τοποθετηθεί μέσα RpcResult. Γιατί όχι έξω; Τι δεν λάβαμε υπόψη μας;.. Κατά συνέπεια, πού είναι η εγγύηση ότι RpcError ΔΕΝ μπορεί να ενσωματωθεί RpcResult, αλλά να είναι άμεσα ή φωλιασμένο σε άλλο τύπο;.. Και αν δεν μπορεί, γιατί δεν είναι στο κορυφαίο επίπεδο, δηλ. λείπει req_msg_id ; ..

Αλλά ας συνεχίσουμε για τα μηνύματα υπηρεσίας. Ο πελάτης μπορεί να πιστεύει ότι ο διακομιστής σκέφτεται για μεγάλο χρονικό διάστημα και να κάνει αυτό το υπέροχο αίτημα:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Υπάρχουν τρεις πιθανές απαντήσεις σε αυτήν την ερώτηση, που διασταυρώνονται και πάλι με τον μηχανισμό επιβεβαίωσης· η προσπάθεια να καταλάβει τι πρέπει να είναι (και ποιος είναι ο γενικός κατάλογος των τύπων που δεν απαιτούν επιβεβαίωση) αφήνεται στον αναγνώστη ως εργασία για το σπίτι (σημείωση: οι πληροφορίες στο ο πηγαίος κώδικας του Telegram Desktop δεν είναι πλήρης).

Εθισμός στα ναρκωτικά: καταστάσεις μηνυμάτων

Γενικά, πολλά μέρη στο TL, το MTProto και γενικά το Telegram αφήνουν ένα αίσθημα πείσματος, αλλά από ευγένεια, διακριτικότητα και άλλα δεξιότητες Σιωπήσαμε ευγενικά γι' αυτό και λογοκρίναμε τις αισχρότητες στους διαλόγους. Ωστόσο, αυτό το μέροςОτο μεγαλύτερο μέρος της σελίδας είναι περίπου μηνύματα σχετικά με μηνύματα Είναι σοκαριστικό ακόμα και για μένα, που εργάζομαι με πρωτόκολλα δικτύου για μεγάλο χρονικό διάστημα και έχω δει ποδήλατα διαφορετικού βαθμού στραβής.

Ξεκινά ακίνδυνα, με επιβεβαιώσεις. Στη συνέχεια μας λένε για

bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification;
bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification;

Λοιπόν, όλοι όσοι ξεκινούν να εργάζονται με το MTProto θα πρέπει να τα αντιμετωπίσουν· στον κύκλο "διορθώθηκε - μεταγλώττιση - εκκίνηση", η λήψη αριθμητικών σφαλμάτων ή αλατιού που έχει καταφέρει να πάει άσχημα κατά τη διάρκεια των επεξεργασιών είναι κάτι συνηθισμένο. Ωστόσο, υπάρχουν δύο σημεία εδώ:

  1. Αυτό σημαίνει ότι το αρχικό μήνυμα έχει χαθεί. Πρέπει να δημιουργήσουμε κάποιες ουρές, θα το δούμε αργότερα.
  2. Ποιοι είναι αυτοί οι περίεργοι αριθμοί σφαλμάτων; 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... πού είναι οι άλλοι αριθμοί, Τόμι;

Η τεκμηρίωση αναφέρει:

Σκοπός είναι να ομαδοποιηθούν οι τιμές error_code (error_code >> 4): για παράδειγμα, οι κωδικοί 0x40 — 0x4f αντιστοιχούν σε σφάλματα στην αποσύνθεση του κοντέινερ.

αλλά, πρώτον, μια στροφή προς την άλλη κατεύθυνση, και δεύτερον, δεν έχει σημασία, πού είναι οι άλλοι κώδικες; Στο κεφάλι του συγγραφέα;.. Ωστόσο, αυτά είναι μικροπράγματα.

Ο εθισμός ξεκινά στα μηνύματα σχετικά με τις καταστάσεις μηνυμάτων και τα αντίγραφα μηνυμάτων:

  • Αίτημα για πληροφορίες κατάστασης μηνύματος
    Εάν κάποιο από τα μέρη δεν έχει λάβει πληροφορίες σχετικά με την κατάσταση των εξερχόμενων μηνυμάτων του για κάποιο χρονικό διάστημα, μπορεί να το ζητήσει ρητά από το άλλο μέρος:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Ενημερωτικό Μήνυμα σχετικά με την Κατάσταση των Μηνυμάτων
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Εδώ, info είναι μια συμβολοσειρά που περιέχει ακριβώς ένα byte κατάστασης μηνύματος για κάθε μήνυμα από τη λίστα εισερχόμενων msg_ids:

    • 1 = τίποτα δεν είναι γνωστό για το μήνυμα (το msg_id είναι πολύ χαμηλό, το άλλο μέρος μπορεί να το έχει ξεχάσει)
    • 2 = μήνυμα δεν ελήφθη (το msg_id εμπίπτει στο εύρος των αποθηκευμένων αναγνωριστικών, ωστόσο, το άλλο μέρος σίγουρα δεν έχει λάβει τέτοιο μήνυμα)
    • 3 = μήνυμα δεν ελήφθη (το msg_id είναι πολύ υψηλό, ωστόσο, το άλλο μέρος σίγουρα δεν το έχει λάβει ακόμη)
    • 4 = μήνυμα ελήφθη (σημειώστε ότι αυτή η απάντηση είναι ταυτόχρονα και επιβεβαίωση παραλαβής)
    • +8 = το μήνυμα έχει ήδη αναγνωριστεί
    • +16 = μήνυμα που δεν απαιτεί επιβεβαίωση
    • +32 = Ερώτημα RPC που περιέχεται στο μήνυμα που υποβάλλεται σε επεξεργασία ή η επεξεργασία έχει ήδη ολοκληρωθεί
    • +64 = απάντηση που σχετίζεται με το περιεχόμενο σε μήνυμα που έχει ήδη δημιουργηθεί
    • +128 = το άλλο μέρος γνωρίζει βεβαίως ότι το μήνυμα έχει ήδη ληφθεί
      Αυτή η απάντηση δεν απαιτεί επιβεβαίωση. Είναι μια αναγνώριση του σχετικού msgs_state_req, από μόνο του.
      Λάβετε υπόψη ότι εάν ξαφνικά αποδειχθεί ότι το άλλο μέρος δεν έχει μήνυμα που φαίνεται ότι του έχει σταλεί, το μήνυμα μπορεί απλώς να αποσταλεί ξανά. Ακόμα κι αν το άλλο μέρος λάβει δύο αντίγραφα του μηνύματος ταυτόχρονα, το αντίγραφο θα αγνοηθεί. (Εάν έχει περάσει πολύς χρόνος και το αρχικό msg_id δεν είναι πλέον έγκυρο, το μήνυμα πρέπει να τυλιχθεί σε msg_copy).
  • Εθελοντική Επικοινωνία Κατάστασης Μηνυμάτων
    Κάθε μέρος μπορεί να ενημερώσει οικειοθελώς το άλλο μέρος για την κατάσταση των μηνυμάτων που μεταδίδονται από το άλλο μέρος.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Εκτεταμένη Εθελοντική Επικοινωνία Κατάστασης Ένα Μήνυμα
    ...
    msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo;
    msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo;
  • Ρητό αίτημα για εκ νέου αποστολή μηνυμάτων
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    Το απομακρυσμένο μέρος απαντά αμέσως στέλνοντας εκ νέου τα ζητούμενα μηνύματα […]
  • Ρητό αίτημα για εκ νέου αποστολή απαντήσεων
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    Το απομακρυσμένο μέρος απαντά αμέσως με εκ νέου αποστολή απαντήσεις στα ζητούμενα μηνύματα […]
  • Αντίγραφα μηνυμάτων
    Σε ορισμένες περιπτώσεις, ένα παλιό μήνυμα με msg_id που δεν είναι πλέον έγκυρο πρέπει να αποσταλεί ξανά. Στη συνέχεια, τυλίγεται σε ένα δοχείο αντιγραφής:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Μόλις ληφθεί, το μήνυμα υποβάλλεται σε επεξεργασία σαν να μην υπήρχε το περιτύλιγμα. Ωστόσο, εάν είναι γνωστό με βεβαιότητα ότι ελήφθη το μήνυμα orig_message.msg_id, τότε το νέο μήνυμα δεν υποβάλλεται σε επεξεργασία (ενώ ταυτόχρονα, αυτό και το orig_message.msg_id επιβεβαιώνονται). Η τιμή του orig_message.msg_id πρέπει να είναι χαμηλότερη από το msg_id του κοντέινερ.

Ας μείνουμε σιωπηλοί για το τι msgs_state_info και πάλι τα αυτιά του ημιτελούς TL προεξέχουν (χρειαζόμασταν ένα διάνυσμα byte, και στα δύο κάτω bit υπήρχε ένα enum, και στα δύο υψηλότερα bit υπήρχαν σημαίες). Το θέμα είναι διαφορετικό. Καταλαβαίνει κανείς γιατί όλα αυτά είναι στην πράξη; σε πραγματικό πελάτη απαραίτητο;.. Με δυσκολία, αλλά μπορεί κανείς να φανταστεί κάποιο όφελος εάν ένα άτομο ασχολείται με τον εντοπισμό σφαλμάτων και σε μια διαδραστική λειτουργία - ρωτήστε τον διακομιστή τι και πώς. Εδώ όμως περιγράφονται τα αιτήματα ταξίδι μετ επιστροφής.

Επομένως, κάθε συμβαλλόμενο μέρος πρέπει όχι μόνο να κρυπτογραφεί και να στέλνει μηνύματα, αλλά και να αποθηκεύει δεδομένα για τον εαυτό του, για τις απαντήσεις σε αυτά, για άγνωστο χρονικό διάστημα. Η τεκμηρίωση δεν περιγράφει ούτε τους χρονισμούς ούτε την πρακτική εφαρμογή αυτών των χαρακτηριστικών. ουδόλως. Το πιο εκπληκτικό είναι ότι χρησιμοποιούνται πραγματικά στον κώδικα των επίσημων πελατών! Προφανώς τους είπαν κάτι που δεν περιλαμβανόταν στη δημόσια τεκμηρίωση. Κατανοήστε από τον κώδικα γιατί, δεν είναι πλέον τόσο απλό όσο στην περίπτωση του TL - δεν είναι ένα (σχετικά) λογικά απομονωμένο μέρος, αλλά ένα κομμάτι που συνδέεται με την αρχιτεκτονική της εφαρμογής, δηλ. θα απαιτήσει πολύ περισσότερο χρόνο για την κατανόηση του κώδικα εφαρμογής.

Pings και χρονισμοί. Ουρές.

Από όλα, αν θυμηθούμε τις εικασίες για την αρχιτεκτονική του διακομιστή (κατανομή αιτημάτων σε backends), ακολουθεί ένα αρκετά λυπηρό πράγμα - παρά όλες τις εγγυήσεις παράδοσης στο TCP (είτε παραδίδονται τα δεδομένα, είτε θα ενημερωθείτε για το κενό, αλλά τα δεδομένα θα παραδοθούν πριν παρουσιαστεί το πρόβλημα), ότι οι επιβεβαιώσεις στο ίδιο το MTProto - καμία εγγύηση. Ο διακομιστής μπορεί εύκολα να χάσει ή να πετάξει το μήνυμά σας και δεν μπορεί να γίνει τίποτα γι 'αυτό, απλώς χρησιμοποιήστε διαφορετικούς τύπους πατερίτσες.

Και πρώτα απ 'όλα - ουρές μηνυμάτων. Λοιπόν, με ένα πράγμα όλα ήταν προφανή από την αρχή - ένα μη επιβεβαιωμένο μήνυμα πρέπει να αποθηκευτεί και να σταλεί ξανά. Και μετά από τι ώρα; Και ο γελωτοποιός τον ξέρει. Ίσως αυτά τα εξαρτημένα μηνύματα υπηρεσίας λύνουν με κάποιο τρόπο αυτό το πρόβλημα με πατερίτσες, ας πούμε, στο Telegram Desktop υπάρχουν περίπου 4 ουρές που αντιστοιχούν σε αυτά (ίσως περισσότερες, όπως ήδη αναφέρθηκε, γι 'αυτό πρέπει να εμβαθύνετε στον κώδικα και την αρχιτεκτονική του πιο σοβαρά. χρόνο, ξέρουμε ότι δεν μπορεί να ληφθεί ως δείγμα· ένας συγκεκριμένος αριθμός τύπων από το σχήμα MTProto δεν χρησιμοποιούνται σε αυτό).

Γιατί συμβαίνει αυτό? Πιθανώς, οι προγραμματιστές διακομιστών δεν μπόρεσαν να διασφαλίσουν την αξιοπιστία εντός του συμπλέγματος, ή ακόμη και την αποθήκευση στο μπροστινό εξισορροπητή, και μετέφεραν αυτό το πρόβλημα στον πελάτη. Από απελπισία, ο Vasily προσπάθησε να εφαρμόσει μια εναλλακτική επιλογή, με μόνο δύο ουρές, χρησιμοποιώντας αλγόριθμους από το TCP - μετρώντας το RTT στον διακομιστή και προσαρμόζοντας το μέγεθος του "παραθύρου" (σε μηνύματα) ανάλογα με τον αριθμό των μη επιβεβαιωμένων αιτημάτων. Δηλαδή, μια τέτοια πρόχειρη ευρετική για την αξιολόγηση του φορτίου του διακομιστή είναι πόσα από τα αιτήματά μας μπορεί να μασήσει ταυτόχρονα και να μην χάσει.

Λοιπόν, δηλαδή, καταλαβαίνεις, σωστά; Εάν πρέπει να εφαρμόσετε ξανά το TCP πάνω από ένα πρωτόκολλο που τρέχει μέσω TCP, αυτό υποδηλώνει ένα πολύ κακώς σχεδιασμένο πρωτόκολλο.

Ω, ναι, γιατί χρειάζεστε περισσότερες από μία ουρές και τι σημαίνει αυτό για ένα άτομο που εργάζεται με ένα API υψηλού επιπέδου ούτως ή άλλως; Κοιτάξτε, κάνετε ένα αίτημα, το κάνετε σειριακό, αλλά συχνά δεν μπορείτε να το στείλετε αμέσως. Γιατί; Γιατί η απάντηση θα είναι msg_id, η οποία είναι προσωρινήаΕίμαι μια ετικέτα, η ανάθεση της οποίας καλύτερα να αναβληθεί για όσο το δυνατόν πιο αργά - σε περίπτωση που ο διακομιστής την απορρίψει λόγω αναντιστοιχίας χρόνου μεταξύ μας και του ίδιου (φυσικά, μπορούμε να κάνουμε ένα δεκανίκι που μετατοπίζει τον χρόνο μας από το παρόν στον διακομιστή προσθέτοντας ένα δέλτα που υπολογίζεται από τις απαντήσεις του διακομιστή - οι επίσημοι πελάτες το κάνουν αυτό, αλλά είναι ακατέργαστο και ανακριβές λόγω αποθήκευσης στην προσωρινή μνήμη). Επομένως, όταν κάνετε ένα αίτημα με μια κλήση τοπικής συνάρτησης από τη βιβλιοθήκη, το μήνυμα περνά από τα ακόλουθα στάδια:

  1. Βρίσκεται σε μία ουρά και περιμένει κρυπτογράφηση.
  2. Καθορισμένος msg_id και το μήνυμα πήγε σε άλλη ουρά - πιθανή προώθηση. στείλτε στην πρίζα.
  3. α) Ο διακομιστής απάντησε MsgsAck - το μήνυμα παραδόθηκε, το διαγράφουμε από την "άλλη ουρά".
    β) Ή το αντίστροφο, δεν του άρεσε κάτι, απάντησε badmsg - στείλτε ξανά από "άλλη ουρά"
    γ) Τίποτα δεν είναι γνωστό, το μήνυμα πρέπει να σταλεί εκ νέου από άλλη ουρά - αλλά δεν είναι γνωστό πότε ακριβώς.
  4. Ο διακομιστής τελικά ανταποκρίθηκε RpcResult - η πραγματική απόκριση (ή το σφάλμα) - όχι μόνο παραδόθηκε, αλλά και υποβλήθηκε σε επεξεργασία.

Ίσως, η χρήση δοχείων θα μπορούσε να λύσει εν μέρει το πρόβλημα. Αυτό συμβαίνει όταν μια δέσμη μηνυμάτων συσκευάζεται σε ένα και ο διακομιστής απάντησε με μια επιβεβαίωση σε όλα ταυτόχρονα, σε ένα msg_id. Αλλά θα απορρίψει και αυτό το πακέτο, αν κάτι πήγε στραβά, στο σύνολό του.

Και σε αυτό το σημείο μπαίνουν στο παιχνίδι μη τεχνικοί προβληματισμοί. Από την εμπειρία, έχουμε δει πολλά δεκανίκια, και επιπλέον, θα δούμε τώρα περισσότερα παραδείγματα κακών συμβουλών και αρχιτεκτονικής - σε τέτοιες συνθήκες, αξίζει να εμπιστευόμαστε και να λαμβάνουμε τέτοιες αποφάσεις; Το ερώτημα είναι ρητορικό (όχι φυσικά).

Για τι πράγμα μιλάμε? Αν στο θέμα των «μηνυμάτων για τα ναρκωτικά για τα μηνύματα» μπορείτε ακόμα να κάνετε εικασίες με αντιρρήσεις όπως «είσαι ηλίθιος, δεν κατάλαβες το λαμπρό σχέδιό μας!». (άρα γράψτε πρώτα την τεκμηρίωση, όπως θα έπρεπε οι κανονικοί άνθρωποι, με το σκεπτικό και τα παραδείγματα ανταλλαγής πακέτων, μετά θα μιλήσουμε), μετά οι χρονισμοί/τα χρονικά όρια είναι μια καθαρά πρακτική και συγκεκριμένη ερώτηση, όλα εδώ είναι γνωστά εδώ και πολύ καιρό. Τι μας λέει η τεκμηρίωση για τα χρονικά όρια;

Ένας διακομιστής συνήθως επιβεβαιώνει τη λήψη ενός μηνύματος από έναν πελάτη (συνήθως, ένα ερώτημα RPC) χρησιμοποιώντας μια απάντηση RPC. Εάν μια απόκριση έρχεται για πολύ καιρό, ένας διακομιστής μπορεί να στείλει πρώτα μια επιβεβαίωση απόδειξης και λίγο αργότερα, την ίδια την απόκριση RPC.

Ένας πελάτης κανονικά επιβεβαιώνει τη λήψη ενός μηνύματος από έναν διακομιστή (συνήθως, μια απάντηση RPC) προσθέτοντας μια επιβεβαίωση στο επόμενο ερώτημα RPC, εάν δεν μεταδοθεί πολύ αργά (αν έχει δημιουργηθεί, ας πούμε, 60-120 δευτερόλεπτα μετά την παραλαβή ενός μηνύματος από τον διακομιστή). Ωστόσο, εάν για μεγάλο χρονικό διάστημα δεν υπάρχει λόγος αποστολής μηνυμάτων στον διακομιστή ή εάν υπάρχει μεγάλος αριθμός μη επιβεβαιωμένων μηνυμάτων από τον διακομιστή (για παράδειγμα, πάνω από 16), ο πελάτης μεταδίδει μια αυτόνομη επιβεβαίωση.

... Μεταφράζω: εμείς οι ίδιοι δεν ξέρουμε πόσο και πώς το χρειαζόμαστε, οπότε ας υποθέσουμε ότι ας είναι έτσι.

Και για τα ping:

Μηνύματα Ping (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Μια απάντηση συνήθως επιστρέφεται στην ίδια σύνδεση:

pong#347773c5 msg_id:long ping_id:long = Pong;

Αυτά τα μηνύματα δεν απαιτούν επιβεβαιώσεις. Ένα pong μεταδίδεται μόνο ως απόκριση σε ένα ping ενώ ένα ping μπορεί να ξεκινήσει και από οποιαδήποτε πλευρά.

Αναβαλλόμενη σύνδεση Κλείσιμο + PING

ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong;

Λειτουργεί σαν ping. Επιπλέον, μετά τη λήψη αυτού, ο διακομιστής ξεκινά ένα χρονόμετρο που θα κλείσει την τρέχουσα σύνδεση disconnect_delay δευτερόλεπτα αργότερα, εκτός εάν λάβει ένα νέο μήνυμα του ίδιου τύπου που επαναφέρει αυτόματα όλους τους προηγούμενους χρονοδιακόπτες. Εάν ο πελάτης στέλνει αυτά τα ping μία φορά κάθε 60 δευτερόλεπτα, για παράδειγμα, μπορεί να ορίσει το disconnect_delay ίσο με 75 δευτερόλεπτα.

Είσαι τρελός?! Σε 60 δευτερόλεπτα, το τρένο θα μπει στο σταθμό, θα αποβιβαστεί και θα παραλάβει επιβάτες και θα χάσει ξανά την επαφή στο τούνελ. Σε 120 δευτερόλεπτα, ενώ το ακούτε, θα φτάσει σε ένα άλλο και πιθανότατα θα διακοπεί η σύνδεση. Λοιπόν, είναι ξεκάθαρο από πού προέρχονται τα πόδια - "Άκουσα ένα κουδούνισμα, αλλά δεν ξέρω πού είναι", υπάρχει ο αλγόριθμος του Nagl και η επιλογή TCP_NODELAY, που προορίζονται για διαδραστική εργασία. Αλλά, με συγχωρείτε, κρατήστε την προεπιλεγμένη τιμή του - 200 Milliδευτερόλεπτα Εάν θέλετε πραγματικά να απεικονίσετε κάτι παρόμοιο και να αποθηκεύσετε σε μερικά πιθανά πακέτα, τότε αναβάλετε το για 5 δευτερόλεπτα ή ό,τι είναι τώρα το χρονικό όριο του μηνύματος "Ο χρήστης πληκτρολογεί...". Αλλά όχι περισσότερο.

Και τέλος, ping. Δηλαδή, έλεγχος της ζωντανότητας της σύνδεσης TCP. Είναι αστείο, αλλά πριν από περίπου 10 χρόνια έγραψα ένα κριτικό κείμενο για τον αγγελιοφόρο του κοιτώνα της σχολής μας - οι συγγραφείς εκεί έκαναν επίσης ping στον διακομιστή από τον πελάτη και όχι το αντίστροφο. Αλλά οι τριτοετείς φοιτητές είναι άλλο και ένα διεθνές γραφείο είναι άλλο, σωστά;..

Πρώτον, ένα μικρό εκπαιδευτικό πρόγραμμα. Μια σύνδεση TCP, απουσία ανταλλαγής πακέτων, μπορεί να διαρκέσει για εβδομάδες. Αυτό είναι και καλό και κακό, ανάλογα με τον σκοπό. Καλό είναι να είχατε μια σύνδεση SSH ανοιχτή στον διακομιστή, να σηκωθείτε από τον υπολογιστή, να επανεκκινήσετε το δρομολογητή, να επιστρέψετε στη θέση σας - η συνεδρία μέσω αυτού του διακομιστή δεν είχε σχιστεί (δεν πληκτρολογήσατε τίποτα, δεν υπήρχαν πακέτα) , είναι βολικό. Είναι κακό αν υπάρχουν χιλιάδες πελάτες στον διακομιστή, που ο καθένας καταλαμβάνει πόρους (γεια σας, Postgres!) και ο κεντρικός υπολογιστής του πελάτη μπορεί να έχει επανεκκινήσει εδώ και πολύ καιρό - αλλά δεν θα το μάθουμε.

Τα συστήματα συνομιλίας/άμεσων μηνυμάτων εμπίπτουν στη δεύτερη περίπτωση για έναν επιπλέον λόγο - τις online καταστάσεις. Εάν ο χρήστης "έπεσε", πρέπει να ενημερώσετε τους συνομιλητές του σχετικά. Διαφορετικά, θα καταλήξετε σε ένα λάθος που έκαναν οι δημιουργοί του Jabber (και το διόρθωσαν για 20 χρόνια) - ο χρήστης έχει αποσυνδεθεί, αλλά συνεχίζουν να του γράφουν μηνύματα, πιστεύοντας ότι είναι συνδεδεμένος (που επίσης χάθηκαν εντελώς σε αυτά λίγα λεπτά πριν ανακαλυφθεί η αποσύνδεση). Όχι, η επιλογή TCP_KEEPALIVE, την οποία πολλοί άνθρωποι που δεν καταλαβαίνουν πώς λειτουργούν τα χρονόμετρα TCP βάζουν τυχαία (καθορίζοντας τιμές wild όπως δεκάδες δευτερόλεπτα), δεν θα βοηθήσει εδώ - πρέπει να βεβαιωθείτε ότι όχι μόνο ο πυρήνας του λειτουργικού συστήματος του μηχανήματος του χρήστη είναι ζωντανός, αλλά και λειτουργεί κανονικά, δεν μπορεί να ανταποκριθεί, και η ίδια η εφαρμογή (νομίζετε ότι δεν μπορεί να παγώσει; Το Telegram Desktop στο Ubuntu 18.04 πάγωσε για μένα περισσότερες από μία φορές).

Γι' αυτό πρέπει να κάνεις ping διακομιστή πελάτη και όχι το αντίστροφο - εάν ο πελάτης το κάνει αυτό, εάν η σύνδεση διακοπεί, το ping δεν θα παραδοθεί, ο στόχος δεν θα επιτευχθεί.

Τι βλέπουμε στο Telegram; Είναι ακριβώς το αντίθετο! Λοιπόν, δηλαδή. Επίσημα, φυσικά, και οι δύο πλευρές μπορούν να κάνουν ping μεταξύ τους. Στην πράξη, οι πελάτες χρησιμοποιούν δεκανίκι ping_delay_disconnect, το οποίο ρυθμίζει το χρονόμετρο στο διακομιστή. Λοιπόν, με συγχωρείτε, δεν εναπόκειται στον πελάτη να αποφασίσει πόσο καιρό θέλει να ζήσει εκεί χωρίς ping. Ο διακομιστής, με βάση το φορτίο του, γνωρίζει καλύτερα. Αλλά, φυσικά, αν δεν σας πειράζουν οι πόροι, τότε θα είστε ο κακός Πινόκιο του εαυτού σας και ένα δεκανίκι θα το κάνει...

Πώς έπρεπε να είχε σχεδιαστεί;

Πιστεύω ότι τα παραπάνω γεγονότα δείχνουν ξεκάθαρα ότι η ομάδα Telegram/VKontakte δεν είναι πολύ ικανή στον τομέα των μεταφορών (και χαμηλότερου) επιπέδου δικτύων υπολογιστών και των χαμηλών προσόντων τους σε σχετικά θέματα.

Γιατί αποδείχθηκε τόσο περίπλοκο και πώς μπορούν οι αρχιτέκτονες του Telegram να προσπαθήσουν να αντιταχθούν; Το γεγονός ότι προσπάθησαν να κάνουν μια συνεδρία που να επιβιώνει διακόπτει τη σύνδεση TCP, δηλαδή ό,τι δεν παραδόθηκε τώρα, θα το παραδώσουμε αργότερα. Μάλλον προσπάθησαν επίσης να κάνουν μεταφορά UDP, αλλά συνάντησαν δυσκολίες και το εγκατέλειψαν (γι' αυτό η τεκμηρίωση είναι άδεια - δεν υπήρχε τίποτα για να καυχιόμαστε). Αλλά λόγω έλλειψης κατανόησης του τρόπου με τον οποίο λειτουργούν τα δίκτυα γενικά και το TCP ειδικότερα, πού μπορείτε να βασιστείτε σε αυτό και πού πρέπει να το κάνετε μόνοι σας (και πώς) και μια προσπάθεια να συνδυαστεί αυτό με την κρυπτογραφία «δύο πουλιά με μια πέτρα», αυτό είναι το αποτέλεσμα.

Πώς ήταν απαραίτητο; Με βάση το γεγονός ότι msg_id είναι μια χρονική σήμανση απαραίτητη από κρυπτογραφική άποψη για την αποτροπή επιθέσεων επανάληψης, είναι λάθος να επισυνάψετε μια μοναδική συνάρτηση αναγνωριστικού σε αυτήν. Επομένως, χωρίς ουσιαστική αλλαγή της τρέχουσας αρχιτεκτονικής (όταν δημιουργείται η ροή ενημερώσεων, αυτό είναι ένα θέμα API υψηλού επιπέδου για ένα άλλο μέρος αυτής της σειράς αναρτήσεων), θα πρέπει:

  1. Ο διακομιστής που διατηρεί τη σύνδεση TCP με τον πελάτη αναλαμβάνει την ευθύνη - εάν έχει διαβάσει από την υποδοχή, παρακαλούμε αναγνωρίστε, επεξεργαστείτε ή επιστρέψτε ένα σφάλμα, χωρίς απώλειες. Τότε η επιβεβαίωση δεν είναι ένα διάνυσμα αναγνωριστικών, αλλά απλώς "η τελευταία ληφθείσα seq_no" - απλώς ένας αριθμός, όπως στο TCP (δύο αριθμοί - η ακολουθία σας και ο επιβεβαιωμένος). Είμαστε πάντα μέσα στη συνεδρία, έτσι δεν είναι;
  2. Η χρονική σήμανση για την αποτροπή επιθέσεων επανάληψης γίνεται ξεχωριστό πεδίο, a la nonce. Ελέγχεται, αλλά δεν επηρεάζει τίποτα άλλο. Αρκετά και uint32 - αν το αλάτι μας αλλάζει τουλάχιστον κάθε μισή μέρα, μπορούμε να διαθέσουμε 16 bit στα χαμηλής τάξης bits ενός ακέραιου μέρους της τρέχουσας ώρας, τα υπόλοιπα - σε ένα κλασματικό μέρος του δευτερολέπτου (όπως τώρα).
  3. Καταργήθηκε msg_id καθόλου - από την άποψη της διάκρισης των αιτημάτων στα backends, υπάρχει, πρώτον, το αναγνωριστικό πελάτη και, δεύτερον, το αναγνωριστικό περιόδου σύνδεσης, τα συνδυάζει. Συνεπώς, μόνο ένα πράγμα αρκεί ως αναγνωριστικό αιτήματος seq_no.

Αυτή δεν είναι επίσης η πιο επιτυχημένη επιλογή· μια εντελώς τυχαία θα μπορούσε να χρησιμεύσει ως αναγνωριστικό - αυτό γίνεται ήδη στο API υψηλού επιπέδου κατά την αποστολή ενός μηνύματος, παρεμπιπτόντως. Θα ήταν καλύτερα να ξαναφτιάξουμε εντελώς την αρχιτεκτονική από σχετική σε απόλυτη, αλλά αυτό είναι ένα θέμα για άλλο μέρος, όχι για αυτήν την ανάρτηση.

API;

Τα-νταμ! Έτσι, έχοντας αγωνιστεί σε μια διαδρομή γεμάτη πόνο και δεκανίκια, μπορέσαμε τελικά να στείλουμε οποιαδήποτε αιτήματα στον διακομιστή και να λάβουμε οποιεσδήποτε απαντήσεις σε αυτές, καθώς και να λάβουμε ενημερώσεις από τον διακομιστή (όχι ως απάντηση σε αίτημα, αλλά από τον ίδιο μας στέλνει, όπως το PUSH, αν κάποιος είναι πιο ξεκάθαρο έτσι).

Προσοχή, τώρα θα υπάρχει το μοναδικό παράδειγμα στο Perl στο άρθρο! (για όσους δεν είναι εξοικειωμένοι με τη σύνταξη, το πρώτο όρισμα του bless είναι η δομή δεδομένων του αντικειμένου, το δεύτερο είναι η κλάση του):

2019.10.24 12:00:51 $1 = {
'cb' => 'TeleUpd::__ANON__',
'out' => bless( {
'filter' => bless( {}, 'Telegram::ChannelMessagesFilterEmpty' ),
'channel' => bless( {
'access_hash' => '-6698103710539760874',
'channel_id' => '1380524958'
}, 'Telegram::InputPeerChannel' ),
'pts' => '158503',
'flags' => 0,
'limit' => 0
}, 'Telegram::Updates::GetChannelDifference' ),
'req_id' => '6751291954012037292'
};
2019.10.24 12:00:51 $1 = {
'in' => bless( {
'req_msg_id' => '6751291954012037292',
'result' => bless( {
'pts' => 158508,
'flags' => 3,
'final' => 1,
'new_messages' => [],
'users' => [],
'chats' => [
bless( {
'title' => 'Хулиномика',
'username' => 'hoolinomics',
'flags' => 8288,
'id' => 1380524958,
'access_hash' => '-6698103710539760874',
'broadcast' => 1,
'version' => 0,
'photo' => bless( {
'photo_small' => bless( {
'volume_id' => 246933270,
'file_reference' => '
'secret' => '1854156056801727328',
'local_id' => 228648,
'dc_id' => 2
}, 'Telegram::FileLocation' ),
'photo_big' => bless( {
'dc_id' => 2,
'local_id' => 228650,
'file_reference' => '
'secret' => '1275570353387113110',
'volume_id' => 246933270
}, 'Telegram::FileLocation' )
}, 'Telegram::ChatPhoto' ),
'date' => 1531221081
}, 'Telegram::Channel' )
],
'timeout' => 300,
'other_updates' => [
bless( {
'pts_count' => 0,
'message' => bless( {
'post' => 1,
'id' => 852,
'flags' => 50368,
'views' => 8013,
'entities' => [
bless( {
'length' => 20,
'offset' => 0
}, 'Telegram::MessageEntityBold' ),
bless( {
'length' => 18,
'offset' => 480,
'url' => 'https://alexeymarkov.livejournal.com/[url_вырезан].html'
}, 'Telegram::MessageEntityTextUrl' )
],
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'text' => '???? 165',
'data' => 'send_reaction_0'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 9'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'message' => 'А вот и новая книга! 
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
напечатаю.',
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'date' => 1571724559,
'edit_date' => 1571907562
}, 'Telegram::Message' ),
'pts' => 158508
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'pts' => 158508,
'message' => bless( {
'edit_date' => 1571907589,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'date' => 1571807301,
'message' => 'Почему Вы считаете Facebook плохой компанией? Можете прокомментировать? По-моему, это шикарная компания. Без долгов, с хорошей прибылью, а если решат дивы платить, то и еще могут нехило подорожать.
Для меня ответ совершенно очевиден: потому что Facebook делает ужасный по качеству продукт. Да, у него монопольное положение и да, им пользуется огромное количество людей. Но мир не стоит на месте. Когда-то владельцам Нокии было смешно от первого Айфона. Они думали, что лучше Нокии ничего быть не может и она навсегда останется самым удобным, красивым и твёрдым телефоном - и доля рынка это красноречиво демонстрировала. Теперь им не смешно.
Конечно, рептилоиды сопротивляются напору молодых гениев: так Цукербергом был пожран Whatsapp, потом Instagram. Но всё им не пожрать, Паша Дуров не продаётся!
Так будет и с Фейсбуком. Нельзя всё время делать говно. Кто-то когда-то сделает хороший продукт, куда всё и уйдут.
#соцсети #facebook #акции #рептилоиды',
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 452'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'text' => '???? 21',
'data' => 'send_reaction_1'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'entities' => [
bless( {
'length' => 199,
'offset' => 0
}, 'Telegram::MessageEntityBold' ),
bless( {
'length' => 8,
'offset' => 919
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'offset' => 928,
'length' => 9
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 6,
'offset' => 938
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 11,
'offset' => 945
}, 'Telegram::MessageEntityHashtag' )
],
'views' => 6964,
'flags' => 50368,
'id' => 854,
'post' => 1
}, 'Telegram::Message' ),
'pts_count' => 0
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'message' => bless( {
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 213'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 8'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'views' => 2940,
'entities' => [
bless( {
'length' => 609,
'offset' => 348
}, 'Telegram::MessageEntityItalic' )
],
'flags' => 50368,
'post' => 1,
'id' => 857,
'edit_date' => 1571907636,
'date' => 1571902479,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'message' => 'Пост про 1С вызвал бурную полемику. Человек 10 (видимо, 1с-программистов) единодушно написали:
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
Я бы добавил, что блестящая у 1С дистрибуция, а маркетинг... ну, такое.'
}, 'Telegram::Message' ),
'pts_count' => 0,
'pts' => 158508
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'pts' => 158508,
'pts_count' => 0,
'message' => bless( {
'message' => 'Здравствуйте, расскажите, пожалуйста, чем вредит экономике 1С?
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
#софт #it #экономика',
'edit_date' => 1571907650,
'date' => 1571893707,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'flags' => 50368,
'post' => 1,
'id' => 856,
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 360'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 32'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'views' => 4416,
'entities' => [
bless( {
'offset' => 0,
'length' => 64
}, 'Telegram::MessageEntityBold' ),
bless( {
'offset' => 1551,
'length' => 5
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 3,
'offset' => 1557
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'offset' => 1561,
'length' => 10
}, 'Telegram::MessageEntityHashtag' )
]
}, 'Telegram::Message' )
}, 'Telegram::UpdateEditChannelMessage' )
]
}, 'Telegram::Updates::ChannelDifference' )
}, 'MTProto::RpcResult' )
};
2019.10.24 12:00:51 $1 = {
'in' => bless( {
'update' => bless( {
'user_id' => 2507460,
'status' => bless( {
'was_online' => 1571907651
}, 'Telegram::UserStatusOffline' )
}, 'Telegram::UpdateUserStatus' ),
'date' => 1571907650
}, 'Telegram::UpdateShort' )
};
2019.10.24 12:05:46 $1 = {
'in' => bless( {
'chats' => [],
'date' => 1571907946,
'seq' => 0,
'updates' => [
bless( {
'max_id' => 141719,
'channel_id' => 1295963795
}, 'Telegram::UpdateReadChannelInbox' )
],
'users' => []
}, 'Telegram::Updates' )
};
2019.10.24 13:01:23 $1 = {
'in' => bless( {
'server_salt' => '4914425622822907323',
'unique_id' => '5297282355827493819',
'first_msg_id' => '6751307555044380692'
}, 'MTProto::NewSessionCreated' )
};
2019.10.24 13:24:21 $1 = {
'in' => bless( {
'chats' => [
bless( {
'username' => 'freebsd_ru',
'version' => 0,
'flags' => 5440,
'title' => 'freebsd_ru',
'min' => 1,
'photo' => bless( {
'photo_small' => bless( {
'local_id' => 328733,
'volume_id' => 235140688,
'dc_id' => 2,
'file_reference' => '
'secret' => '4426006807282303416'
}, 'Telegram::FileLocation' ),
'photo_big' => bless( {
'dc_id' => 2,
'file_reference' => '
'volume_id' => 235140688,
'local_id' => 328735,
'secret' => '71251192991540083'
}, 'Telegram::FileLocation' )
}, 'Telegram::ChatPhoto' ),
'date' => 1461248502,
'id' => 1038300508,
'democracy' => 1,
'megagroup' => 1
}, 'Telegram::Channel' )
],
'users' => [
bless( {
'last_name' => 'Panov',
'flags' => 1048646,
'min' => 1,
'id' => 82234609,
'status' => bless( {}, 'Telegram::UserStatusRecently' ),
'first_name' => 'Dima'
}, 'Telegram::User' )
],
'seq' => 0,
'date' => 1571912647,
'updates' => [
bless( {
'pts' => 137596,
'message' => bless( {
'flags' => 256,
'message' => 'Создать джейл с именем покороче ??',
'to_id' => bless( {
'channel_id' => 1038300508
}, 'Telegram::PeerChannel' ),
'id' => 119634,
'date' => 1571912647,
'from_id' => 82234609
}, 'Telegram::Message' ),
'pts_count' => 1
}, 'Telegram::UpdateNewChannelMessage' )
]
}, 'Telegram::Updates' )
};

Ναι, όχι ένα spoiler επίτηδες - αν δεν το έχετε διαβάσει ακόμα, προχωρήστε και κάντε το!

Ω, ρε~~... πώς μοιάζει αυτό; Κάτι πολύ οικείο... ίσως αυτή είναι η δομή δεδομένων ενός τυπικού Web API σε JSON, εκτός από το ότι οι κλάσεις συνδέονται επίσης με αντικείμενα;..

Λοιπόν έτσι αποδεικνύεται... Τι είναι όλα αυτά, σύντροφοι;... Τόση προσπάθεια - και σταματήσαμε να ξεκουραστούμε εκεί που οι προγραμματιστές του Ιστού μόλις ξεκινάει?..Δεν θα ήταν απλούστερο μόνο το JSON μέσω HTTPS; Τι πήραμε ως αντάλλαγμα; Άξιζε τον κόπο η προσπάθεια;

Ας αξιολογήσουμε τι μας έδωσε το TL+MTProto και ποιες εναλλακτικές είναι δυνατές. Λοιπόν, το HTTP, το οποίο εστιάζει στο μοντέλο αιτήματος-απόκρισης, δεν ταιριάζει, αλλά τουλάχιστον κάτι πάνω από το TLS;

Συμπαγής σειριοποίηση. Βλέποντας αυτή τη δομή δεδομένων, παρόμοια με το JSON, θυμάμαι ότι υπάρχουν δυαδικές εκδόσεις του. Ας επισημάνουμε το MsgPack ως ανεπαρκώς επεκτάσιμο, αλλά υπάρχει, για παράδειγμα, το CBOR - παρεμπιπτόντως, ένα πρότυπο που περιγράφεται στο RFC 7049. Είναι αξιοσημείωτο για το γεγονός ότι ορίζει ετικέτες, ως μηχανισμός επέκτασης, και μεταξύ ήδη τυποποιημένη διαθέσιμος:

  • 25 + 256 - αντικατάσταση επαναλαμβανόμενων γραμμών με αναφορά στον αριθμό γραμμής, μια τέτοια φθηνή μέθοδος συμπίεσης
  • 26 - σειριακό αντικείμενο Perl με ορίσματα ονόματος κλάσης και κατασκευαστή
  • 27 - σειριακό αντικείμενο ανεξάρτητο από τη γλώσσα με ορίσματα όνομα τύπου και κατασκευαστή

Λοιπόν, προσπάθησα να σειριοποιήσω τα ίδια δεδομένα σε TL και CBOR με ενεργοποιημένη τη συμβολοσειρά και τη συσκευασία αντικειμένων. Το αποτέλεσμα άρχισε να ποικίλλει υπέρ του CBOR κάπου από ένα megabyte:

cborlen=1039673 tl_len=1095092

Ετσι, συμπέρασμα: Υπάρχουν ουσιαστικά απλούστερες μορφές που δεν υπόκεινται σε πρόβλημα αποτυχίας συγχρονισμού ή άγνωστου αναγνωριστικού, με συγκρίσιμη απόδοση.

Γρήγορη εγκατάσταση σύνδεσης. Αυτό σημαίνει μηδέν RTT μετά την επανασύνδεση (όταν το κλειδί έχει ήδη δημιουργηθεί μία φορά) - ισχύει από το πρώτο κιόλας μήνυμα MTProto, αλλά με κάποιες επιφυλάξεις - χτυπήστε το ίδιο αλάτι, η συνεδρία δεν είναι σάπια κ.λπ. Τι μας προσφέρει το TLS; Παράθεση για το θέμα:

Όταν χρησιμοποιείτε PFS σε TLS, εισιτήρια συνεδρίας TLS (RFC 5077) για να συνεχίσετε μια κρυπτογραφημένη συνεδρία χωρίς επαναδιαπραγμάτευση κλειδιών και χωρίς αποθήκευση βασικών πληροφοριών στον διακομιστή. Κατά το άνοιγμα της πρώτης σύνδεσης και τη δημιουργία κλειδιών, ο διακομιστής κρυπτογραφεί την κατάσταση σύνδεσης και τη μεταδίδει στον πελάτη (με τη μορφή εισιτηρίου συνεδρίας). Αντίστοιχα, όταν συνεχιστεί η σύνδεση, ο πελάτης στέλνει ένα δελτίο συνεδρίας, συμπεριλαμβανομένου του κλειδιού συνεδρίας, πίσω στον διακομιστή. Το ίδιο το εισιτήριο είναι κρυπτογραφημένο με ένα προσωρινό κλειδί (κλειδί εισιτηρίου περιόδου λειτουργίας), το οποίο αποθηκεύεται στον διακομιστή και πρέπει να διανεμηθεί σε όλους τους διακομιστές διεπαφής που επεξεργάζονται το SSL σε συμπλεγμένες λύσεις.[10]. Έτσι, η εισαγωγή ενός δελτίου συνεδρίας μπορεί να παραβιάζει το PFS εάν τα προσωρινά κλειδιά διακομιστή παραβιάζονται, για παράδειγμα, όταν είναι αποθηκευμένα για μεγάλο χρονικό διάστημα (το OpenSSL, nginx, Apache τα αποθηκεύει από προεπιλογή για όλη τη διάρκεια του προγράμματος, οι δημοφιλείς ιστότοποι χρησιμοποιούν το κλειδί για αρκετές ώρες, έως και ημέρες).

Εδώ το RTT δεν είναι μηδέν, πρέπει να ανταλλάξετε τουλάχιστον το ClientHello και το ServerHello, μετά το οποίο ο πελάτης μπορεί να στείλει δεδομένα μαζί με το Finished. Αλλά εδώ θα πρέπει να θυμόμαστε ότι δεν έχουμε τον Ιστό, με τις νέες συνδέσεις του, αλλά έναν αγγελιοφόρο, η σύνδεση του οποίου είναι συχνά ένα και λίγο πολύ μακροχρόνια, σχετικά σύντομα αιτήματα σε ιστοσελίδες - όλα είναι πολυπλεξία εσωτερικώς. Δηλαδή, είναι αρκετά αποδεκτό αν δεν συναντήσουμε ένα πολύ κακό τμήμα του μετρό.

Ξεχάσατε κάτι άλλο; Γράψτε στα σχόλια.

Συνεχίζεται!

Στο δεύτερο μέρος αυτής της σειράς αναρτήσεων θα εξετάσουμε όχι τεχνικά, αλλά οργανωτικά ζητήματα - προσεγγίσεις, ιδεολογία, διεπαφή, στάση απέναντι στους χρήστες κ.λπ. Με βάση, ωστόσο, τις τεχνικές πληροφορίες που παρουσιάστηκαν εδώ.

Το τρίτο μέρος θα συνεχίσει να αναλύει το τεχνικό στοιχείο / εμπειρία ανάπτυξης. Θα μάθετε, συγκεκριμένα:

  • συνέχιση της πανδαισίας με την ποικιλία των τύπων TL
  • άγνωστα πράγματα για κανάλια και υπερομάδες
  • γιατί οι διάλογοι είναι χειρότεροι από τον κατάλογο
  • σχετικά με την απόλυτη έναντι της σχετικής διεύθυνσης μηνυμάτων
  • ποια είναι η διαφορά μεταξύ φωτογραφίας και εικόνας
  • πώς τα emoji παρεμβαίνουν στο πλάγιο κείμενο

και άλλα δεκανίκια! Μείνετε συντονισμένοι!

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο