En minimal http-server i Ruby
- Monday, February 22nd, 2010
- Skriv en kommentar
I denne oppfølgingsposten til En minimal http-server i .Net viser jeg hvordan jeg raskt kan sette opp en tilsvarende løsning i Ruby. Jeg skal altså implementere en tjeneste som lytter på http, og som responderer på ulike argumenter. Løsningen skal være enkel å utvide med flere “respondere” – det skal ikke være nødvendig å editere eksisterende kode for å håndtere nye typer forespørsler (se forrige post om du vil vite mer om oppgaven).
Ruby shipper med mange, nyttige moduler - blant annet en søt, liten tjeneste som heter WEBrick, som kan brukes ganske likt som .Net’s HttpListener egentlig. I følgende program setter jeg opp en server til å lytte på port 8081:
1 require ‘webrick’
2 include WEBrick
3
4 #DSL method for defining responders
5 def respond_to config
6 key = config[:key]
7 $server.mount_proc(key) do |request, response|
8 response.body = yield request.query.to_s
9 end
10 end
11
12 def load_responders
13 responder_definitions = Dir.glob(”*.responder”)
14 responder_definitions.each { |d| load d }
15 end
16
17 $server = HTTPServer.new( :Port => 8081 )
18 load_responders
19 trap(”INT”) { $server.shutdown }
20 $server.start
Servicen opprettes i linje 17, og i neste linje kaller jeg en metode jeg har kalt load_responders. Den henter alle filer med .responder extension, og kjører innholdet. Responder-filene i sin tur benytter respond_to metoden definert fra linje 5 til å konfigurere WEBrick.
Nedefor er responder-filen for add-tjenesten. Se forrige post for å se hvordan denne responderen ser ut i .net. Som du kanskje ser er dette rett og slett et kall til respond_to. Som argument til metoden sendes nøkkelen “/add”, som er det responderen skal håndtere (ref bruk av attributtet RespondTo i .Net-løsningen). Resten er en kodeblokk som tar som input argumentene fra requesten, og returnerer et svar. Denne kodeblokken brukes til å håndtere forespørselen (magien ligger i “yield” i linje 8 i programmet over).
1 respond_to :key => “/add” do |arguments|
2 sum = 0
3 numbers = arguments.split(’,')
4 numbers.each { |n| sum += n.to_i }
5 “The answer is #{sum}”
6 end
Koden i denne responderen er litt C#-ish, jeg har gjort nøyaktig det samme som jeg gjorde i C#-varianten, bare oversatt det til Ruby. For å gjøre den mere rubyesque benytte vi et par array-metoder som Ruby har arvet fra SmallTalk: map (som egentlig er en alias fro collect, men jeg liker map bedre) tar et array, kjører en gitt transformasjon på hvert element (i dette tilfellet eksplisit konvertering til integer), og returnerer et nytt array med resultatet. Dette føles nok ikke så fremmed for .Net-utviklere lengre, nå som vi har vendt oss til Linq, som tilbyr samme funksjonalitet via Select-metoden.
Det andre trikset er metoden inject. Den kan brukes til å “samle informasjon” fra et array, i dette tilfellet summen av alle argumentene. Dermed kan spesifikasjonen av add-responderen modifiseres til å se slik ut:
1 respond_to :key => “/add” do |arguments|
2 numbers = arguments.split(’,').map {|arg| arg.to_i}
3 “The answer is #{numbers.inject {|x,n| x+n }}”
4 end
Resultatet er altså at jeg på 20 linjer har satt opp en dynamisk webserver som jeg kan utvide ved å legge til flere .responder-filer. Definisjonen av hver responder er veldig konsis og grei, og står på ingen måte tilbake for .Net-løsningen. I Ruby har jeg ikke behøvd å definere interface for respondere, og lastingen av dem – som er basert på fil-extension i stedet for refleksion og attributter – er mye enklere. Når du tar med i betraktning at jeg ikke engang behøver å kompilere Ruby-løsningen, så er det ikke vanskelig for meg å foretrekke denne når jeg får behov for å raskt sette opp web-tjenester av ulik art f.eks. for å simulere tjenester jeg skal integrere mot.
Sinatra entrer scenen
Å bruke WEBrick til dette her er ganske “low level” (på samme måte som HttpListener var det). I .NET-verden har vi rammeverk for webutvikling på et høyere nivå som blant andre WebForms, ASP.NET MVC, FubuMVC og MonoRail (det er egentlig alle jeg vet om). Ruby har også dette; det desidert mest kjente er Ruby on Rails, som gjør deg ekstremt produktiv så sant du er villig til å følge Rails konvensjoner og måter å gjøre ting på. Ramaze er et rammeverk med mye større frihet, hvor man kan velge mellom et hav av moduler og måter å gjøre ting på. Begge disse baserer seg i hovedsak på Model-View-Controller paradigmet.
Sinatra er et tredje ruby-biblotek som er ganske nyttig til å utvikle mindre websider og tjenester. Det minner mye om det jeg har gjort i denne artikkelen, og ved å bruke Sinatra kan jeg forenkle tjenesten min ganske mye (som om den ikke var enkel nok allerede).
Sinatra-versjonen av selve tjenesten min ser slik ut:
1 require ’sinatra’
2 Dir.glob(”*.responder”).each { |d| load d }
Ved å inkludere sinatra-bibloteket startes automatisk en webserver. Det eneste jeg da trenger er å dynamisk laste alle responder-filene. Jeg har slått sammen linje 13 og 14 fra det orginale skriptet, og står dermed igjen med én require og én kodelinje.
Responder-filen ser nesten ut som tidligere, men kallet til respond_to, som jeg selv definerte, har vi nå et kall til sinatras get-metode (’get’ som i REST-metoden get):
1 get “/add” do
2 numbers = params.to_s.split(’,').map {|arg| arg.to_i}
3 “The answer is #{numbers.inject {|x,n| x+n }}”
4 end
Dermed har jeg gått fra 24 til 6 linjer. Det er latterlig lite!
Og den tilsvarende C#-løsningen fra forrige post var på over 170 linjer. Det finnes selvfølgelig mere optimale løsninger, men jeg har vært en C#-utvikler i åtte år, og 170 liner ++ var det jeg havnet på. Jeg har vært Ruby-utvikler på hobbybasis i et par måneder, og landet på 6 linjer. Det MÅ jo si noe om Ruby og dynamisk programmering!
Kategorier: Ruby, Webutvikling.
RSS feed for kommentarene.
Tilbaketråkk.



February 22nd, 2010 at 10:12 am
Flott post.
Dog til C#’s forsvar så er løsningene basert på forskjellige abstraksjonsnivå (selv om du sammenligner WEBRick som lavnivå HttpListener). I .NET posten går store deler av koden med på å implementere selve HTTP serveren, noe du får gratis fra WEBrick hvor du kun implementerer “handlerene”. Dersom du hadde valgt et eksternt .NET bibliotek framfor HttpListener ville du kunne fått koden langt mer kompakt.
Men du har et viktig poeng at du som C# utvikler over 8 år lander på en løsning på 170 linjer, mens som Ruby utvikler klarer å lande på 6 kodelinjer. Det sier litt om forskjellen i mindset når man koder Ruby og C#. .NET utviklere generelt har en tendens til å lene seg alt for tungt på Microsoft (noe ALT.NET er reaksjon på), noe som gjør at man får suboptimale dersom .NET ikke har funksjonaliteten “out of the box”. Når du koder Ruby er det helt naturlig å bruke open source bibliotek som Sinatra og WEBrick.
February 22nd, 2010 at 10:59 am
Her er et kjapt eksempel på C#/.NET løsning basert på Kayak som er en mer reel sammenligning. Fortsatt ikke like kompakt som Ruby versjonen, men samme type funksjonalitet. [Path] attributter i stedenfor responder filer, og KayakServer i stedenfor Sinatra/WEBrick.
http://codepaste.net/p2d875
Kan evt. forenkles ytterligere med en generisk Kayak-modul hvor man kan sende inn lambda-uttrykk for å håndtere de forskjellige HTTP GET kallene.
February 22nd, 2010 at 11:17 am
Ja, jeg ble gjort oppmerksom på Kayak etter at jeg skrev koden min, og Kayak-løsningen er absolutt elegant. Jeg begynte heller ikke blogpostene med et mål om vise at Ruby er “bedre” enn C#, og ble selv overrasket over hvor kompakt jeg fikk Ruby’en. Jeg synes likevel mitt siste poeng, som du også var enig i, er viktig - og gjør ofte mer jobb i C# enn nødvendig. Ruby hjelper meg å endre dette mindsettet.
February 22nd, 2010 at 11:22 am
En viktig forskjell er også hvordan jeg i Ruby kan loade kode i runtime - i mitt tilfelle basert på filnavn. I C# brukte jeg reflection, og jeg antar at Kayako har implementert den biten omtrent på samme måte. Reflection er C#’s mest dynamiske feature, men siden C# ikke er særlig dynamisk i utgangspunktet er reflection noe tungvindt.
February 22nd, 2010 at 11:45 am
Jepp, Kayak vil måtte basere seg på reflection for å finne klasser som skal håndtere en request. Ønsker vi ny funksjonalitet må denne kompileres og lastes via refleksjon. Dette håndteres automatisk av Kayak, så du slipper å forholde deg til refleksjon som utvikler. Dog må du kopilere koden før den kan lastes, siden C# ikke har støtte for scripting out of the box.
Når vi snakker om dynamisk funksjonalitet så handler refleksjon ikke om dynamisk tying (som i dynamiske programmerignsspråk), men dynamisk lasting av funksjonalitet (som er to forskjellige greier). Dynamisk typing får vi først i C# 4.0, og da vil man enkelt kunne f.eks laste handler filer direkte uten å måtte kompilere kode først. Mulighet å kjøre C# kompilatoren som en “service” og kompilere kode “on the fly” vil og muliggjøre C# scripting (dynamisk lasting av ukompilert kode under runtime) som ville gjort det enklere å legge til nye KayakService klasser (i dag må de kompileres).
Ang. mindset så er jo det største argumentet for å lære nye språk. Man har kanskje ikke et Ruby prosjekt på jobben, men ved å lære det på fritiden kan man endre måten man tenker på, og løse problemer i C#/.NET på en mer elegant/effektiv måte ved å bruke disse ideene.
Har f.eks selv hatt kjempe nytte av det jeg lærte i Funksjonell Programmering i Scheme på universitetet når jeg lærte meg LINQ, og generelt måten jeg tenker funksjonell programmering. Så kjempe flott at du kjører på med Ruby blog posts på Norsk!
February 22nd, 2010 at 12:46 pm
Det blir enda mer om diverse programmeringsspråk og paradigmer her snart.., har bl.a. så smått begynt å lære meg Haskell.
Og siden du nevnte det - dynamiske programmeringsspråk er mer enn bare dynamisk typing, selv om det ofte er det vi fokuserer på. Språk er ikke bare statiske eller bare dynamiske, men befinner seg alltid et sted imellom ytterpunktene. Men mer om dette i en blogpost om en ukes tid.