Til nå har vi fort tatt for oss det nødvendigste, og man kan forsåvidt løpe ut og skrive noen webapplikasjoner nå. Men HTML og en database utgjør ikke det mest spennende i verden, så la oss titte dypere ned i verktøykassa.
Hva er vel en webapplikasjon som ikke kan spamme brukerne sine?
1| require 'net/smtp' 2| til = 'kent_dahl@hotmail.com' # Ikke /dev/null, men nært nok. 3| fra = 'kentda@pvv.org' 4| Net::SMTP.start('smtp.pvv.ntnu.no') do |smtp| 5| tekst = [ "To: #{til}\n", 6| "Subject: En liten test e-post\n", "\n", 7| "Hei, hei, alle sammen. Har vi det bra dere?\n" 8| ] 9| smtp.sendmail( tekst, fra, [til] ) 10| end |
Vi angir mottakerne med en Array
, slik at vi kan sende en e-post til flere om gangen. I dette tilfellet kunne vi sendt en String
direkte, siden det bare var en.
Dersom man skal skrive en stor e-post og ikke ønsker å bruke så mye minne
på å bygge den opp i en String
eller Array
før man sender,
kan man benytte
instansmetoden
Net::SMTP#ready
etter
å ha startet koblingen. Den tar en blokk med et argument,
et adapter-objekt som du kan skrive til fortløpende.
Ruby har et lett lite trådsystem internt. Det kjører fullstendig inne i fortolkeren, benytter ikke plattformspesifikke tråder, kan ikke dra nytte av flere CPUer og dersom en tråd kaller en C-metode som blokker, kan det sulte ut alle de andre trådene også. Men dersom mye av prosesseringen du gjør skjer i Ruby, er de veldig nyttige.
1| puts 'A1' 2| traad = Thread.new do 3| puts 'B1' 4| sleep 2 5| puts 'B2' 6| end 7| puts 'A2' 8| puts traad.join # Vent på at tråden avslutter. 9| # Utput blir: A1 B1 A2 B2 |
Merk at når hovedtråden (main) avslutter, drepes alle andre tråder. Man må kalle join
eller lignende på trådene om man ønsker å vente på dem.
Ønsker man å returnere en verdi fra en tråd, kan man kalle value
i stedet for join
.
1| pi_traad = Thread.new do 2| # Regn ut PI til ørten desimaler ... 3| puts 'Regne, regne, regne!' 4| sleep 5 # Vanlig student-innsats fra denne tråden... 5| Math::PI # ... og tror du ikke den koker også! 6| end 7| puts 'Tråden går i bakgrunnen.' 8| print 'PI er ', pi_traad.value # Vent på returverdien fra tråden. |
NB: Unntak som heves i tråder vil svinne hen i stillhet, med mindre vi eksplisitt venter på at tråden skal avslutte, eller Thread.abort_on_exception
er satt til true
.
Tråder kan også brukes som sikre(re?) sandkasser. Husker du $SAFE
-variabelen fra tidligere? Den er ikke en global variabel, men en tråd-lokal variabel. Hver tråd har sitt eget sikkerhetsnivå, og det kan vi benytte oss av for å kjøre "skumle" kodebiter i en noe tryggere "sandkasse".
1| # Pakker sandkasselekingen i en metode slik at det 2| # ikke vil være (mange) variabler tilgjengelig i konteksten. 3| def sandkasse_lek( kode ) 4| sandkasse = Thread.new do 5| # "evil" eval kan være skummel, så la oss være paranoide. 6| $SAFE = 4 # Nivå 1-3 lar oss ikke bruke eval på 7| eval kode # besudlet data, men det gjør nivå 4! 8| end 9| print 'Koden din returnerte: ', sandkasse.value.inspect, "\n" 10| end 11| 12| begin 13| print 'Skriv inn vilkårlig Ruby-kode: ' 14| kildekode = gets.chomp # Brukerdata er tainted. 15| sandkasse_lek( kildekode ) 16| end while kildekode.size.nonzero? |
Med trådene følger det en håndfull verktøy for synkronisering av tråder;
Thread.critical=,
Mutex
og ConditionVariable
.
1| require 'thread' 2| $delt_teller = 0 3| $mutex = Mutex.new 4| 5| # Lag ti tråder som øker den delte telleren 6| # gradvis tjuefem ganger. 7| traader = (1..10).collect do 8| Thread.new do 9| 25.times do |i| 10| $mutex.synchronize do ### Synkronisert 11| gammel_verdi = $delt_teller # kodebit. 12| ny_verdi = gammel_verdi + 1 # 13| sleep 0 # Framtvinge trådproblematikk # 14| $delt_teller = ny_verdi # 15| end ### 16| end 17| end 18| end 19| 20| # Vent på alle trådene før vi skriver ut den endelige verdien. 21| traader.each{|t| t.join} 22| puts $delt_teller # Skal skrive ut "250" |
Det er flere synkroniseringsverktøy tilgjengelig, slik
som Queue
, SizedQueue
,
Synchronizer
og Monitor
for
å nevne noen.
Distribuert Ruby (druby a.k.a. DRb) lar oss kommunisere og samhandle enkelt med andre Ruby-programmer over nettet, ikke ulikt RMI, CORBA ( rinn) eller XML-RPC ( xmlrpc4r) og lignende.
Et lite eksempel hvor vi deler ut et tjenerobjekt over nettet. Serveren:
1| require 'drb' 2| class Tjener 3| def si_hei 4| puts 'Hei!' 5| return 'Hei fra tjeneren!' 6| end 7| end 8| tjener = Tjener.new 9| DRb.start_service( 'druby://localhost:4242', tjener ) 10| DRb.thread.join |
... hvor vi må vente på at druby sin tråd skal avslutte, slik at serveren ikke stopper med en gang.
Dernest kobler vi oss opp med en klient:
1| require 'drb' 2| DRb.start_service 3| tjener = DRbObject.new( nil, 'druby://localhost:4242' ) 4| svar = tjener.si_hei 5| print "Fikk fra tjener: '", svar, "'.\n" |
Merk at vi angir nil
som første argument til DRbObject
-konstruktøren, siden vi ikke har et ordentlig objekt på "vår" side.
Oversending av parametre og returnverdier i distribuerte systemer deles i to hovedkategorier: verdioverføring (pass-by-value) og referanseoverføring (pass-by-reference).
Standard i druby er at objekter sendes ved verdioverføring. Objektene serialiseres (se
Marshal
-modulen), sendes over nettet og en kopi opprettes på mottakerens side. Dersom objektet ikke kan serialiseres/marshalles, fanges unntaket og det sendes over en referanse. Mottakeren ender da opp med en DRbObject
-instans som videresender metodekall over nettet.
1| require 'drb' 2| $hvor = 'i stua' 3| class Videospiller 4| # Gjør slik at Videospiller ikke kan serialiseres. 5| include DRbUndumped 6| def start 7| puts "Videospiller: 'Press play on tape #{$hvor}.'" 8| end 9| 10| class Kontroll 11| def initialize( spiller ) 12| @spiller = spiller 13| end 14| 15| def hvor_er_du? 16| puts "Kontroll: 'Jeg er #{$hvor}.'" 17| end 18| def start 19| @spiller.start 20| end 21| end # Kontroll 22| 23| end # Videospiller 24| 25| class Tjener 26| def initialize 27| @spiller = Videospiller.new 28| end 29| def ny_fjernkontroll # pass-by-value 30| Videospiller::Kontroll.new( DRbObject.new( @spiller ) ) 31| end 32| def ny_kontroll # pass-by-reference 33| Videospiller::Kontroll.new( @spiller ) 34| end 35| def hent_streng # pass-by-value 36| 'En streng, en streng, mitt kongerike for en streng.' 37| end 38| def hent_standard_utput # pass-by-reference da IO er en av 39| $stdout # klassene som ikke kan serialiseres. 40| end 41| end 42| 43| if __FILE__ == $0 then # Start tjeneren med våre utvidelser, og 44| load 'drb_server.rb' # gjenbruker koden fra forrige eksempel. 45| end |
1| require 'drb_videospiller' # Trenger definisjonen til Kontroller 2| $hvor = 'på soverommet' 3| DRb.start_service 4| vcr_tjener = DRbObject.new( nil, 'druby://localhost:4242' ) 5| 6| tjener_utput = vcr_tjener.hent_standard_utput 7| puts_begge_steder = proc do |tekst| 8| puts tekst 9| tjener_utput.puts tekst 10| end 11| 12| puts_begge_steder.call('--Fjernkontrollen kan vi ta med inn på soverommet') 13| fjernkontroll = vcr_tjener.ny_fjernkontroll 14| p fjernkontroll 15| fjernkontroll.hvor_er_du? #=> Kontroll: 'Jeg er på soverommet.' 16| fjernkontroll.start #=> Videospiller: 'Press play on tape i stua.' 17| 18| puts_begge_steder.call('--Mens kontrollen som er tett knyttet til ' + 19| "videospilleren\n--må forbli i stua.") 20| # Så vi får bare en referanse til, og ikke en kopi av, kontrollen. 21| kontroll = vcr_tjener.ny_kontroll 22| p kontroll 23| kontroll.hvor_er_du? #=> Kontroll: 'Jeg er i stua.' 24| kontroll.start #=> Videospiller: 'Press play on tape i stua.' |
Dette kan benyttes for å flytte logikk i fra Apache-prosessene og over i en samlet druby-prosess.