- Node.js er overhodet ikke egna til sånne prosjekter!

Derfor gikk Aslak fra C++ til Node.js - og tilbake igjen.

Aslak lager intet mindre enn sitt eget MMO. Spørsmålet var hva serveren skulle lages i. 📸: Aslak Berby
Aslak lager intet mindre enn sitt eget MMO. Spørsmålet var hva serveren skulle lages i. 📸: Aslak Berby Vis mer

Jeg driver med programmering som yrke og på fritiden dataspill - spesielt en eller annen variant av MMO.

Har noen andre interesser også, da, men som 50-åring har jeg hatt tid til å drive med begge deler en stund.

Når du spiller mye i et spill, er det ikke fritt for at du får dine egne tanker om hvordan et spillsystem kunne vært. Hvordan skal ferdighetssystemet bygges opp, hvordan kan konstruksjoner av våpen og utstyr gjøres og alt det andre som så mange før deg har klekket ut noe lurt.

Når du da i tillegg kommer med noen ønsker om litt mer PvE-vennlighet i et ekstremt PvP-orientert spill, og alt du får slengt etter deg fra andre spillere er «Du får lage ditt eget, da», så blir det som det blir.

Etter å ha gått og grublet på dette noen år, så tenkte jeg at jeg like godt kunne hive meg ut i det og forene det profesjonelle med hobbyen og lage et MMO selv.

Server, klient

Selv jeg skjønner at dette er mer enn hva en stakkar klarer på fritiden når en skal spille, skrive bøker, gå med hund, ta hensyn til kona også videre, men likevel.

La oss tenke enkelt.

*Skuler bort på EvE og tenker*

Det må da være et greit utgangspunkt. Et romspill. Ikke for mye animasjoner eller annet kjafs.

Fast forward noen år, så har jeg en server og klient oppe og gå. Ikke noe spennende, men du kan hoppe mellom prosedyregenererte solsystemer, parkere på en av mange romstasjoner og ha en enkel inventory med drag and drop mellom kister, skip og stasjon. Med andre ord mye som gjenstår før jeg slipper noen som helst andre i nærheten av en klient.

Uansett: Dette var bare bakgrunnen og ikke det jeg egentlig ville snakke om.

📸: Aslak Berby
📸: Aslak Berby Vis mer

Python er pyton

Serveren er så langt kodet i C++. Den består av en UDP-server, en klientmodul, en hendelsesserver og world (domination)-modul til å håndtere alle aktive planeter, solsystemer, asteroidebelter og ekle NPC-er som vil gjøre livet surt for deg.

I grunn ganske klassisk, vil jeg tro, om du snakker med noen som faktisk kan dette.

Så en dag, da jeg surfet meningsløst rundt på nettet kom jeg over noen som nevnte at de første utgavene av EVE sin server var laget i Stackless Python. Jeg aner ikke om dette stemmer eller ikke, men det pirret nysgjerrigheten min nok til at jeg slo opp hva det var.

Wikipedia så står det ganske mye om Stackless Pyton, men det jeg bet meg merke i var følgende setning:

«The most prominent feature of Stackless is microthreads, which avoid much of the overhead associated with usual operating system threads.»

Etter å ha vridd hodet noen ganger rundt dette, og antagelig fullstendig misforstått hva dette dreier seg om, gikk tankene videre.

En MMO server består i veldig stor grad av hendelser som skjer asynkront. En bruker logger på, noen gjør en handling som serveren vil ha en mening om, en tidshendelse gjør at noe skjer i verden og så videre. Kort sagt: En mengde små oppgaver som egentlig lever sitt eget liv innenfor den definerte verden av MMO-serveren. Eller «microthreads».

En idé var skapt, som ikke bare kunne forsvinne sånn uten videre. Den måtte testes, men Python er ganske pyton. Jeg kan ikke fordra det.

Men et annet språk som jeg har jobbet noe i er Javascript, og det slo meg at muligens den noe irriterende asynk-modellen virkelig kunne komme til nytte her.

«Den måtte testes, men Python er ganske pyton. Jeg kan ikke fordra det.»

Fra C++ til Node.js

Første gang jeg forsøkte meg på Node.js, da jeg kom fra en mer normal programmeringsverden, så var asynk noe av det mest frustrerende jeg hadde vært ute for.

Jeg skal kjøre to SQL-kall. Når den første har kjørt, skal dette benyttes inn i den neste. Lyder kjent? Vel, i asynk må du da angi en funksjon som kjøres når SQL-kallet er ferdig. Det vil si: Den opprinnelige tråden dør, og du må fortsette et annet sted.

Vel, greit nok, helt til du begynner å få mange slike etter hverandre. Da får du en kode som ville fått Picasso til å bli rørt. Fullstendig kaotisk. (Ja, jeg vet det er teknikker her, men det hjelper ikke så mye egentlig. Koden blir ikke noe bedre av den grunn).

Vel, tilbake til det vi snakker om. En MMO-server. Tenk nå dette brukt i forhold til denne: En bruker logger inn. Venter du på svar fra SQL-serveren? Neida. Du gjør SQL-kallet og rusler videre med andre oppgaver. Når svaret er klart, vil den tråden starte igjen, og sende dette tilbake til klienten enten han får logget inn eller ikke. Dette gjøres da i sin egen lille boble som (nesten) ikke bryr resten av serveren.

Vel, idéen ville ikke gi slipp, og hva annet fornuftig kunne en helg brukes til, om ikke det var nettopp å skrive om en C++-server til Node.js. Bortsett fra kona, så syns resten husstanden (meg) at dette var en glimrende ide.

📸: Aslak Berby
📸: Aslak Berby Vis mer

"Ding"

Som sagt så gjort. Ny VM opp, inn med Ubuntu, VS Code, Node.js og alt som må høre til.

Etter å ha fått børstet støv av noen rimelig rustne Node.js-erfaringer, så er jeg klar til dyst. Jeg har teknisk sett fasiten så langt i C++-serveren så det er mer snakk om å skrive denne om, men den må skrives på Node sine premisser. Ikke forsøke å presse C++-tankegangen inn i Node.

Vel, Google is your friend. Eller som min mor pleier å si: «Google the shit».

Det første som må opp er UDP-serveren. Dette er ganske grei skuring, og etter å ha bekreftet at «Buffer» fungerer slik jeg husker, klinker jeg inn de første faktiske kodesnuttene som har med kommunikasjon å gjøre og fyrer den opp. Her snakker vi nå om maksimalt 20 linjer kode alt i alt, så det er ingenting i det hele tatt.

Starter klienten og ingenting skjer. Etter et minutt febrilsk leting kommer jeg på at jeg satte opp en ny VM, får byttet IP-adressen og forsøker igjen.

«Ding». Det første kallet kommer inn og akkurat slik som forventet. Glimrende! Jeg er i gang.

Moduler som skal snakke sammen

Det første kallet fra klienten er egentlig bare et «HELO». Helt plain tekst. Den vet ikke en gang om serveren er der. Serveren på sin side, lurer på hvem i all verden dette er, men akkurat her og nå må den opprette en sesjon til denne ukjente fremmede slik at den ved neste korsvei kjenner igjen vedkommende.

Den må derfor finne på et nummer, eller +1 fra forrige fremmede, og sende dette tilbake. Men den må også opprette en post hvor den kan holde rede på hvem den er, hva den er og alt det der etter hvert. Kort sagt: Et klientobjekt må opprettes, og dette må legges i en klient-konteiner.

Ikke noen big deal. Fort gjort, men hei: Hvordan skal disse kommunisere seg i mellom?

En «let clientContainer = require("./clientcontainer.js")» gir en lokal kopi av «clientContainer». Tar jeg en require på den samme filen fra en annen fil vil jeg få en helt frisk kopi fullstendig blottet for de nye klientene som UDP serveren nettopp la til. Det går ikke, men det er slik Node må fungere. Alternativet hadde vært galskap.

I C++ ville du løst dette med å ha en instans gående av «clientContainer» og sørget for at alle som inkluderte header-filen også fikk med seg en ekstern definert variabel som pekte til denne instansen. (Du må selvfølgelig ha kode for å opprette den og rydde den på korrekt måte, men det er en annen sak.)

Noe lignende må også gjøres i Node, men her må dette gjøres via «global». Igjen måtte Google til pers. Des mer jeg leste om den, des mer mislikte jeg den. Jeg lærte nok ikke nok om den, men endte med å definere «global» i hovedfilen.

«Vel, det var et skudd for baugen da det er mange frittstående moduler i en slik server, og alle bør ha mulighet til å snakke med de andre.»

Alternativet var egentlig å opprette disse forløpende og la dem gli som variable rundt slik at alle fikk sin lokale kopi av pekeren til det samme objektet. Det er heller ikke noen ideell teknikk for å si det pent.

Vel, det var et skudd for baugen da det er mange frittstående moduler i en slik server, og alle bør ha mulighet til å snakke med de andre. En event-server må ha mulighet til å finne klienter innenfor det området hvor eventen skjer, og klient-modulen må ha mulighet til å sende informasjon tilbake til den faktiske spill-klienten gjennom UDP-biblioteket og så videre.

Løsningen ble global. Som nevnt ikke ideelt, men jeg lar ikke det drepe entusiasmen. Ikke helt i alle fall. Dette var ikke det jeg uansett skulle teste.

Meldingsprotokollen

Så, la oss få i gang asynk. Det er det som skal bli moro, men først må meldingsprotokollen på plass.

En melding har for øyeblikket en ganske enkel kryptering som består av en trippel kryptering på eksisterende buffer. Dette er ikke noe som ikke noen kan klare å knekke om de virkelig vil gå inn for det, men til mitt formål akkurat nå bekymrer det meg mindre.

Denne kan lett skrives om en gang i fremtiden. Fint! Lager funksjonen «Dekrypt». Denne tar to parameter. Bufferet og klientnøkkelen. Her skal den bruke klientnøkkelen og løpe gjennom bufferen noen ganger. Når den returnerer er bufferet klar til å brukes. Eller vent. Jeg får ikke bufferet sendt over, men en kopi.

Flott. Igjen vendes blikket olmt mot Google. Variabel-overføring med reference. Som en skrev på en Stack Overflow side: «That is not possible.» Er det noen stor case? Vel, det blir en veldig grisete kode om alle parametre som skal kunne endres, først må wrappes i et objekt eller at du skal returnere objekter med flere verdier bare fordi du ikke kan endre på noen av de som er input.

En stor greie? Antagelig ikke for de som er vant med å jobbe med dette. Selv gikk jeg og spiste hjemmelaget pizza. (Smaker fantastisk forresten)

Tilbake igjen til C++

Med godt svidd gane satt jeg med ned og reflekterte litt over mine små erfaringer.

Jeg er ingen Node-ekspert og det er sikkert en bråte med folk som alt nå hamrer i stykker tastaturet for å fortelle meg alt det jeg har gjort feil så langt, selv om dere ikke har sett noe kode, men for meg dreide dette seg om å lage en mer elegant kode. En kode som ville være naturlig og lett og lese. Obskure globals-modeller og wrappere for å få originalobjektet med meg til enhver tid er ikke det jeg anser som elegant. Jeg er heller ikke så veldig interessert i å importere ørten obskure node moduler for å løse dette.

Tankene pendlet så tilbake. Hvordan løse det da?

Vel, UDP-serveren er allerede i sin egen tråd. Denne forer klient konteiner objektet med pakkene etter hvert som de kommer. Den bryr seg lite om hva som skjer med dem videre. På tilsvarende måte har den en ut kø som den tømmer ved første anledning. Igjen ikke noe den bruker mye krefter på. En kunne laget en SQL-kø på samme måte. Sende inn SQL-kallet med parameter og en callback til eget objekt. Helt hvordan dette skal gjøres i praksis vet jeg ikke enda, men litt moro må en ha for fremtiden også.

«På papiret virket det veldig spennende, men i praksis er det nok andre språk som egner seg bedre til akkurat dette.»

Konklusjonen min er at Node JS er overhodet ikke egnet til slike prosjekter.

Har du en "liten" fil som kan styre alt og bruke moduler til å hjelpe til med arbeidet, er det flott.

Skal du fordele oppgaver rundt i en struktur hvor ting må kunne snakke med hverandre på eget initiativ, og ikke bare som et resultat av at en "master-modul" styrer alt, virket det som om Node har noen mangler.

På papiret virket det veldig spennende, men i praksis er det nok andre språk som egner seg bedre til akkurat dette.

Og med det, byttet jeg IP-adressen tilbake, fyrte opp klienten og warpet ensom inn i solnedgangen.