Ping Ring del 2: C#


mandag 6. september 2010 Diverse prosjekter Ping Ring C# Samtidighet

Dette er del 2 i artikkelserien Ping Ring hvor jeg implementerer et og samme program i et utall ulike programmeringsspråk - for å se om det er noe å lære gjennom å gjøre det. Introduksjonen kan du lese her.

Det første språket jeg har valgt å implementere PingRing-programmet i er det jeg kjenner aller best, nemlig C#. Først får du kildekoden til programmet i sin helhet. Deretter følger en nærmere forklaring av hva jeg har gjort, og til slutt en litt mere kritisk vurdering av designet. Trenger du å friske opp hukommelsen i forhold til spesifikasjonen kan du jo skumlese del 1 en gang til.

1 using System;
2 using System.IO;
3 using System.Net;
4 using System.Net.Sockets;
5 using System.Threading;
6
7 public class RingServer
8 {
9     public static void Main(string[] args)
10     {
11         Console.WriteLine("** C# Ring Server ({0})", args[0]);
12         new RingServer // Parse arguments and start server..
13         {
14             ThisPort = int.Parse(args[0]),
15             OtherPort = int.Parse(args[1]),
16             MaxDelay = new TimeSpan(0, 0, int.Parse(args[2]))
17         }.Start(bool.Parse(args[3]));
18     }
19
20     private DateTime _lastPingTime = DateTime.Now;
21     public TimeSpan MaxDelay { get; set; }
22     public int OtherPort { get; set; }
23     public int ThisPort { get; set; }
24
25     public void Start(bool sendStartupPing)
26     {
27         if (sendStartupPing)
28             SendDelayedPing();
29         StartMissingPingAlertThread();
30         StartPingListener().Join();
31     }
32
33     private void SendDelayedPing()
34     {
35         new Thread(() =>
36         {
37             try
38             {
39                 Thread.Sleep(1000);
40                 using (var tcpClient = new TcpClient("127.0.0.1", OtherPort))
41                 using (var streamWriter = new StreamWriter(tcpClient.GetStream()))
42                     streamWriter.Write("PING from " + ThisPort);
43             }
44             catch (Exception ex)
45             {
46                 Console.WriteLine("*** Failed sending ping: {0}", ex.Message);
47             }
48         }).Start();
49     }
50
51     private void StartMissingPingAlertThread()
52     {
53         new Thread(() =>
54         {
55             while (true)
56             {
57                 Thread.Sleep(5000);
58                 var pingDelay = DateTime.Now - _lastPingTime;
59                 if (pingDelay > MaxDelay)
60                 {
61                     Console.WriteLine("*** ALERT, RING BROKEN! No ping in {0} seconds.",
62                         pingDelay.TotalSeconds);
63                     SendDelayedPing(); // try to wake up ring
64                 }
65             }
66         }).Start();
67     }
68
69     private Thread StartPingListener()
70     {
71         var listenerThread = new Thread(() =>
72         {
73             var tcpListener = new TcpListener(IPAddress.Parse("127.0.0.1"), ThisPort);
74             tcpListener.Start();
75             while (true)
76                 using (var connection = tcpListener.AcceptTcpClient())
77                 using (var streamReader = new StreamReader(connection.GetStream()))
78                     ProcessIncomingPing(streamReader.ReadToEnd());
79         });
80         listenerThread.Start();
81         return listenerThread;
82     }
83
84     private void ProcessIncomingPing(string message)
85     {
86         _lastPingTime = DateTime.Now;
87         Console.WriteLine("Received {0}", message);
88         SendDelayedPing();
89     }
90 }

Programmet du nettopp har sett kan illustreres med diagrammet nedenfor. To tråder startes opp, én for å ta imot innkommende pings, og én for å sjekke at det har kommet en ping innen tidsfristen. De to trådene kommuniserer gjennom at lytte-tråden oppdaterer feltet _lastPingTime. Når det skal sendes en ping ut til neste server opprettes det en ny tråd for dette.

pingring_alg_1For å starte jobber i egne tråder bruker jeg Thread-klassen sin konstruktør som kan ta imot en lambda-funksjon, for så å kalle Start() på tråden. Lambda'en for lytteren og alerteren inneholder uendelige løkker (while true). I RingServer.Start() kaller jeg til slutt Join() på en av trådene slik at programmet blir stående og vente på at tråden skal bli ferdig, noen den selvfølgelig aldri gjør.

Det hadde kanskje vært mer naturlig å implementere StartMissingPingAlertThread med en Timer, men jeg valgte å holde meg til en felles måte å håndtere samtidigheten på.

Resten av programmet bør være nokså enkelt å forstå, og jeg går ikke mer i dybden nå. Jeg vil derimot komme tilbake til noen detaljer når jeg i senere blogposter kan sammenligne denne løsningen med implementasjoner i andre språk.

Er dette et bra design?

Vel, det kommer kanskje an på øyet som ser. Designet er i alle fall ikke drevet frem av tester, det er tydelig. For en mere objektorientert løsning burde jeg kanskje ha skilt ut de ulike ansvarene til egne klasser – en sender, en listener, en alerter – RingServer-klassen gjør nå alt for mye. Dessuten burde jeg kanskje laget abstraksjoner rundt TcpListener og TcpClient, slik at jeg kunne erstattet dem med falske implementasjoner under testing.

Men enkelhet er også en viktig egenskap, og selv om programmet i sin nåværende form ikke er enkelt å lage enhetstester for, så er det veldig oversiktelig. 90 linjer C# klarer man stort sett å wrappe hjernen rundt. Den funksjonelle kompleksiteten i programmet er altså ikke stor nok til å rettferdigjøre et mer fleksibelt design (slik jeg har vurdert det). Jeg kan heller ikke forutse fremtidige endringer som jeg bør ta høyde for – det koster mindre å starte fra scratch om det kommer endringer som ikke passer inn.

Dessuten egner det nåværende formatet seg bedre for denne artikkelserien, og til å sammenligne med andre implementasjoner, enn en mer kompleks objektstruktur ville gjort. Men det er altså verdt å merke seg at jeg bryter med endel SOLID-prinsipper med dette designet.

Kildekoden fra denne blogposten er tilgjengelig på Github. Der står du fritt til å forgrene løsningen og gjøre egne modifikasjoner om du ønsker det (for å illustrere et poeng eller lignende). Som alt annet på bloggen er koden lisensiert under Creative Commons.


comments powered by Disqus