Update
MongoDB probleemi sain osaliselt lahendatud, nüüd saab binary andmeid edasi ja tagasi GridFS vahel jooksutada. Küll aga tekkis uus probleem - kuna see klient on suhteliselt “noor”, siis GridFS’i andmete salvestamine striimina töötab korrektselt, aga lugemine mitte. Ehk et kirjutada võin jupikaupa, aga välja lugeda saan ainult korraga (liides teeb küll nägu, et annab mulle vajadusel jaokaupa infot, aga tegelikkuses on tal terve fail mällu loetud). not cool
Mailiserveri tagasilöök
Tundub, et sain esimese tõsise tagasilöögi oma emailiserveri projekti juures - MongoDB GridFS moodul on pisut poolik ja ei tööta veel päris nii nagu vaja. Minu konkreetne probleem seisneb selles, et ma ei suuda lugeda korralikult faile, mis sisaldavad suuremaid baite kui 127 (ehk siis need baidid, kus 8nda positsiooni väärtus on seatud). Millegipärast on kõik selliste baitide väärtuseks 253 (FD).
Kokkuvõttes - ma ei saa kirja manuseid kettale salvestada. Tava-failisüsteem on suur ei-ei, aga GridFS asemele ei ole ka nagu midagi kuskilt võtta.
Primitiiv ja objektitüübid JavaScriptis
JavaScriptis saab numbreid, teksti jms esitada nii primitiivväärtusena kui ka objektina. Tehete puhul ei ole kummalgi variandil erilist vahet - numbrite liitmisel on tulemus ikka sama, hoolimata sellest kas kumbki operand on primitiivnumber (n. 10) või objekt (n. new Number(10)), vastus on ikka täpselt sama.
var nr_primitiiv = 10,
nr_objekt = new Number(10);
nr_primitiiv + nr_objekt; // 20
Samuti saab primitiivtüübina defineeritud väärtust kasutada sama tüübi objekti väärtusena, sellisel juhul teisendatakse väärtus operatsiooni läbiviimise ajaks objektiks.
var nr_primitiiv = 10;
nr_objekt = new Number(10);
nr_objekt.toFixed(2); // "10.00"
nr_primitiiv.toFixed(2); // "10.00"
Kusjuures viimast saab teha ka otse numbri endaga - arvestada tuleb ainult, et numbri ja punkti vahele (objekti meetodi eraldaja) tuleb jätta tühik, kuna vastasel juhul peab interpretaator punkti mitte meetodi, vaid komakoha eraldajaks.
10 .toFixed(2); // "10.00"
Samuti saab kasutada nurksulge, sellisel juhul pole vaja isegi tühikut kasutada
10["toFixed"](2); // "10.00"
Erinevused
Seega, mis kasu on üldse primitiivväärtuse objektina defineerimisel, kui toimingute läbiviimisel pole mingit vahet?
Tuleb välja, et üks erinevus siiski on. Selle erinevuse tuvastamiseks kontrollime kõigepealt väärtuste tüüpe.
typeof 10; // "number"
typeof new Number(10); // "object"
Nagu näha, on tavanumbri tüübiks number, aga objektina defineeritud väärtuse tüübiks oodatult object. Mis aga on nende kahe tüübi vahe? Vahe on väärtuse edastamises - kui primitiivväärtuse edastamisel edastatakse väärtus (by value), siis objekti edastamisel edastatakse objekt ise (by ref).
Kuna aga numbriobjekt on objekt, siis saab sellele lisada omadusi ja meetodeid nagu kõikidele teistele objektidele ning sellise numbriobjekti edastamisel (näiteks funktsiooni parameetrina) liiguvad need lisatud meetodid ja omadused numbriobjektiga kaasa.
var nr_objekt = new Number(10);
// defineeri numbriobjektile täiendav meetod
nr_objekt.teavita = function(){
alert(this);
}
// funktsioon ootab sisendiks objekti meetodiga "teavita"
function teavitus(nr){
nr.teavita();
}
// edasta numbriobjekt funktsioonile - by ref
teavitus(nr_objekt); // alert(10);
Ainukseks probleemiks on vaid see, et taolise objekti numbriväärtust ei saa enam muuta. Muutes objekti primitiivväärtust, lõhutakse objekt selle väärtuse ümber. Küll aga saab muuta väärtust, mis maailmale välja paistab, vt. järgmist punkti.
Veel rohkem lahedust
Numbriprimitiivi ja numbriobjekti liitmisel õnnestub tehe seetõttu, et objekti väärtuse puhul ei kasutata mitte objekti ennast, vaid selle meetodit valueOf, mille väärtuseks on samuti numbriprimitiiv.
10 + new Number(10).valueOf()
JavaScriptis aga on enamvähem kõik ülekirjutatav. Seega võib proovida üle kirjutada ka valueOf meetodit.
var a = new Number(10);
a.valueOf = function(){
return 5;
}
5*a; // 25 (5 * 5)
NB!
Kasutasin siin näitena vaid numbriprimitiive ja -objekte, aga sama laieneb ka kõikidele teistele väärtustele (string, boolean). Erineb vaid objekti väärtuse teisendamise meetod, stringi puhul on selleks valueOf asemel toString. Täpsemalt saab vaadata kuidas väärtusi konteksti alusel teisendatakse siit.
Objektid JavaScriptis
Panen siia kirja väikese ülevaate objektidest JavaScriptis.
Objektid JavaScriptis on lihtsad võtme-väärtuste paaride kogumid, millega on täiendavalt seotud veel ka kontekstimuutuja this. Juhul kui võtme väärtuseks on funktsioon, nimetatakse seda võtit objekti meetodiks, muudel juhtudel aga omaduseks. Objektideks võib pidada lisaks spetsiaalselt defineerituile JavaScriptis ka kõiki teisi kasutatavaid väärtusi, sealhulgas numbreid, teksti, loogilisi väärtusi, funktsioone jne. Kõikidel nendel väärtustel on olemas omad omadused ja meetodid. Näiteks numbriprimitiiv 10 ei ole iseeneest küll objekt, vaid 64 bitine number, aga proovides kasutada seda numbrit objekti kontekstis (n. 10 .toFixed(2)), teisendatakse primitiivtüüpi number taustal tehte läbiviimise ajaks automaatselt Number tüüpi objektiks.
Kui teistes keeltes defineeritakse reeglina kõigepealt looodavat objekti kirjeldav klass ja seejärel tuletatakse loodud klassi alusel objektid (kusjuures samast klassist tuletatud objektid on struktuurilt identsed), siis JavaScriptis klassi mõiste puudub ja objektide loomine on tunduvalt vabam. Alustada võib näiteks tühjast objektist (selleks saab kasutada objektiliteraale, loogelisi sulge var tyhi_objekt = {};), millele saab siis hiljem vajadusele vastavalt uusi omadusi/meetodeid lisada ning vanu kustutada.
Klasside asemel on JavaScriptis kasutusel nn. prototüüpobjektid. Ühe objekti prototüübiks võib olla suvaline teine objekt. Kui erinevad objektid on üksteise prototüüpideks, tekib sellega prototüübiahel - iga ahelas olev objekt saab kasutada kõiki ülemistes kihtides defineeritud prototüüpide omadusi ja meetodeid kui endagi omi. Juhul kui muuta prototüüpobjektis mõnd meetodit või omadust, muutub see automaatselt ka kõikides sellest prototüübist põlvnevates objektides. Samas ülespoole muutused ei kajastu - muudatused objektis selle prototüüpi ei mõjuta. Juhul kui objektis kirjutada mõni prototüübilt päritud omadus või meetod üle, katkeb selles kohas ahel ning objekt saab endale “isikliku” väärtuse (seda kas tegu on “isikliku” või päritud väärtusega, saab kotrollida meetodiga hasOwnProperty).
Ühetüübiliste objektide loomiseks saab klasside asemel kasutada konstruktorfunktsioone, kuid konstruktor pole sisuliselt midagi muud kui otsetee prototüüpobjekti kloonimisest ja initsialiseerimisfunktsioonist. Kuna JavaScripti esialgne disain jäi suhteliselt poolikuks, siis kuni praeguseni on konstruktorid olnud ainsad reaalsed (brauseriülesed) vahendid prototüübiahelate loomiseks. Alles ECMAScript5 tutvustas käsklust Object.create, mis võtab parameetriks loodava objektile omistatava prototüübi. Osad brauserid on mõnda aega toetanud ka objektide omadust __proto__, mis on otselingiks prototüüpobjekti juurde. Tegu on siiski mittestandardse ja vähe toetatud omadusega, seega reaalselt seda kasutusele võetud kunagi ei ole.
Objektide kirjeldamine
Objekte kirjeldatakse loogeliste sulgude vahel olevate komadega eraldatud väljadega, kus väljad koosnevad võtme nimest ja väärtusest. Võtme nime (võib olla nii jutumärkide vahel, kui ka ilma) järel on võtit ja selle väärtust eraldav koolon.
{
omadus: "väärtus1",
"meetod": function(){}
}
NB! Tähele tasub panna, et võti ”omadus” on esitatud ilma-, aga ”meetod” koos jutumärkidega. Jutumärkide kasutamine on oluline juhul kui võtme nimes on sümboleid, mida ei saa muutuja nimes kasutada, näiteks kui soovitakse kasutada miinusmärki või tühikut.
{
"eba-standardne nimetus":"väärtus"
}
Objekti omadustele ja meetoditele saab ligi kahel viisil - a) punktnotatsiooniga, kus objekt ja selle võtme nimetus on eraldatud punktiga või kui võtme nimetus on nurgelistes sulgudes objekti taga.
var objekt = {"nimi":"objekt"};
objekt.nimi === objekt["nimi"];
Kontekstimuutuja “this”
Iga objektiga on seotud ka kontekstimuutuja this mis võimaldab objekti meetodites pääseda ligi sama objekti teistele omadustele ja meetoditele. this sõltub ka skoobist. Kui tegu ei ole otseselt objekti meetodiga (näiteks anonüümne funktsioon meetodi sees), siis viitab this globaalsele objektile, milleks brauseri puhul on alati window.
var objekt = {
meetod: function(){
this == objekt; // true, meetodi skoop
function sisemine(){
this == window; //true, anonüümne skoop
}
}
}
Konteksti on võimalik ka funktsioonile ise ette anda, kasutades selleks funktsiooni meetodeid call ja apply. Sisuliselt on tegu samade meetoditega, ainult et kui call edastab argumendid nii nagu ta need ise saab, siis apply kasutab funktsioonile edastatavateks argumentideks sisendina saadud massiivi väärtusi.
function kontekst(){
alert(this==window);
}
kontekst(); // true, vaikimisi kontekstis
kontekst.call({}); // false, kontekstiks on anonüümne objekt
Alati ei ole aga vaja funktsiooni kohe käima panna, vaid millalgi hiljem (sündmuste jms tagasikutsed). Selliseks juhuks võimaldab ECMAScript5 kasutada meetodit bind, mis seob funktsiooni kontekstiga. Kuna paljud brauserid veel ECMAScript5 ei toeta, tuleb nendes see meetod ise defineerida.
var context = {text:"text to be displayed"};
function callback(){
alert(this.text);
}
window.onload = callback.bind(context);
NB! meetod bind mitte ei käivita funktsiooni ja ei tagasta selle tagastusväärtust nagu näiteks call ja apply, vaid tagastavad sama funktsiooniobjekti, kuid koos seatud kontekstiga! Väga lihtsustatult näeb sidumine välja nii:
Function.prototype.bind = function(context){
var func = this;
return function(){
func.call(context);
}
}
Funktsiooni meetodi näites saab func väärtuseks käesoleva funktsiooni enda. Seejärel tagastatakse väljakutsekohta anonüümne funktsioon. Kui nüüd see funktsioon käima panna (eelnevas näites window.onload sündmuse korral) käivitataksegi juba rida func.call(context) ehk et käivitatakse originaalne funktsioon, millele määratakse teine kontekst (sulundi kaudu tulnud context).
vene keele õpik
Kuna mu vene keel lonkab kõiki jalgu, tellisin enda järeleaitamiseks Amazonist õpiku, mis seda olukorda veidi parendaks. Standardne lähenemine tundus igav ja valisin hoopis ühe mingi ime-meetodi alusel koostatud raamatu.
Õpikus kasutatud meetod nimelt ei pööra absoluutselt mitte mingisugust rõhku näiteks grammatikale või kirjale üldse, tegu on hoopis ladina tähtedes kuulmise järgi kirja pandud sõnadega. Meetodi idee seisneb selles, et inimene peab kõigepealt õppima rääkima ja aru saama, küll ta hiljem tegeleb siis vene kirjakeele omapäradega.
Kuna aga “kuulmise järgi kirja pandud” ei ole mitte eesti-kuulmise, vaid ikka ingliskeelse kuulmise alusel, osutusid kõige keerulisemaks hoopis mitte sõnad ise, vaid nende sõnade kuju desifreerimine, aru saamaks kuidas need üldse kõlada võiks ja kuidas neid tegelikult kirjutatakse.
Näiteks proovige aru saada mis sõnad need on:
- deeveetee
- eelee
- tiseechya
- cheesi
- mi khateem
Võibolla mingi valemi alusel on need kohe arusaadavad, aga eesti päraselt lugedes ei saanud mina esimese hooga küll mitte millelegi pihta.
Vastused on siin:
- девяти
- или
- тысяч
- часы
- мы хотим
panin mime formaadis kirjade dekodeerija eraldi mooduline githubi välja. teeb lihtsaks e-kirjade raw source sisu teisendamise masinloetavaks objektiks.
Mime-stream
Sain lõpuks ometi tööle enam-vähem töötava mime formaadis striimi parseri. See tähendab, et ühest otsast visatakse mime-formaadis kirja sisu parserile jaokaupa ette ja teisest otsast tuleb struktureeritud objekt välja. Kui keegi viitsib jälge ajada, siis detaile saab näha siin.
Momendil on kõige suuremaks teoreetiliseks probleemiks, kui mime boundary satub kogemata poolikuna edastatavate andmete hulka. See tähendab, et andmed tulevad umbes 10kB juppidena ning on vähemalt teoreetiline võimalus, et see vajalik serva tähis tuleb pooleldi ühe ja pooleldi teise andmehulgaga. Niikaua aga kui seda ei juhtu, tundub kõik toimivat.
Kirja manustes olevate binaarsete andmetega ei tehta tegelikult hetkel veel midagi, loetakse ainult baitide arv kokku, kuid edaspidi hakkab minema binary stream otse faili (GridFS) ning e-kirja andmete objektis asendatakse manuse sisu võtmega manuse juurde GridFS andmebaasis. Nii ei ole vaja tervet manusefaili korraga mällu lugeda (see aga tähendab, et manuste suurus ei ole põhimõtteliselt oluline - katsetasin näiteks 16 MB manuse vastuvõtmist ja erilist koormust ei paistnud see tekitavat).
Avastasin ka, et juhul kui kiri sisaldab manuseid, kipuvad mitmedki e-posti kliendid panema kogu kirja sisu nested multipart bloki sisse. Välise kirja sees on veel üks väiksem kiri, mis siis sisaldab tegelikku kirja sisu. Sellisel juhul välises kirjas on a) vaid text/plain sisublokk või b) ei ole üldse midagi ja kogu kirja sisu tuleb multipart blokist otsida.
Igatahes, tundub, et kõik suuremad probleemid on praeguseks suunal saatja -> vastuvõttev server -> kliendile edastamine lahendatud. Lõpliku lahenduseni on veel palju maad, kuid see on rohkem juba tegemise vaev.
Kolisin blogi ümber
Kolisin blogi Tumblr.com teenusesse ümber (varem oli Blogspotis). Tegu on veidi mugavama lahendusega ja eriti oluline on, et Tumblr toetab teksti sisestamisel Markdown formaati. Eriti just koodinäidete puhul on kõik enamlevinud WYSIWYG toimetid täielikud jamad.
Blogi domeen jäi samaks, aga postituste aadressid läksid järjekordselt katki :S
Progressist JavaScript põhise e-posti serveri kirjutamisel
Mõnda aega tagasi kirjutasin kuidas sain valmis POP3 protokolli implementatsiooni Javascriptis. Sain sellest katsetusest pisiku külge ja otsustasin proovida luua samas stiilis täiemahulist e-posti serveri tarkvara. Väga kaugele pole jõudnud, alustalade seadmine on palju aega ja mõtlemist nõudnud, aga tundub, et kõik on ületatav. Hetke progressi saab näha siit.
Kuna eri serverid (SMTP, POP3, IMAP) on üles ehitatud käsklus-vastus stiilis tekstipõhiste serveritena, lõin sarnaste situatsioonide jaoks serveri baasobjekti, mis sisaldab kõike toimimiseks vajalikku ning mida tuleb lihtsalt konkreetse serveri vajadustest lähtuvalt uute käsklustega täiendada. Näiteks POP3 server saab defineerida käskluse “STLS”, aga SMTP hoopis “STARTTLS”. Nimetasin selle serveri baasobjekti Request-Answer-Interface‘ks ja selle source on nähtaval siin (faili lõpus on kommentaarina ka minimaalne implementatsioon). Täpsemat näiterakendust saab uurida failidest pop3.js ja smtp.js.
Hetke staatuseks on, et SMTP server suudab kirju vastu võtta ning töödelda täielikult läbi kirja päised. Järgmise sammuna on kavas siis kirja sisu töötlemine. Kuna kirja sisu võib olla väga suur (sõltuvalt manuste olemasolust ja suurusest) tuleb seda teha jupikaupa, mitte korraga ja see lisab ühe keerukusastme juurde. Kõige suurem probleem, Base64 dekodeerimine jupi kaupa on praeguseks lahendatud.
Süsteemist siis niipalju, et põhiosa on kirjutatud JavaScriptis (Node.JS), vähemalt üks moodul C’s (node-iconv), kasutajate autoriseerimise andmed ja sessioonide andmed asuvad Redis andmebaasis, kirjade sisu ja meta-info läheb MongoDB andmebaasi ning manused MongoDB GridFS failihoidlasse. GridFS annab mugava võimaluse jagada faile suure hulga serverite vahel - selleks tuleb luua MongoDB klaster ning selles saab failide jupitamise ja kokkukogumisega GridFS juba kergelt hakkama. Samuti saab faili juurde salvestada suure hulga suvalist meta-infot. See on midagi, mida tavaline failisüsteem ei võimalda.
Kogu idee asjal on selles, et reeglina hoitakse e-posti originaalkujul (mime formaat) tekstifailides. Kas siis kogu postkast on ühes suures failis või siis on eri kirjad eraldi failides samas kataloogis. Selge on see, et taoline struktuur ei skaleeru ja on üldse ebaefektiivne (näiteks manused salvestatakse kettal Base64 kodeeringus, mis on kolmandiku võrra suurem, kui originaalmaht jne). Minu lahendus hoiaks süsteemselt siis kõiki andmeid neile sobivaimas hoidlas (Redis, MongoDB, GridFS) ning nende andmete põhjal pandaks alles vajadusel (klient üritab kirja alla laadida või tuleb see serverist välja saata) kokku standardne mime formaadis e-kirja dokument.
Selline struktuur annab ka meeletu eelise näiteks veebipõhise e-posti kliendi loomiseks - pole vaja keskenduda millelegi muule kui kasutajaliidesele, kogu info on programmi jaoks loetaval kujul andmebaasis juba olemas, midagi teisendada pole vaja.
Base64 striimi parsimine
Kuna POP3 protokolli implementeerimine node.js platvormil õnnestus ootamatult hästi, proovisin katsetada ka maailmast kirjade vastuvõtmist SMTP protokolli abil. Ka see õnnestus üsnagi valutult (vähemalt see osa, mis protokolli puutub) - lisasin nimeserverisse MX kirje; kirjutasin lihtsa skripti mis kuulab porti 25 liiklust; skript tunnistas lihtsamaid vajaminevaid käske nagu HELO, DATA jne. Panin seejärel skripti tööle ja üritasin sellele Gmaili’ist kirja saata. Suureks üllatuseks töötas kõik väga kenasti.
Seega tuligi mõte teha valmis juba täismahuline e-maili server. Koos e-posti kontodega, kirjade saatmise ja vastuvõtmisega ning POP3 ja IMAP liidesega kliendi jaoks. Eks paista, kas sellest projektist ka midagi reaalselt välja tuleb, aga hetkel tundub et kõik vajalik on säärase lihtsama projekti jaoks olemas (st. et ei arvesta väga standardiväliste quirkide’ga jne).
Üheks esimeseks probleemiks tekkis kirja vastuvõtul manuste töötlemine. Manused on MIME formaadis kirjades lihtsustatult esitatud nii:
-----suvaline-unikaalne-ääre-tähis-ascii-tähdega+\r\n Content-Type: text/plain; name="manus.zip" Content-Disposition: attachment; filename="manus.zip" Content-Transfer-Encoding: base64 pOb3RlczoNCg0KTmV2ZXIgaW1wbGVtZW50IGEgSlVNUCB0YWcgd2l0aG91dCBhIGNvcnJlc3Bv bmRpbmcgQUQgdGFnLCBhcyB0aGlzIHdpbGwgcmVzdWx0IGluIG5vIGltcHJlc3Npb25zIG9yIGNs aWNrcyBiZWluZyBjb3VudGVkIGZvciB0aGUgYXNzb2NpYXRlZCB0YW [....] -----suvaline-unikaalne-ääre-tähis-ascii-tähdega+\r\n
Kuna reeglina on manuseks mitte tekstifailid, vaid binaarsed failid, on kõikide baitide probleemivabaks transpordiks kasutusel base64 kodeering. Base64 kasutab vaid 64 võimalikku sümbolit, mis kõik on “prinditavad”, mis tähendab et ühe “päris” baidi asemel (kaheksa bitti) saab ühe sümboliga edasi anda vaid 6 bitti (64 = 26).
Probleem tekib suurte manustega - juhul kui manus pole näiteks mitte 2 kB või 100 MB siis kogu selle faili korraga mällu lugemine ja seejärel base64->baitideks teisendamine kõlab üsna kahtlase väärtusega ideena. Seega tuleks teisendus teha juba faili sisselugemisel. Node.JS näiteks ei annagi kogu infot korraga edasi, vaid lõikab sissetuleva info umbkaudu 10 kB suurusteks juppideks.
socket.on("data", function(data){
console.log(data); // data suurus ei ole suurem kui 10kB
});
Sellise umbmäärase pikkusega lõikude korral aga ei ole base64 dekodeerimine alati võimalik. Oluline on õigest sümbolist alustada ja õigest lõpetada, vastasel korral läheb bitijada nihkesse ja seetõttu tekivad ka baidid väga valed.
Kuidas seda olukorda siis lahendada, nii et saaks dekodeerida base64 andmestriimi?
Väga lihtsalt, kui algus on teada, saab sellest andmeid teisendada 4 baidiste lõikudena ning ülejääk jätta järgmise korrani.
var current = "";
socket.on("data", function(data){
// NB! tegelikult on andmetüüp mitte string vaid Buffer!
current += data;
base64_decode(current.substr(0, current.length-current.length % 4));
current = current.substr(-current + data.length % 4); // jääk
});
Juhul kui tegu oleks tavalise striimiga, siis piisaks ka määrangust, et striimi kodeeringuks on base64. MIME tüüpi kirja puhul aga on tegu ühe pika stringiga, kus kodeeringud vahelduvad vastavalt kontekstile. Seega tuleb ise jälgida mis andmetega parasjagu tegemist on.