(Mod. otsikoi aiheen.)
nio sockets!
Minulle on suositeltu palvelimeni käyttöön nio socketteja, mutta, en tunne niitä lainkaan, aloittaisin tästä yhden ketjun, jonka tarkoituksena on opettaa minulle kuinka ohjelmoidaan yksinkertainen time server, serveriin voisi olla samaan aikaan vaikkapa max 512 käyttäjää, ja serveri ohjelman ainoa tehtävä on palauttaa kellon aika asiakkaalle, tämä esimerkki olisi riittävä jotta voin sitten rakentaa monimutkaisempia palvelin ohjelmia tarpeisiini.
Löysin tämän seuraavan linkin kun googlailin -> http://www.exampledepot.com/taxonomy/term/196
Sieltä sitten olisi tarkoitus alkeet kasata tieto taitoon.
Voisiko joku hieman rakentaa minulle vaiheita ylös joita tarvitse yllä olevan palvelimen rakentamiseen, en itse ole siis ohjelmoija, harrastan java pelejä ja tämä netti liikenne puoli on aika tuntematonta vielä.
Kirjoitan huomenna ekoja esimerkki vaihtoehtoja tänne ja toivoisin että mahdollisimman tarkasti saisin ymmärtää mitkä kohdat lähestymis tavassani sitten olisi parempi rakentaa toisin.
---
kpzpt kirjoitti:
nio sockets!
Voisiko joku hieman rakentaa minulle vaiheita ylös joita tarvitse yllä olevan palvelimen rakentamiseen, en itse ole siis ohjelmoija, harrastan java pelejä ja tämä netti liikenne puoli on aika tuntematonta vielä.
Tässä vaiheet, näitä käytetään töissäkin kun oikeesti koodataan:
1) Asian opiskelu
2) Koodaus
Joo, siis tuolla linkin takanahan on valmiit esimerkit melkeinpä kaikesta mahdollisesta. Sitten vain muista seurata esimerkkejä äläkä virittele mitään omia "parannuksiasi", joista yleensä on vain haittaa.
(Mod. yhdisti aiheet. Turha tässä on joka Java-luokalle uutta keskustelua tehdä.)
Serversocket !
Jos käytän NET socketteja, eli näitä joita perus javan serversocketti luovuttaa.
Jos en laita threadia jokaiselle input/output streamille, vaan kutsuisin threadia käyttöön mainloopista vain kun sellaista tarvitsee.
Read stream:
Timerin avulla kerran sekunnissa hurautan 25 ms sockettimeouttin readin vapaalla threadilla ja näin lukisin saapuvia TCP paketteja.
Ymmärränkö oikein kun ajattelen että saapuva data menee aina automaattisesti java muistiin, vai jääkö se modeemiin.
Entä jos tulee kaksi pakettia ennen kuin tyhjennän sockettia read avulla, hukkuuko toinen paketti, määritelläänkö tämä receivebuffersize ja sendbuffersize toimilla ?
Send stream:
Myöskin en tekisi asiakkaille omia send threadeja vaan käyttäisin threadia vain kun asiakkaalle on tarkoitus lähettää paketti, ja tämän jälkeen thread joka on lähettänyt menee suspend tilaan, odottamaan uutta osoitetta ja messagea jonka voi taas lähettää.
On helpompaa asettaa jokaiselle input ja output streamille oma thread, mutta, mitä tuumaatte tästä tavasta, jonka esitin, vaikka jos olisi max 4096 asiakasta ja 2048 threadia käytössä, niin pitäisin 1512 read varten ja 512 send, joita sitten mainloop kutsuu käyttöön tarpeen mukaan, ja käytön jälkeen aina suspend tila päälle.
Olenko taas tollo ja jos olen niin perusteluja, ajattelin tälläisen rakentaa.
---
Juustonaksu!
kpzpt kirjoitti:
Ymmärränkö oikein kun ajattelen että saapuva data menee aina automaattisesti java muistiin, vai jääkö se modeemiin.
Siis hetkinen, puhutaanko tässä nyt jossain adsl-modeemissa toimivasta palvelimesta jossa ajetaan Javaa, vai miten Java voisi aiheuttaa jonkin jäämisen modeemiin?
kpzpt kirjoitti:
On helpompaa asettaa jokaiselle input ja output streamille oma thread, mutta, mitä tuumaatte tästä tavasta, jonka esitin
Eli oliko ehdottamassasi tavassa jotain muitakin "hyötyjä", kuin että se on vaikeampaa?
Mielestäni ohjelmoinnissakin voi soveltaa Occamin partaveistä. Eli kilpailevista, yhtä toimivista menetelmistä tulisi valita kaikkein yksinkertaisin. Tästä tosin käytetään ohjelmoinnissa yleisemmin lyhennettä KISS.
KISS!
Mietin vain kun tässä minun serversocket palvelimessani kytkeytyi ainoastaan noin 3700 threadia, ennen muistin loppumista ja tahtoisin rakentaa ainakin tuollaiset 5000 asiakasta kestävän servu ohjelman, liekö tämä nio sitten se ainoa oikea ratkaisu, kuten minulle on hieman jo monet vihjailleetkin ?
Mutta, minulla on myöskin epäselvää tämä kuinka socket käsittelee saapuvaa dataa, kuinka kauan socket muistaa datan joka on saapunut, entä voiko sockettiin lähettää useita paketteja ilman että sockettia lukee pakettien välillä, kertyvätkö saapuvat paketit jonoon, josta lukea, vai kirjautuvatko paketit aina edellisen ylitse ?
Edit.
Entä jos lukee socketteja 1ms sockettimeout kera, jos laittaakin vain 1024 threadia valmiuteen vaikka asiakkaita olisikin tuo max 5000 ?, ja näillä 1024 kappaleella sitten hurauttelee input/output streameja lävitse, esimerkkinä tuo read sockettimeout 1ms.
Täytyypä rakentaa pari testiä ;)
---
NIO on minusta käytännöllisempi ratkaisu, eikä sitä ole edes yhtään hankalampi käyttää. Jos kuvauksesi dataliikenteestä pitää paikkansa, yksikin säie voisi hyvin riittää lukemiseen. Rinnalle voi tarvittaessa koodata tarkistuksen, että erityisen harvakseltaan liikennöivät socketit tarkistetaan muita harvemmin.
Oletko miettinyt lainkaan sitä vaihtoehtoa, että käyttäisit tavallista HTTP-palvelinta ja kommunikoisit appletista HTTP-pyynnöillä? Näin sinun ei tarvitsisi itse huolehtia palvelimen verkkotoiminnoista. Äärimmillään et tarvitsisi myöskään applettiin verkkotoimintoja, vaan voisit toteuttaa kirjautumisen ja verkkotoiminnot näppärästi JavaScriptilla, esimerkiksi jQuery- tai Prototype-kirjaston valmiilla AJAX-rakennelmilla.
Edit: Jos ja kun tiedät, että palvelimesi käyttöjärjestelmässä socketin available antaa luotettavan tuloksen, saat tavallisillakin socketeilla käytännössä 0-timeoutin, kun tarkistat ennen readia, että socket.getInputStream().available() > 0
. Muista kuitenkin silloin tarkistaa jollain tavalla, onko vastapuoli sulkenut socketin. Ajoittainen pingaus on tähän yksi ratkaisu.
NIO!
Kyllä vain, nio on parempi, minulla on nyt yksinkertainen timestamp serveri ja se toimii ihan ok, olen nyt asettanut sen non-blocking moodiin, ja ainoa ongelma on se että bytet saapuvat hieman mitenkä saapuvat, eli ei joka luku kerralla kaikkia vielä olekkaan luettuna, tämä on se nio piirre joka minulla on aiheuttana huolta, mutta, ajattelin sitten palvelimessa ratkoa sen sillä tavalla, että, laitan vaikkapa paketin ensimmäiset ja viimeiset characterin vaikkapa vaikka "***" eli kolmella tähdellä, enkä sitten käytä pelipaketeissani tähteä lainkaan.
Vielä täytyy tuota selectoria hieman lueskella, se taitaa myös olla aika yksinkertainen, mutta, minulla on muutama kiusallinen exception vielä ratkomatta.
---
kpzpt kirjoitti:
bytet saapuvat hieman mitenkä saapuvat, eli ei joka luku kerralla kaikkia vielä olekkaan luettuna
Ongelma tuskin johtuu NIO:sta. Satuitko tietämään, että TCP-yhteydessä ei ole paketteja, vaan data käsitellään aina yhtenäisenä virtana?
kpzpt kirjoitti:
laitan vaikkapa paketin ensimmäiset ja viimeiset characterin vaikkapa vaikka "***" eli kolmella tähdellä
Ei kuulosta kovin järkevältä ratkaisulta. Fiksumpaa on joko laittaa paketin alkuun tieto paketin pituudesta tai käyttää jotain yksittäistä lopetusmerkkiä. Jos siirrät tekstimuotoista dataa, voit hyvin katkaista paketin esimerkiksi rivinvaihdolla ('\n') ja lukea tietoa rivi kerrallaan. Toinen tyypillinen lopetustapa on 0-tavu.
Metabolix kirjoitti:
kpzpt kirjoitti:
bytet saapuvat hieman mitenkä saapuvat, eli ei joka luku kerralla kaikkia vielä olekkaan luettuna
Ongelma tuskin johtuu NIO:sta. Satuitko tietämään, että TCP-yhteydessä ei ole paketteja, vaan data käsitellään aina yhtenäisenä virtana?
Metabolix on oikeassa. Itse yleensä omissa vuoropohjaisissa peleissäni yksinkertaisesti vastaanotan tavu kerrallaan ja muodostan vastaanotetuista tavuista merkkijonon. Kun merkkijonon loppuun on saatu rivinvaihto, niin komento on valmis.
Esim. omassa Limbolla kirjoitetussa shakkipelissäni Infernolle käytän tällä hetkellä alla olevan kaltaista säiettä lukemaan vastustajan syötettä verkosta.
workerthread(conn : Connection, player : int) { workerpid = sys->pctl(0, nil); # pid talteen, jotta voidaan tappaa väkivalloin. buf := array [1] of byte; # Avaa datatiedosto lukemista varten. Serverin tapauksessa hyväksyy myös saapuneen yhteyden. rdfd := sys->open(conn.dir + "/data", Sys->OREAD); output := ""; # Tähän kootaan syöte merkki kerrallaan. # Luetaan dataa, kunnes vastapuoli sulkee yhteyden. while( (n := sys->read(rdfd, buf, len buf ) ) > 0 ) { output[len output] = int buf[0]; # Lisätään vastaanotettu merkki syötemerkkijonon päähän. if(len output >= 2) { if(output[len output - 2:] == "\r\n") { # Käsitellään tarvittaessa nimimerkin asetusviesti. if(len output > 7 && output[:5] == "NICK ") { (count, nil) := sys->tokenize(output[5:], " "); if(count == 1 && len output[5:len output - 2] <= 12) { if (player == WHITE) cmd(mainwin, ".p2name configure -text " + output[5:len output - 2]); else cmd(mainwin, ".p1name configure -text " + output[5:len output - 2]); cmd(mainwin, "update"); output = ""; } } else { # Muussa tapauksessa lähetetään vastaanotettu rivi pelisäikeelle # mahdollisena vastustajan siirtona. remotecmdch <- = output[:len output - 2]; output = ""; } } } if (len output >= 1024) # Rivi liian pitkä jo? Heitetään pois. output = ""; } # Vastapuoli sulki yhteyden. Ilmoitus pelisäikeelle. gamecmdch <- = "close"; }
Palvelimen ulimit !
Minulla on nyt ensimmäinen nio pohjainen palvelin testi valmiina, testi vain sisältää threadeja ( < 10 ) joita tulen käyttämään ja testi hyväksyy connectit ja myös vastaan ottaa paketteja ja toimii testi ajossa ( alle <1000 konnektia ) ihan hyvin, tosin vaatii System.gc() käskyä ajoittain.
Nyt on kuitenkin hassusti, näin, että, tämä nio, kun käyttää socketteja, niin, se laskee aina yhden avoimen socketin avoimeksi fileeksi, tämä on nyt ongelma että en tiedä kuinka nostaa ubuntussa tätä avointen fileitten määrää, olen yrittänyt "ulimit -n 10240", mutta, linux sanoo että "toiminto ei ole sallittu", olen yrittänyt myös sudon kera, mutta ei toimi ?
Kuinka tämä "ulimit -n" arvo asennetaan tuohon 10240 ja voiko tästä olla koneen muussa käytössä ongelmia, vai onko minun jotenkin kytkettävä nuo nio socketit jotka ovat avoin pois file kytkennästä ?
Kuinka tämä ulimit ongelma tulisi oikein käsitellä ?
---
Mainitsen jälleen, että kannattaa opetella myös hakemaan tietoa. Kirjoitin Googleen "ubuntu open files limit" ja ensimmäisessä löytyneessä sivussa kerrottiin:
edit /etc/security/limits.conf and add : [username] soft nofile 4096 [username] hard nofile 4096
Eli tuonne määrittelet sille tunnukselle, jolla softaasi ajetaan, isommat limitit. Jos laitat ne itsellesi, niin kirjaudu ulos ja uudelleen sisään jotta tulevat voimaan.
ulimit!
Kysymykseni oli laajempi mitä google tarjosi, kun etsin, "ulimit too many open files".
Eli, onko systeemin muulle toiminnalle haittaa jos nostan tuon filerajan, ja myös onko mahdollista vain kytkeä nuo nio socketit pois fileistä, niin, että ne käsitellään vain socketteina ?
Kiitos, kuitenkin tuosta, testaan hetimiten..
---
Taas hölmösti syytät NIO-rajapintaa, vaikka sillä ei ole mitään tekemistä asian kanssa. Yrititkö edes selvittää asiaa? Kokeilitko tavallisilla Socketeilla? UNIXissa kaikki socketit ovat tiedostokuvaajia (file descriptor), joten tilanne on sama NIO:lla ja ilman.
Rajan nostamisesta voi teoriassa olla lievää muistin- ja tehonkäyttöhaittaa. Käytännössä tällä ei pitäisi olla merkitystä.
Muista myös, että TCP sisältää vain 65536 porttia. Tämä siis viime kädessä rajoittaa mahdollista käyttäjämäärää, kun testailet omalta koneeltasi.
Heh!
Nyt on metabolixilla jo iltapullat syötynä, en ole syyttänyt niota, minä ihmettelin vain että voiko tuon yhteyden irroittaa jotenkin niosta, eli että socketteja ei käsiteltäisikään file descriptoreina, mutta, tuo kohta on jo ratkennut asetin 10240 open filettä.
TCP Java nio on valintani, en enää tule muuttamaan valintaani, nyt on enää valittavana tämä TCP virran paloitteleminen ja mitenkä näitä tieto paloja sitten käsitellään, jotta, peli siirrot ovat helpoiten luettavissa.
Ja mitä tuohon portein käyttöön, niin ajattelin käyttää maximi kahta taikka kolmea, yksi jossa siirretään serverin tilaan kohdistuvia paketteja, ja yksi josta välitetään suoraan palvelimen huoneissa olijoille paketit, eli yhtä TCP porttia voi kyllä käyttää myös monta eri henkilöä, tuhansia, minun kaistani on 100 mbit ja tuo yksi portti siirtää sen kyllä, mutta en ihan noin korkeata siirtokaistaa tietenkään tarvitse kuitenkaan, eli mutta, tuo serversocket kyllä osaa jakaa porttiin saapuvat tiedon muruset sitten oikeisiin socketteihin.
UDP vaihtoehdon kävin lävitse ensimmäisenä, mutta en koe sitä yhtä käytännöllisenä mitä koen tämän juuri oppimani nion.
---
Applet jossa nio !
Tässä olisi taas testi ajoa niille jotka vielä jaksavat, täytyy nyt sanoa että en ala hypittää niitä joita kiinnostaa ihan joka vaiheen kanssa, mitä tässä palvelin rakentelussa on edessä, mutta, nyt on sellainen isompi hetki, eli, palvelin on toiminnassa ajoittain ja lähettää vastaus viestin aina kun sinne jotakin syöttää, tällä hetkellä AsiakasOhjelmani ainoastaan laskee PING viivettä, viiveessä on mukana muutama millisekuntti ohjelmallista viivettä, ei sitä ihan hampaat irvessä viitti tarkistella näitä saapuvia paketteja, siellä on pieni java thread.sleep (ms) mukana.
tässä nämä samat vanhat linkit ->
http://temp4322.dy.fi/PeliAppletti_Windows.html
http://temp4322.dy.fi/
PING viive lukee harmaassa yläpalkissa.
Lähettelen seuraavia testi pyyntöjä sitten kunhan on ekat pelit ja menut sitten linjoilla.
---
Palvelimen maksimi käyttäjät on säädettynä 2048 ja maksimi käyttäjät jotka voidaan palvelimeen säätää on 8192, en sitten tiedä mitenkä palvelin kestää ajoa kun kaikki pikku tärkeä koodi on mukana, kuten pelien tallentelut, ja eri pelien pakettien välittäminen, tämä PING mikä tässä nyt on, niin heilauttaa tehot johonkin 30% tienoille, kun testaan 2048 käyttäjän kanssa.
Myöskin olen huomannut että jokin osa koodissani syö RAM muistia aika reippaasti, minulla on System.gc() eli tämä kuuluisa Java garbage collecti päällä kerran 2500 ms, mutta, kuitenkin muistin kulutus kasvaa mitä enemmän socketteja laitetaan, minulla on sockettien lisäksi vielä stringbufferit joihinka lähtevät ja vastaanotettavat paketit tallentuvat, en sitten tiedä mikä niitten default kapasiteetti on, taikka myöskään mikä on yhden SocketChannelin default buffersize receiveen ja sendiin ?
---
Turha sitä pingiä on appletillasi testata, kun palvelimelle tulee enintään pari käyttäjää kerralla. Pitäisi olla jokin vakava suunnitteluvirhe, jos tuossa tilanteessa ping menisi huonoksi.
Tein sen sijaan oman testausohjelman, joka avasi suuren määrän socketteja ja pingasi niillä 100 kertaa. Sadalla socketilla keskiarvo oli 0,05 sekuntia ja pisin yksittäinen aika 0,09 sekuntia. Tuhannella socketilla keskimääräinen aika oli noin 0,3 sekuntia ja pisin yksittäinen aika 3,5 sekuntia. Toki testausohjelmani on hieman puutteellinen, testi ei vastaa todellista käyttöä ja linja voi mennä tukkoon monessakin välissä.
Muuten, appletissa voisit ihan hyvin käyttää tavallista blokkaavaa sockettia, koska siellä on vain yksi yhteys.
Olen edelleen myös sitä mieltä, että StringBuffer-kikkailustasi on luultavasti enemmän haittaa kuin hyötyä.
Hmmh!
En siis tietenkään testaa aikaa jonka ping kestää, vaan yleisesti, että toimiiko minun palvelin ohjelmani ja asiakasohjelmani yhdessä.
Appletissani käytetään samaa socketti tapaa mitä palvelimessa, en tarkkaan tunne kuinka onnistuisi, mutta, olin tähän asti ymmärtänyt että socket ja socketchannel eivät toimisi yhdessä.
Mitä tarkoitat stringbuffer kikkailulla, minulla ei ole tiedossa mikä on stringbufferin perus bufferin koko, ja se aiheuttaa ihmettelyjä, tässä tilanteessa kun 2048 sockettia ja 2x2048 stringbufferia sisältävä palvelin ohjelma rupeaa installissa viemään yli 125 megaa muistia ? jotakin on tuossa varmaankin vielä pielessä, mutta javakin haukkaa aika ison osan käydessään, täytyy varmaankin käydä testi ajamassa ohjelmiani netbeans profielerin kera, jotta tarkasti näen mikä ohjelman osa tuon mega määrän haukkaa.
---
kpzpt kirjoitti:
olin tähän asti ymmärtänyt että socket ja socketchannel eivät toimisi yhdessä.
Voit aivan hyvin yhdistää NIO-palvelimeen tavallisella socketilla tai tavalliseen palvelimeen NIO-socketilla – molemmissa tapauksissa kyseessä on tavallinen TCP-yhteys.
kpzpt kirjoitti:
Mitä tarkoitat stringbuffer kikkailulla
Sitä, että yleensäkään varaat mitään StringBuffereita. Mihin tarvitset niitä? Lähetettävän datan voisi suoraan lähettää, ja vastaanottamiseen byte-taulukko olisi kaikin puolin loogisempi.
Jos on siis tarkoitus siirtää tekstimuotoista dataa, yhteyden voisi toteuttaa tähän tapaan:
import java.io.*; class Yhteys { private InputStream input; private OutputStream output; private int pituus, luettu; private byte[] puskuri; public Yhteys(InputStream input, OutputStream output) { this.input = input; this.output = output; this.puskuri = new byte[1024]; } public void laheta(String str, Object... args) throws IOException { laheta(String.format(str, args).getBytes()); } public void laheta(String str) throws IOException { laheta(str.getBytes()); } private synchronized void laheta(byte[] data) throws IOException { output.write((data.length) & 0xff); output.write((data.length >> 8) & 0xff); output.write((data.length >> 16) & 0xff); output.write(data); } public synchronized String vastaanota() throws IOException { if (luettu == pituus) { if (input.available() < 3) { return null; } pituus = luettu = 0; pituus += input.read(); pituus += input.read() << 8; pituus += input.read() << 16; if (puskuri.length < pituus) { puskuri = new byte[pituus]; } } while (input.available() > 0) { int ret = input.read(puskuri, luettu, pituus - luettu); if (ret == -1) { throw new IOException("EOF"); } luettu += ret; if (luettu == pituus) { return new String(puskuri, 0, pituus); } } return null; } }
Virheentarkistukset tuohon toki pitäisi vielä lisätä, ja lisäksi suljetusta socketista lukeminen ei aiheuta virhettä (koska available on silloin nolla), joten sulkeminen pitää havaita muuten (esim. kirjoittamalla tai pingin avulla). Mutta idea lienee selvä. Tällä luokalla lähettäminen ja vastaanottaminen onnistuvat näin helposti:
Yhteys y = new Yhteys(socket.getInputStream(), socket.getOutputStream()); y.laheta("Tiesitkö, että %d + %d = %d?", 1, 1, 2); String str = y.vastaanota(); if (str == null) { System.out.println("Vastausta ei tullut vielä..."); } else { System.out.println("Vastattiin: " + str); }
Palvelin ohjelmistosta!
Minulla on tällä hetkellä neljä luokka threadia palvelin ohjelmassani ja käytän siis nio non-blocking tcp socketchanneleja.
Threads ->
ReadNWriteToHardDisk - tämä thread hoitaa pelien tallentamisen ja lataamisen kovalevyltä(lle).
AcceptConnections - tämä thread vastaanottaa uusia käyttäjiä palvelimeen.
ReadSocketChannels - tämä thread lukee loopissa jossa sleep (100ms), kaikkia socketteja jotka ovat avoinna, data tallennetaan stringbuffereihin.
WriteSocketChannels - tämä thread kirjoittaa loopissa jossa sleep (100ms), kaikkia socketteja joittenka stringbuffereissa on dataa.
PeliPakettienKäsittelyThread - tämä thread käsittelee kaikki palvelimeen saapuvat paketit ja toimii pakettien mukaan, mutta, on vielä kirjoittamatta.
SocketVariables - tämä luokka sisältää kaikki socket ja stringbuffer muuttujat joita käytetään palvelin ohjelmistoissa.
Minulla on kaksi pitkää stringbuffer jonoa, en vielä ole määritellyt kuinka pitkät asetan, mutta tuhansia kuitenkin, eli, lähteville paketeille vaikka 10000 stringbufferia [] ja saapuville samanverran.
Nämä käsitellään sillä tavoin, että tuo WriteSocketChannels thread lukee tuota lähtevien pakettien stringbuffer jonoa ja lähettää aina kun sinne ilmestyy dataa, ja tämä vastaanottava thread sitten lukee vastaanotettuihin stringbuffer [] jonoon ja sitä sitten käsitellään vielä yhdessä ylimääräisessä threadissa jossa on peli ja serveri pakettein käsittely koodit, mutta tämä vastaanotetun datan käsittely on vielä kirjoittamati.
Uskoisin että kirjoitan koko koodin vielä muutaman kerran uuteen järjestelyyn.
---
kpzpt kirjoitti:
...eli, lähteville paketeille vaikka 10000 stringbufferia [] ja saapuville samanverran.
Nämä käsitellään sillä tavoin, että tuo WriteSocketChannels thread lukee tuota lähtevien pakettien stringbuffer jonoa ja lähettää aina kun sinne ilmestyy dataa, ja tämä vastaanottava thread sitten lukee vastaanotettuihin stringbuffer [] jonoon ja sitä sitten käsitellään vielä yhdessä ylimääräisessä threadissa jossa on peli ja serveri pakettein käsittely koodit, mutta tämä vastaanotetun datan käsittely on vielä kirjoittamati.
Kuten Metabolix on yrittänyt selittää, tarvitsetko oikeasti noin hirveän määrän stringbufferia?
Lähetys ja vastaanotto tapahtuu kuitenkin aina tavuissa ja yksi tavu ei välttämättä ole yhtä, kuin yksi merkki (esim. UTF-8). Eikös nuo vastaanotto ja lähetyspuskurit voisi yksinkertaisesti olla tavutaulukkoja? Tavutaulukosta voit muuntaa merkkijonon ja antaa eteenpäin viestit käsittelevälle säikeelle käsiteltäväksi.
Selittää!
Minulla on tarkoitus rakentaa palvelin joka kykenee palvelemaan n.2-5 tuhatta asiakasta kerrallaansa, käytän stringbuffer jonoja joihinka tallennan tiedon, ja sitten toisessa threadissa käsittelen tätä stringbuffer jonoa.
Minusta on turha puhua siintä onko tarpeen käyttää stringbuffereita vaiko tavubuffereita, molemmissa olisi sama tieto ja molemmat täytyy käsitellä, tämä on vain tallennus tapa ja koen että sb on helpompi ratkaisuna.
Entä kuinka rakentaisin palvelimen joka kykenee 5000 asiakkaaseen samanaikaisena ja useita paketteja sekuntti jokaiselta, ilman mittavaa tavupuskuri jonoa taikka sb jonoa ? tosin myönnän että en ole kuitenkaan ihan lopulliseen versioon asti vielä ajatellut palvelin ohjelmaani ;)
Edit.
Käytän UTF-8 merkistö koodausta.
---
Juurihan sinä itse ihmettelit, mihin sitä muistia hukkuu. Väitän, että StringBuffer ja **-viritelmäsi vie selvästi enemmän sekä muistia että tehoa kuin esittämäni taulukkoratkaisu ja viestin alussa ilmoitettava pituus.
Muutenkin väärin tai huonosti toteutettu koodi yleensä on myös hitaampi ja raskaampi kuin hyvin toteutettu koodi, joten kaikki nämä vuosien saatossa antamamme vinkit eivät suinkaan ole pilkunviilausta vaan ihan totista totta siitä, miten hyviä ja tehokkaita ja toimivia ohjelmia tehdään.
Sitä paitsi UTF-8-enkoodauksella voi käydä niin, että kun palvelimellesi saapuu esimerkiksi ä-kirjain (kaksi tavua), koodisi vastaanottaa ensin yhden tavun ja vasta myöhemmin toisen. Jos yrität joka vastaanoton jälkeen muuttaa tavuja tekstiksi, seuraa virhe, koska puolikasta ä-kirjainta ei voi muuttaa, vaan tarvitaan molemmat puolet samaan aikaan. (Aivan kuten jalski yllä sanoi.)
kpzpt kirjoitti:
Palvelin ohjelmistosta!
Minulla on kaksi pitkää stringbuffer jonoa, en vielä ole määritellyt kuinka pitkät asetan,
Olet tainnut tämän(kin) asian ymmärtää väärin. StringBuffer:lla ei ole mitään tiettyä kokoa. Sinne voi pistää tekstiä "loputtomiin", se kasvattaa sisäistä kokoaan tarpeen mukaan. Eli siis todennäiköisesti tuosta ei ole käyttötarkoituksestasi mitään hyötyä.
kpzpt kirjoitti:
Uskoisin että kirjoitan koko koodin vielä muutaman kerran uuteen järjestelyyn.
Kerrankin kirjoitat jotain, joka voi pitää paikkaansa, minäkin uskon!
Metabolix kirjoitti:
Sitä paitsi UTF-8-enkoodauksella voi käydä niin, että kun palvelimellesi saapuu esimerkiksi ä-kirjain (kaksi tavua), koodisi vastaanottaa ensin yhden tavun ja vasta myöhemmin toisen. Jos yrität joka vastaanoton jälkeen muuttaa tavuja tekstiksi, seuraa virhe, koska puolikasta ä-kirjainta ei voi muuttaa, vaan tarvitaan molemmat puolet samaan aikaan. (Aivan kuten jalski yllä sanoi.)
Ongelmaahan ei tietysti ole, jos rajaa pelin käyttämät viestit englannin kielisiin komentoihin, mutta tavu kerrallaan vastaanotettaessa merkkijonomuunnoksen ongelma on silti hyvä tiedostaa.
Alla esimerkkinä Infernolle Limbolla toteutettu Chat-Serverin vastaanotto säie, mikä vastaanottaa tavu kerrallaan ja muodostaa vastaanotetuista tavuista UTF-8 merkkejä.
workerthread(conn : Connection) { buf := array [32] of byte; # Pitäisi riittää. # sys->listen palauttaa Connection adt:n, jossa dfd = nil # Käyttäjän vastuulla on avata data-tiedosto itse ja samalla # hyväksyä yhteys. Data-tiedostoa lukemalla ja kirjoittamalla hoidetaan # tiedon vastaanotto ja lähetys. rdfd := sys->open(conn.dir + "/data", Sys->OREAD); # data-tiedosto rfd := sys->open(conn.dir + "/remote", Sys->OREAD); # remote-tiedosto # remote-tiedostosta lukemalla saadaan vastapuolen osoite selville. n := sys->read(rfd, buf, len buf); remote:= string buf[:n-1]; # Ilmoitetaan managerille uudesta asiakkaasta. commandch <- = ("new", "", "", (remote, conn)); output := ""; error := ""; buf = array [1] of byte; # Luetaan tavu kerrallaan. buf2 := array [3] of byte; # Unicode merkki 1-3 tavua. counter := 0; # Laskuri unicode tavuille. while( (n=sys->read(rdfd, buf, len buf ) ) > 0 ) { buf2[counter] = buf[0]; # Vastaanotettu tavu unicode tavu puskuriin. (c, size, status) := sys->byte2char(buf2[:counter + 1], 0); if(status == 0 && size == 1) { # Virheellinen unicode. output[len output] = c; # Tallenna virheellisestä syötteestä ilmoittava unicode. # Jos vain yksi tavu luettu, niin takaisin alkuun. if(counter == 0) continue; # Siirretään virheellisen tavun jälkeinen tavu unicode puskurin alkuun. buf2[0] = buf2[counter]; counter = 0; (c, size, status) = sys->byte2char(buf2[:counter + 1], 0); } if(status == 0 && size == 0) { # Tavu OK, mutta unicode merkki ei valmis. counter ++; if(counter <= 2) continue; # takaisin while-loopin alkuun. } if(status != 1) continue; # unicode merkki valmis ja OK. output[len output] = c; # Tallenna unicode merkki. counter = 0; if(len output > 1024) { # Viesti hiukan liian pitkä? output = ""; # Heitetään pois. continue; } # Tarkistetaan viestit ja lähetetään tarvittaessa komentokanavan kautta # managerille lisätoimia varten. if(len output >= 2) { if(output[len output - 2:] == "\r\n") { if(len output > 7 && output[:5] == "NICK ") { (count, nil) := sys->tokenize(output[5:], " "); if(count == 1) { commandch <- = ("nick", output[5:], "", (remote, conn)); output = ""; } else error = "EXPECTED NICKNAME WITH NO SPACES ALLOWED\r\n"; } else if(len output > 8 && output[:6] == "MSGTO ") { (count, tokens) := sys->tokenize(output[6:], " "); if(count >= 2) { commandch <- = ("msgto", output[6 + len hd tokens + 1:], hd tokens, (remote, conn)); output = ""; } else error = "EXPECTED MESSAGE TARGET AND MESSAGE\r\n"; } else if(len output > 6 && output[:4] == "MSG ") { commandch <- = ("broadcast", output[4:], "", (remote, conn)); output = ""; } if(output != "") { if(error == "") error = "UNKNOWN COMMAND MESSAGE\r\n"; commandch <- = ("unknown", error, "", (remote, conn)); output = ""; } error = ""; } } } # Ilmoita managerille, että asiakas sulki yhteyden. commandch <- = ("bye", "", "", (remote, conn)); }
jalski kirjoitti:
Ongelmaahan ei tietysti ole, jos rajaa pelin käyttämät viestit englannin kielisiin komentoihin
Silloin taas olisi typerää puhua UTF-8-enkoodauksesta, kun merkistö on rajoitettu ASCIIhin. Sitä paitsi kun kerran Javassa on natiivi tuki laajemmalle merkistölle, tokihan sitä kannattaa käyttää (varsinkin suomalaisella sivustolla).
Errorit koodissani!
Joo, tuossa minulla oli bugi ohjelmassani, kun tuo TCP siis siirtää bytebufferiin ja todellakin jos nio siirto keskeytyy tällöin kun vain tämän esimerkin ä kirjaimen ensimmäinen byte on siirtynyt, niin, UTF-8 rutiini ei sitten ilmeisesti osaisikaan kääntää tekstiä oikein, hienoa, tämä oli minulle uutta.
Minä itsekkin mietin tuon että laittaisin paketin koon ensimmäisiin tavuihin, mutta en sitäkään pitänyt tarpeellisena, ajattelin laittaa kaksi "**" tähteä jokaisen "paketin" loppuun, mutta, tämä voi vähän kämähtää ainakin chat viestien kohdalla.
Kiitosta avusta, minä laitan tuon bytebufferin jokaiselle socketille, ja lisäilen sitten siihen näitä saapuvia kirjaimia, mietin myös tätä että josko käyttäisin sittenkin tuota paketin kokoa viestin alkuun käytäntöä, taitaa olla parempi luottaa vaan teihin ja tehdä sitten vastaisuudessa juuri noin.
Sockettien bytebufferit sitten paketti käsittely rutiinissa joko luen sellaisenaan taikka käännän ennen käsittelyä stringbufferiin, encode UTF-8 avulla, täytyy jotain testi ajoja ajaa kumpi on sitten nopeampaa, käsitellä stringbuffereita vaiko tutkailla bytebuffereita.
---
Aihe on jo aika vanha, joten et voi enää vastata siihen.