Cum să decodați un AppEngine db.Key Protobuf fără biblioteci

Deci, pentru oricine a folosit cheile bazei de date AppEngine, poate fi familiarizat cu faptul că uuid-ul unic la nivel global al fiecărei entități este practic un șir codificat în baza 64 de câteva informații de bază, și anume: numele aplicației, numele modelului și id_or_name.

Acest lucru permite AppEngine să găsească orice entitate de bază de date din lume cu un șir mic și simplu. De fapt, toate tipurile de ReferenceProperty din AppEngine sunt, de asemenea, stocate ca acest același șir de bază64, astfel încât să poată fi rezolvat foarte rapid într-o lume a bazelor de date non-relaționale. Deoarece aceasta este baza depozitului de date, are sens să împachetăm toate aceste informații în cât mai puțini octeți posibil, așa că, în mod natural, Google folosește Protocol Buffers pentru a codifica un obiect db.Key într-un șir pe care să îl trimită. firul.

Deci, ce se întâmplă acum dacă vi se oferă una dintre aceste chei urâte și trebuie să faceți o alăturare BigQuery împotriva unei entity.key().id_or_name()? Sau clientul dvs. Javascript vrea doar să obțină tipul modelului sau este id-ul de partea clientului fără RPC și JSON-ul dvs. nu are aceste lucruri la îndemână seriate? Sau ce se întâmplă dacă ești al naibii de curios cum acești octeți sunt cu adevărat codificați!

Minunat, AppEngine oferă un SDK complet pentru a-și rula întreaga stivă local pe mașina dvs. și chiar a încărca totul într-un UnitTest, astfel încât să putem parcurge ceea ce se întâmplă la nivelul serverului de fiecare dată când o cheie este instanțiată.

Deci haideți să trecem la asta...

Mai întâi vom începe cu orice șir db.Key

Acum magia începe să se întâmple adânc în entity_pb.py, unde începe să proceseze buffer-ul de șir:

Analizorul practic trece de la verificarea unui octet pentru a vedea ce tip este și cât de lung este, preia datele imediat după și apoi continuă să zboare înainte. Primul nostru octet „j” nu este de fapt un caracter deloc (amintiți-vă că 1 octet poate reprezenta 256 de bucăți de informații, dar doar un subset din acești octeți sunt de fapt caractere imprimabile în ASCII. Întregul șir este de fapt doar o combinație de semnalizare octeți, șiruri de caractere și numere cu lungime variabilă În multe cazuri, este mai ușor să ne gândim la codul lor hexadecimal sau la valoarea ord...j = „106”.

Și după cum puteți vedea, acesta este analizatorul care încearcă să descopere partea appname a cheii. Dar să trecem mai departe pentru a obține mai întâi tipul modelului, care este un șir de lungime variabilă

Toate acestea se întâmplă la octetul magic 0x12 (valoarea de bază 10 de 18) unde acum știm că suntem pe cale să analizăm un șir. Următorul octet după acesta ne spune de câți octeți lung este șirul nostru: „\n” care este de fapt doar python care încearcă să redea octeți în ascii atunci când poate. Ord(‘\n’) este 0x0a sau 10.

Este 10 pentru cele 10 personaje din „HelloWorld”!

Acum totul a fost destul de simplu, nu?

Acum obținerea ID-ului entității este puțin mai dificilă. Acestea sunt numere Int64 odată ce sunt încărcate în memorie sau bigquery, dar prin cablu sunt codificate puțin diferit. În loc de cei 8 octeți standard, numărul este codificat mai extensibil. Practic avem 7 biți de date și fiecare octet are un al 8-lea bit de terminare. Deci, atâta timp cât al 8-lea bit este „1”, continuăm să luăm următorul octet din buffer, concatenând toți cei 7 biți pe care i-am găsit pe parcurs prin deplasarea biților într-un Int64.

Să aruncăm o privire la exemplu

Întâlnim în continuare un alt octet magic 0x18, care este 24 în baza 10, iar acum știm că avem un Int64 cu lungime variabilă care coboară prin conductă.

Deci, în landul de octeți nesemnați, deoarece orice octet cu un „1” ca al 8-lea bit este ≥ 128, practic continuăm să citim octeți până când atingem un număr mai mic decât acesta, care este adesea un caracter ascii imprimabil, în acest caz acel octet ar fi „\x15”, care este un caracter neimprimabil NAK (confirmare negativă)

Și iată bucla reală a efectuării deplasării de biți a matricei de octeți:

Și voila! Avem aceeași entitate.key().id() pe care am tipărit-o la început în consolă.

Așa că mergeți mai departe și încercați singur - luați orice șir de cheie și deschideți interpretul Python fără a încărca biblioteci și rulați asta:

# in py2 
import base64
key = ‘agd0ZXN0YmVkchcLEgpIZWxsb1dvcmxkGIeXrevFivcVDA’
urlsafe = str(key)
mod = len(urlsafe) % 4
urlsafe += (‘=’*(4-mod)) if mod else ‘’
b = base64.b64decode(urlsafe.replace(‘-’, ‘+’).replace(‘_’, ‘/’))
id_bytes = b.split(‘\x18’)[-1][0:-1]
ba = bytearray(id_bytes)
length = ord(b.split(‘\x12’)[1][0])
db_type = b.split(‘\x12’)[1][1:length+1]
db_id = sum([(ba[i] & 127) << i*7 for i in xrange(len(ba))])
print ‘%s:%s’ % (db_type, db_id)

# Here's a function declaration that works for id_or_name in py2+3
def decode_key(key):
  # turn b64 string into some bytes
  urlsafe = str(key)
  mod = len(urlsafe) % 4
  urlsafe += ('='*(4-mod)) if mod else ''
  b = base64.b64decode(urlsafe.replace('-', '+').replace('_', '/'))

  # skip ahead to where the class name/id bytes are
  cls_and_id = b.split(b'\x0b\x12')[1]
  cls_len = cls_and_id[0] # ord() this in py2
  db_type = cls_and_id[1:cls_len + 1].decode()

  # figure out if this is id or name
  id_type = cls_and_id[cls_len + 1] # ord() this in py2
  assert id_type in [34, 24], "key fail id_type=%s" % id_type
  id_or_name = cls_and_id[cls_len + 2:]

  if id_type == 34: # name()
    name_len = id_or_name[0] # ord() this in py2
    db_id = id_or_name[1:name_len + 1].decode()
  else: # id()
    ba = bytearray(id_or_name[0:-1])
    db_id = sum([(ba[i] & 127) << i*7 for i in range(len(ba))])

  return db_type, db_id

Prolog:

Acesta este pur și simplu un exercițiu teoretic pentru a înțelege ce se întâmplă sub capotă și aș rezerva această tehnică doar pentru depanare sau pentru un stopgap. Deși mi s-a părut destul de util de-a lungul anilor, când am o relație 1:1 a unei entități cu alta, în care vreau ca o căutare în bloc a numelui_cheie să fie rapidă, făcând ca numele cheii al entității derivate să fie cheia însăși, eu de obicei încercați să utilizați ceea ce eu numesc short_key a cheii unei entități. de exemplu.

De asemenea, atunci când în cazurile în care am o structură de moștenire în designul modelului meu, veți găsi că este necesar să returnați cheile în loc de id_or_name() în JSON clienților - și aici am început să folosesc short_keys. Atâta timp cât toate entitățile lucrează în aceeași aplicație AppEngine, aceasta ajunge să fie mai mică decât versiunea protobuf în majoritatea cazurilor, precum și să fie mult mai ușor de citit și de depanat

De exemplu, făcând numele-cheie al unei entități care reprezintă apartenența unui obiect asemănător unui utilizator la un obiect canal:

Acum pot găsi o entitate de membru fără o interogare compozită lentă... și chiar pot înțelege răspunsul meu JSON