Hajautettujen sovellusten muodostamistekniikat (Java-kielellä), TKO_2014 Aineopinnot, syksy 2009 Turun yliopisto / Tietotekniikka



Samankaltaiset tiedostot
HOJ Säikeet (Java) Ville Leppänen. HOJ, c Ville Leppänen, IT, Turun yliopisto, 2012 p.1/55

Hajautettujen sovellusten muodostamistekniikat, TKO_2014 Johdatus kurssiin

Luento 6. T Ohjelmoinnin jatkokurssi T1 & T Ohjelmoinnin jatkokurssi L1. Luennoitsija: Otto Seppälä

Concurrency - Rinnakkaisuus. Group: 9 Joni Laine Juho Vähätalo

Ohjelmoinnin peruskurssien laaja oppimäärä

Rinnakkaisohjelmointi kurssi. Opintopiiri työskentelyn raportti

Projekti 1 Säikeet ja kriittisen vaiheen kontrollointi javalla

Liite 1. Projektin tulokset (Semaforit Javassa) Jukka Hyvärinen Aleksanteri Aaltonen

Monitorit -projekti Rinnakkaisohjelmointi

812315A Ohjelmiston rakentaminen. Asynkronisuus

Käyttöjärjestelmät: poissulkeminen ja synkronointi

12. Javan toistorakenteet 12.1

11. Javan toistorakenteet 11.1

JAVA on ohjelmointikieli, mikä on kieliopiltaan hyvin samankaltainen, jopa identtinen mm. C++

Graafisen käyttöliittymän ohjelmointi Syksy 2013

HSMT J2EE & EJB & SOAP &...

HOJ Haja-aiheita. Ville Leppänen. HOJ, c Ville Leppänen, IT, Turun yliopisto, 2012 p.1/10

Ohjelmoinnin jatkokurssi, kurssikoe

Rinnakkaisuus. parallel tietokoneissa rinnakkaisia laskentayksiköitä concurrent asioita tapahtuu yhtaikaa. TTY Ohjelmistotekniikka

Ohjelmassa henkilön etunimi ja sukunimi luetaan kahteen muuttujaan seuraavasti:

Rinnakkaisohjelmointi, Syksy 2006

14. Poikkeukset 14.1

JAVA-PERUSTEET. JAVA-OHJELMOINTI 3op A JAVAN PERUSTEET LYHYT KERTAUS JAVAN OMINAISUUKSISTA JAVAN OMINAISUUKSIA. Java vs. C++?

Ohjelmointitaito (ict1td002, 12 op) Kevät Java-ohjelmoinnin alkeita. Tietokoneohjelma. Raine Kauppinen

Informaatioteknologian laitos Olio-ohjelmoinnin perusteet / Salo

Sisällys. 14. Poikkeukset. Johdanto. Johdanto

4. Luento: Prosessit ja säikeets. Tommi Mikkonen,

Sisällys. 11. Javan toistorakenteet. Laskurimuuttujat. Yleistä

Lohkot. if (ehto1) { if (ehto2) { lause 1;... lause n; } } else { lause 1;... lause m; } 15.3

12. Javan toistorakenteet 12.1

1 Tehtävän kuvaus ja analysointi

Yleistä. Nyt käsitellään vain taulukko (array), joka on saman tyyppisten muuttujien eli alkioiden (element) kokoelma.

Metodit. Metodien määrittely. Metodin parametrit ja paluuarvo. Metodien suorittaminen eli kutsuminen. Metodien kuormittaminen

9. Periytyminen Javassa 9.1

Sisältö. 2. Taulukot. Yleistä. Yleistä

Ohjelmoinnin peruskurssien laaja oppimäärä

HOJ J2EE & EJB & SOAP &...

Sisältö. 22. Taulukot. Yleistä. Yleistä

Ohjelmoinnin perusteet Y Python

1. Mitä tehdään ensiksi?

Lohkot. if (ehto1) { if (ehto2) { lause 1;... lause n; } } else { lause 1;... lause m; } 16.3

812341A Olio-ohjelmointi Peruskäsitteet jatkoa

Rajapinta (interface)

Sisällys. 15. Lohkot. Lohkot. Lohkot

Olio-ohjelmointi Javalla

Sisällys. Metodien kuormittaminen. Luokkametodit ja -attribuutit. Rakentajat. Metodien ja muun luokan sisällön järjestäminen. 6.2

11/20: Konepelti auki

Sisällys. 14. Poikkeukset. Johdanto. Johdanto

10 Lock Lock-lause

Javan perusteita. Janne Käki

Oppimistavoitteet kurssilla Rinnakkaisohjelmointi

5. Luento: Rinnakkaisuus ja reaaliaika. Tommi Mikkonen,

815338A Ohjelmointikielten periaatteet

P e d a c o d e ohjelmointikoulutus verkossa

Poikkeustenkäsittely

Harjoitus Olkoon olemassa luokat Lintu ja Pelikaani seuraavasti:

Sisällys. 6. Metodit. Oliot viestivät metodeja kutsuen. Oliot viestivät metodeja kutsuen

14. Poikkeukset 14.1

T Henkilökohtainen harjoitus: FASTAXON

Ohjelmointi 2 / 2008 Välikoe / Pöytätestaa seuraava ohjelma.

2 Konekieli, aliohjelmat, keskeytykset

Sisällys. 12. Näppäimistöltä lukeminen. Yleistä. Yleistä

Taulukot. Jukka Harju, Jukka Juslin

Oliosuunnitteluesimerkki: Yrityksen palkanlaskentajärjestelmä

Ohjelmointitaito (ict1td002, 12 op) Kevät Java-ohjelmoinnin alkeita. Tietokoneohjelma. Raine Kauppinen

Java-kielen perusteet

Java kahdessa tunnissa. Jyry Suvilehto

Javan semaforit. Joel Rybicki, Aleksi Nur mi, Jara Uitto. Helsingin yliopisto

Palvelut. Sulautetut järjestelmät Luku 2 Sivu 1 (??) Sulautetut käyttöjärjestelmät

Opintojakso TT00AA11 Ohjelmoinnin jatko (Java): 3 op. Poikkeukset ja tietovirrat: Virhetilanteiden ja syötevirtojen käsittely

Vertailulauseet. Ehtolausekkeet. Vertailulauseet. Vertailulauseet. if-lauseke. if-lauseke. Javan perusteet 2004

Java-kielen perusteita

Se mistä tilasta aloitetaan, merkitään tyhjästä tulevalla nuolella. Yllä olevassa esimerkissä aloitustila on A.

Tietotekniikan valintakoe

Alkuarvot ja tyyppimuunnokset (1/5) Alkuarvot ja tyyppimuunnokset (2/5) Alkuarvot ja tyyppimuunnokset (3/5)

Opintojakso TT00AA11 Ohjelmoinnin jatko (Java): 3 op Taulukot & Periytyminen

812347A Olio-ohjelmointi, 2015 syksy 2. vsk. X Poikkeusten käsittelystä

Sisällys. 12. Javan toistorakenteet. Yleistä. Laskurimuuttujat

on ohjelmoijan itse tekemä tietotyyppi, joka kuvaa käsitettä

Mikä yhteyssuhde on?

Ohjelmoinnin perusteet, kurssikoe

Jaana Diakite Projekti 1 JAVA-Monitorit 1(13) Rinnakkaisohjelmointi Anu Uusitalo

Ohjelmoinnin perusteet Y Python

Java-kielen perusteita

Ohjelmistojen mallintaminen, sekvenssikaaviot

Ohjelmointi 2 / 2010 Välikoe / 26.3

JavaRMI 1 JAVA RMI. Rinnakkaisohjelmoinnin projekti 1 osa C Tekijät: Taru Itäpelto-Hu Jaakko Nissi Mikko Ikävalko

ITKP102 Ohjelmointi 1 (6 op)

13. Loogiset operaatiot 13.1

58131 Tietorakenteet ja algoritmit (syksy 2015)

812341A Olio-ohjelmointi, IX Olioiden välisistä yhteyksistä

Sisällys. 16. Lohkot. Lohkot. Lohkot

Luokat ja oliot. Ville Sundberg

Tehtävä 1. Tehtävä 2. Arvosteluperusteet Koherentti selitys Koherentti esimerkki

Tutoriaaliläsnäoloista

Zeon PDF Driver Trial

Listarakenne (ArrayList-luokka)

HSMT Tietokannoista. Ville Leppänen. HSMT, c Ville Leppänen, IT, Turun yliopisto, 2008 p.1/32

UML -mallinnus TILAKAAVIO

Java-kielen perusteet

Transkriptio:

Hajautettujen sovellusten muodostamistekniikat (Java-kielellä), TKO_2014 Aineopinnot, syksy 2009 Turun yliopisto / Tietotekniikka c Ville Leppänen

This page is intentionally left blank.

Esipuhe Tämän oppimateriaalin tarkoituksena on opettaa kahteen kokonaisuuteen liittyviä asioita. Nämä kokonaisuudet ovat (1) verkon käyttöön perustuvien asiakas-palvelin sovellusten tekeminen niin asiakkaan kuin palvelimenkin osalta ja (2) säikeiden käytön hallinnan opettaminen. Näiden kahden kokonaisuuden alle mahtuu runsaasti yksityiskohtia ja ohjelmoinnin kannalta kokonaisuudet on jäsennetty useiksi rajapinnoiksi. Toisaalta tarkasteltavat kokonaisuudet ovat selkeästi osa nykyaikaista ohjelmointia. Oppimateriaali käsittelee edellisiä asioita lähinnä Java-kielen kautta tarkastellen erilaisia välikerrostekniikoita (middleware). Opettamisen kannalta konkretisointi jonkin kielen kautta on välttämätöntä, vaikka se osaltaan haittaakin kokonaisuuksiin liittyvien Javasta riippumattomien yleisten käsitteiden ja ratkaisumenetelmien tunnistamista JDK:n kirjastojen yksityiskohtien viidakosta. Useimmat kokonaisuudet esimerkiksi soketit ja XML ovat täysin Javasta riippumattomia, vaikka ne onkin jäsennetty JDK:n paketeiksi. JDK:n mielessä materiaali on kirjoitettu käyttäen enimmäkseen JDK 1.4:ää. Yleisesti ottaen lähes kaikki tässä käsitellyt asiat toimivat JDK:n versioissa 1.2 1.6. Säikeiden osalta tässä esitettävä materiaali pohjautuu aikoinaan Timo Raidan kirjoittamaan materiaaliin. Säikeet ovat keskeisiä käytettäessä tietoverkkoa sovelluksissa. Erityisesti palvelinsovellukset nojautuvat säikeisiin hyvin voimakkaasti. Säikeitä käytetään myös GUI-pohjaisessa asiakassovelluksessa. Asioiden käsittely jatkossa perustuu pitkälti esimerkkien antamiseen käsiteltävistä aiheista. Luokkia (ja sitä kautta ohjelmia) materiaalissa esitetään noin 20 kappaletta. Joihinkin tarkasteltaviin asioihin liittyy varsin vankka teoria siitä, miten sovellukset kuuluisi rakentaan käyttämään tarkasteltavaa asiakokonaisuutta. Toisten kohdalla on vain hyvin runsaasti yksityiskohtia mutta vähemmän teoriaa soveltamisesta. Yksityiskohtien osalta materiaali ei pyri olemaan tyhjentävä lähinnä tarkoituksena niiden osalta on muodostaa lukijalle näkemys siitä, mitä kaikkea on tarjolla, miten eri konstruktioita tulisi käyttää ja mitä niillä voi saada aikaan. Lopuksi, verkon käyttäminen on hauska ja kenties jopa jännittävä asia oppia toivotan miellyttäviä ahaa-elämyksiä! Turussa, lokakuussa 2009, Ville Leppänen i

ii

Sisältö 1 Johdanto 1 1.1 Internet-verkon taustaa................................. 1 1.2 Samanaikaisuuden taustaa............................... 2 1.3 Oppimateriaalin rakenne................................ 2 2 Säikeet 5 3 Säikeet (by Timo Raita) 9 3.1 Perusteita........................................ 9 3.2 Säikeen luonti ja käynnistys.............................. 11 3.3 Säikeiden välinen kontrollin hallinta.......................... 14 3.4 Resurssien jakaminen.................................. 18 3.5 Säieryhmät ja demonit................................. 26 4 Johdatus hajautettuihin sovelluksiin 29 4.1 Asiakas-palvelin sovelluksista yleensä......................... 30 4.2 Verkosta ja verkkoprotokollista............................. 32 5 Soketit ja niiden käyttö 35 5.1 Perustietoa soketeista.................................. 35 5.1.1 Luokka InetAddress.............................. 36 5.2 Yhteydetön UDP.................................... 37 5.2.1 Esimerkki UDP-paketin välityksestä...................... 38 5.3 Yhteydellinen TCP................................... 41 5.3.1 Palvelimen yleinen rakenne.......................... 43 5.3.2 Esimerkki Finger................................ 43 5.3.3 Esimerkki olion välittäminen TCP-yhteyden yli................ 44 5.4 Erikoisempia sokettien käyttötilanteita......................... 46 5.4.1 Ryhmälähetykset................................ 46 5.4.2 Appletit ja soketit................................ 47 6 RMI-pohjaiset sovellukset 49 6.1 Yleinen tilanne ja käsitteitä............................... 49 6.2 Mitä RMI-kutsussa tapahtuu?.............................. 50 6.2.1 Parametrien koodaus.............................. 52 6.3 Etäolioon liittyvät luokat................................ 52 6.4 Esimerkkejä....................................... 54 iii

iv SISÄLTÖ 6.4.1 Vain yksi etäolio................................ 54 6.4.2 RMI:n käyttäminen käytännössä........................ 56 6.4.3 Useita etäolioita käyttävä esimerkki...................... 57 6.5 Yleisiä periaatteita RMI-sovellusten muodostamisesta................. 63 7 RPC 65 7.1 Taustaa......................................... 65 7.2 Sovelluksen rakentaminen............................... 65 7.3 Esimerkki........................................ 66 7.4 Huomioita ja yhteenveto................................ 69 8 Corba 71 8.1 Taustaa......................................... 71 8.2 Sovelluksen rakentaminen............................... 73 8.3 Esimerkki........................................ 74 8.4 Mitä jäi kertomatta................................... 76 9 Verkkoprotokollat ja XML 77 9.1 Verkkoprotokollat.................................... 78 9.1.1 Taustaa..................................... 78 9.1.2 Protokollan tilat asiakkaan ja palvelimen kannalta............... 79 9.1.3 Protokollan muodostaminen ja kuvaaminen.................. 81 9.1.4 Protokollista luokiksi.............................. 84 9.1.5 Uusia painotuksia: REST- ja ohut käyttöliittymä-tekniikat.......... 86 9.2 Johdatus XML:ään................................... 88 9.3 XML:n käyttötilanteista................................. 89 9.4 Tiedon esittäminen XML:llä.............................. 91 9.4.1 DTD = Document Type Definition....................... 93 9.5 XML ja Java....................................... 95 9.5.1 DOM:n luokista................................ 95 9.5.2 Jäsentäminen Xercesillä............................ 95 10 WWW ja servletit 101 10.1 Johdanto......................................... 101 10.2 Lomakkeista....................................... 103 10.2.1 FORM:n määrityksistä............................. 103 Kenttä...................................... 103 Valintalista................................... 105 Tekstialue.................................... 105 Muita...................................... 105 10.3 Servlettien tekemisessä tarvittavia luokkia....................... 105 10.4 Esimerkkejä....................................... 106 10.4.1 Esimerkki HelloServlet............................. 106 10.4.2 Esimerkki hieman monimutkaisemmasta servletistä.............. 110 10.5 Lopuksi......................................... 113

SISÄLTÖ v 11 Tietokannoista 115 11.1 Relaatiotietokannoista.................................. 115 11.1.1 Mitä operaatioita kantaan kohdistettavissa?.................. 116 11.2 SQL-kielestä...................................... 117 11.3 JDBC.......................................... 119 11.3.1 Yhteys verkkotietokantaan........................... 119 11.3.2 Komentojen suorittaminen........................... 120 11.3.3 Luokista.................................... 120 11.4 MySQL esimerkki................................... 121 11.5 Lopuksi......................................... 128 12 Dokumentointi 129 13 Testaus 131 13.1 Testausmenetelmiä................................... 131 14 Katsaus muihin tekniikkoihin ja suoritusalustoihin 133 14.1 J2EE........................................... 133 14.2 SOAP ja web-palvelut.................................. 135 14.3.NET Remoting..................................... 136 14.4 J2EE........................................... 138 14.5 Virtuaalikoneet suoritusalustana............................ 140 14.6 Ohuet käyttöliittymät, SaaS ja pilvilaskenta...................... 142 15 Loppusanat 145

vi SISÄLTÖ

Luku 1 Johdanto Tämän oppimateriaalin tarkoituksena on johdatella lukija nykypäivän ohjelmointikieliin liittyviin moderneihin välikerrostekniikkoihin, joita käytetään hajautettujen sovellusten muodostamiseen. Kyseiset tekniikat eivät ole osa kieltä, vaan ne esiintyvät ohjelmointikielten yhteydessä kirjastojen kautta. Erityisesti tarkastellaan verkon olemassaoloon perustuvien asiakas-palvelin sovellusten tekemistä ja säikeiden kautta tutustutaan myös koneen prosessorivoiman parempaan hyödyntämiseen samanaikaisuuden 1 avulla. 1960- ja 1970-luvuilla (ja laajasti myöhemminkin) ohjelmointi on tarkoittanut lähinnä keskusmuistissa olevan (ja tiedostoista keskusmuistiin tuodun) tiedon käsittelyä algoritmisesti. Ohjelmia käytettiin tuolloin eräajon kautta tai alkeellisen riviorientoituneen käyttöliittymän ((paperi)pääte) kautta. Tietokoneita käytettiin silloin lähinnä eräajosovelluksiin, kuten palkanlaskennan tekemiseen. Vuorovaikutteinen käyttö (ja sitä kautta graafinen käyttöliittymä) ja hajauteutut järjestelmät tulivat merkittävään rooliin vasta paljon myöhemmin. 1.1 Internet-verkon taustaa Nykyisen Internet-verkon kehityksen juuret ovat 1970-luvun ARPAnet hankkeessa (USAn puolustusvoimat). Kehitys oli aluksi aika hidasta: 1970-luvun lopussa oli jo useita erillisiä verkkoja ja ARPAnet:iinkin oli kytketty jo noin 200 konetta. 1980-luku edusti räjähdysmäistä laajenemista ja kehitystä: viimeistään 1980-luvun lopussa 2 voitiin jo puhua maailmanlaajuisesta Internet-verkosta (jossa tuolloin oli jo noin 100 000 konetta). Internet:ssä hyvin yleisesti käytetty TCP/IP-protokollien perhe muotoitui jo suhteellisen aikaisin: 1970-luvun loppupuolella. Nykyisinkin verkkosovellukset tyypillisesti hyödyntävät tämän protokollaperheen protokollia kirjastojen kautta. Ohjelmointikielten yhteyteen verkko tuli kirjastojen kautta lähinnä Unix:n kehityksen myötä. Unix:n ensimmäiset versiot olivat olemassa jo 1960-luvun lopussa (Ken Thompson). Monien kehitysvaiheiden jälkeen ARPAnet:n takana oleva organisaatio (DARPA) näki myös Unix:n mahdollisuudet ja niinpä 1970- ja 1980-lukujen taitteessa DARPA rahoitti verkko-ominaisuuksien liittämistä Unix:iin. Koska Unix oli toteutettu pääsääntöisesti C-kielellä, oli tällä konkrettinen seuraus C-kielen kirjastojen kannalta: verkon ja sen protokollien käytölle toteutettiin tuki C:n kirjastoihin. Voisi siis ajatella, että 1 Rinnakkaisuus tarkoittaa usean kontrollivirran suorittamista yht aikaa usean prosessorin toimesta. Samanaikaisuus on rinnakkaisuuden heikompi muoto, jossa vähintäänkin näennäisesti samaanaikaan suoritetaan useita käskyvirtoja käytännössä samanaikaisuus usein toteutetaan yhden laskentayksikön avulla lomittamalla jotenkin käskyvirtojen suoritusta. Javan säikeet perustuvat samanaikaisuuteen. Aidosti rinnakkaisuuteen perustuvia kieliäkin on niiden voima tyypillisesti perustuu suoritettavien käskyvirtojen tiukasti synkroniseen suoritukseen. Usein rinnakkaisuus saavutetaan ohjelmissa ilman rinnakkaisuuskonstruktioita kielen tasolla; esim. MPI-kirjaston avulla. 2 Verkossa jo vuodesta 1985... 1

2 LUKU 1. JOHDANTO verkot tulivat varsinaisesti osaksi ohjelmointia 1980-luvun alkupuolella. 1980-luvulla syntyivät myös monet maailmanlaajuiset asiakas-palvelin sovelluskokonaisuudet, kuten esimerkiksi verkkouutiset. Verkkojen kehitys on jatkunut voimakkaasti 1980-luvun jälkeenkin ja on edelleenkin hyvin voimakasta. Syynä on lähinnä ollut WWW:n suosio ja viimeaikainen verkkojen yhtenäistymiskehitys. Yhtenäistymiskehitys on johtamassa siihen, että IP-pohjaiset verkot levittyvät (ja ovat jo osin levittäytyneet) kaikkialle: töihin, yhteiskunnan palveluorganisaatioihin, mobiililaitteisiin ja jopa kotiin (digitv, yms). Verkkojen merkitys osana ohjelmointia on selvästi noussut viime vuosina. 1.2 Samanaikaisuuden taustaa Rinnakkaisuuden (ja samanaikaisuuden) historia on myös hyvin tapahtumarikas 3. Rinnakkaisuutta on esiintynyt jo joissakin 1950-luvun lopun tietokoneissa, mutta varsinaiset ensimmäiset rinnakkaistietokoneet rakennettiin 1960-luvulla (Univac, CDC-sarjan koneet, ILLIAC IV). Rinnakkaiskoneiden ohjelmointia varten kehitettiin myös tukea korkean tason ohjelmointikieliin. Ensimmäiset tällaiset kielet olivat Fortran 66:n variaatioita, ja ne kehitettiin jo 1960-luvun lopussa. Sen jälkeen tukea koneen laskentavoiman mahdollisimman suureen hyödyntämiseen yhden ohjelman toimesta on kehitetty ohjelmointikieliin varsin eri tavoin. Moniajo tuli osaksi käyttöjärjestelmiä (esim. Unix) 1970-luvun puolivälissä ja sitä kautta ohjelmointikieliin (erityisesti C-kieli) tuli myös mahdollisuus käynnistää toisia säikeitä. Tämä edustaa samanaikaisuuden alkua. Rinnakkaisuuden toteuttaminen ja rinnakkaisuuden tukeminen ohjelmointikielen kannalta on kuitenkin vielä kehityslinja, joka ei ole ilmeisesti stabilisoitunut. Esim. Fortranin 1990-luvulla kehitetyt rinnakkaisuutta tukevat variaatiot ovat sellaisia, että niissä ei ohjelmointikielen tasolla ole edes prosessorin käsitettä (lähinnä rinnakkaisuus perustuu rinnakkaiseen silmukkaan ). Useita funktionaalisia kieliä on myös käytetty rinnakkaistietokoneiden ohjelmointiin. Viimeaikoina rinnakkaistietokoneiden ohjelmointi on usein perustunut jaetun muistin käsitteeseen (esim. Cray T3E tarjoaa tällaisen mahdollisuuden) ja/tai erilaisiin viestin välityskirjastoihin (MPI, PVM). Java ei ole rinnakkaisohjelmointikieli, vaan se tukee lähinnä samanaikaisuutta. Java-ohjelmia suoritetaan Java-virtuaalikoneen päällä (JVM, Java Virtual Machine). JVM tuntee säikeet ja eräissä toteutuksissa se jopa osaa käyttää useita prosessoreita ja käyttöjärjestelmätason säikeitä hyväkseen. Käyttöjärjestelmätason säikeiden (eli prosessien) ja Javan säikeiden välillä ei kuitenkaan ole mitään kiinteää 1:1-vastaavuutta. 1.3 Oppimateriaalin rakenne Ennen hajautettujen järjestelmien muodostamistekniikkojen tarkastelua tutustutaan ensin säikeisiin. Luvussa 3 kerrotaan säikeiden muodostamiseen ja käyttämiseen liittyvät yleisen asiat. Samalla pohditaan säikeisiin liittyvän samanaikaisuuden ja sen vaikutusten hallintaa. Javassahan on synchronized avainsana, jonka avulla voidaan estää säikeitä käsittelemästä esim. tiettyjä muuttujia samanaikaisesti. Vaikka säikeet eivät olekaan sama asia kuin rinnakkaisuus 4, niin tarkastelemalla samanaikaisuuteen 3 Gregory Wilson on koonnut verkkoonkin mielenkiintoisen historiikin (hae hakukoneella). 4 Rinnakkaisuus ei nykyään vielä ole osa perusohjelmointia. Kirjallisuudessa on kuitenkin esitetty hyvin perusteltuja kannanottoja sen puolesta, että ohjelmointia kuuluisi oikeastaan opettaa rinnakkaisohjelmointina peräkkäisohjelmoinnin sijaan. Syynä tähän on, että ongelmien ratkaisujen esittäminen rinnakkaisuuden avulla on luonnollisempaa kuin laskennan pakottaminen johonkin mahdollisista peräkkäisistä suoritusjärjestyksistä tällaisia järjestyksiähän on usein hyvin monia (mielivaltaisesti valittu peräkkäinen järjestys vaikeuttaa ohjelman merkityksen ymmärtämistä). Toinen rinnakkaisohjelmoinnin opettamista puoltava seikka on rinnakkaisuuden yleistyminen tietokoneissa. Varsin monissa tietokoneissa (ns. PC-koneissakin) on useita prosessoreita. Lisäksi (transistorien) pakkaustekniikka on kehittynyt viime aikoina huimasti:

1.3. OPPIMATERIAALIN RAKENNE 3 liittyvää problematiikkaa, tarkastellaan samantapaisia ongelmia kuin mitä liittyy aitoihin rinnakkaisohjelmointikieliin. Materiaali esitetään yhtenä lukuna, sillä se on edesmenneen Timo Raidan kirjoittamaa 5. Luvussa 2 summaan ne keskeiset asiat, joita lukijan tulisi oppia luvuista 3. Luvussa 4 teema vaihtuu säikeistä asiakas-palvelin sovelluksiin ja verkkoa hyödyntävään ohjelmointiin. Yleinen ja vanha tapa muodostaa asiakas-palvelin sovelluksia on käyttää C:stä (tai Unix:sta) tuttuja ns. soketteja. Tuolloin voi hyvin olla, että vain kommunikoinnin toinen osapuoli on Java-ohjelma. Verkkoyhteyden muodostamiseen sokettien avulla perehdytään luvussa 5. Sokettien avulla voidaan muodostaa lyhyt- ja pitkäkestoisia yhteyksiä. Pitkäkestoiset yhteydet perustuvat yhteydellisen TCP-protokolla käyttöön. Sokettien jälkeen luvussa 6 perehdytään Javan RMI-pakettiin, jonka avulla voidaan helposti muodostaa asiakas- ja palvelinsovellus, jotka kommunikoivat keskenään RMI:n välityksellä tietokoneverkon yli. Tässä tapauksessa kumpikin osapuoli on Java-ohjelma. RMI on lyhenne sanoista Remote Method Invocation, eli RMI on tapa kutsua etäällä toisen ohjelman muistissa (kenties toisessa koneessa) olevan olion metodia. RMI:tä vanhempi tapa kutsua toisen ohjelman muistiavaruudessa olevaa ohjelman osaa on RPCtekniikka (Remote Procedure Call). RPC:n lähtökohta on C-kielessä, vaikka siitä kyllä löytyy toteutuksia useille muillekin ohjelmointikielille Javalle ei suoraan ole toteutusta osana JDK:ta, mutta esim. XML-RPC:stä löytyy kyllä toteutus Javalle. RPC on toiminnaltaan ja toteutusperiaatteiltaan hyvin samanlainen kuin RMI. RPC:tä tarkastellaan luvussa 7. Historiallisessa mielessä RMI:tä edeltää myös Corba, jota tarkastellaan suppeasti luvussa 8. Corba on OMG (Object Management Group) luoma standardi (jolle löytyy useita toteutuksia; suppea toteutus on myös osa JDK:ta) etäolioiden määrittelemiseksi. Corba pyrkii olemaan RMI:stä poiketen yleinen, kieliriippumaton tapa tehdä hajautettuja järjestelmiä. Siihen liittyy myös tukea erilaisille korkeamman tason ominaisuuksille, kuten vikasietoisuudelle, kuormantasaukselle ja olioiden replikoinnille. RMI, RPC ja Corba kaikki rakentuvat TCP-sokettien päälle. Käytännössä ne määrittelevät tietynlaisen protokollan, jonka avulla kyseisen tekniikan mukaiset sovellukset voivat keskustella keskenään. TCP:tä käyttäville sovelluksille ominaista on, että osapuolten välinen kommunikaation etenee tietynlaista vuoropuhelua käyttäen tällöin usein ajatellaan, että protokolla on tietyssä tilassa. Protokollan tila-asioita käsitellään luvussa 9. Kyseisessä luvussa esitetään myös perusteet XML:n käyttämisestä Java-ohjelmien yhteydessä. Kun protokollalla välitetään tietoa sovelluksesta toiseen, välitettävä tieto pitää koodata jotenkin. XML soveltuu tällaiseen tiedon esittämiseen kohtuullisen hyvin. Java-ohjelmia käytetään verkossa varsin usein WWW:n yhteydessä. Appletteja voidaan käynnistää WWW-sivuilta ja selain osaa suorittaa niitä. Luvussa 10 käsitellään hieman suosittuun WWW:hen liittyviä asioita ja perehdytään erityisesti Javan servletteihin. Servletit ovat WWW-palvelimessa suoritettavia Java-ohjelmia, ja niitä voisi siten kutsua palvelinsovelmiksi. Vaikka edellisissä luvuissa onkin tarkasteltu palvelinpuolen toteuttamista itse ohjelmallisesti, niin usein palvelinsovelluksena viimekädessä toimii tietokanta. Luvussa 11 perehdytään lyhyesti tietokantojen käyttämiseen Java-ohjelmista. Luvuissa 12 ja 13 esitetään suppeasti huomioita hajautettujen järjestelmien dokumentoinnista ja testauksesta. Luvussa 14 puolestaan tehdään lyhyt katsaus muihin hajautettujen järjestelmien muo- Pentium-prosessorien toteuttaminen on vaatinut n. 5-10 miljoonaa transistoria, mutta pakkaustekniikan kehittyminen mahdollistaa moninkymmenkertaisen transistorimäärän (jopa miljardien) sijoittamisen yhden prosessorin alalle. Tätä ylimääräistä tilaa voi käyttää kahdella tavalla: muistina tai lisäämällä prosessorin rinnakkaisuutta. Uusimmat prosessorit sisältävät lähes poikkeuksetta rinnakkaista suoritusta tukevia piirteitä. 5 Kauan sitten itselläni oli Timo Raidan kanssa projekti usemman kurssin oppimateriaalin yhdistämiseksi isoksi kirjaksi, mutta sitä ei koskaan saatu vietyä loppuun asti.

4 LUKU 1. JOHDANTO dostamistekniikkoihin (erityisesti J2EE olisi saanut saada suuremman huomion) ja suoritusalustoihin. Loppusanat esitetään luvussa 15.

Luku 2 Säikeet Tämän luvun tarkoituksena on selvittää säikeisiin liittyvät käsitteet ja Java-kielen yhteydessä esiintyvät säikeisiin liittyvät konstruktiot ja luokat. Teoreettisen taustatarkastelun tarkoituksena on myös havaita säikeiden määrittelemän samanaikaisuuden mukanaan tuoma tiedon jakamiseen liittyvä problematiikka. Tämä problematiikka on Javan käyttämässä ratkaisussa, mutta Javan ratkaisuja ei kuitenkaan voi pitää huonoina, sillä samanaikaisuuden ja jakamisen problematiikka on hyvin syvällinen ja mitään universaalia ratkaisua ongelmiin ei kielen konstruktioiden avulla liene saavutettavissa (vaan ohjelmoijan täytyy ottaa vastuu tietynlaisten ongelmatilanteiden välttämisestä). Materiaalina käytetään Raidan Timon aikoinaan eri yhteyteen kirjoittamaa materiaalia sellaisenaan. Seuraavassa on lyhyesti lueteltu ne keskeiset seikat, jotka lukijan tulisi oppia Raidan Timon materiaalin (luku 3) perusteella. Säikeet ovat Javassa olioita. Ne luodaan tekemällä itse luokka, joka perii luokasta Thread tai rajapinnasta Runnable ja toteuttaa metodin run. Säikeen toiminnallisuus laitetaan run - metodiin, mutta itse run -metodia ei ohjelmassa tule suoraan kutsua, vaan säie-olion luonnin jälkeen start -metodin kutsuminen aiheuttaa itsenäisen säikeen käynnistymisen. Säikeisiin liittyy viisi tilaa: Uusi, Valmis, Suorituksessa, Estynyt ja Päättynyt. Tulee ymmärtää tilojen merkitys ja säikeiden siirtyminen tilasta toiseen (milloin, miksi ja mihin). Kun säikeen suoritus on päättynyt, niin kyseinen säie-olio on silti olemassa. Sitä ei voida enää käynnistää uudestaan, mutta säie-olion tietosisältöä voidaan toki havainnoida. Laskentaa suorittavien säikeiden kohdalla tämä antaa kätevän toimintatavan: Säikeen suorituksessa tuottamat tulokset talletetaan säie-olioon. Säikeisiin liittyvistä metodeista tulee ymmärtää erityisesti metodien sleep, yield, interrupt, join, wait, notify ja notifyall merkitys. Javan säikeitä suoritetaan samanaikaisesti ja mahdollisesti rinnakkain. Rinnakkaisuus tarkoittaa, että kahden tai useamman laskentayksikön (prosessorin) toimesta suoritetaan useita käskyvirtoja samalla ajan hetkellä. Samanaikaisuus on heikompaa : ohjelmassa on useita käskyvirtoja ja niiden kaikkien suoritus etenee, mutta samanaikaisuuden tapauksessa ei vaadita, että suorituksen pitäisi edetä samanaikaisesti tai samalla vauhdilla. Samanaikaisuuden tapauksessa voidaan ääritapauksessa toimia niin, että kaikkien säikeiden suoritus lomitetaan (jollakin tavalla) yhden prosessorin tehtäväksi. 5

6 LUKU 2. SÄIKEET Javan säikeitä suoritetaan JVM:n alaisuudessa ja siten JVM toimii tavallisen käyttöjärjestelmän tapaan antaen säikeille suoritusaikaa prosessorilta. Tätä varten säikeisiin liittyy prioriteetti, jonka mukaan JVM (ja yleisemmin käyttöjärjestelmä) kontrolloin säikeiden suoritukseen ottamista. Myös säikeiden tila vaikuttaa prioriteetin lisäksi siihen, mikä (tai mitkä) säie on milloinkin suorituksessa. Javan säikeet eivät vastaa käyttöjärjestelmätason säikeitä automaattisesti 1:1, vaan JVM:n toteutus viime kädessä määrää, voiko JVM käynnistää käyttöjärjestelmätason säikeen yhtä Javan säiettä kohti. Mahdollisesti JVM joutuu toteuttamaan yhden käyttöjärjestelmätason säikeen alaisuudessa kaikki Java-ohjelman säikeet. (Tämä asia on muutoksen alla, eikä sitä määritellä kielen tasolla.) Javan säikeiden pääasiallinen käyttötarkoitus ei liene lisätehon saaminen irti tietokoneesta (käyttämällä sen useita prosessoreita, jos niitä on), vaan pikemminkin tarkoituksena on mahdollistaa samanaikaisesti suoritettavien toimintojen ilmaiseminen helposti. Samanaikaisuus on hyödyllistä GUI-sovellusten yhteydessä (tapahtumankäsittelystä vastaa yksi säie) animaatioiden ja ympäristön tilan tarkkailun kannalta. Toisaalta säikeet ovat erityisen käteviä verkkosovellusten toteuttamisen yhteydessä. Javan tapauksessa säikeiden yhteistoiminta nojautuu lähinnä siihen, että ohjelmassa käynnistetyt säikeet jakavat saman muistiavaruuden eli ne pääsevät käsiksi samoihin olioihin. Tuolloin viestiminen yhdeltä säikeeltä toiselle tarkoittaa, että säie kirjoittaa jotakin yhteiseen muistiavaruuteen (johonkin muuttujaan tai olioon) ja toinen säie lukee tiedon kyseisestä paikasta. Javassa ei ole mitään komentoa tai kirjastossa olevaa metodia, jolla yksi säie voisi lähettää suoraan viestin toiselle säikeelle. Säikeiden yhteistoiminta on hieno asia ja muistiavaruuden jakaminen mahdollistaa sen ohjelmoijan kannalta suhteellisen vaivattomasti, mutta valitettavasti siihen liittyy erilaisia ongelmatilanteita. Olioiden (ja ylipäänsä) muistin jakamisessa täytyy ymmärtää siihen liittyvä problematiikka. Jos kaksi tai useampaa säiettä tekee päivityksiä samanaikaisesti yhteen muistialueeseen, esimerkiksi olion tietosisältöön, tulos saattaa hyvin olla jotakin muuta kuin mitä on saavutettavissa millään yksittäisellä säikeiden päivitystoimenpiteiden suoritusjärjestyksellä. Esimerkiksi jos pankkitili-olion sisältöön kohdistuu samaan aikaan otto- ja panotapahtuma. Saattaa toisen tapahtuman periaatteessa aiheuttama muutos tili-olion sisältöön jäädä kokonaan toteutumatta tuolloin koko järjestelmä voi mennä epäkonsistenttiin tilaan. Syynä tähän tapahtumaan on, että useasta yksittäisestä toimenpiteestä koostuvia operaatioita ei suoritetakaan atomaarisena toimenpiteenä, vaan kesken yhden toimenpidesarjan voi suoritus siirtyä toiseen säikeeseen, joka voi muuttaa samoja muuttujia kuin kesken jäänyt toimenpidesarjakin. Tietoresurssien jakamisen kontrolloimista varten Javassa on avainsana synchronized, jonka avulla voidaan lukita jokin olio niin, ettei usea säie voi samaan aikaan operoida kyseisellä oliolla. Synchronzied-määre voidaan liittää olio-arvoiseen lausekkeeseen, mutta myös jonkin metodiin. Kyseisen määreen kohtaava säie yrittää saada itselleen lukon kyseiseen olioon. Se saa lukon, jos jokin muu säie ei jo ole lukinnut kyseistä oliota. Muussa tapauksessa säie jää odottamaan, että lukon haltija vapauttaa lukon.

7 Edelliseen liittyy potentiaalinen ongelma. On tilanteita, jossa halutaan tehdä keskeytymätön toimenpidesarja, joka kohdistuu useaan olioon. Tuolloin säikeen tulee lukita kaikki kyseiset oliot ennen toimepidesarjan aloittamista. Tällainen usean olion peräkkäinen lukitseminen voi aiheuttaa sen, että säie on jo lukinnut tietyt oliot ja haluaa seuraavaksi lukita olion, joka on lukittu jo jonkun toisen säikeen toimesta. Tästä muodostuu ongelma, jos tämä toinen säie pyrkii lukitsemaan jotakin sellaista, joka on jo lukittu ensimmäisen säikeen toimesta. Yleisesti, lukitsemiset ja lukitsemisyritykset voivat muodostaa säikeiden kesken syklin. Tuolloin on syntynyt ns. lukkiumatilanne (deadlock), josta voidaan toipua vain vapauttamalla joitakin lukkoja. Valitettavasti, Javassa ei ole mitään automaattista menetelmää tällaisesta tilanteesta toipumiseksi, joten tilanteen välttäminen jää ohjelmoijan vastuulle. Toisenlainen häiriötilanne säikeiden suorittamisessa on, että jokin säikeen suoritus estyy koko ajan siitä syystä, että se ei saa lukittua kaikkia haluamiaan olioita samanaikaisesti. Tällaista tilannetta kutsutaan nälkiintymiseksi (starvation). Potentiaalisen lukkiumatilanteen estäminen siten, että jo muodostettuja lukkoja vapautetaan osittain, saattaa myös aiheuttaa ongelmatilanteen. On nimittäin mahdollista, että säikeet lukitsevat ja vapauttavat lukitsemiaan lukkoja (tilanteen ratkaisemiseksi) niin, että laskenta ei pääsekään etenemään (eli mikään säie ei saakaan lukko kaikkiin haluamiinsa kohteisiin siksi aikaa, että se voisi tehdä haluamansa toimenpidesarjan. Tällaista tilannetta kutsutaan eläväksi lukkiumaksi (livelock). Lukon voi olion lisäksi kohdistaa myös itse luokkaan, sen staattisiin piirteisiin. Säieryhmien ja demonien merkitys on myös selitetty luvun 3 materiaalissa. Luvussa 3 on hyvä esimerkki (luokka SharedData) siitä, miten tiedon jakaminen voidaan tehdä hallitusti ilman ongelmia. Esimerkki esittää kaksi synkronoitua jonoa jaettuun kommunikaatioon tuottajien ja kuluttajien välille (kumpiakin yksi per jono esimerkissä). Esimerkissä jonojen koko on radikaalisti supistettu äärimmäisyyteen, eli yhden alkion mittaiseksi. Huonona puolena esimerkissä voi pitää kahden rooliltaan erilaisen mutta sisällöltään samanlaisen jonon yhdistämistä yhdeksi luokaksi, kahden (yhtä jonoa esittävän luokan) instanssin sijaan. Tästä suunnittelupäätöksestä seuraa jopa se, että vaihtamalla esimerkissä olevat notifyall()-kutsut notify()-kutsuiksi, saa luokkaa SharedData käyttävät sovellukset deadlock-tilaan (mahdollisesti, kokeile!).

8 LUKU 2. SÄIKEET

Luku 3 Säikeet (by Timo Raita) 3.1 Perusteita Rinnakkaisuus on osa jokapäiväistä elämää, myös tietokoneiden kanssa peuhattaessa, halutaanhan koneella samanaikaisesti ladata verkosta materiaalia, tulostaa, lukea säköpostia jne. Säikeiden (threads) avulla Java-ohjelman eri toiminnot voidaan jakaa rinnakkaisesti suoritettaviin osiin. Näin matalan tason käsitteiden tuominen ohjelmointikieleen lisää sen ilmaisuvoimaa, mutta tuo samalla potentiaalisia vaaratilanteita, joita ei ole aina kovin helppo nähdä: lukkiintumisia, resurssien väärinkäyttöä jne. Tämän takia saattaisi olla parempi, että rinnakkaistaminen annettaisiin ajoaikaisen systeemin hoidettavaksi esimerkiksi kiinnittämällä kukin tunniste tiettyyn prosessoriin, jolloin kyseinen prosessori on vastuussa tunnisteen viitaamalle objektille tehtävistä toimenpiteistä 1. Koska Javan säikeet suoritetaan JVM:n alaisuudessa, niiden hallinta pitää hoitaa JVM:n sisällä. Tästä syystä säikeiden käsittely ei suoranaisesti liity allaolevan koneen käyttöjärjestelmän osaamistasoon, joskin JVM:n on helppo delegoida tehtävät sille, jos käyttöjärjestelmä tukee samoja toimintoja. Selvää tietysti on, että JVM on eräs käyttöjärjestelmän prosessi. Säikeiden käyttäytymistä voidaan kuvata kaikista käyttöjärjestelmäkirjoista tutulla transitiokaaviolla (kts. seuraavan sivun kuva), joka kertoo minkälaisissa tiloissa säie voi olla suorituksen aikana. Kun säie luodaan, se tulee tilaan Uusi. Säikeen käynnistys start-komennolla aiheuttaa sen siirtymisen tilaan Valmis. Kun säie saa prosessorin käyttöönsä, se siirtyy tilaan Suorituksessa, jossa säikeelle tehtyä run-rutiinia ajetaan. Tästä se voi siirtyä tilaan Päättynyt, jos sen suoritus saadaan loppuun tai nousee käsittelemätön keskytys, tilaan Valmis, jos se on käyttänyt aikaviipaleensa (time slice) loppuun, tai tilaan Estynyt, jos se odottaa IO-toiminnon loppumista, jää odottamaan signaalia toiselta säikeeltä tms. Koska ohjelman kirjoittajan on mahdotonta ennustaa miten säikeet aikaviipaleensa saavat ja käyttävät, rinnakkaisohjelmointi vaatii erityistä tarkkuutta, jotta objektit olisivat aina oikeassa tilassa silloin kun niitä tarvitaan. Tilassa Valmis ovat kaikki säikeet, jotka odottavat prosessoria käyttöönsä. Kun prosessori vapautuu, töidenjärjestelijän (scheduler) on valittava yksi säikeistä suoritukseen. Apunaan se käyttää taulua 1 Näin tehdään Eiffel-kielessä. Hämmästyttävää tässä mekanismissa on se, että kielen syntaksiin tarvitaan vain yksi uusi sana, separate, jolla tunnisteet leimataan. Ajoaikainen systeemi pitää tämän jälkeen huolen siitä, että prosessorit allokoidaan ja synkronointi hoidetaan järkevällä tavalla. 9

10 LUKU 3. SÄIKEET (BY TIMO RAITA) Suorituksen loppuminen normaalisti tai epänormaalisti Päättynyt (kuollut) Suorituksessa Uusi Säie valitaan suoritukseen Valmis Säie käynnistetään start komennolla Aikakvantti loppunut, yield, interrupt IO suoritettu, uniaika loppunut, notify, notifyall Säie luodaan IO pyyntö, sleep, wait Estynyt MAX_PRIORITY p1 p2 p3 MIN_PRIORITY p4 johon säikeet on järjestetty prioriteetin mukaan. Suuremman prioriteetin omaava säie suoritetaan aina ennen alemman prioriteetin omaavia 2. Prioriteettien minimiarvo on MIN_PRIORITY (1) ja maksimiarvo MAX_PRIORITY (10). Suorituksen alkaessa säikeellä on prioriteetti NORM_PRIORITY (5). Kun suorituksessa ollut säie on käyttänyt aikaviipaleensa tai siirtynyt tilaan Estynyt, prosessori vapautuu muiden käyttöön. Tällöin töidenjärjestelijä hakee taulukosta säikeen, jolla on suurin prioriteetti ja jos tällaisia on useita, valitaan se, jonka edellisestä aikaviipaleesta on kulunut kauimmin aikaa. Kuvan mukaisessa tilanteessa valitaan säie p1. Kun p1 vapauttaa prosessorin, se siirretään saman jonon hännille säikeen p3 perään (round robin -periaate) ja p2 otetaan suoritukseen. Alemman prioriteetin omaava p4 pääsee suoritukseen ainakin silloin,kun ylemmän prioriteetin omaavat on suoritettu loppuun tai ne kaikki ovat estyneitä. Järkevä töidenjärjestelypolitiikka tosin antaa sille aikaa silloinkin, kun ylemmän prioriteetin omaavat ovat suorituskelpoisia, mutta harvemmin. Jokaisella Java-ohjelmalla on ainakin yksi säie, nimittäin main. Se käsittelee tyypillisesti useita objekteja, joiden ohjaamana ohjelman kontrolli siirtyy paikasta toiseen. Tämän takia ohjelman suoritus on hyvin determinististä, ja me pystymme seuraamaan sitä käsky käskyltä. Tilanne muuttuu, kun rinnakkaisuus tulee mukaan, koska rinnakkaisprosessointi tuo ongelmia, joiden ratkaisut eivät aina 2 Jotta alemman prioriteetin omaavat tulisi suoritettua kohtuuajassa, niillekin pitäisi antaa prosessoriaikaa aina silloin tällöin. Töidenjärjestelijöiden toiminta vaihtelee eri JVM:issä. Joissakin säie suoritetaan aina loppuun (tai kunnes korkeamman prioriteetin omaava tulee suoritusvalmiiksi) ennen kuin saman tai alemman prioriteetin omaava otetaan käsittelyyn. Toisissa käytetään aikaviipaletta, jolloin saman prioriteetin omaavat pääsevät vuorottelemaan suorituksessa.

3.2. SÄIKEEN LUONTI JA KÄYNNISTYS 11 ole ilmeisiä. Syy tähän ei ole Javassa, vaan samanaikaisuudessa. Seuraavassa säikeiden toimintaa kuvataan tilakaavioon perustuen. Ensin tarkastellaan yksittäisen säikeen käynnistämistä ja lopettamista, sitten välineitä, joilla voidaan vaikuttaa kontrollin siirtymiseen säikeiden välillä ja lopuksi säikeiden välistä kommunikointia sekä säieryhmiä. 3.2 Säikeen luonti ja käynnistys Javassa on kaksi erikoisluokkaa säikeiden luontiin: rajapinta Runnable ja sen toteuttava konkreetti luokka Thread. Luokalla Runnable on vain yksi piirre, rutiini run, johon kirjoitetaan perijässä säikeen toiminta. Thread-luokkaan on koottu keskeiset säietoiminnot, joten me keskitymme seuraavassa niihin. Myöhemmin tarkastellaan luokkaa ThreadGroup, jonka avulla säikeitä voidaan niputtaa ja käsitellä siten yhtenä kokonaisuutena. Muita säikeisiin liittyviä luokkia ovat Object (josta löytyy rutiinit wait, notify ja notifyall), luokka ThreadLocal (jonka avulla saadaan käsiin säikeen sisäistä informaatiota) sekä poikkeusluokka ThreadDeath. Säikeen luonti tapahtuu normaaliin tapaan konstruktorin avulla. Kaikkein yleisin säiekonstruktori saa argumentikseen säieryhmän, johon säie halutaan liittää. Ryhmä on oletuksena sama kuin luontisäikeen ryhmä, Runnable-tyyppisen objektin, jos luokka ei halua periä konkreetilta luokalta Thread, ja säikeen nimen. Jos nimeä ei anneta, systeemi generoi automaattisesti nimen Thread-xx, missä xx on juokseva kokonaisluku. Muut konstruktorit ovat tämän yleisen muodon erikoistapauksia. Argumentit, joihin ei oteta kantaa, saavat oletusarvot. Luotavan säikeen prioriteetti on sama kuin luovan säikeen. Jos prioriteettia halutaan muuttaa, se voidaan tehdä rutiinilla setpriority. Vastaava havainnointioperaatio on getpriority. Myös säienimelle ja -ryhmälle on havainnointioperaatiot. Kun säie on luotu, sen suoritus alkaa start-kutsulla, jolloin säie siirtyy tilaan Valmis odottamaan prosessorin vapautumista. Kun se pääsee suoritukseen, se suorittaa run-rutiiniin kirjoitettua toimintoa. Katsotaanpa yksinkertaista esimerkkiä: public class ThreadTester public static void main( String args[] ) PrintThread säie1, säie2, säie3, säie4; säie1 = new PrintThread("Säie1"); säie2 = new PrintThread("Säie2"); säie3 = new PrintThread("Säie3"); säie4 = new PrintThread("Säie4"); System.err.println("\nSäikeiden käynnistys."); säie1.start(); säie2.start(); säie3.start(); säie4.start();

12 LUKU 3. SÄIKEET (BY TIMO RAITA) System.err.println( "Käynnistetty.\n"); class PrintThread extends Thread private int sleeptime; public PrintThread(String name) super(name); sleeptime = (int) (Math.random() * 5000 ); System.err.println( "Nimi: "+ getname() + "; Unessa: "+ sleeptime ); public void run() try System.err.println(getName() + "menee nukkumaan"); Thread.sleep(sleepTime); catch (InterruptedException exception) System.err.println( exception.tostring() ); System.err.println(getName() + "heräsi"); Rutiini main luo neljä säiettä ja käynnistää niiden suorituksen. Tämän tehtyään main jatkaa toimintaansa. Luonnin yhteydessä kullekin säikeelle arvotaan aika, jonka se nukkuu ja antaa siten muille säikeille mahdollisuuden päästä suoritukseen. Koska joku toinen voi herättää säikeen interruptkutsulla, sleep saattaa nostaa poikkeuksen InterruptedException, joten sen kutsu on sijoitettava try-lohkoon. Säikeiden run-toiminto ei ole kovin tyypillinen siinä mielessä, että se tulostaa vain merkkijonon ja lopettaa suorituksen. Normaalisti siinä suoritetaan silmukkaa, kunnes jokin ehto tulee täyteen. Ohjelman tulostus voisi näyttää vaikka seuraavalta: Nimi: Säie1; Unessa: 470 Nimi: Säie2; Unessa: 4677 Nimi: Säie3; Unessa: 1097 Nimi: Säie4; Unessa: 2419 Säikeiden käynnistys. Käynnistetty. Säie1 menee nukkumaan Säie2 menee nukkumaan Säie3 menee nukkumaan Säie4 menee nukkumaan Säie1 heräsi Säie3 heräsi Säie4 heräsi

3.2. SÄIKEEN LUONTI JA KÄYNNISTYS 13 Säie2 heräsi main-rutiinin herättämä start-kutsu aiheuttaa main-säikeen ja uuden säikeen suoritusten eroamisen omikseen (vrt. Unixin fork). main jatkaa suoritustaan välittömästi kutsun jälkeen ja kutsuttava siirtyy tilaan Valmis aloittaen run-rutiinin suorituksen siitä syystä, että start kutsuu runnimistä rutiinia. Thread-luokan run-toteutus ei tee oletuksena mitään, joten perijäluokan on aina syytä korvata se omallaan 3. Jos start kohdistetaan virheellisesti säikeeseen, joka on jo käynnissä, nousee poikkeus IllegalThreadStateException. Kun run on suoritettu loppuun, säie siirtyy kuolleeseen tilaan (dead state). Vaikka kuolleessa tilassa olevaa säiettä ei voi enää käynnistää, se on edelleen olemassa ja sen tilaa voi kysellä. Threadluokan isalive palauttaa totuusarvon true niin kauan kun säikeen run-rutiini on toiminnassa. Säie saa tietoa itsestään kutsumalla piirrettä currentthread, joka palauttaa Thread-tyyppisen objektin. Sen avulla säie saa selville esimerkiksi oman ryhmänsä, prioriteettinsa yms. Esimerkkimme main voisi antaa luomilleen säikeille vähän suuremman prioriteetin seuraavasti: Thread mythread = Thread.currentThread(); int mypriority = mythread.getpriority(); säie1.setpriority(mypriority+1); säie2.setpriority(mypriority+1); säie3.setpriority(mypriority+1); säie4.setpriority(mypriority+1); jolloin tuloksena on se, että luodut säikeet suorittavat toimintansa ja kuolevat ennen kuin main pääsee jatkamaan suoritustaan. Jotta kaikilla säikeillä olisi yhtäläiset mahdollisuudet päästä suoritukseen aika ajoin, pitää säikeiden siirtyä välillä odotustilaan. Tämä voidaan toteuttaa esimerkiksi kutsumalla sleep-proseduuria. Edellisessä esimerkissä säieluokka peri suoraan Thread-luokalta. Tämä on usein epäkäytännöllistä sen takia, että säie haluaisi pikemminkin periä joltain mielekkäämmältä, sen toimintaa mallintavalta luokalta ja Thread-luokalta periminen liittyy vain ajoaikaiseen käyttäytymiseen. Koska Javassa ei ole moniperintää, ongelma ratkaistaan rajapinnan Runnable avulla. Rajapinnan runoperaatiolla on tietysti tarkalleen sama signatuuri kuin Thread-luokassa (koska Thread on perinyt sen Runnable-rajapinnalta). Aiempaan esimerkkiin tulee tällöin vain kaksi muutosta: (a) PrintThreadluokan otsikko tulee muotoon class PrintThread implements Runnable ja (b) säikeen luonti tapahtuu main-rutiinissa kirjoittamalla Runnable alustus1 = new PrintThread(); Runnable alustus2 = new PrintThread(); Runnable alustus3 = new PrintThread(); Runnable alustus4 = new PrintThread(); Thread säie1 = new Thread(alustus1, "Säie1"); Thread säie2 = new Thread(alustus2, "Säie2"); Thread säie3 = new Thread(alustus3, "Säie3"); Thread säie4 = new Thread(alustus4, "Säie4"); Uuden Runnable-säikeen suoritusta ei voi aloittaa siis suoraan start-käskyllä, vaan sitä varten on luotava Thread-tyypin objekti, jonka konstruktorille Runnable-objekti annetaan argumenttina. Kun start aloitetaan, Thread-luokan run kutsuu konstruktorissa annetun Runnable-objektin run-rutiinia. Jos Runnable-objekti tarvitsee tietoa säikeestä, jossa se on, se voi kutsua funktiota currentthread. 3 Jos run on lyhyt ja se tehdään vain yhdelle säikeelle, Thread-luokan perijä voidaan kirjoittaa sisäluokaksi.

14 LUKU 3. SÄIKEET (BY TIMO RAITA) 3.3 Säikeiden välinen kontrollin hallinta Säie voi siirtyä suoritustilasta kolmeen eri tilaan: (a) Päättynyt: säie saadaan tehtyä loppuun tai se keskeytetään väkisin jonkin erikoistilanteen takia. (b) Valmis: säie on käyttänyt oman aikaviipaleensa ja luovuttaa prosessorin muiden käyttöön pakon (eli töidenjärjestelypolitiikan) sanelemana. Säie voi tehdä saman vapaaehtoisesti kutsumalla rutiinia yield tai interrupt. (c) Estynyt: säie suorittaa toimenpiteen, joka estää sen suorituksen väliaikaisesti. Kuvan mukaisesti tämä siirtymä tapahtuu silloin, kun (1) Säie odottaa syötön tai tulostuksen loppumista. (2) Säikeen suorituksessa kohdataan käsky sleep(ms) tai sleep(ms,ns), missä argumentti antaa millisekunteina (ja nanosekunteina) ajan, jonka säie on tilassa Estynyt. (3) Säikeen suorituksessa kohdataan jokin Object-luokasta löytyvän wait-käskyn muoto: wait(), wait(ms) tai wait(ms,ns). Tällöin säie pysyy tilassa Estynyt niin kauan, että jokin toinen säie herättää sen Object-luokan rutiinilla notify tai notifyall. (4) Säie yrittää kutsua toisen objektin synchronized-piirrettä ja joutuu odottamaan, koska kyseisen objektin lukko estää piirteen suorittamisen. (5) Säikeen suoritus on pysäytetty käskyllä suspend(). Suoritus jatkuu vasta, kun joku toinen kutsuu sitä resume-piirteellä 4. Jos säie haluaa itse vapaaehtoisesti poistua suorituksesta ja luovuttaa prosessorin muiden säikeiden käyttöön, se voi kutsua operaatiota yield. Itse asiassa on tietysti niin, että korkeamman prioriteetin omaava säie poistaa tällä hetkellä suorituksessa olevan säikeen automaattisesti, joten niitä varten ei voi tehdä yieldiä. Rutiinin yield semantiikan mukaan suoritusmahdollisuus annetaan korkeimman prioriteetin omaavalle suoritusvalmiille säikeelle. Jos yield-kutsun tekevä säie on ainoana omassa prioriteettiluokassaan, se tulee tällöin itse suoritukseen! Täten yield ei voi johtaa alempiprioriteettisten säikeiden suoritukseen. Ainoaksi mahdollisuudeksi jää se, että säie antaa suoritusmahdollisuuden vain saman prioriteetin omaaville säikeille. Aikaviipalesysteemissä tämä on tarpeetonta, joten rutiini on käyttökelpoinen vain niissä JVM:issä, jotka suorittavat säikeen aina kokonaan loppuun ennen uuden aloittamista. Mietitäänpä sitten hieman yleisellä tasolla niitä välineitä, joilla säikeet voivat kontrolloida toisiaan. Edellä puhutun perusteella on selvää, että säikeen suoritus voi erkautua toisesta rutiinin start kutsun johdosta ja ne voivat lähettää toisilleen signaaleja rutiinien notify, interrupt ja notifyall välityksellä. Jos säie on keskeyttänyt toimintansa oman wait-kutsun takia, se saadaan jatkamaan suoritustaan (eli siirtymään tilaan Valmis) joko notify- tai interrupt-kutsun seurauksena. Kutsun notify suorittava säie ei voi kuitenkaan kohdistaa herätystä mihinkään tiettyyn säikeeseen. Sen sijaan notifyall herättää kaikki odottavat säikeet. Rutiini wait ja objektin lukitseminen synchronized-määreellä selitetään tarkemmin seuraavassa kappaleessa. Säikeiden välinen toiminta saadaan synkronoitua rutiinilla join (tai join(ms), join(ms,ns)). Tällöin kutsun suorittava säie jää odottamaan kutsun kohteena olevan säikeen loppumista ja jatkaa eteenpäin vasta sen tapahduttua. Näin kutsuva säie voi varmistaa, että halutut toimenpiteet on tehty stop. 4 Näitä ei suositella enää käytettäväksi. Vanhentuneita ovat myös destroy (jota ei ole vielä edes implementoitu!) sekä

3.3. SÄIKEIDEN VÄLINEN KONTROLLIN HALLINTA 15 ennen suorituksen jatkamista. Jos join-rutiinille ei anneta argumenttia, kutsuja odottaa toisen loppumista maailman tappiin asti. Muussa tapauksessa kutsuja odottaa enintään argumentin ilmaiseman ajanjakson ja jatkaa sitten suoritustaan riippumatta siitä pääsikö kutsuttu säie loppuun vai ei. Katsotaanpa pari yksinkertaista esimerkkiä säikeiden synkronoinnista. Ensimmäinen toteutus käyttää luotujen säikeiden pollausta eli säikeet luonut main pitää kaikki langat käsisssään käyden läpi kaikki luomansa säikeet ja testaamalla niiden loppumista rutiinilla isalive. Aluksi säikeen toiminnan määrittelevä luokka, joka tulostaa ruudulle konstruktorille annetun välin [min, max] kokonaisluvut: class Counter implements Runnable public Counter(int min,int max) this.min = min; this.max = max; public void run() final int max = getmax(); for ( int i = getmin(); i <= max; i++) System.out.print(i + " "); try \\Annetaan muillekin säikeille tilaisuus. Thread.sleep(100); catch (InterruptedException e) System.out.println(); public int getmin() return min; public int getmax() return max; \\Range of numbers to be printed. private final int min, max; Seuraavaksi muodostetaan säieluokka Coordinator, joka luo ja kontrolloi kolmea Countersäiettä run-rutiinissa. Ennen alisäikeiden käynnistämistä Coordinator pienentää niiden prioriteettia, jotta se saisi käynnistettyä ne samanaikaisesti (silloin kun sen oma prioriteetti laskee riittävän alas): class Coordinator implements Runnable public Coordinator() initcounters(); protected void initcounters() Thread[] counters = getcounters(); for (int i = 0; i < counters.length; i++)

16 LUKU 3. SÄIKEET (BY TIMO RAITA) \\Annetaan kullekin luotavalle säikeelle oma toiminta-alueensa. final int min = 10*i+1, max = min+9; counters[i] = new Thread(new Counter(min,max)); System.out.println("Säikeet luotu."); public void run() Thread mythread = Thread.currentThread(); int mypriority = mythread.getpriority(); Thread[] counters = getcounters(); for (int i = 0; i < counters.length; i++) \\Lasketaan säikeiden prioriteettia, jotta ne eivät lähtisi \\käyntiin ennen aikojaan. counters[i].setpriority(mypriority-1); counters[i].start(); System.out.println("Kaikki valmiina."); waitforcounterstofinish(mypriority); System.out.println("Kaikki säikeet ovat lopettaneet."); protected void waitforcounterstofinish(int mypriority) \\Lasketaan omaa prioriteettia, jotta alisäikeet pääsevät suoritukseen. Thread mythread = Thread.currentThread(); mythread.setpriority(mypriority-1); do try Thread.sleep(500); catch (InterruptedException e) while (stillrunning()); protected boolean stillrunning() \\Loppuehto: Palauttaa arvon true, jos yksikin säie on käynnissä. Thread[] counters = getcounters(); for (int i = 0; i < counters.length; i++) if (counters[i].isalive()) return true; return false; protected Thread[] getcounters() return counters; private final int numcounters = 3;

3.3. SÄIKEIDEN VÄLINEN KONTROLLIN HALLINTA 17 private final Thread[] counters = new Thread[numCounters]; Tämän jälkeen tarvitaan vielä main, joka luo Coordinator-tyyppisen säikeen. Ohjelman tulostus näyttää seuraavalta: Säikeet luotu. Kaikki valmiina. 1 11 21 2 12 22 3 13 23 4 14 24 5 15 25 6 16 26 7 17 27 8 18 28 9 19 29 10 20 30 Kaikki säikeet ovat lopettaneet. Counter-säikeet generoiva säie alentaa omaa prioriteettiaan, jotta kolme luotua säiettä saisivat vapaasti kilpailla keskenään. Kovin paljon kilpailua ei kuitenkaan synny, koska alimman tason säikeet nukkuvat aina saman verran yhden luvun tulostettuaan. Nukkumisaika on pitkä verrattuna tulostamiseen, joten säikeet siirtyvät vuorollaan aina ko. prioriteettitason jonon hännille nukkumisjakson päätyttyä, josta ne sitten otetaan suoritukseen round robin -periaatteella. Tulostus muuttuu tietysti aika lailla, jos sleep-rutiinin argumentiksi annetaan vaikkapa max, koska silloin alkupään numerot tulevat tulostettua loppupään numeroita nopeammin. Alisäikeiden loppumista testataan rutiinissa waitforcounterstofinish silmukan sisällä. Vaihtoehtoisesti systeemi voitaisiin rakentaa sellaiseksi, että alisäikeet ilmoittavat luojalleen oman toimintansa loppumisesta. Ainoa ero edelliseen toteutukseen on, että (stillrunning poistetaan ja) waitforcounterstofinish käyttää join-operaatiota: protected void waitforcounterstofinish(int mypriority) Thread mythread = Thread.currentThread(); mythread.setpriority(mypriority-1); Thread[] counters = getcounters(); for (int i = 0; i < counters.length; i++) \\Anticipate being interrupted before the join is complete. boolean interrupted; do try interrupted = false; counters[i].join(); catch (InterruptedException e) interrupted = true; while(interrupted); Nyt alisäikeet luonut Coordinator odottaa join-operaatiossa, kunnes kyseinen alisäie on lopettanut toimintansa. Toteutuksessa tarvitaan kaksinkertainen silmukka: ulompi käy läpi kaikki alisäikeet ja sisempää tarvitaan sen takia, että join-operaation odotus voi keskeytyä muustakin syystä kuin odotettavan säikeen loppumisesta. Samasta syystä tarvitaan myös try-lohkoa. Jos alisäie on jo lopettanut toimintansa tultaessa join-kutsuun, jatketaan ohjelman suoritusta välittömästi. Threadluokan muut join-variaatiot ovat turvallisempia kuin ylläkäytetty, siinä mielessä, että ne odottavat vain argumenttina annetun ajan ja jatkavat sitten suoritustaan. Tämä on hyödyllistä, koska säie, jonka loppumista odotetaan voi odottaa puolestaan itse jotain (jopa epäsuorasti omaa loppumistaan), pahimmassa tapauksessa loputtomiin.

18 LUKU 3. SÄIKEET (BY TIMO RAITA) 3.4 Resurssien jakaminen Säikeet toimivat normaalisti yhteisen päämäärän hyväksi ja tähän pyrkiessään niiden pitää aina silloin tällöin kommunikoida keskenään. Tavallinen kommunikointitapa on käyttää yhteistä muistialuetta, johon säikeet kirjoittavat ja josta ne lukevat tietoa. Tällainen tilanne saadaan helposti aikaan luomalla ohjelmassa ensin muistialue, jonka kautta kommunikointi tapahtuu. Tämän jälkeen säikeille välitetään tieto yhteisesta alueesta antamalla siihen viittaus, jonka säikeet voivat tallettaa omiin sisäisiin attribuutteihinsa myöhempää käyttöä varten. Yleisesti ottaen kirjoittavia ja lukevia säikeitä voi olla useita. Jos säikeillä on tiukasti rajatut roolit siinä mielessä, että tietty säie voi vain lukea tai kirjoittaa, mutta ei tee koskaan molempia, ohjelmoija pääsee helpommalla, sillä synkronointi tehdään hänen puolestaan, jos hän käyttää putkea. Tarkastellaan tapausta, jossa kirjoittajia on kaksi ja lukijoita on vain yksi. Toinen kirjoittavista säikeistä on PrimeMaker, joka generoi alkulukuja ja kirjoittaa ne putkeen: import java.io.*; class PrimeMaker extends Thread private DataOutputStream out; public PrimeMaker(DataOutputStream o) out = o; public void run() int newvalue = 1; try while (newvalue < 10000) newvalue = newvalue + 1; boolean isprime = true; for ( int i=2; i*i <= newvalue; i++) if (newvalue % i == 0) isprime = false; break; if (isprime) System.out.println("writing new prime "+ newvalue); out.writeint(newvalue); out.close(); catch (IOException e) return; FibMaker on periaatteeltaan aivan identtinen, mutta generoi Fibonaccin lukuja: import java.io.*; class FibMaker extends Thread private DataOutputStream out; public FibMaker(DataOutputStream o) out = o;

3.4. RESURSSIEN JAKAMINEN 19 public void run() int n=0; int m=1; try out.writeint(m); while (m < 10000) int newvalue = n+m; n = m; m = newvalue; System.out.println("writing new Fibo "+ newvalue); out.writeint(newvalue); out.close(); catch (IOException e) return; Koko hommaa kontrolloi luokka PipeReader. Luokka luo putket ja käynnistää sitten PrimeMakerja FibMaker-tyyppiset säikeet, jotka generoivat kokonaislukuja: PrimeMaker FibMaker PipeReader PipeReader etsii lukuvirrasta duplikaatit (tietäen, että kumpikin lähde tuottaa niitä nousevassa suuruusjärjestyksessä) ja tulostaa ne: import java.io.*; public class PipeReader static public void main(string[] args) PipeReader world = new PipeReader(System.out); private PipeReader(PrintStream out) DataInputStream fibs = makefibs(); DataInputStream primes = makeprimes(); try int x = fibs.readint(); int y = primes.readint(); while (x < 10000) if (x == y) out.println("integer "+ x + "is both fib and prime");

20 LUKU 3. SÄIKEET (BY TIMO RAITA) x = fibs.readint(); y = primes.readint(); else if (x < y) x = fibs.readint(); else y = primes.readint(); catch (IOException e) System.exit(0); private DataInputStream makefibs() try PipedInputStream in = new PipedInputStream(); PipedOutputStream out = new PipedOutputStream(in); Thread fibthread = new FibMaker(new DataOutputStream(out)); fibthread.start(); return new DataInputStream(in); catch (IOException e) return null; private DataInputStream makeprimes() try PipedInputStream in = new PipedInputStream(); PipedOutputStream out = new PipedOutputStream(in); Thread primethread = new PrimeMaker(new DataOutputStream(out)); primethread.start(); return new DataInputStream(in); catch (IOException e) return null; Ohjelman tulostus näyttää seuraavalta (ylimääräisiä rivejä poistettu): writing new Fibo 1 writing new Fibo 2 writing new Fibo 3 writing new Fibo 5 writing new Fibo 8. writing new prime 2 writing new prime 3 writing new prime 5 writing new prime 7. Integer 2 is both fib and prime Integer 3 is both fib and prime Integer 5 is both fib and prime Integer 13 is both fib and prime. Tämä on esimerkki yleisestä tilanteesta, jossa säikeet yrittävät päästä samanaikaisesti käsiksi samoihin tietoihin. Nämä säikeet olivat kuitenkin toisistaan riippumattomia siinä mielessä, että ne eivät

3.4. RESURSSIEN JAKAMINEN 21 tienneet olevansa toistensa kanssa tekemisissä, eikä niiden näin ollen myöskään tarvinnut synkronoida toimintojaan millään tavalla (ja sekin mikä tarvittiin, tarjottiin ilmaiseksi putken avulla). OOsysteemi koostuu kuitenkin toistensa kanssa vuorovaikutuksessa olevista objekteista ja jos ne ovat eri säikeissä, pitää säikeiden toimintaa synkronoida jollakin tavalla. Synkronointia tarvitaan aina resurssien jakamisen takia. Resurssi on jokin ohjelman ympäristöstä löytyvä tarpeellinen väline: keskusmuistialue, objektijoukko, IO-laite, tietokanta jne. Jos edellisessä esimnerkissä haluttaisiin itse kontrolloida tarkemmin tapahtumia ja käyttää tiedon välitykseen vain kahta erillistä int-muuttujaa, kirjoittaminen ja lukeminen pitää synkronoida, jotta lukija ei lue samaa tietoa useaan eri otteeseen ja kirjoittaja ei kirjoita uutta tietoa vanhan päälle ennen kuin lukija on ehtinyt lukea aiemman. Tällaisesta tilanteesta käytetään nimitystä kilpailutilanne (race condition, race hazard) ja se on hyvin yleinen monessa tilanteessa (esimerkiksi luvun 4.1.2 lipunvarausjärjestelmässä useasta eri konttorista voidaan yrittää varata sama istumapaikka). Java käyttää synkronointiin C.A.R. Hoaren vuonna 1974 esittelemää monitori-käsitettä (monitor). Ajatuksena on etsiä koodista nk. kriittiset osat (critical sections), joiden aikana toinen säie ei saa keskeyttää suoritusta. Javassa on kaksi tapaa merkitä kriittinen osa. Ensiksi tarkasteltavassa tavassa yksittäinen rutiini on yksikkö, joka voidaan leimata haluttaessa kriittiseksi (myöhemmin esitellään tapa, jolla kriittiseksi osaksi voidaan valita mielivaltainen lauselohko). Rutiini on kriittinen, jos se on varustettu synchronized-määreellä. Jokainen objekti, johon liittyy ainakin yksi synkronoitu rutiini, on puolestaan monitori. Monitoriobjektille on ominaista, että siihen voidaan kohdistaa vain yksi synkronoitu operaatio kerrallaan (muut säikeet voivat kyllä suorittaa samanaikaisesti ei-synkronoituja rutiineja). Suorituksessa olevalla säikeen sanotaan tällöin lukitsevan (lock) objektin. Kun synkronoitua rutiinia kutsutaan, JVM tarkistaa onko monitori lukittu. Jos on, asiakkaan kutsu laitetaan jonoon niiden kutsujen perään, jotka odottavat lukon vapautumista. Jos ei, asiakas saa lukon ja pystyy jatkamaan suoritustaan. Kun synkronoidun rutiinin runko on suoritettu loppuun, lukko luovutetaan muiden käyttöön ja korkeimman prioriteetin omaava odottava säie pääsee suoritukseen (ja saa lukon itselleen). Joissakin tilanteissa lukon omaava säie voi todeta, että se ei voi edetä suorituksessa ja se kutsuu rutiinia wait. Tällöin säie siirtyy odotustilaan (tilan Estynyt erikoistapaus) ja vapauttaa lukon. Säie voi kutsua synkronoidun rutiinin lopussa piirrettä notify, joka vapauttaa yhden odotustilassa olevista säikeistä ja siirtää sen tilaan Valmis. Rutiinin notify kutsua ei voi kohdistaa mihinkään tiettyyn säikeeseen, vaan systeemi valitsee jonkin odottavista korkeimman prioriteetin omaavista säikeistä. Jos lukon omaava piirre kutsuu rutiinia notifyall, kaikki odottavat säikeet siirretään tilaan Valmis, joskin vain yksi niistä saa itselleen lukon. On huomattava pieni, mutta selkeä ero niiden säikeiden välillä, jotka ovat joutuneet odotustilaan wait-käskyn ansiosta ja niiden, jotka ovat jonottamassa lukkoa. Kun lukko vapautuu, jälkimmäisistä joku valitaan suoritukseen, mutta edelliset voivat päästä suoritukseen vasta, kun joku toinen säie kutsuu rutiinia notify tai notifyall. Koska wait, notify ja notifyall on sijoitettu luokkaan Object, kaikilla objekteilla on mahdollisuus tulla monitoriksi. Ratkaistaan edellinen Fibonaccin lukuja ja alkulukuja ksäittelevä systeemi nyt synkronointia käyttäen. Tätä varten muodostetaan ensin luokka, jossa on omat private-tyyppiset kokonaislukuattribuuttinsa kumpaakin varten. Molemmille on myös get- ja set-operaationsa, joiden pitää olla synkronoituja. Luvun asettavat set-rutiinit tarkistavat totuusarvoisen muuttujan avulla, onko edellinen luku jo luettu attribuutista. Jos on, uusi luku voidaan asettaa attribuuttiin. Muussa tapauksessa säie jää odottamaan tehtyään wait-kutsun. Odotus katkeaa siinä vaiheessa, kun lukeva säie antaa notify-kutsun tiedon luettuaan. Vastaava toiminto on myös get-rutiineissa: public class SharedData private int sharefib = 0;

22 LUKU 3. SÄIKEET (BY TIMO RAITA) private int shareprime = 0; private boolean writeablefib = true; private boolean writeableprime = true; public synchronized void setfib(int newvalue) while (!writeablefib) try wait(); catch (InterruptedException e) e.printstacktrace(); System.out.println(Thread.currentThread().getName() + "setting sharefib to "+ newvalue); sharefib = newvalue; writeablefib = false; notify(); public synchronized void setprime(int newvalue) while (!writeableprime) try wait(); catch (InterruptedException e) e.printstacktrace(); System.out.println(Thread.currentThread().getName() + "setting shareprime to "+ newvalue); shareprime = newvalue; writeableprime = false; notify(); public synchronized int getfib() while (writeablefib) try wait(); catch (InterruptedException e) e.printstacktrace(); writeablefib = true; notify(); System.out.println(Thread.currentThread().getName() + "retrieving sharefib value "+ sharefib); return sharefib; public synchronized int getprime() while (writeableprime)

3.4. RESURSSIEN JAKAMINEN 23 try wait(); catch (InterruptedException e) e.printstacktrace(); writeableprime = true; notify(); System.out.println(Thread.currentThread().getName() + "retrieving shareprime value "+ shareprime); return shareprime; Huomaa kussakin synkronoidussa rutiinissa käytetty while-rakenne. Kun säie poistuu odotustilasta, ei ole tietenkään mitään takeita siitä, että silmukkaehto olisi edelleen voimassa. Tämä johtuu siitä, että (a) joku muu säie on voinut muuttaa ehtolausekkeen arvoa odotusaikana, ja (b) odotus voi loppua muuhunkin kuin juuri kyseiselle säikeelle kohdistetusta notify-operaatiosta. Kirjoittava säie käyttää rutiineja aivan normaalisti: public class FibThread extends Thread private SharedData handle; public FibThread(SharedData sd) super("fibthread"); handle = sd; public void run() int n=0, m=1; handle.setfib(m); while (m < 1000) int newvalue = n+m; n = m; m = newvalue; handle.setfib(newvalue); return; Luokka PrimeThread toimii vastaavasti. Lukeva säie näyttää seuraavalta: public class ReaderThread extends Thread private SharedData handle; ReaderThread(SharedData sd) super("readerthread"); handle = sd; public void run() int x = handle.getfib(); int y = handle.getprime(); while (x < 1000)

24 LUKU 3. SÄIKEET (BY TIMO RAITA) if (x == y) System.out.println("Integer "+ x + "is both fib and prime"); x = handle.getfib(); y = handle.getprime(); else if (x < y) x = handle.getfib(); else y = handle.getprime(); ja ohjelman tulostus seuraavalta: PrimeThread setting shareprime to 2 FibThread setting sharefib to 1 ReaderThread retrieving sharefib value 1 ReaderThread retrieving shareprime value 2 FibThread setting sharefib to 1 ReaderThread retrieving sharefib value 1 PrimeThread setting shareprime to 3 FibThread setting sharefib to 2 ReaderThread retrieving sharefib value 2 Integer 2 is both fib and prime FibThread setting sharefib to 3 ReaderThread retrieving sharefib value 3 FibThread setting sharefib to 5 ReaderThread retrieving shareprime value 3 Integer 3 is both fib and prime PrimeThread setting shareprime to 5 ReaderThread retrieving sharefib value 5 FibThread setting sharefib to 8 ReaderThread retrieving shareprime value 5 Integer 5 is both fib and prime. Tämä toteutus antaa ratkaisun tilanteelle, jossa kirjoittaja ja lukija toimivat tasatahtiin: kirjoittaja asettaa arvon, lukija lukee sen ja tämä toistuu kunnes kaikki tiedot on generoitu ja luettu. Yleisessä tapauksessa ei ole kuitenkaan mukavaa, että kirjoittaja ja lukija on sidottu toisiinsa näin voimakkaasti. Tällöin voidaan käyttää putkia tai yleistää ylläoleva ratkaisu omaan puskurialueen hallintaan. Attribuuttia ei voi esitellä synkronoiduksi. Tässä on taas yksi lisäsyy määritellä attribuutit privatetyyppisiksi ja kirjoittaa niille järkevillä suojaus- ja synkronointimääreillä varustetut get- ja setoperaatiot. Attribuuttiin voidaan liittää määre volatile, joka kertoo Java-kääntäjälle, että attribuutin käsittelyyn liittyvää koodia ei kannata optimoida viimeiseen asti. Erityisesti sillä halutaan kertoa, että ko. attribuutin arvoa ei ole syytä pitää suoritusaikana rekisterissä sinä aikana kun muistipaikan arvoa käsitellään, koska kyseinen arvo saattaa muuttua. Määre volatile siis tarkoittaa sitä, että attribuutin käsittely alkaa aina tiedon lataamisella muistipaikasta, ei rekisteristä. Jos kuitenkin noudatat ylläesiteltyjä sääntöjä attribuuttien suojauksesta ja merkitset niitä käsittelevät rutiinit synkronoiduiksi, et tarvitse volatile-attribuutteja. Monitorit eivät ratkaise kaikkia reaaliaikaohjelmoinnissa esiintulevia ongelmia, mutta auttavat tapahtumien synkronoinnissa. Tärkeätä on huomata, että synkronoitua rutiinia suorittava säie ei saa pysäyttää toimintaansa esimerkiksi sleep-käskyllä, koska sleep ei anna lukkoa muiden käyttöön,

3.4. RESURSSIEN JAKAMINEN 25 ainoastaan wait tekee sen (toisaalta, wait-käskyä saa käyttää vain synkronoidun rutiinin tai lohkon sisällä, sleep-käskyä missä tahansa). Jos siis haluat estää lukkiutumiset, käytä monitorin sisällä aina wait-käskyä silloin, kun pysähtyminen on tarpeen. Sitäpaitsi vaikka kaikkia esitettyjä ohjeita noudatettaisiin, virhetilanteita saattaa syntyä silti. Tarkastellaanpa säikeitä, jotka on kirjoitettu pankkitilin käsittelyyn. Tyypillisiä tapahtumia ovat otto automaatilta, tilisiirto, suoraveloitus jne., jotka kaikki voivat tapahtua samaan aikaan. Ensimmäinen toimenpide on tietysti tehdä tililtä nostava rutiini nosto ja tilikyselyrutiini saldo synkronoiduiksi, koska muuten useat eri säikeet voivat kysellä saldoa samanaikaisesti ja nostaa samat rahat tililtä moneen kertaan. Mutta tämäkään ei riitä. Vaikka kukin säie saakin monitorin yksin haltuunsa näitä rutiineja suorittaessaan, voi rutiinikutsujen saldo ja nosto välillä tapahtua kuitenkin muita tilitapahtumia. Esimerkiksi pankkiautomaattia mallintavassa säikeessä tehdyn saldotarkistuksen jälkeen kyseisen säikeen suoritus saatetaan keskeyttää suoraveloitussäikeen ansiosta ja jälkimmäinen tekee veloituksen. Tämän jälkeen automaattiottosäie saa taas kontrollin ja yrittää nostaa annetun rahamäärän (joka alkujaan todettiin pienemmäksi kuin saldo), mutta ei onnistu, koska suoraveloitussäie on vetänyt välistä. Ongelma saadaan ratkaistua, mikäli meillä on mahdollisuus lukita koko tiliobjekti siksi ajaksi kun joku on pankkiautomaatilla. Tätä varten Javassa on käytettävissä nk. synkronoitu lohko (synchronized statement), joka on muotoa synchronized (lauseke) /* Käskyt, joiden aikana lukitus on päällä */ Normaalisti lauseke on viittaus siihen objektiin, joka halutaan lukita, mutta yleisessä tapauksessa se voi olla lauseke, joka palauttaa objektiviittauksen. Lukitsemalla koko tiliobjekti saldon ja noston ajaksi toiminnasta saadaan atomaarinen siinä mielessä, että muut säikeet eivät pääse väliin vaan joutuvat odottamaan lukon vapautumista. Nyt kun tämäkin ongelma on saatu ratkaistua, muistutetaan vielä siitä, että reaaliaikaohjelmoinnissa täytyy ottaa vielä muitakin asioita huomioon: automaatilla oleva käyttäjä voi tumpuloida pahasti, tietoliikenneyhteydet voivat mennä poikki jne. Automaattiottosäikeen täytyy kuitenkin pystyä näissäkin tilanteissa vapauttamaan lukko, koska muuten koko systeemi lukkiutuu (deadlock). Ja vielä muutama huomio säikeiden ja synkronoinnin käytöstä. Synkronoitu lauseke mahdollistaa yleisesti tavattavan, muotoa public synchronized void method() \\Ei-kriittinen osa; \\Kriittinen osa; \\Ei-kriittinen osa; olevan rutiinin muuttamisen muotoon public void method() \\Ei-kriittinen osa; synchronized(this) /* Kriittinen osa; */ \\Ei-kriittinen osa; Luokan konstruktoria ei tarvitse esitellä synkronoiduiksi, koska objektin luova säie on aina yksinään vastuussa luontioperaatiosta. Jos synkronoitu rutiini kutsuu toista synkronoitua rutiinia, säikeen ei tarvitse saada lukkoa toistamiseen (eihän siitä ole kuin yksi esiintymä olemassa!) vaan voi jatkaa kutsumaansa piirrettä normaalisti. Yleisperiaatteena voidaan siis sanoa, että lukon omaavalla säikeellä on oikeus käsitellä objektia kuinka haluaa muiden häiritsemättä. synchronized-määreeseen ei liity samanlaisia sääntöjä kuin suojausmääreisiin. Erityisesti jos isäluokka esittelee synkronoidun rutiinin

26 LUKU 3. SÄIKEET (BY TIMO RAITA) ja perijä antaa sille korvauksen, jälkimmäisen ei tarvitse olla synkronoitu. Jos korvaava toteutus kuitenkin käyttää isäluokan versiota toimintansa osana, se on synkronoitu ko. rutiinin suorituksen ajan. Luokalla on myös oma lukkonsa, joten synchronized static -määreillä leimatut operaatiot voivat estää muiden samanaikaisen pääsyn static-tyyppisiin luokkatietoihin. 3.5 Säieryhmät ja demonit Kaikki säikeet liitetään luonnin yhteydessä johonkin säieryhmään (thread group). Oletuksena säie liitetään siihen ryhmään, johon sen luova säiekin kuuluu. Säie main kuuluu säieluokkaan main. Säieryhmä voidaan luoda ja nimetä kutsumalla luokan ThreadGroup konstruktoria: ThreadGroup folders = new ThreadGroup( database ); Uusi ryhmä kiinnitetään oletuksena sen ryhmän lapseksi, missä ko. ryhmän luova säie on. Isäluokka voidaan antaa myös eksplisiittisesti argumenttina onstruktorille. Ryhmät muodostavat siis puurakenteen, jonka juurena on main. Niputtamalla luodut lapsisäikeet samaan ryhmään niitä voidaan käsitellä yhtenä kokonaisuutena: ryhmän kaikkien säikeiden suoritus voidaan keskeyttää, maksimiprioriteettia muuttaa, asettaa demoneiksi jne. Tämän nojalla aiemman Coordinator-luokan waitforcounterstofinish voidaan kirjoittaa uudelleen niin, että se testaa lapsisäikeiden muodostaman ryhmän kokoa: class GroupCoordinator implements Runnable public GroupCoordinator() initcounters(); protected void initcounters() Thread[] counters = getcounters(); ThreadGroup group = getgroup(); for (int i = 0; i < counters.length; i++) final int min = 10*i+1, max = min+9; counters[i] = new Thread(group, new Counter(min,max)); System.out.println("All counters created."); public void run() Thread[] counters = getcounters(); for ( int i = 0; i < counters.length; i++) counters[i].start(); System.out.println("All counters ready to run."); waitforcounterstofinish(); System.out.println("All the threads have finished."); protected void waitforcounterstofinish() ThreadGroup group = getgroup();

3.5. SÄIERYHMÄT JA DEMONIT 27 do try Thread.sleep(500); catch (InterruptedException e) while (group.activecount() > 0); protected Thread[] getcounters() return counters; protected ThreadGroup getgroup() return countergroup; private final int numcounters = 3; private final Thread[] counters = new Thread[numCounters]; private final ThreadGroup countergroup = new ThreadGroup("counters"); Säikeitä ryhmitellään pääsääntöisesti turvallisuussyistä, sillä luokka SecurityManager hoitaa turvallisuusnäkökohdat säieryhmien pohjalta. Ryhmäpohjainen turvallisuus perustuu siihen, että yhdessä ryhmässä oleva säie ei saa muuttaa toisessa ryhmässä olevaa säiettä muuta kuin siinä tapauksessa, että muutettava säie on muuttavan säikeen aliryhmässä. Esimerkiksi ladattavaan applettiin kuuluvat säikeet sijoitetaan aina omaan ryhmäänsä, jotta ne eivät voisi häiritä muita JVM:n alaisuudessa samanaikaisesti toimivia säikeitä. PiirteencheckAccess (löytyy luokista Thread ja ThreadGroup) avulla voidaan tarkistaa, onko nykyisellä säikeellä lupa mennä muuttamaan toista säiettä tai ryhmää. Säieryhmien hallintaan on myös setmaxpriority, joka määrittää annetun ryhmän uusille säikeille suurimman mahdollisen prioriteetin, jota ne voivat käyttää. Tällä tavoin voidaan estää se, että vihamieliset säikeet ottaisivat hallintaansa koko systeemin antamatta kontrollia muille. Demonisäikeiden (daemon threads) on tarkoitus tarjota taustalla yleispalveluja (laiteajurit, postipalvelin yms.), mutta normaalisti niiden ei katsota muodostavan oleellista osaa itse sovellusalueesta. Demonisäikeet luodaan normaaliin tapaan, mutta niille annetaan erityisstatus luokan Thread rutiinilla setdaemon(boolean on), jonka argumentiksi annetaan true. Demonisäie voidaan myöhemmin palauttaa tavalliseksi kuolevaiseksi antamalla samalle rutiinille argumentti false. Säie on julistettava demoniksi setdaemon-kutsulla ennen säikeen käynnistämistä (start). Demonin luomat alisäikeet ovat automaattisesti myös demoneja. Demonit poikkeavat normaaleista ns. user-säikeistä myös ohjelman suorituksen loppuessa: main-säie odottaa tällöin aina normaalisäikeiden loppumista, mutta demonisäikeiden suoritus katkaistaan väkisin.

28 LUKU 3. SÄIKEET (BY TIMO RAITA)

Luku 4 Johdatus hajautettuihin sovelluksiin Nykyisin tietokoneverkkoja käyttävät ohjelmat perustuvat lähinnä TCP/IP-protokollaperheen (Transmission Control Protocol/Internet Protocol) protokollien käyttöön. Itse asiassa kyseiset protokollat kehitettiin ja vietiin ohjelmointikieliin kirjastojen muodossa jo 70/80-lukujen taitteessa. Vaikka verkossa käytettävät laitteet ovatkin ajan saatossa muuttuneet ja protokollia on tullut runsaasti lisää, niin TCP/IP:n olemus on pysynyt käytännössä muuttumattomana. Muutos on kuitenkin tulossa kun nykyinen IPv4 on tarkoitus lähiaikoina muuttaa IPv6:ksi 1. TCP/IP-pohjainen kommunikaatio perustuu ns. sokettien käyttöön siihen perehdytään luvussa 5. Soketit muodostavat helpon tavan välittää tietoa ohjelmasta toiseen TCP-soketit nähdään Javaohjelmassa kommunikoivien osapuolten kannalta tiedostovirroiksi, joiden kautta voidaan kirjoittaa ja lukea vaikkapa tekstiä tai yleisemmin sarjoittuvia olioita. Luvussa 5 perehdytään myös toiseen IP:n päällä ajettavaan protokollaan ja siihen liittyviin toisenlaisiin soketteihin, nimittäin UDP:hen (User Datagram Protocol). Vaikka verkossa onkin kätevää välittää tietoa sarjoittuvina Java-olioina, niin aina tämä ei ole mahdollista. Saattaa nimittäin olla, että toinen osapuoli ei ole Java-ohjelma. Lisäksi Java-ohjelmien keskenkin sarjoittuvuuden käyttöä haittaa mahdollisuus siitä, että ohjelmat käyttävät eri versiota ko. luokista. Eräs yleinen ratkaisu tällaiseen rakenteisen tiedonvälittämisongelmaan eri verkkosovellusten välillä on XML (extensible Markup Language). XML:ään ja sen tarjoamiin mahdollisuuksiin tässä yhteydessä perehdytään luvussa 9. Java tarjoaa myös muita tapoja muodostaa keskenään verkon välityksellä kommunikoivia ohjelmia. Näistä RMI (Remote Method Invocation) on sellainen, että kommunikoivien ohjelmien tulee olla Java-ohjelmia. Nimensä mukaan RMI mahdollistaa metodien etäkutsumisen eli ohjelma voi soveltaa verkon yli metodin kutsua toisessa verkon koneessa olevaan Java-olioon. RMI on selvästi saanut vaikutteita RPC:stä (Remote Procedure Call; Sun Microsystems) ja CORBAsta (Common Object Request Broker Architecture). RMI:hin perehdytään luvussa 6. CORBAan ei tämän materiaalin puitteissa tutustuta, mutta CORBAn käyttäminen on hyvin samantapaista kuin RMI:kin käyttäminen. Luvussa 10 perehdytään vielä ns. servlettien, eli palvelinsovelmien, yhteydessä käytettävään varsin erilaiseen kommunikaatiotapaan. Servlettejä on nimittäin tarkoitus suorittaa WWW-palvelimessa ja niitä kutsutaan HTTP-protokollaa (HyperText Transfer Protocol) käyttäen. 1 IPv6:n tekninen valmius on jo useilla käyttöjärjestelmillä ja tuki sille on myös mallinnettu ohjelmointikielten kirjastoihin. Itse käyttöönotto on viivästynyt ja viivästynyt, vaikka tekniset edellytykset ovat olleet kunnossa jo kauan. Kysymys on siirtymisen kustannuksista yksittäisille organisaatioille, mutta ennen kaikkea siitä, että IPv4-osoiteavaruuden omistajuus on miljardien dollarien vuosittainen bisnes muutamille Yhdysvaltalaisille yrityksille. 29

30 LUKU 4. JOHDATUS HAJAUTETTUIHIN SOVELLUKSIIN 4.1 Asiakas-palvelin sovelluksista yleensä Tietokoneverkot ovat nykyisellään varsin monimutkaisia, mutta onneksi verkkosovellusten käyttämiseksi eikä oikeastaan niiden ohjelmoimiseksikaan tarvitse tietää niiden rakenteen monimutkaisuudesta juuri mitään. Sovellusten kannalta verkko on yksinkertainen abstraktio, joka koostuu osoitettavissa olevista osapuolista verkossa (IP-osoitteet, porttinumerot,... ) ja yhteyden muodostus- ja keskustelutavoista. Pakettien reittäminen verkossa on keskustelevien osapuolten kannalta (lähes) täysin automaattista. Sovellusten tekemisen kannalta tulee tosin ymmärtää, että verkko ei aina ole kaikilta osiltaan ehjä jokin yhteys/laite voi olla hetkellisesti poissa verkosta tai jopa rikki. Lisäksi ongelmatilanteita aiheuttaa se, miten paketteja kuljetetaan verkossa. Kun verkon osapuoli A haluaa lähettää jotakin osapuolelle B, niin se ei tarkoita, että verkossa olisi varattu / varattaisiin 2 tätä varten jokin erityinen kanava, jota pitkin A ja B voisivat keskustella häritsemättä muita verkossa tapahtuvia samanaikaisia keskusteluita. Ethernet-verkossa useat laitteet voivat samanaikaisesti yrittää lähettää jotakin ja jos niin tapahtuu, niin lähetykset törmäävät 3 ja sen seurauksena mitään ei saada sillä kertaa perille vaan lähettävät osapuolet joutuvat yrittämään lähettämistä myöhemmin uudelleen. Toinen huomion arvoinen seikka on, että paketit eivät suinkaan kulje lyhintä reittiä pitkin lähteestä kohteeseen, vaan esim. muutamat Ethernet-verkon laitteet ovat lähes täysin tietämättömiä siitä, missä paketin vastaanottaja tarkalleen ottaen on. Siksi ne toimittavat paketin laajalle alueelle 4 sillä ajatuksella, että vastaanottaja on kyseisellä alueella (tai alueella on sellainen verkon elementti, joka tietää, missä suunnassa vastaanottaja on). Hajautettu sovellus koostuu useista eri koneissa toimivista ohjelmista, jotka keskustelevat toistensa kanssa verkon yli. Itse asiassa ohjelmien ei tarvitse toimia eri koneissa, vaan kaikki voi tapahtua myös koneen sisällä mutta tuolloinkin keskustelu ohjelmien välillä tapahtuu yleensä verkkoprotokollia käyttäen. Kaikkien ohjelmien ei tarvitse keskustella kaikkien kanssa yleensä jokin tai jotkin ohjelmista toimivat palvelimina muiden ollessa asiakkaita ja muodostettavissa verkkoyhteyksissä toisena osapuolena on aina palvelin. Ohjelmien välinen kommunikointi tarkoittaa sitä, että sovellukset keskustelevat keskenään käyttäen jotakin sovellustason kieltä siis vaihtaen tietynlaisia viestejä keskenään (tietynlaisen järjestyksen mukaan). Kyseiset viestit kuljetetaan lähettäjältä vastaanottajalle käyttäen verkkoprotokollia (ja ohjelmien ollessa eri koneissa, käyttäen tietokoneverkkoa). Kommunikoivien ohjelmien ei tarvitse olla tehty samalla ohjelmointikielellä. Esimerkiksi soketit (luku 5) on aluksi esitelty ohjelmointikieliin C-kielen kirjastojen kautta, mutta Java-kieliset soketteja käyttävät ohjelmat voivat hyvin keskustella C-kielisten ohjelmien kanssa. Kommunikoinnin suhteen ohjelmat voidaan pyrkiä jakamaan kahteen kategoriaan: asiakkaisiin ja palvelimiin. Jako ei kuitenkaan ole täydellinen, sillä ohjelma voi toimia molemmissa rooleissa samanaikaisesti (mahdollisesti eri ohjelmajoukkojen suuntaan eri roolissa). Jako perustuu rooliajatteluun ja siihen, että palvelimeksi mielletään sellainen, jolla on jotain resurssia (tietoa, välityskykyä, tms) jaettavanaan, ja asiakas puolestaan on ohjelma, joka haluaa jotain resurssia. Usein palvelin on sovellus, joka kuuntelee yhteydenottopyyntöjä. TCP:n ja RMI:n tapauksessa tämä näkyy selkeästi ohjelmasta UDP:n tapauksessa tunnistaminen on vaikeampaa. Verkkokeskustelun aloittava osapuoli on lähes aina asiakkaan roolissa. 2 ATM-verkoissa toimitaan itse asissa tähän tapaan, mutta Ethernet-verkoissa törmäykset ovat mahdollisia. 3 Törmääminen edellyttää, että lähetysten lähteiden ja kohteiden välillä on jokin yhteinen alue, jota ne kumpikin käyttävät. 4 Tämä tarkoittaa, että lähettäjä-vastaanottaja pariin liittyy tietynlainen (verkosta riippuva) törmäysalue, jossa paketti voi Ethernet-verkossa törmätä toisen kenties aivan eri kohteeseen menevän paketin kanssa.

4.1. ASIAKAS-PALVELIN SOVELLUKSISTA YLEENSÄ 31 Verkkosovellukset ja monisäieohjelmat voi tietyssä mielessä rinnastaa. Kummatkin ovat tietyssä mielessä itsenäisiä samanaikaisesti suoritettavista osista koostuvia kokonaisuuksia. Säikeiden tapauksessa keskinäisellä vuorovaikutuksella usein pyritään ohjaamaan toisten säikeiden suoritusta. Toisaalta verkkosovellukset vaihtavat yleensä keskenään passiivista tietoa, jolla ei suoranaisesti pyritä ohjaamaan toisen osapuolen toimintaa epäsuorasti välitettävä tieto toki vaikuttaa ohjelman suoritukseen. Verkkosovellusten ja monisäieohjelmien osalta kannattaa kuitenkin huomata, että periaatteessa kaikki monisäieohjelmien tekemiseen liittyvät ongelmat (luvut 2 ja 3) ovat myös verkkosovellusten ongelmia. Esimerkkejä verkkosovelluksista on helppo löytää: Erilaiset verkkopelit ovat sovelluksia, jossa palvelin ylläpitää pelipöytää ja asiakkaat vain tekevät siirtoja sekä esittävät pelitilannetta omalta kannaltaan. Huomaa kuitenkin, että tällaisissa sovelluksissa palvelimen tehtävä on varsinaisesti pitää kirjaa pelitilanteesta. Informaatiopalvelut perustuvat usein ratkaisuun, jossa palvelinsovellus ylläpitää tietovarastoa yleensä jonkin tietokannan muodossa ja asiakkaat tekevät tiedon hakuja sekä mahdollisesti päivityksiä. Asiakkaiden rooli on erilaisten toimenpiteiden ilmaisemisen mahdollistamisessa sekä luonnollisesti tulosten esittämisessä. Edellisen yksinkertaistettuja muotoja ovat esimerkiksi erilaiset tiedoituspalvelut: kellon ajan kertominen, noppien arpomispalvelu, WWW-palvelu,.... Tähän kategoriaan voidaan sovittaa myös esimerkiksi sähköpostin luku- ja välityspalvelut tai monimutkaisemmat WWW:n kautta tapahtuvat palvelut, kuten esimerkiksi verkkokauppa. Kun verkkosovelluksia tehdään, pitää ratkaista mm. seuraavia ongelmia. Miten konkreettisesti välittää tietoa? Miten asiakas tietää, missä palvelin on? Miten palvelin osaa vastata asiakkaalle? Miten siis yhteyden muodostaminen & lopetus tapahtuu? Entä jos palvelimella on useita asiakkaita samanaikaisesti? Entä jos asiakas käyttää useita palvelimia? Mitä ongelmia samanaikaisuudesta on? Palvelin: miten palvella kaikkia tasapuolisesti? Miten hallita hajautetun sovelluksen siirtymistä tilasta toiseen? Mitä tietoa asiakkaan ja palvelimen välillä tulisi missäkin vaiheessa välittää? Moniin edellisistä kysymyksistä antaa vastauksen käytettävä verkkoprotokolla, mutta samanaikaisuuteen ja kokonaisuuden hallintaan liittyvät ongelmat ovat lähinnä suunnittelulla hallittavia kysymyksiä.

32 LUKU 4. JOHDATUS HAJAUTETTUIHIN SOVELLUKSIIN 4.2 Verkosta ja verkkoprotokollista Kommunikointi tietoverkoissa on hyvin kurinalaista, se tapahtuu käyttäen jotakin protokollaa. Protokolla tarkoittaa muotokieltä, eli lähetettävien viestien muoto on tarkkaan määritelty, mutta tyypillisesti protokolla on enemmän kuin pelkkä viestien muoto protokolla usein etenee tietyn vuoropuhelukaavan mukaan. Tuolloin voidaan ajatella, että protokolla on keskustelevien osapuolten (siis sovellusten) kannalta tietyssä tilassa. Protokollaan tyypillisesti liittyy myös mahdollisuus nimetä keskustelevat osapuolet. Nykyisin suuri osa tietokoneverkoista on kytketty toisiinsa ja yhdessä ne muodostavat maailmanlaajuisen ns. Internet-verkon. Internet-verkossa koneilla on ns. Internet-nimi, esimerkiksi alhena.utu.fi. Internet-nimi on symbolinen nimi. Varsinainen liikennöinti verkossa ei perustu siihen vaan ns. IP-osoitteeseen, esimerkiksi 130.232.1.1. IP-osoitteiden muoto on tarkkaan määritelty ja IPv4 5 :n yhteydessä osoite koostuu neljästä kokonaisluvusta väliltä 0... 255, eli osoitteista 0.0.0.0... 255.255.255.255. Yhtä IP-osoitetta kohti on yksi Internet-nimi 6. Tyypillisesti koneella on yksi verkkokortti ja sitä kautta koneella on yleensä yksi IP-osoite, mutta koneella voi olla useita verkkokortteja ja sitä kautta se voi olla kytkettynä useaan verkkosegmenttiin 7. Kuhunkin koneeseen liittyy yksi yhteinen Internet-osoite, localhost. Ohjelman kannalta nimi localhost tarkoittaa sitä konetta, jossa ohjelmaa suoritetaan. Vaikka localhost onkin verkkoosoite, niin itse asiassa sen käyttäminen ei vaadi, että kone olisi kytketty verkkoon. Huomaa, että osoitteen localhost avulla sovellukset voivat koneen sisällä keskustella IP-protokollalla ilman todellista verkkoyhteyttä. Internet-verkon käyttäjän sen enempää kuin ohjelmoijankaan ei pääsääntöisesti tarvitse tuntea koneiden IP-osoitteita. Tämä johtuu siitä, että Internet-verkossa on (maailmanlaajuinen, hierarkinen) nimipalvelu 8, jolta sovellukset voivat kysyä symbolista nimeä vastaavaa IP-osoitetta (tai kääntäen). Vaikka nimipalvelu itsessään on varsin kompleksinen järjestelmä, verkkosovellusten tekijän ei tarvitse tietää siitä juuri muuta kuin, että se on potentiaalinen ongelmien lähde osa verkon laitteista on nimittäin aina rikki ja siten kaiken nimipalvelun takana olevan tiedon ei aina tarvitse olla saavutettavissa. Internet-verkkoa käyttävien Java-ohjelmien yhteydessä Internet-nimiä vastaavien IPosoitteiden selvittäminen tapahtuu tyypillisesti automaattisesti. Vaikka liikennöinti Internet-verkossa perustuukin IP-protokollan käyttöön, niin yleensä sovellukset välittävät tietoa käyttäen IP-protokollan päällä suoritettavaa TCP- tai UDP-protokollaa. Verkkoliikenteen ja verkon ylipäänsä voidaan nähdä olevan monikerroksista. ISO (International Organization for Standards) on määritellyt ns. OSI-mallin (Open System Interconnection) verkon rakenteen ja toiminnan selittämiseksi. OSI-malli on 80-luvun alkupuolelta ja siinä on 7 kerrosta. Nykyisin verkko yleensä kuvataan 4-kerroksiseksi 9 heijastaen suoraan verkossa välitettävien pakettien rakennetta. Kuvassa 4.1 on esitettynä tyypillisesti verkossa liikutettavien pakettien rakenne. Kuten kuvasta näkyy, pakettien rakenne on kerroksittainen: esimerkiksi fyysisen verkkokerroksen Ethernet-paketti pitää sisällään IP-paketin, joka puolestaan pitää sisällään TCP-paketin, joka puolestaan koostuu sovellukseen liittyvästä tiedosta. Internet-verkko koostuu paikallisista pienistä verkoista (LAN, Local Area Network), jotka on yhdistetty toisiinsa erilaisilla reititinlaitteilla (toistin, keskitin, silta, kytkin, reititin,... ). Verkon laitteita ja rakennetta on havainnollistettu kuvassa 4.2. Yksinkertaisimmillaan paikallinen verkko koostuu esi- 5 Internet Protocol, version 4. 6 Tosin sillä nimellä voi olla useita aliaksia. 7 Itse asiassa yhteen verkkokortiinkin voi liittyä monta IP-osoitetta. 8 DNS, Domain Name Service. 9 Joskus käytetään vain kolmea kerrosta yhdistäen 4-kerroksisen mallin kaksi keskimmäistä kerrosta.

4.2. VERKOSTA JA VERKKOPROTOKOLLISTA 33 välitettävä data sovellusotsikko TCP-otsikko IP-otsikko LAN-otsikko Kuva 4.1: Verkon kerrokset. merkiksi Ethernet-verkon segmentistä. Paikallisessa verkossa siirretään paketteja käyttäen sen kieltä esimerkiksi Ethernet-verkossa siirretään Ethernet-paketteja. Tätä verkon alinta kerrosta kutsutaan fyysiseksi kerrokseksi tai siirtoyhteyskerrokseksi. Yleensä tämä kerros perustuu Ethernet- tai ATM-verkkoon. Verkossa välitettävien pakettien kannalta tämä tarkoittaa, että paketit ovat aina LANpaketteja, jotka pitävät sisällään jotain tietoa. Niihin kuitenkin liittyy kyseisen verkon mukaisia tietoja esimerkiksi Ethernet-verkkokorteilla on osoite, joka on esimerkiksi 0:0:3b:80:37:80 se koostuu siis kuudesta 2-numeroisesta 16-kantaisesta luvusta. ATM-verkoissa koneilla on myös osoitteet, mutta ne ovat toisenlaisia. Siirtoyhteyskerroksen päällä toimii verkkokerros, jota IP-verkkojen yhteydessä usein kutsutaan IP-kerrokseksi. IP-pakettien otsikoissa on fyysisen kerroksen pakettien tapaan tietoja, joita tarvitaan pakettien reitittämiseksi verkossa läheteestä kohteeseen. Suuri osa verkon laitteista siirtää (kopioi) paketteja yhdestä verkon osasta toiseen käyttäen fyysisen kerroksen otsikkotietoja, mutta esimerkiksi reitittimet käyttävät jo IP-kerroksen tietoja reitityksen toteuttamisessa. Ylimmällä sovelluskerroksella välitetään sovelluksiin liittyvää dataa niiden määrittelemässä muodossa tosin, jos sovellusten välillä halutaan siirtää paljon dataa, pitää se yleensä jakaa useisiin IP-paketteihin (ja samalla useisiin TCP/UDP-paketteihin). Sovelluskerroksen sovelluksia ovat mm. SSH, TELNET, FTP, WWW-selaimet ja palvelimet sekä sähköpostia käsittelevät ohjelmat.

34 LUKU 4. JOHDATUS HAJAUTETTUIHIN SOVELLUKSIIN 00 11 1100 Silta 00 11 01 0 1 Ethernet kytkin 01 01 01 01 Ethernet Switch 00 11 01 01 00 11 01 00 11 Silta 00 11 01 01 Telco/LAN Router ATM kytkin Reititin 00 11 01 01 00 11 01 01 00 11 01 01 00 11 01 01 00 11 01 01 Kuva 4.2: Verkon komponentteja.

Luku 5 Soketit ja niiden käyttö Tässä luvussa perehdytään jo 70/80-lukujen taitteessa muotoutuneeseen tapaan välittää tietoa eri koneissa suoritettavien sovellusten välillä tietokoneverkkoa käyttäen. Soketti (engl. socket) on sovellusten välinen kytkös (kone1, portti1) (kone2, portti2) siten, että kumpikin kone tietää kytköksen olemassaolon (UDP-sokettien tapauksessa kytkös on tosin varsin löyhä, lähes olematon). Seuraavaksi luvussa 5.1 kerrotaan sokettien perusteita ja muodostetaan näkemys sokettien käytöstä koneen ja sovelluksen kannalta. Luvuissa 5.2 ja 5.3 perehdytään UDP- ja TCP-soketteihin ja niihin liittyviin luokkiin esimerkkien avulla. Luvussa 5.4 perehdytään hieman erikoisempiin sokettien käyttötilanteisiin: ryhmälähetykseen ja sokettien käyttöön applettien yhteydessä. 5.1 Perustietoa soketeista Soketti on IP:n päällä toimiva kanava, jonka kautta sovellus voi sekä lähettää että vastaanottaa paketteja 1. IP-osoitteen lisäksi sokettiin liittyy aina porttinumero, joka on kokonaisluku 0... 65535. Periaatteessa ohjelmoija voi vapaasti valita luomalleen sokettioliolle sen käyttämän porttinumeron, mutta käytännössä omien sokettiyhteyksien tulisi käyttää portinumeroita 1024... 65536, sillä porteille 0... 1023 on määritelty stardardimerkitys 2 IANAn (Internet Assigned Numbers Authority) toimesta. Stardardimerkitys tarkoittaa sitä, että kyseinen portti on varattu tietyn Internet-palvelun käyttöön. Yleistä sokettien käyttöön liittyvää tilannetta yksittäisen koneen kannalta on havainnollistettu kuvassa 5.1. Koneen sisällä tavallisesti toimii useita sovelluksia samanaikaisesti ja kukin niistä voi käsitellä samanaikaisesti yhtä tai useampaa sokettia (ja sitä kautta porttia). Koneen sisällä toimiviin sovelluksiin liittyvien sokettien tulee käyttää eri porttinumeroita. Käytännössä sokettiolion 3 luominen aiheuttaa sen rekisteröimisen koneen sisällä verkkokorttin toimintaan liittyvälle ohjelmalle. Tuon rekisteröitymisen seurauksena verkkokortin nappaamat kyseiselle koneelle ja tietyyn porttiin osoitetut paketit osataan koneen sisällä toimittaa oikealle sovellukselle. Vastaavasti lähetystoiminnan yhteydessä sokettiyhteyden kautta lähetettyihin paketteihin osataan liittää lähettäjän soketin porttinumero (IP-numeron lisäksi). Sokettiyhteyden kautta lähetetään tietoa lähettävästä soketista vastaanottavaan sokettiin. Tietoa voidaan sokettien avulla välittää kolmella eri tavalla. 1 Soketit voivat toimia myös koneen sisällä ilman IP-verkkoa, mutta sellaista tilannetta ei seuraavassa tarkastella. Esimerkiksi, soketteja on käytetty Unix:n puitteissa jopa levyoperaatioiden toteutuksessa. 2 Esim. SSH käyttää porttia numero 22. Ks. http://www.iana.org/assignments/port-numbers. Joillakin porttinumeroilla 1024... 65536 on myös standardinomainen merkitys. 3 Oliokielissä sokettiolion mutta esim. C-kielen tapauksessa socket-tyyppisen struct:n. 35

36 LUKU 5. SOKETIT JA NIIDEN KÄYTTÖ Tietokone Ohjelma Ohjelma Ohjelma Portti Portti Portti Verkko Kuva 5.1: Yleiskuva sokettien käytöstä yksittäisen koneen kannalta. Yhteydettömästi UDP-sokettien avulla. Yhteydettömässä lähetyksessäkin on soketti kummassakin päässä, mutta ideana on, että yhteydenotot ovat kestoltaan aina vain yhden paketin mittaisia. UDP-soketteihin ja niiden toteuttamiseen liittyviin luokkiin DatagramPacket ja DatagramSocket perehdytään luvussa 5.2. Yhteydellisesti TCP-sokettien avulla. Yhteydellisyys tarkoittaa, että ennen varsinaista datan lähettämistä sokettiyhteyden ylitse osapuolet neuvottelevat välilleen sokettiyhteyden, joka on pisteestä pisteeseen -tyyppinen (point-to-point). Osapuolet sitoutuvat TCP-sokettien kohdalla yhteyteen aivan eri tavalla kuin UDP-sokettien kohdalla. TCP-sokettiyhteys muodostaa tosiasiallisesti tiedostovirran molempiin suuntiin sokettiyhteyden yli. UDP:n tapauksessa sokettiyhteyden yli lähetetään yksittäisiä paketteja, mutta TCP:n tapauksessa vain kirjoitetaan (ja vastaavasti luetaan) sokettiin liittyvään tiedostovirtaan 4. Luvussa 5.3 perehdytään TCP-sokettien olemukseen ja käyttöön esimerkkien avulla. Samalla tehdään selkoa Javan luokista Socket ja ServerSocket sekä tutkitaan soketteihin liittyvien tiedostovirtojen käyttöä (luokat InputStream, DataInputStream, ObjectInputStream,... ). Edellä mainitut tavat käsitellä paketteja ovat ns. täsmälähetyksiä (unicast-tyyppisiä), eli niissä lähetetään tietoa yhdestä paikasta yhdelle vastaanottajalle. Verkossa voidaan lähettää tieto samanaikaisesti myös usealle vastaanottajalle. Ns. ryhmälähetyksissä (engl. multicast) lähetetty paketti toimitetaan usealle vastaanottajalle ryhmään kuuluville vastaanottajille. Ryhmälähetykset edellyttävät vastaanottajilta ryhmään rekisteröitymistä. Toinen yhdestä moneen - lähetysten muoto ovat yleislähetykset (engl. broadcast), joissa vastaanottajat määritetään niiden IP-osoitteiden perusteella (ei tarkastella tässä). Ryhmälähetyksiä käsitellään kohdassa 5.4. 5.1.1 Luokka InetAddress Soketteihin liittyvät luokat ovat Javan yhteydessä paketissa java.net. Kuten on tullut ilmi, sokettien käyttämisen yhteydessä tarvitaan porttinumeroita ja IP-osoitteita. IP-osoitteita varten luokkakirjastossa on luokka java.net.inetaddress, jonka metodeita on lueteltu taulukossa 5.1. 4 Viime kädessä TCP-sokettiyhteyden käyttäminen tosin tarkoittaa pakettien lähettämistä, mutta TCP:n tapauksessa käyttäjän ei tarvitse itse tehdä pakettien muodostamista.

5.2. YHTEYDETÖN UDP 37 Vaikka luokan mukaisia olioita onkin olemassa, niin luokalla ei ole lainkaan julkisia konstruktoreita. Ideana on, että oliot luodaan luokkametodeilla getbyname ja getlocalhost. Näistä edellinen muodostaa olion käyttäen joko internet-nimeä tai IP-osoitetta. static InetAddress getbyname(string host) Muodostaa annettavan internet-nimen tai IPosoitteen perusteella IP-osoitteen. Esim. alhena.utu.fi 130.232.1.1. Tuottaa poikkeuksen UnknownHostException, jos nimipalvelusta ei löydy internet-nimeä vastaavaa IP-osoitetta. static InetAddress getlocalhost() Koneen, jossa ohjelmaa ajetaan, IP-osoite. Tuottaa poikkeuksen UnknownHostException, jos koneen IP-osoitetta ei ole määritetty. String gethostaddress() Palauttaa osoitteen numeromuodossa (esim. 130.232.1.1). String gethostname() Palauttaa IP-osoitetta vastaavan internet-osoitteen. boolean ismulticastaddress() Onko multicast-osoite? Taulukko 5.1: Joitakin luokan InetAddress metodeja. Kun taulukon 5.1 metodeissa tarvitaan yhteyttä IP-osoitteen ja internet-nimen välille, niin kyseinen tieto yritetään hakea nimipalvelusta (DNS). Tätä kautta voi tulla ongelmia, jos tietoja ei löydy nimipalvelusta. Toinen mahdollinen ongelma on turvallisuuspoikkeuksen syntyminen, jos yhteydenottoa nimipalveluun ei ole (turvallisuusmanagerin toimesta) sallittu. 5.2 Yhteydetön UDP UDP-yhteyden ideana on olla kevyt ja nopea yhteys. UDP-soketit muodostavat yhteydettömän kanavan. Tämä tarkoittaa käytännössä, että pakettien järjestys voi matkalla vaihtua, tai jotkin paketit voivat kokonaan kadota matkalla. Vastaanotettuja paketteja ei kuitata mitenkään, ja ylipäänsä pakettien lähettämisessä ja vastaanottamisessa ei ole mitään automaattista kontrollia, jonka avulla virheelliset 5 tai matkalla kadonneet paketit voitaisiin pyytää lähetettäväksi uudelleen. UDP on siis pakettien perilletoimittamisen kannalta varsin epäluotettava yhteys mutta sillä on kuitenkin monia järkeviä käyttötarkoituksia. UDP-yhteyttä käytetään Java-ohjelmissa siten, että lähettäjä ja vastaanottaja luovat kummatkin itselleen DatagramSocket-tyyppisen olion. Sen jälkeen lähettäjä luo yhden tai useamman paketin, jotka se sokettiyhteyden kautta lähettää vastaanottajalle. Paketit ovat luokan DatagramPacket olioita ja vastaanottajankin pitää luoda itselleen sellaisia vastaanottamisen yhteydessä verkosta tuleva tieto kopioidaan vastaanottajan sokettiyhteydelle tarjoamaan vastaanottavaan DatagramPacket-olioon. Javan DatagramPacket-luokan metodeita ja konstruktoreita on esitelty taulukossa 5.2. Verkossa liikkuvaan UDP-pakettiin liittyy lähettäjän ja vastaanottajan porttinumero (ja UDP-paketin sisäänsä sulkevaan IP-pakettiin liittyy vastaavasti lähettäjän ja vastaanottajan IP-osoite). Tässä mielessä saattaa tuntua hieman oudolta, että DatagramPacket-olion tietosisältönä on vain (lähetettävä) data tavuina, yksi IP-osoite ja yksi porttinumero. Selitys yhdelle IP-osoitteelle ja porttinumerolle on, että lähettämisen yhteydessä ne tiedot tarkoittavan kohteen tietoja ja vastaanotettujen pakettien kohdalla kyseiset tiedot ovat puolestaan lähettäneen tahon tietoja! Taulukossa 5.3 on esitetty luokka DatagramSocket, joka on soketti datagrammien lähettämiseksi ja vastaanottamiseksi. Oletusarvoisesti UDP-soketin kautta voi lähettää paketteja minne tahansa ja vastaavasti ottaa vastaan mistä tahansa koneesta, mutta connect -metodilla on mahdollista rajoittaa soketin toimintaa tässä mielessä. UDP-soketin luonnin yhteydessä se kiinnitetään johonkin paikallisen 5 UDP-paketeissa on tarkistussumma, joten korruptoituneet paketit sentään voidaan havaita.

38 LUKU 5. SOKETIT JA NIIDEN KÄYTTÖ DatagramPacket(byte[] b, int l, InetAddress a, int p) Luo paketin, jonka sisältö on l tavua b:stä, kohteena koneen a portti p. DatagramPacket(byte[] b, int l) Luo paketin vastaanottamista varten. InetAddress getaddress() Palauttaa (vastaanotetun / luodun) paketin (lähettäjän / vastaanottajan) osoitteen. int getport() Palauttaa pakettiin liittyvän porttinumeron. byte[] getdata() Paketin data. int getlength() Paketin datan pituus. Lisäksi vastaavat set-metodit (data, IP-osoite, portti). Taulukko 5.2: Joitakin luokan DatagramPacket konstruktoreita ja metodeja. koneen porttiin. Peruskäyttö perustuu tämän jälkeen kahteen metodiin: send ja receive. Niiden avulla lähetetään ja vastaavasti vastaanotetaan yksi UDP-paketti (DatagramPacket). DatagramSocket(int port) Luo soketin paikallisen koneen porttiin port. Tuottaa poikkeuksen SocketException, jos sokettia ei voitu luoda ko. porttiin. Poikkeus SecurityException puolestaan tarkoittaa, että turvallisuusmanageri ei salli operaatiota. DatagramSocket() Luo soketin johonkin vapaaseen paikallisen koneen porttiin (vapaa porttinumero allokoidaan kysymällä sitä koneen sisäisesti rekisteröintipalvelulta ). DatagramSocket(int port, InetAddress laddr) Kuten edellä, mutta käyttäen paikallisen koneen osoitetta laddr (koneella voi olla useita osoitteita). int getlocalport() Palauttaa soketin portin. InetAddress getlocaladdress() Palauttaa sokettiin liittyvän paikallisen koneen osoitteen. void send(datagrampacket p) Lähettää soketin kautta paketin p paketissa kerrottuun kohteeseen. void receive(datagrampacket p) Vastaanottaa soketin kautta paketin p:hen (jos joku siis sellaisen lähettää vastaanottamisen käynnistämisen jälkeen). Huomaa osoitteiden käyttö paketissa ja soketissa. Vastaanotetulla paketilla on lähettävän soketin tiedot! Poikkeus IOException syntyy, jos paketin vastaanottamisessa tulee jokin virhetilanne. void setsotimeout(int timeout) Asettaa odotusajan paketin vastaanottamiselle. Jos aikaraja = 0, niin odotusaika on rajoittamaton. Metodi nostaa poikkeuksen SocketException, jos aikarajan asettaminen ei onnistu. Aikarajan merkitys liittyy receive -metodin toimintaan. Jos aikaraja ylittyy: receive tuottaa poikkeuksen InterruptedIOException (tai siis sitä suorittavaan säikeeseen kohdistuu kyseinen poikkeus). 5.2.1 Esimerkki UDP-paketin välityksestä Seuraavaksi tarkastellaan yksinkertaista esimerkkiä, jossa asiakassovellus (luokka UDPLahettaja esimerkissä 5.1) mahdollistaa komentoriviparametrien avulla paketin lähettämisen ilmaistuun kohteeseen. Vastaanottavana puolena toimii GUI:lla varustettu ohjelma (luokka UDPVastaanottaja esimerkissä 5.2), joka kuuntelee tietyssä UDP-portissa sille osoitettuja lähetyksiä ja vastaanotettuaan viestin, tulostaa sen GUI:n ylimmässä tekstikomponentissa. Kuva vastaanottajan käyttöliittymästä on kuvassa 5.2.

5.2. YHTEYDETÖN UDP 39 int getsotimeout() Palauttaa odotusajan. void close() Sulkee soketin. void connect(inetaddress a, int p) Kytkee soketin operoimaan vain kohteen (a, p) kanssa (tarkistukset puutteellisia). Oletusarvoisesti UDP-sokettiyhteys voi operoida kaikkien kohteiden kanssa. void disconnect() Purkaa edellisen kytkennän. InetAddress getinetaddress() Kohdekoneen osoite, johon soketti on kytketty. Arvo null tarkoittaa, että sokettia ei ole kytketty johonkin tiettyyn kohteeseen. int getport() Portti, johon soketti on kytketty (arvo -1 = ei kytketty). Taulukko 5.3: Joitakin luokan DatagramSocket konstruktoreita ja metodeja. Esimerkki 5.1 Viestin lähettämien UDP-pakettina. import java.io. ; import java.net. ; public class UDPLahettaja public static void main(string[] args) throws Exception // Parametrit: kohdekone, portti, viesti if (args.length < 3) System.out.println("Parametrit: kone, portti, viesti"); System.exit(0); InetAddress kohdekone = InetAddress.getByName(args[0]); int kohdeportti = Integer.parseInt(args[1]); DatagramSocket soketti = new DatagramSocket(); byte[] sisältö = args[2].getbytes(); DatagramPacket paketti = new DatagramPacket(sisältö, sisältö.length, kohdekone, kohdeportti); soketti.send(paketti); // main // UDPLahettaja Kuten esimerkistä 5.1 voi havaita, lähettäjän toiminta on hyvin suoraviivaista. Luodan vain UDPsokettiolio, UDP-pakettiolio ja lähetetään pakettiolion sisältö kohteelle sokettiolion send :llä.

40 LUKU 5. SOKETIT JA NIIDEN KÄYTTÖ Esimerkki 5.2 GUI-asiakas, joka ottaa vastaan UDP-pohjaisia viestejä. import java.net. ; import javax.swing. ; import java.awt. ; import java.awt.event. ; import java.io.interruptedioexception; public class UDPVastaanottaja public static final int PORTTI = 2000; public static void main(string[] args) throws Exception JFrame f = new JFrame("UDPVastaanottaja"); Container c = f.getcontentpane(); Font tr18 = new Font("Times-Roman", Font.BOLD, 18); JTextField kohde = new JTextField(60); final JTextField tf = new JTextField(30); kohde.setfont(tr18); tf.setfont(tr18); final PorttiKuuntelija kuuntelija = new PorttiKuuntelija(PORTTI,kohde); JButton b = new JButton("Aseta portti"); b.addactionlistener(new ActionListener() public void actionperformed(actionevent e) int p = PORTTI; try p = Integer.parseInt(tf.getText().trim()); catch (Exception e1) kuuntelija.setnewport(p); ); c.add(kohde, BorderLayout.NORTH); c.add(tf, BorderLayout.SOUTH); c.add(b); f.pack(); f.setvisible(true); f.addwindowlistener(new IkkunanSulkija()); kuuntelija.start(); // main static class PorttiKuuntelija extends Thread private int portti; private JTextField kohde; private DatagramSocket soketti; private boolean validi; public void setnewport(int p) validi = (p == portti); portti = p; public PorttiKuuntelija(int p, JTextField t) throws Exception portti = p; kohde = t; validi = true; soketti = new DatagramSocket(p); soketti.setsotimeout(5000); // PorttiKuuntelija public void run() try byte[] alue = new byte[256]; DatagramPacket saatu = new DatagramPacket(alue, alue.length); while (true) if (!validi) soketti.close(); soketti = new DatagramSocket(portti); soketti.setsotimeout(5000); try soketti.receive(saatu); catch (InterruptedIOException e) continue; String viesti = new String(saatu.getData(), 0, saatu.getlength()); String uusi = saatu.getaddress().gethostname() + ":" + saatu.getport() + "says " + viesti; kohde.settext(uusi); // while catch (Exception e) throw new Error(e.toString()); // run // class PorttiKuuntelija // class UDPVastaanottaja

5.3. YHTEYDELLINEN TCP 41 Luokassa UDPVastaanottaja luodaan käyttöliittymä normaaliin tapaan käyttöliittymään on liitetty yksi tapahtumankäsittelijä, jonka avulla alimpaan kenttään kirjoitettu numero voidaan asettaa keskellä olevaa nappulaa painamalla uudeksi porttinumeroksi (jota jatkossa tarkkaillaan). Huomaa lisäksi, että itse portin kuuntelu ei tapahdu GUI:n luoneen säikeen toimesta, vaan sitä varten käynnistetään main -metodissa erillinen säie (lopussa oleva kuuntelija.start();). Huomaa, miten PorttiKuuntelija luo UDP-sokettiyhteyden, johon liittyy viiden sekunnin aikaraja. Tämän aikarajan avulla käyttöliittymän tapahtumankäsittelijän mahdollisesti päivittämä tieto uudesta kuunneltavasta porttinumerosta saavuttaa PorttiKuuntelija :n enintään tuon viiden sekunnin kuluttua. Huomaa myös, miten run :n while-silmukan alussa mahdollisesti luodaan uusi sokettiyhteys, jos GUI:n toimesta on päätetty alkaa kuuntelemaan eri porttia. Kuva 5.2: UDP-pakettien vastaanottaminen GUI-sovelluksella. Kuvan 5.2 tilanteessa on otettu vastaan paketti koneelta bg.cs.utu.fi lähettäjän portista 3109 (vastaanottajan portti on ollut 2000). UDP-paketin sisältönä on ollut Heips... mitä kuuluu?. Alimpaan tekstikenttään käyttäjä on kirjoittanut 1000 joka päivittyy uudeksi kuunneltavaksi porttinumeroksi painamalla keskellä olevaa nappulaa. 5.3 Yhteydellinen TCP Yhteydellisen TCP-sokettiyhden ideana on, että pakettien järjestys ja perille pääsy taataan sokettiyhteyden toimesta ilman, että soketteja käyttävien sovellusten tarvitsee tehdä mitään. TCP-soketit muodostavat 2-suuntaisen tietovirran kahden (yleensä eri koneissa olevan) sovelluksen välille. Tässä kohtaa Javan tiedostovirrat ovat parhaimmillaan: sovelluksen kannalta tällaisia soketteihin liittyviä tietovirtoja käytetään kuten mitä tahansa Javan tiedostovirtoja. Sokettiyhteyden luominen kahden sovelluksen välille tapahtuu TCP:n tapauksessa hieman eri tavalla kuin UDP:n kohdalla. Asiakassovellus luo itselleen Socket-olion (ks. taulukko 5.4), ja luonnin yhteydessä ilmaistaan, mihin kohteeseen yhteys muodostetaan. Palvelinsovellus on vastaavasti jo ajallisesti aiemmin luonut ServerSocket-tyyppisen olion (ks. taulukko 5.5), joka kautta palvelin kuuntelee yhteydenottopyyntöjä konkreettisesti palvelinsovellus kuuntelee ServerSocketyhteyden accept -metodia kutsumalla. Asiakkaan tekemä Socket-olion luonti on samalla yhteydenottopyyntö, jonka perusteella palvelinsovellus muodostaa itselleen Socket-olion accept -metodin kutsun tuloksena. Palvelimen näin muodostama Socket-olio on kiinnittynyt asiakkaan vastaavaan sokettiolioon. Luonnin jälkeen käyttäminen on suoraviivaista. Socket-tyyppisistä olioista otetaan esiin niihin liittyvät sisään- ja ulosmenevät tiedostovirrat, ja käytetään kyseisiä tiedostovirtoja keskustelemiseen. Huomaa, että TCP-sokettien käyttämisen yhteydessä ei itse eksplisiittisesti tarvitse luoda lähetettäviä paketteja, kirjoitetaan vain tiedostovirtaan!