En minimal http-server i .Net

.Net-rammeverket er fullt av moduler for å lage webtjenester; du kan bruke WebForms eller ASP.NET MVC, SOAP web services eller WCF, RIA services eller Astoria data services, alt etter dagsform og hvilket behov du har. Noen ganger kan det derimot være greit å vite hvordan man på aller enklest måte kan lage en http-basert server. I Chrome-vinduet under ser du hvordan jeg aksesserer en “no-fuss” service som kan fortelle meg hva klokka er…

CropperCapture[48]

I denne artikkelen presenterer jeg den ikke så veldig godt kjente klassen HttpListener (i System.Net namespacet), og viser hvordan man enkelt kan bruke den til å lage en slags webserver. Denne teknikken kan være aktuell om man f.eks. skulle trenge å raskt mocke opp noen webservicer som ikke følger de vanlige standardene, eller om man skal lage moduler som kommuniserer over http med en proprietær protokoll.

Men jeg har mer på lur: Http-serveren jeg presenterer her er designet for å være utvidbar, og bruker derfor STRATEGY PATTERN i håndteringen av forespørslene. Du vil også få se hvordan jeg kombinerer attributter og reflection for å kunne dynamisk legge til ny adferd uten å måtte editere eksisterende kode.

Her følger selve server-klassen: SimpleHttpServer. Det sentrale skjer i linje 30 til 33, hvor jeg oppretter en HttpListener, registrerer adresse og port for lytting, og starter å lytte. Dette tilsvarer mer eller mindre hvordan Internet Information Server (IIS) selv registrerer seg for lytting mot operativsystemet.

    9 public class SimpleHttpServer

   10 {

   11     private string _address;

   12     private int _port;

   13     private ResponderFactory _dispatcher;

   14     public SimpleHttpServer(string address, int port, ResponderFactory dispatcher)

   15     {

   16         _port = port;

   17         _address = address;

   18         _dispatcher = dispatcher;

   19     }

   20 

   21     private HttpListener _httpListener;

   22     public void Start()

   23     {

   24         StartHttpListener();

   25         while (true)

   26             WaitForRequestThenHandle();

   27     }

   28     private void StartHttpListener()

   29     {

   30         _httpListener = new HttpListener();

   31         _httpListener.Prefixes.Add(

   32             String.Format(“http://{0}:{1}/”, _address, _port));

   33         _httpListener.Start();

   34     }

   35     private void WaitForRequestThenHandle()

   36     {

   37         var incomingRequestContext = _httpListener.GetContext();

   38 

   39         ThreadPool.QueueUserWorkItem((state) =>

   40         {

   41             try

   42             {

   43                 var context = state as HttpListenerContext;

   44                 var url = context.Request.Url;

   45                 var encoding = context.Request.ContentEncoding;

   46 

   47                 var result = _dispatcher

   48                     .GetResponder(GetCommandKey(url, encoding))

   49                     .RespondTo(GetCommandArguments(url, encoding));

   50 

   51                 Respond(context, encoding, result);

   52             }

   53             catch (Exception ex)

   54             {

   55                 // Don’t want the service to die, just log it..

   56                 Console.WriteLine(ex.ToString());

   57                 // Could then responde with some error code…

   58             }

   59         },

   60         incomingRequestContext);

   61     }

   62 

   63     private static string GetCommandKey(Uri url, Encoding encoding)

   64     {

   65         // AbsolutePath always starts with a ‘/’

   66         return HttpUtility.UrlDecode(url.AbsolutePath.Substring(1), encoding);

   67     }

   68     private static string GetCommandArguments(Uri url, Encoding encoding)

   69     {

   70         // Query always starts with a ‘?’, but may be null

   71         return url.Query != null && url.Query.Length > 1

   72                         ? HttpUtility.UrlDecode(url.Query.Substring(1), encoding)

   73                         : string.Empty;

   74     }

   75     private static void Respond(HttpListenerContext context, Encoding encoding, string result)

   76     {

   77         var bytes = encoding.GetBytes(result);

   78         context.Response.ContentLength64 = bytes.Length;

   79         context.Response.OutputStream.Write(bytes, 0, bytes.Length);

   80         context.Response.StatusCode = 200; // everything is ok :)

   81         context.Response.Close();

   82     }

   83 }

Etter å ha opprettet HttpListener kjører jeg en uendelig løkke som mottar innkommende forespørsler og håndterer dem (en robust implementasjon ville også hatt en Stop-metode som terminerte løkken). Kallet til GetContext() i linje 37 returnerer når en request mottas, og så bruker jeg ThreadPool til å spawn’e en ny tråd som håndterer den og svarer tilbake.

Denne serveren bruker selve URL’en til å avgjøre hva den skal gjøre. I eksempelet i starten av artikkelen var requesten http://127.0.0.1:8081/time?. Alt etter domenet og porten men før spørsmålstegnet bruker jeg til å avgjøre hvilken Responder som skal brukes. Det er her STARTEGY pattern kommer inn i bildet – en responder er en klasse som implementerer interfacet Responder

    5 public interface Responder

    6 {

    7     string RespondTo(string arguments);

    8 }

    5 public interface ResponderFactory

    6 {

    7     Responder GetResponder(string responderKey);

    8 }

Argumentene som sendes til responderen er alt som kommer etter spørsmålstegnet i requesten. HttpListener støtter mye mer enn dette, men alt jeg er interessert i denne gangen er selve URL’en.

SimpleHttpServer ble initialisert med en ResponderFactory. Denne vil – gitt en nøkkel – returnere riktig responder. Ta en titt til på serveren om du ikke fikk det helt med deg første gangen, spesielt linje 47 til 49.

En første versjon av ResponderFactory (den faktiske implementasjonen kommer lengre nede) kunne vært en klasse som uansett nøkkel returnerte en UnknownCommandResponder:

    5 public class UnknownCommandResponder : Responder

    6 {

    7     private string _request;

    8     public UnknownCommandResponder(string request)

    9     {

   10         _request = request;

   11     }

   12     public string RespondTo(string arguments)

   13     {

   14         return String.Format(“Unknown command: ‘{0}’ with arguments ‘{1}’”,

   15             _request,

   16             arguments);

   17     }

   18 }

Det kan være greit å ha en slik default strategi/responder til å svare på alle mulige ting man måtte finne på å etterspørre. Her ser du den i aksjon:

CropperCapture[46]

Opprette flere tjenester: Attributter og reflection

Før jeg legger til den første “fornuftige” responderen oppretter jeg et .net-attributt. Det trenger ikke være noe mer avansert enn å lage en klassen som arver fra Attribute.

    6 public class RespondToAttribute : Attribute

    7 {

    8     public string Key { get; set; }

    9     public RespondToAttribute(string key) {

   10         Key = key;

   11     }

   12 }

Jeg har laget svært få attributter i min karriære, men det er en teknikk som kan gi veldig elegante løsninger om det brukes riktig. Måten jeg bruker det på her er ganske vanlig. Og enkel! Den vil rett og slett la meg legge til nye respondere uten at jeg behøver å endre noen eksisterende kode.

Men first thing first: Når jeg implementerer mine respondere vil RespondToAttribute la meg spesifisere hvilken nøkkel hver responder gjelder for. Her er f.eks. en enkel responder som legger sammen en rekke med tall separert med komma:

    6 [RespondTo("add")]

    7 public class Add : Responder

    8 {

    9     public string RespondTo(string arguments)

   10     {

   11         int result = 0;

   12         Array.ForEach(arguments.Split(‘,’),

   13             (arg) => result += Int32.Parse(arg));           

   14         return string.Format(“The answer is {0}”, result);

   15     }

   16 }

(Det er en konvensjon i .Net at man slipper å skrive “Attribute”-delen av attributt-navnet. Dermed blir linje 6 så fin.., denne klassen “responderer på add”.)

Når jeg gjør ting som dette er det typisk endel implisitte regler jeg må følge. I dette tilfelle vil det for eksempel ikke gi mening om mer enn én klasse responderer på “add”. Nøkkelen må med andre ord være unik. For å håndheve slike regler oppretter jeg som regel validerende enhetstester. Følgende test bruker reflection til å hente ut alle typer i prosjektet, samle opp alle RespondToAttributes fra typene, og sjekke at de er unike..

   12 [Test]

   13 public void Should_all_be_unique()

   14 {

   15     var keys = new List<string>();

   16     var allTypes = Assembly.GetExecutingAssembly().GetTypes();

   17     foreach (var t in allTypes)

   18     {

   19         var attribute = Attribute

   20             .GetCustomAttribute(t, typeof(RespondToAttribute))

   21             as RespondToAttribute;

   22 

   23         if (attribute == null)

   24             continue;

   25 

   26         if (keys.Contains(attribute.Key))

   27             Assert.Fail(attribute.Key + ” appear more than once!”);

   28 

   29         keys.Add(attribute.Key);

   30     }

   31 }

Og denne samme teknikken vil jeg bruke når jeg implementerer den endelige ResponderFactory-klassen. Den oppretter en dictionary med nøkler mappet til respondere (eller mer nøyaktig mappet til funksjoner som oppretter respondere).

    7 public class ReflectiveResponderFactory : ResponderFactory

    8 {

    9     private Dictionary<string, Func<Responder>> _dispatchTable;

   10 

   11     public ReflectiveResponderFactory()

   12     {

   13         _dispatchTable = new Dictionary<string, Func<Responder>>();

   14         Array.ForEach(Assembly.GetExecutingAssembly().GetTypes(),

   15             (type) => AddDispatchIfResponder(type, GetResponderInfo(type)));

   16     }

   17     private static RespondToAttribute GetResponderInfo(Type maybeResponderType)

   18     {

   19         return Attribute.GetCustomAttribute(maybeResponderType,

   20             typeof(RespondToAttribute)) as RespondToAttribute;

   21     }

   22     private void AddDispatchIfResponder(Type type, RespondToAttribute responderInfo)

   23     {

   24         if (responderInfo != null)

   25             _dispatchTable.Add(

   26                 responderInfo.Key,

   27                 () => Activator.CreateInstance(type) as Responder);

   28     }

   29 

   30     public Responder GetResponder(string responderKey)

   31     {

   32         if (!_dispatchTable.ContainsKey(responderKey))

   33             return new UnknownCommandResponder(responderKey);

   34         return _dispatchTable[responderKey].Invoke();

   35     }

   36 }

Her forutsettes det at alle respondere (med RespondTo-attributt) har en default, parameter-løs konstruktør, slik at den kan opprettes i lambda-uttrykket i linje 27. Du bør opprette en enhetstest for å validere dette også, slik at ingen lager en responder med konstruktør-parametre i fremtiden og dermed introduserer en bug (ikke at de ikke ville ha oppdaget det, men rask tilbakemelding er alltid kjekt).

Om du nå har skjønt hvordan disse klassene henger sammen gjenstår det bare å opprette og starte en SimpleHttpServer for at dette skal fungere. Her kjører jeg serveren i et konsollprogram, men i et mer realistisk senario vil du typisk kjøre den i en enkel Windows service.

    7 static void Main(string[] args)

    8 {

    9     new SimpleHttpServer

   10         (“*”, 8081, new ReflectiveResponderFactory())

   11         .Start();

   12 }

Og nå kan vi endelig få vite hvor mye 10 + 20 + 30 er…

CropperCapture[47]

For å utvide serveren med mer funksjonalitet er det nå bare til å opprette flere klasser som arver fra Responder-interfacet, og legge til et RespondToAttribute. ReflectiveResponderFactory vil finne og registrere den nye klassen under oppstart, og delegere til en ny instans av responderen om nøkkelen kommer i en forespørsel.

Jeg håper noen klarte å henge med óg få noe fornuftig ut av dette. Mer info om HttpListener finner du her, og info om .net attributes finner du via google. I neste blogpost vil du få se hvordan jeg implementerer nøyaktig samme funksjonalitet ved hjelp av Ruby.

Knagger: , ,

Kategorier: OO-design/clean code, Webutvikling.
RSS feed for kommentarene. Tilbaketråkk.

2 kommentarer til “En minimal http-server i .Net”

  1. Bjarte Says:

    Artig artig. Kayak er også et alternativ til IIS i .NET verden. http://kayakhttp.com/

  2. Torbjørn Says:

    Takk for tipset! Ser ut som et rammeverk etter mitt hjerte – minner om Ruby’s Sinatra (som jeg kommer til å nevne i neste blogpost). http://www.sinatrarb.com/

Skriv en kommentar

Tillatte tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>


Torbjørn: La oss anta to ulike definisjoner av Template Method pattern - de to ytterpunkte...

Lars-Petter: Hei igjen. Siden du inviterer til å ta diskusjonen i bloggen, og har tatt deg t...

Torbjørn: Lars-Petter: Det er én måte å se det på. Det er absolutt fortsatt Template M...

Lars-Petter: Hei. Har du ikke i prinsippet her gått over fra Template Method (arv) til Strat...

Christian Abildsø: I alle fall i C#, så føles dette som kode som blir mer fleksibel men vanskelig...

Torbjørn: Hei Henrik, og takk for tilbudet. Ble oppmerksom på Rasberry Pi for under en uk...

Henrik Sandaker Palm: Ang. større hobby prosjekt. Du er som er en slik rakker på programmering har j...

Øivind Nilsen: Slutt å bruke mobilnummeret mitt som eksempel !...

Bjørn Einar Bjartnes: Jeg har også latt meg fascinere av Clojure, uten at jeg har kommet så veldig l...

Bjørn Einar Bjartnes: Sweet :) Jeg tror egentlig jeg liker det som det er, med musikk. Litt av utford...

Mulig relaterte linker

 Hold deg oppdatert

Søk i bloggen

Ferske innlegg

  • Template Method del 4: Multippel arv
  • Template Method Intermesso
  • Template Method del 3: Bare funksjoner
  • Template Method del 2: På vei mot funksjonell programmering
  • Kategorier

  • .net ninja (37)
  • Bøker (17)
  • Diverse prosjekter (35)
  • DSL (10)
  • Erlang (10)
  • F# (5)
  • Hardware (1)
  • Jobb (77)
  • Julekalender (51)
  • kjempekjekt.com (23)
  • LISP/Clojure (33)
  • NNUG / community (60)
  • O/RM & databaser (10)
  • Off topic (116)
  • OO-design/clean code (30)
  • Podcasts (14)
  • Polyglot (77)
  • Ruby (27)
  • Silverlight / RIA (3)
  • Software/verktøy (20)
  • Softwareutvikling (20)
  • Testing / TDD (30)
  • the contiki strip (13)
  • User experience (3)
  • WCF (3)
  • Webutvikling (32)
  • WPF (9)
  • WTF (12)
  • Last ned Wallpaper

    Programmeringsbloggens tøffe skrivebordsbakgrunn med snippets fra ulike språk laster du ned her!

    Abonner via epost

    Om du vil kan du få alle nye blogposter tilsendt til din epost. Abonner nå, det er kjempeenkelt!

    Meta