Typesikkerhet i heterogene nettverk med MQTT og Protobuf

I et av de siste prosjektene våre var det gitt at at API-grensesnittet skal settes opp med MQTT og Protobuf. Dette muliggjør pub-sub med heterogene klienter, men med garantert leveranse og typesikkerhet, uten at det går på bekostning av datamengde eller prosesseringskraft. Hva betyr dette, og er det noe du burde bruke?

Som utvikler har jeg brukt mange av de vanligste API-teknologiene, som XML SOAP, REST, GraphQL, tRPC. Alle har kjente fordeler og ulemper, og resten av tech-stacken legger ofte premissene for hva som passer best. Men felles for dem alle er at de er fungerer best i en standard klient-tjener-arkitektur der man har en kjent tjener som besvarer forespørslene som tilkoblede klienter forespør på en én-til-én-basis. Men under andre omstendigheter og med andre utfordringer, kan helt andre kombinasjoner av teknologier fungere enda bedre.

Først en liten forklaring av begreper. MQTT er en nettverksprotokoll som først og fremst brukes for IoT. Den baserer seg på et publish-subscribe-tankesett der man setter opp en utvekslingssentral (eng: broker) som klientene kobler seg mot. Klientene sender kun til denne sentralen, som i sin tur vil distribuere kopier av denne meldingen til alle klientene den kjenner til som har abonnert på slike meldinger. Måten sentralen vet hvem den skal sende hva til, er via såkalte emner. Alle meldinger som sendes må ha et emne, og når man setter opp abonnement gjelder det for ett eller et mønster av emner. Ved hjelp av wildcards kan man lage et nett som er så vidt eller finmasket man selv ønsker på meldingene man vil motta.

På denne måten vil man dermed kunne ha nær sagt uendelig mange sendere og mottakere som kan utveksle data uten at de må kjenne til hverandre. Via emnene man abonnerer på eller identifiserer meldingene med, vil kun de mottakerene som ønsker å få en melding, motta den. Her kan også klientene være veldig forskjellige, noen vil kanskje bare sende data, noen vil både sende og motta data, og noen vil kun motta data. Noen vil ønske å motta alle typer data, mens noen kun er interessert i et svært spisset subsett.

Et tenkt eksempel kan være i et smarthus, der man kan ha følgende oppsett:

  • Enkle temperatursensorer inne og ute, som kun sender data
  • Termostater på varmeovner som både kan motta styresignaler, og som vil sende data om sin temperatur og tilstand
  • Et display som viser informasjon om inne- og utetemperaturer, som kun vil motta dataene som sendes fra temperatursensorer og termostater
  • En tablet satt opp til å styre hele smarthuset, som kan motta temperaturer og kan styre termostater, og i tillegg styre alle andre enheter man har koblet til.

Når man opererer med såpass mange ulike typer klienter som kan koble seg til, risikerer man at det blir kluss med datamodeller. Noen av klientene har kanskje kode skrevet i JavaScript, noen i Python, noen i C++ eller C. Kanskje har man en app som bruker Swift, Android eller Flutter. Dersom man endrer en datamodell ett sted, vil det ha ringvirkninger på alle andre som bruker disse beskjedene, og man risikerer å måtte implementere den samme endringen på en håndfull forskjellige språk og teste alle API-kall på nytt. Det er her Protobuf kommer inn.

Google kom opp med konseptet om protokollbuffere (Protobuf) som sin løsning på dette problemet. Det er et eget, domenespesifikt språk for datamodeller, som kan minne om en minste felles multiplum av mange av de mest populære språkene. Det er laget for å serialisere data for sending over et nettverk, og fungere på kryss og tvers mellom ulike programmeringsspråk. Man kan generere opp modeller i de aller fleste programmeringsspråk utifra en felles definisjon i et .proto-format. Per i dag finnes offisiell støtte for C++, Java, Go, Ruby, C# og Python, men også fungerende tredjepartsbibliotek for bl.a. C, Haskell, Kotlin, OCaml, PHP, Swift, Rust, Zig og TypeScript. På denne måten kan man være trygg på at en modell som ble serialisert i ett språk vil fungere når den deserialiseres et annet sted. Samtidig er de serialiserte dataene designet for å være så kompakte som mulig, noe som jo er ganske nyttig om man opererer med små, begrensede enheter. La oss fortsette eksempelet fra tidligere:

La oss si at vi i dag har temperatursensorer som sender enkle oppdateringer som kun inneholder hvor mange grader den måler. Da vil .proto-filen bli omtrent slik:


message Temperature {	
	int32 degrees = 1;
}


Her viser "int32" til datatypen, "degrees" er navnet på variablen, og 1 er feltets identifikator, som brukes i det enkodede binærformatet.

Etter en stund vil man kanskje sette inn et par nye sensorer fra en leverandør man ikke har brukt før. Etter at de er montert, viser det seg at disse måler temperaturen i fahrenheit fremfor celsius. For å spare mest mulig på batteriene, ønsker du ikke at enheten i seg selv skal gjøre konverteringen. Det man kan gjøre, er å utvide modellen til å inneholde informasjon om temperaturen som sendes er celsius eller fahrenheit. Her kan man for eksempel legge på en boolsk verdi som er true dersom temperaturen er i fahrenheit. Modellen blir da slik:


message Temperature {	
	int32 degrees = 1;	
	bool isFahrenheit = 2;
}

Med denne modellen vil de nye sensorene kunne sende temperaturen sin i fahrenheit og sette flagget isFahrenheit til true, og de eldre vil kunne sende med flagget satt til false. De nye modellene man genererer ut gir compile-time typesikkerhet i alt fra en embedded sensor-software skrevet i C, og til kontroll-appen skrevet i Swift ut ifra den samme typedefinisjonen. Dette er utrolig nyttig da MQTT i seg selv ikke begrenser eller validerer meldingene som sendes ut, men så lenge man vet at alle som ønsker å sende eller motta meldinger forholder seg til samme versjon av Protobuf-modellene, kan man være trygg på at dataene som sendes, mottaes korrekt på andre siden.

💡 I pragmatismens navn kunne man også vurdert å sette feltet isFahrenheit til optional, da dette legger opp til at man ikke blir tvunget til å oppdatere enheter der dette flagget ikke er relevant, men jeg er av prinsipp ikke spesielt glad i valgfrie boolske verdier da dette i praksis gir tre muligheter fremfor to.

Dersom dette systemet hadde blitt satt opp med et mer tradisjonelt REST-API med JSON-enkodet data, hadde man sittet med to utfordringer. Det første er at man må gå gjennom alle de ulike systemkomponentene som kan sende eller motta temperaturendringer og vurdere om man enten skal implementere den nye endringen i modellobjektene i dette prosjektet, eller om man skal satse på at man kan la to nesten identiske modeller kan leve side om side. Kanskje ender man opp med å måtte versjonere APIet sitt. Det andre blir da å sikre at alle integrasjoner fungerer, og oppdatere kode og dokumentasjon med de nye sensorene man har satt inn. Ettersom vårt system ikke har eksplisitte endepunkter og vi kan stole på genererte modeller, vil MQTT og Protobuf gjøre denne jobben betydelig mindre.

For en som nær utelukkende har jobbet med klient-tjener, og som i det siste har vært begeistret for ende-til-ende typesikkerheten man får med tRPC, har det vært spennende å se en helt annen løsning med mange av de samme fordelene. Spesielt MQTT er en protokoll som er best i miljøer der skillet mellom klient og tjener til dels er visket ut, og nye noder kan komme og gå på tilfeldige tidspunkt. Hvis garantert leveranse i et slikt miljø er viktig, kan man sette et ønsket QoS-nivå på hver melding, der brokeren vil sørge for enten at hver melding leveres enten minst én gang (nivå 1) eller nøyaktig én gang (nivå 2). Nivå 1 påkrever at mottakeren besvarer hver melding med en bekreftelse, og nivå to krever en fireveis handshake, så i tilfeller der lavest mulig bruk av båndbredde eller batteri er viktig, må man vurdere om disse garantiene kan rettferdiggjøres.

Totalpakken disse to protokollene kan tilby, er absolutt besnærende for systemer der man har mange ulike komponenter og noder som ikke nødvendigvis trenger å vite om hverandre, men der man ønsker typesikkerhet, leveransegarantier og som vil fungere i de aller fleste større programmeringsspråk.

Skrevet av

Andre artikler