Skip to content

11. Internetprogrammering

11.1. Généralités

11.1.1. Internetprotocollen

Hier geven we een inleiding tot de communicatieprotocollen van het internet, ook wel de TCP/IP-protocolsuite (Transfer Control Protocol / Internet Protocol) genoemd, naar de twee belangrijkste protocollen. Het kan nuttig zijn dat de lezer een algemeen begrip heeft van de werking van netwerken en met name van de protocollen TCP/IP, voordat hij zich bezighoudt met het bouwen van gedistribueerde applicaties. De volgende tekst is een gedeeltelijke vertaling van een tekst uit het document „Lan Workplace for Dos – Administrator’s Guide“ van NOVELL, een document uit het begin van de jaren 90.


Het algemene concept van het opzetten van een netwerk van heterogene computers is voortgekomen uit onderzoek dat is uitgevoerd door het DARPA (Defense Advanced Research Projects Agency) in de Verenigde Staten. Het DARPA heeft de reeks protocollen ontwikkeld die bekend staat onder de naam TCP/IP, waarmee heterogene machines met elkaar kunnen communiceren. Deze protocollen zijn getest op een netwerk met de naam ARPAnet, een netwerk dat later het INTERNET-netwerk werd. De protocollen TCP/IP definiëren formaten en regels voor verzending en ontvangst die onafhankelijk zijn van de organisatie van de netwerken en de gebruikte apparatuur.

Het netwerk dat is ontworpen door het DARPA en wordt beheerd door de protocollen TCP/IP is een pakketgeschakeld netwerk. Een dergelijk netwerk verzendt informatie over het netwerk in kleine stukjes die pakketten worden genoemd. Als een computer bijvoorbeeld een groot bestand verzendt, wordt dit in kleine stukjes opgedeeld die over het netwerk worden verzonden om op de bestemming weer te worden samengevoegd. TCP/IP definieert het formaat van deze pakketten, namelijk:

  • herkomst van het pakket
  • bestemming
  • lengte
  • type

11.1.2. Het model OSI

De protocollen TCP/IP volgen grotendeels het open netwerkmodel genaamd OSI (Open Systems Interconnection Reference Model), gedefinieerd door de ISO (International Standards Organisation). Dit model beschrijft een ideaal netwerk waarin de communicatie tussen machines kan worden weergegeven door een model met zeven lagen:

Elke laag ontvangt diensten van de onderliggende laag en biedt haar eigen diensten aan de bovenliggende laag aan. Stel dat twee applicaties op verschillende machines A en B met elkaar willen communiceren: dat doen ze op het niveau van de Application-laag. Ze hoeven niet alle details van de werking van het netwerk te kennen: elke applicatie geeft de informatie die ze wil verzenden door aan de onderliggende laag: de laag Présentation. De applicatie hoeft dus alleen de regels voor de koppeling met de laag Présentation te kennen.

Zodra de informatie zich in de laag Présentation bevindt, wordt deze volgens andere regels doorgegeven aan de laag Session en zo verder, totdat de informatie op het fysieke medium terechtkomt en fysiek naar de bestemmingsmachine wordt verzonden. Daar ondergaat de informatie het omgekeerde proces van wat er op de verzendende machine is gebeurd.

Op elke laag stuurt het verzendproces, dat verantwoordelijk is voor het verzenden van de informatie, deze naar een ontvangstproces op de andere machine die tot dezelfde laag behoort. Dit gebeurt volgens bepaalde regels die het protocol van de laag worden genoemd. We krijgen dus het volgende uiteindelijke communicatieschema:

De rol van de verschillende lagen is als volgt:

Physique
Zorgt voor de overdracht van bits via een fysiek medium. In deze laag bevinden zich eindapparatuur voor gegevensverwerking (E.T.T.D.), zoals terminals of computers, evenals apparatuur voor het afsluiten van datacircuits (E.T.C.D.), zoals modulators/demodulators, multiplexers en concentrators. De aandachtspunten op dit niveau zijn:
  • de keuze van de codering van de informatie (analoog of digitaal)
  • de keuze van de transmissiemodus (synchroon of asynchroon).
Liaison de données
Verbergt de fysieke kenmerken van de fysieke laag. Detecteert en corrigeert transmissiefouten.
Réseau
Beheert het pad dat de via het netwerk verzonden informatie moet volgen. Dit wordt de routage genoemd: het bepalen van de route die informatie moet volgen om bij de ontvanger aan te komen.
Transport
Maakt communicatie tussen twee applicaties mogelijk, terwijl de voorgaande lagen alleen communicatie tussen machines toelieten. Een dienst die door deze laag wordt geleverd, kan multiplexing zijn: de transportlaag kan één en dezelfde netwerkverbinding (van machine naar machine) gebruiken om informatie van meerdere applicaties te verzenden.
Session
In deze laag vinden we diensten waarmee een applicatie een werksessie op een externe machine kan openen en in stand houden.
Présentation
Deze laag heeft tot doel de weergave van gegevens op de verschillende machines te uniformiseren. Zo worden gegevens afkomstig van machine A door de Présentation-laag van machine A in een standaardformaat ‘verpakt’ voordat ze over het netwerk worden verzonden. Zodra de gegevens de laag Présentation van de ontvangende machine B bereiken – die ze dankzij hun standaardformaat herkent – worden ze op een andere manier verpakt, zodat de applicatie van machine B ze kan herkennen.
Application
Op dit niveau bevinden zich de toepassingen die doorgaans dicht bij de gebruiker staan, zoals e-mail of bestandsoverdracht.

11.1.3. Het model TCP/IP

Het model OSI is een ideaal model dat nog nooit is gerealiseerd. De reeks protocollen TCP/IP komt hier dicht bij in de volgende vorm:

Fysieke laag

In een lokaal netwerk wordt doorgaans gebruikgemaakt van Ethernet- of Token-Ring-technologie. We behandelen hier alleen de Ethernet-technologie.

Ethernet

Dit is de naam van een technologie voor lokale, pakketgeschakelde netwerken die begin jaren zeventig bij PARC Xerox werd uitgevonden en in 1978 door Xerox, Intel en Digital Equipment werd gestandaardiseerd. Het netwerk bestaat fysiek uit een coaxkabel met een diameter van ongeveer 1,27 cm en een lengte van maximaal 500 m. Het kan worden uitgebreid met behulp van répéteurs, waarbij twee apparaten niet door meer dan twee repeaters van elkaar gescheiden mogen zijn. De kabel is passief: alle actieve componenten bevinden zich op de apparaten die op de kabel zijn aangesloten. Elk apparaat is via een netwerktoegangskaart met de kabel verbonden, die bestaat uit:

  • een zender (transceiver) die de aanwezigheid van signalen op de kabel detecteert en de analoge signalen omzet in digitale signalen en omgekeerd.
  • een koppelaar die de digitale signalen van de zender ontvangt en deze doorgeeft aan de computer voor verwerking, of omgekeerd.

De belangrijkste kenmerken van de Ethernet-technologie zijn:

  • Capaciteit van 10 megabit/seconde.
  • Bustopologie: alle apparaten zijn aangesloten op dezelfde kabel
  • Broadcast-netwerk – Een zendende computer verzendt informatie via de kabel met het adres van de ontvangende computer. Alle aangesloten computers ontvangen deze informatie, maar alleen de computer waarvoor de informatie bestemd is, bewaart deze.
  • De toegangsmethode is als volgt: de zender die wil uitzenden, luistert naar de kabel – hij detecteert dan of er al dan niet een draaggolf aanwezig is, wat zou betekenen dat er een transmissie gaande is. Dit is de CSMA-techniek (Carrier Sense Multiple Access). Als er geen draaggolf aanwezig is, kan een zender besluiten om op zijn beurt te zenden. Er kunnen meerdere zenders zijn die deze beslissing nemen. De uitgezonden signalen vermengen zich: men spreekt dan van een botsing. De zender detecteert deze situatie: terwijl hij naar de kabel zendt, luistert hij tegelijkertijd naar wat er daadwerkelijk over de kabel gaat. Als hij detecteert dat de informatie die over de kabel gaat niet dezelfde is als die hij heeft verzonden, concludeert hij dat er een botsing is en stopt hij met zenden. De andere zenders die aan het zenden waren, zullen hetzelfde doen. Elke zender hervat zijn uitzending na een willekeurige tijd die afhankelijk is van de betreffende zender. Deze techniek wordt CD (Collision Detect) genoemd. De toegangsmethode wordt daarom CSMA/CD genoemd.
  • een 48-bits adressering. Elke machine heeft een adres, hier het fysieke adres genoemd, dat is vastgelegd op de kaart die de machine met de kabel verbindt. Dit adres wordt het Ethernet-adres van de machine genoemd.

Netwerklaag

Op deze laag vinden we de protocollen IP, ICMP, ARP en RARP.

IP (Internet Protocol)
Verzendt pakketten tussen twee knooppunten in het netwerk
ICMP 
(Internet Control Message Protocol)
ICMP zorgt voor de communicatie tussen het protocolprogramma IP van de ene machine en dat van een andere machine. Het is dus een protocol voor het uitwisselen van berichten binnen het protocol IP zelf.
ARP
(Address Resolution Protocol)
zorgt voor de koppeling tussen het internetadres van de machine en het fysieke adres van de machine
RARP
(Reverse Address Resolution Protocol)
zet het fysieke adres van de machine om naar het internetadres van de machine

Transport-/sessielagen

In deze laag bevinden zich de volgende protocollen:

TCP (Transmission Control Protocol)
Zorgt voor een betrouwbare overdracht van informatie tussen twee clients
UDP (User Datagram Protocol)
Zorgt voor een onbetrouwbare overdracht van informatie tussen twee clients

Toepassings-/presentatie-/sessielagen

Hier zijn verschillende protocollen te vinden:

TELNET
Terminalemulator waarmee machine A verbinding kan maken met machine B als terminal
FTP (File Transfer Protocol)
maakt bestandsoverdracht mogelijk
TFTP (Trivial File Transfer Protocol)
maakt bestandsoverdracht mogelijk
SMTP (Simple Mail Transfer protocol)
maakt het uitwisselen van berichten tussen netwerkgebruikers mogelijk
DNS (Domain Name System)
zet een computernaam om in het internetadres van de computer
XDR (eXternal Data Representation)
gemaakt door Sun MicroSystems, specificeert een standaard, machine-onafhankelijke weergave van de gegevens
RPC(Remote Procedures Call)
ook gedefinieerd door Sun; het is een communicatieprotocol tussen externe applicaties, onafhankelijk van de transportlaag. Dit protocol is belangrijk: het ontlast de programmeur van de noodzaak om de details van de transportlaag te kennen en maakt applicaties draagbaar. Dit protocol is gebaseerd op het protocol XDR
NFS (Network File System)
, eveneens gedefinieerd door Sun. Dit protocol stelt een machine in staat om het bestandssysteem van een andere machine te „zien”. Het is gebaseerd op het voorgaande protocol RPC

11.1.4. Werking van internetprotocollen

Toepassingen die zijn ontwikkeld in de TCP/IP-omgeving maken doorgaans gebruik van meerdere protocollen uit deze omgeving. Een applicatieprogramma communiceert met de hoogste laag van de protocollen. Deze laag geeft de informatie door aan de laag eronder, en zo verder, totdat deze de fysieke drager bereikt. Daar wordt de informatie fysiek doorgestuurd naar de ontvangende machine, waar ze dezelfde lagen weer doorloopt, ditmaal in omgekeerde volgorde, totdat ze de ontvangende applicatie van de verzonden informatie bereikt. Het volgende schema toont het traject van de informatie:

Laten we een voorbeeld nemen: de applicatie FTP, gedefinieerd op het niveau van de laag Application, die bestandsoverdrachten tussen computers mogelijk maakt.

  • De applicatie levert een reeks bytes af die moet worden doorgegeven aan de laag transport.
  • De laag transport verdeelt deze reeks bytes in segments en TCP, en voegt aan het begin van elk segment het nummer van dat segment toe. De segmenten worden doorgegeven aan de netwerklaag, die wordt bestuurd door het protocol IP.
  • De IP-laag creëert een pakket waarin het ontvangen segment TCP wordt ingekapseld. Aan het begin van dit pakket plaatst deze laag de internetadressen van de bron- en bestemmingscomputer. Ook bepaalt deze laag het fysieke adres van de bestemmingscomputer. Het geheel wordt doorgegeven aan de laag voor gegevensverbinding en fysieke verbinding, dat wil zeggen aan de netwerkkaart die de computer met het fysieke netwerk verbindt.
  • Daar wordt het pakket IP op zijn beurt ingekapseld in een fysiek frame en via de kabel naar de ontvanger verzonden.
  • Op de ontvangende machine doet de laag ‘Datalink & Fysieke verbinding’ het omgekeerde: deze ontkapselt het pakket IP uit het fysieke frame en geeft het door aan de laag IP.
  • De laag IP controleert of het pakket correct is: deze berekent een checksum op basis van de ontvangen bits (checksum), die in de header van het pakket moet terug te vinden zijn. Als dat niet het geval is, wordt het pakket afgewezen.
  • Als het pakket als correct wordt aangemerkt, ontkapselt de laag IP het segment TCP dat zich daarin bevindt en geeft het door aan de bovenliggende laag transport.
  • De laag transport – in ons voorbeeld de laag TCP – controleert het segmentnummer om de juiste volgorde van de segmenten te herstellen.
  • Ze berekent ook een controlesom voor het segment TCP. Als deze correct blijkt te zijn, stuurt de laag TCP een ontvangstbevestiging naar de bronmachine; anders wordt het segment TCP geweigerd.
  • De laag TCP hoeft nu alleen nog maar het gegevensgedeelte van het segment door te sturen naar de toepassing die deze gegevens ontvangt in de bovenliggende laag.

11.1.5. Adresseringsproblemen op het internet

Een noeud in een netwerk kan een computer zijn, een slimme printer, een bestandsserver, eigenlijk alles wat kan communiceren met behulp van de TCP/IP-protocollen. Elk knooppunt heeft een fysiek adres waarvan het formaat afhankelijk is van het type netwerk. Op een Ethernet-netwerk wordt het fysieke adres gecodeerd in 6 bytes. Een adres in een X25-netwerk is een getal van 14 cijfers.

Het internetadres van een knooppunt is een logisch adres: het is onafhankelijk van de gebruikte hardware en het netwerk. Het is een adres van 4 bytes dat zowel een lokaal netwerk als een knooppunt van dat netwerk identificeert. Het internetadres wordt gewoonlijk weergegeven in de vorm van 4 getallen, de waarden van de 4 bytes, gescheiden door een punt. Zo wordt het adres van de computer Lagaffe van de faculteit Wetenschappen van Angers weergegeven als 193.49.144.1 en dat van de computer Liny als 193.49.144.9. Hieruit volgt dat het internetadres van het lokale netwerk 193.49.144.0 is. Er kunnen maximaal 254 knooppunten op dit netwerk aanwezig zijn.

Omdat internetadressen of IP-adressen onafhankelijk zijn van het netwerk, kan een computer in netwerk A communiceren met een computer in netwerk B zonder zich zorgen te hoeven maken over het type netwerk waarop deze zich bevindt: het volstaat dat hij zijn IP-adres kent. Het IP-protocol van elk netwerk zorgt voor de conversie van het IP-adres naar het fysieke adres en vice versa.

De IP-adressen moeten allemaal verschillend zijn. In Frankrijk is het de INRIA die de IP-adressen toewijst. In feite kent deze instantie een adres toe aan uw lokale netwerk, bijvoorbeeld 193.49.144.0 voor het netwerk van de faculteit der wetenschappen van Angers. De beheerder van dit netwerk kan vervolgens de IP-adressen 193.49.144.1 tot en met 193.49.144.254 naar eigen inzicht toewijzen. Dit adres wordt doorgaans vastgelegd in een speciaal bestand op elke computer die op het netwerk is aangesloten.

11.1.5.1. De adresklassen IP

Een IP-adres is een reeks van 4 bytes, vaak aangeduid als I1.I2.I3.I4, die in feite twee adressen bevat:

  • het netwerkadres
  • het adres van een knooppunt in dit netwerk

Afhankelijk van de grootte van deze twee velden worden de IP-adressen onderverdeeld in drie klassen: klasse A, B en C.

Klasse A

Het adres IP: I1.I2.I3.I4 heeft de vorm R1.N1.N2.N3, waarbij

R1 het netwerkadres is

N1.N2.N3 het adres is van een computer in dit netwerk

Om precies te zijn, de vorm van een adres van klasse A, IP, is als volgt:

Het netwerkadres bestaat uit 7 bits en het knooppuntadres uit 24 bits. Er kunnen dus 127 klasse A-netwerken zijn, die elk maximaal 224 knooppunten bevatten.

Klasse B

Hier heeft het adres IP: I1.I2.I3.I4 de vorm R1.R2.N1.N2, waarbij

R1.R2 het adres van het netwerk is

N1.N2 het adres is van een computer in dit netwerk

Om precies te zijn, de vorm van een klasse B-adres IP is als volgt:

Zowel het netwerkadres als het knooppuntadres bestaat uit 2 bytes (precies 14 bits). Er kunnen dus 2¹⁴ klasse B-netwerken zijn, die elk maximaal 2¹⁶ knooppunten bevatten.

Klasse C

In deze klasse heeft het adres IP: I1.I2.I3.I4 de vorm R1.R2.R3.N1, waarbij

R1.R2.R3 het adres van het netwerk is

N1 het adres is van een computer in dit netwerk

Om precies te zijn, de vorm van een klasse C-adres IP is als volgt:

Het netwerkadres beslaat 3 bytes (minus 3 bits) en het knooppuntadres 1 byte. Er kunnen dus 221 klasse C-netwerken zijn met elk maximaal 256 knooppunten.

Aangezien het adres van de computer Lagaffe van de faculteit der wetenschappen van Angers 193.49.144.1 is, zien we dat de hoogste byte 193 is, dat wil zeggen in binair 11000001. Hieruit kunnen we afleiden dat het netwerk van klasse C is.

Gereserveerde adressen

  • Sommige adressen, zoals IP, zijn netwerkadressen in plaats van knooppuntadressen in het netwerk. Dit zijn de adressen waarbij het knooppuntadres op 0 is gezet. Zo is het adres 193.49.144.0 het adres IP van het netwerk van de Faculteit der Wetenschappen van Angers. Bijgevolg kan geen enkel knooppunt in een netwerk het adres nul hebben.
  • Wanneer in een adres IP het knooppuntadres uitsluitend uit 1-en bestaat, is er sprake van een broadcast-adres: dit adres verwijst naar alle knooppunten in het netwerk.
  • In een klasse C-netwerk, dat in theorie 28=256 knooppunten toestaat, blijven er, als we de twee verboden adressen aftrekken, slechts 254 toegestane adressen over.

11.1.5.2. De conversieprotocollen Internetadres <--> Fysiek adres

We hebben gezien dat bij het verzenden van gegevens van de ene machine naar de andere, deze bij het passeren van de IP-laag in pakketten werden ingekapseld. Deze hebben de volgende vorm:

Het pakket IP bevat dus de internetadressen van de bron- en bestemmingsmachines. Wanneer dit pakket wordt doorgegeven aan de laag die verantwoordelijk is voor het verzenden ervan over het fysieke netwerk, wordt er andere informatie aan toegevoegd om het fysieke frame te vormen dat uiteindelijk over het netwerk wordt verzonden. Het formaat van een frame op een Ethernet-netwerk is bijvoorbeeld als volgt:

In het uiteindelijke frame staan de fysieke adressen van de bron- en bestemmingscomputer. Hoe worden deze verkregen?

De verzendende machine, die het adres IP kent van de machine waarmee zij wil communiceren, verkrijgt het fysieke adres daarvan door gebruik te maken van een speciaal protocol genaamd ARP (Address Resolution Protocol).

  • Hij verstuurt een pakket van een speciaal type, een zogenaamd ARP-pakket, dat het adres IP bevat van de machine waarvan het fysieke adres wordt gezocht. Hij heeft er ook voor gezorgd dat zijn eigen adres IP en zijn fysieke adres in dit pakket zijn opgenomen.
  • Dit pakket wordt naar alle knooppunten in het netwerk verzonden.
  • Deze herkennen het speciale karakter van het pakket. Het knooppunt dat zijn adres IP in het pakket herkent, reageert door zijn fysieke adres naar de afzender van het pakket te sturen. Hoe kan het dat? Het heeft in het pakket de adressen IP en het fysieke adres van de afzender gevonden.
  • De afzender ontvangt dus het fysieke adres dat hij zocht. Hij slaat dit op in het geheugen om het later te kunnen gebruiken als er nog meer pakketten naar dezelfde ontvanger moeten worden verzonden.

Het IP-adres van een computer staat normaal gesproken in een van zijn bestanden, dat hij dus kan raadplegen om het te achterhalen. Dit adres kan worden gewijzigd: het volstaat om het bestand te bewerken. Het fysieke adres daarentegen staat opgeslagen in het geheugen van de netwerkkaart en kan niet worden gewijzigd.

Wanneer een beheerder zijn netwerk anders wil inrichten, kan het nodig zijn om de adressen IP van alle knooppunten te wijzigen en dus de verschillende configuratiebestanden van de verschillende knooppunten te bewerken. Dit kan vervelend zijn en tot fouten leiden als er veel computers zijn. Een methode bestaat erin geen IP-adres aan de machines toe te wijzen: men voert dan een speciale code in het bestand in waarin de machine haar IP-adres zou moeten vinden. Wanneer de machine ontdekt dat hij geen adres IP heeft, vraagt hij dit op via een protocol dat RARP (Reverse Address Resolution Protocol) heet. Vervolgens verstuurt de machine via het netwerk een speciaal pakket, het zogenaamde RARP-pakket, vergelijkbaar met het voorgaande ARP-pakket, waarin het zijn fysieke adres vermeldt. Dit pakket wordt naar alle knooppunten gestuurd, die het vervolgens herkennen als een RARP-pakket. Een van deze knooppunten, de zogenaamde server RARP, beschikt over een bestand met de koppeling tussen het fysieke adres en het IP-adres van alle knooppunten. Vervolgens antwoordt deze aan de afzender van het pakket RARP door zijn adres IP terug te sturen. Een beheerder die zijn netwerk opnieuw wil configureren, hoeft dus alleen maar het toewijzingsbestand van de server RARP te bewerken. Deze moet normaal gesproken een vast adres IP hebben, dat hij moet kunnen kennen zonder zelf het protocol RARP te hoeven gebruiken.

11.1.6. De zogenaamde IP-netwerklaag van het internet

Het protocol IP (Internet Protocol) definieert de vorm die de pakketten moeten aannemen en de manier waarop ze moeten worden verwerkt bij het verzenden of ontvangen ervan. Dit specifieke type pakket wordt een IP-datagram genoemd. We hebben dit al eerder besproken:

Belangrijk is dat het IP-datagram, naast de te verzenden gegevens, ook de internetadressen van de bron- en bestemmingscomputer bevat. Zo weet de bestemmingscomputer wie hem een bericht stuurt.

In tegenstelling tot een netwerkframe, waarvan de lengte wordt bepaald door de fysieke kenmerken van het netwerk waarover het wordt verzonden, wordt de lengte van het datagram IP door de software vastgesteld en zal deze dus op verschillende fysieke netwerken hetzelfde zijn. We hebben gezien dat het datagram IP, naarmate we van de netwerklaag naar de fysieke laag afdalen, in een fysiek frame wordt ingekapseld. We hebben het voorbeeld gegeven van het fysieke frame van een Ethernet-netwerk:

De fysieke frames worden van knooppunt naar knooppunt doorgestuurd naar hun bestemming, die zich mogelijk niet op hetzelfde fysieke netwerk bevindt als de verzendende machine. Het pakket IP kan dus achtereenvolgens in verschillende fysieke frames worden ingekapseld op de knooppunten die de verbinding vormen tussen twee netwerken van verschillende typen. Het is ook mogelijk dat het pakket IP te groot is om in een fysiek frame te worden ingekapseld. De software IP van het knooppunt waar dit probleem zich voordoet, splitst het pakket IP vervolgens volgens precieze regels op in fragments, waarna elk deel afzonderlijk over het fysieke netwerk wordt verzonden. Ze worden pas op hun eindbestemming weer samengevoegd.

11.1.6.1. Routering

Routering is de methode waarmee de pakketten IP naar hun bestemming worden geleid. Er zijn twee methoden: directe routering en indirecte routering.

Directe routering

Directe routering betekent dat een pakket IP rechtstreeks van de afzender naar de ontvanger binnen hetzelfde netwerk wordt geleid:

  • De machine die een datagram IP verstuurt, beschikt over het adres IP van de ontvanger.
  • De machine verkrijgt het fysieke adres van de ontvanger via het protocol ARP of uit haar tabellen, indien dit adres al bekend is.
  • Hij verstuurt het pakket via het netwerk naar dit fysieke adres.

Indirecte routering

Indirecte routering verwijst naar het doorsturen van een pakket IP naar een bestemming die zich op een ander netwerk bevindt dan dat waartoe de afzender behoort. In dit geval verschillen de netwerkadresdelen van de adressen IP van de bron- en bestemmingsmachine. De bronmachine herkent dit. Vervolgens stuurt hij het pakket naar een speciaal knooppunt, een zogenaamde router (router). Dit knooppunt verbindt een lokaal netwerk met andere netwerken en het adres IP ervan is in zijn tabellen opgeslagen; dit adres is aanvankelijk verkregen uit een bestand, uit een permanent geheugen of via informatie die over het netwerk circuleert.

Een router is gekoppeld aan twee netwerken en heeft binnen deze twee netwerken een adres IP.

In ons bovenstaande voorbeeld:

. Netwerk nr. 1 heeft het internetadres 193.49.144.0 en netwerk nr. 2 het adres 193.49.145.0.

. Binnen netwerk nr. 1 heeft de router het adres 193.49.144.6 en binnen netwerk nr. 2 het adres 193.49.145.3.

De router heeft als taak het pakket IP dat hij ontvangt – en dat is verpakt in een fysiek frame dat typisch is voor netwerk nr. 1 – om te zetten in een fysiek frame dat over netwerk nr. 2 kan worden verzonden. Als het adres IP van de ontvanger van het pakket zich in netwerk nr. 2 bevindt, stuurt de router het pakket rechtstreeks naar hem; anders stuurt hij het naar een andere router, die netwerk nr. 2 met netwerk nr. 3 verbindt, enzovoort.

11.1.6.2. Fout- en controlemeldingen

Nog steeds in de netwerklaag, dus op hetzelfde niveau als het protocol IP, bestaat het protocol ICMP (Internet Control Message Protocol). Dit protocol dient om berichten te verzenden over de interne werking van het netwerk: defecte knooppunten, opstoppingen bij een router, enzovoort ... De ICMP-berichten worden ingekapseld in IP-pakketten en over het netwerk verzonden. De IP-lagen van de verschillende knooppunten ondernemen de juiste acties op basis van de ICMP-berichten die ze ontvangen. Zo krijgt een applicatie zelf deze netwerkgerelateerde problemen nooit te zien.

Een knooppunt gebruikt de ICMP-informatie om zijn routeringstabellen bij te werken.

11.1.7. De transportlaag: de protocollen UDP en TCP

11.1.7.1. Het protocol UDP: User Datagram Protocol

Het protocol UDP maakt een onbetrouwbare gegevensuitwisseling tussen twee punten mogelijk, wat betekent dat de correcte bezorging van een pakket op de bestemming niet gegarandeerd is. De toepassing kan dit desgewenst zelf beheren, bijvoorbeeld door na het verzenden van een bericht te wachten op een ontvangstbevestiging alvorens het volgende bericht te verzenden.

Tot nu toe hebben we op netwerkniveau gesproken over IP-adressen van machines. Op één machine kunnen echter tegelijkertijd verschillende processen bestaan die allemaal met elkaar kunnen communiceren. Bij het verzenden van een bericht moet daarom niet alleen het adres IP van de ontvangende machine worden opgegeven, maar ook de „naam” van het ontvangende proces. Deze naam is in feite een nummer, het zogenaamde poortnummer. Bepaalde nummers zijn gereserveerd voor standaardtoepassingen: poort 69 voor de tftp-toepassing (trivial file transfer protocol) bijvoorbeeld.

De pakketten die door het UDP-protocol worden verwerkt, worden ook wel datagrammen genoemd. Ze hebben de volgende vorm:

Deze datagrammen worden ingekapseld in IP-pakketten en vervolgens in fysieke frames.

11.1.7.2. Het protocol TCP: Transfer Control Protocol

Voor veilige communicatie volstaat het protocol UDP niet: de applicatieontwikkelaar moet zelf een protocol ontwikkelen waarmee hij kan controleren of de pakketten correct worden doorgestuurd. Het protocol TCP (Transfer Control Protocol) voorkomt deze problemen. De kenmerken ervan zijn als volgt:

  • Het proces dat gegevens wil verzenden, brengt eerst een verbinding tot stand met het proces dat de informatie zal ontvangen. Deze verbinding wordt tot stand gebracht tussen een poort van de verzendende machine en een poort van de ontvangende machine. Tussen beide poorten ontstaat zo een virtueel pad dat uitsluitend is gereserveerd voor de twee processen die de verbinding tot stand hebben gebracht.
  • Alle pakketten die door het bronproces worden verzonden, volgen dit virtuele pad en komen aan in de volgorde waarin ze zijn verzonden, wat niet gegarandeerd was in het protocol UDP, aangezien de pakketten verschillende paden konden volgen.
  • De verzonden informatie heeft een continu karakter. Het verzendende proces verstuurt informatie in zijn eigen tempo. Deze informatie wordt niet noodzakelijkerwijs onmiddellijk verzonden: het protocol TCP wacht tot er voldoende informatie is verzameld om deze te verzenden. De informatie wordt opgeslagen in een structuur die het TCP-segment wordt genoemd. Zodra dit segment gevuld is, wordt het doorgestuurd naar de laag IP, waar het wordt ingekapseld in een pakket IP.
  • Elk segment dat via het protocol TCP wordt verzonden, krijgt een nummer. Het ontvangende protocol TCP controleert of het de segmenten in de juiste volgorde ontvangt. Voor elk correct ontvangen segment stuurt het een ontvangstbevestiging naar de afzender.
  • Zodra de afzender deze ontvangstbevestiging ontvangt, geeft hij dit door aan het verzendende proces. Dit proces kan dus vaststellen dat een segment goed is aangekomen, wat met het protocol UDP niet mogelijk was.
  • Als het protocol TCP, dat een segment heeft verzonden, na verloop van tijd geen ontvangstbevestiging ontvangt, verzendt het het betreffende segment opnieuw, waardoor de kwaliteit van de dienstverlening bij de doorgifte van de informatie wordt gewaarborgd.
  • De virtuele verbinding die tussen de twee communicerende processen tot stand is gebracht, is full-duplex: dit betekent dat de informatie in beide richtingen kan worden verzonden. Zo kan het bestemmingsproces ontvangstbevestigingen versturen terwijl het bronproces doorgaat met het verzenden van informatie. Hierdoor kan bijvoorbeeld het bronprotocol TCP meerdere segmenten verzenden zonder op een ontvangstbevestiging te wachten. Als het na verloop van tijd vaststelt dat het geen ontvangstbevestiging heeft ontvangen voor een bepaald segment met nummer n, zal het de verzending van de segmenten vanaf dat punt hervatten.

11.1.8. De applicatielaag

Bovenop de protocollen UDP en TCP bestaan diverse standaardprotocollen:

TELNET

Dit protocol stelt een gebruiker van machine A in het netwerk in staat verbinding te maken met machine B (vaak de hostmachine genoemd). TELNET emuleert op machine A een zogenaamde universele terminal. De gebruiker gedraagt zich dus alsof hij beschikt over een terminal die is aangesloten op machine B. Telnet maakt gebruik van het protocol TCP.

FTP: (File Transfer Protocol)

Dit protocol maakt de uitwisseling van bestanden tussen twee machines op afstand mogelijk, evenals bestandsbewerkingen zoals het aanmaken van mappen. Het is gebaseerd op het protocol TCP.

TFTP: (Trivial File Transfer Control)

Dit protocol is een variant van FTP. Het is gebaseerd op het protocol UDP en is minder geavanceerd dan FTP.

DNS: (Domain Name System)

Wanneer een gebruiker bestanden wil uitwisselen met een computer op afstand, bijvoorbeeld via FTP, moet hij het internetadres van die computer kennen. Om bijvoorbeeld FTP uit te voeren op de computer Lagaffe van de universiteit van Angers, zou men FTP als volgt moeten starten: FTP 193.49.144.1

Hiervoor is een lijst nodig waarin de machines worden gekoppeld aan hun IP-adressen. Waarschijnlijk zouden de machines in deze lijst worden aangeduid met symbolische namen zoals:

machine DPX2/320 van de universiteit van Angers

Sun-computer van de Universiteit van Angers met ISERPA

Het is duidelijk dat het prettiger zou zijn om een computer aan te duiden met een naam in plaats van met zijn adres IP. Daarmee rijst het probleem van de uniekheid van de naam: er zijn miljoenen computers met elkaar verbonden. Je zou je kunnen voorstellen dat een centrale instantie de namen toekent. Dat zou ongetwijfeld nogal omslachtig zijn. Het beheer van de namen is in feite verdeeld over domeinen. Elk domein wordt beheerd door een doorgaans zeer lichte organisatie die volledige vrijheid heeft bij de keuze van de namen van computers. Zo behoren de computers in Frankrijk tot het domein fr, dat wordt beheerd door het Inria in Parijs. Om de zaken nog verder te vereenvoudigen, wordt het beheer nog verder gedecentraliseerd: er worden domeinen gecreëerd binnen het domein fr. Zo behoort de universiteit van Angers tot het domein univ-Angers. De dienst die dit domein beheert, heeft alle vrijheid om de computers in het netwerk van de Universiteit van Angers een naam te geven. Voorlopig is dit domein nog niet onderverdeeld. Maar bij een grote universiteit met veel computers in het netwerk zou dat wel kunnen gebeuren.

De computer DPX2/320 van de Universiteit van Angers is Lagaffe genoemd, terwijl een PC en een 486DX50 de naam liny hebben gekregen. Hoe kunnen deze machines van buitenaf worden aangeduid? Door de hiërarchie van de domeinen waartoe ze behoren te vermelden. De volledige naam van de machine Lagaffe is dus:

Lagaffe.univ-Angers.fr

Binnen de domeinen kunnen relatieve namen worden gebruikt. Binnen het domein fr en buiten het domein univ-Angers kan de machine Lagaffe dus worden aangeduid met

Lagaffe.univ-Angers

Ten slotte kan de machine binnen het domein univ-Angers eenvoudig worden aangeduid met

Lagaffe

Een toepassing kan dus naar een machine verwijzen via haar naam. Uiteindelijk moet men echter toch het internetadres van deze machine achterhalen. Hoe gebeurt dat? Stel dat men vanaf machine A wil communiceren met machine B.

  • Als machine B tot hetzelfde domein behoort als machine A, zal het adres IP waarschijnlijk in een bestand op machine A te vinden zijn.
  • Zo niet, dan vindt computer A in een ander bestand – of in hetzelfde bestand als hiervoor – een lijst met enkele naamservers en hun adressen IP. Een naamserver heeft als taak de koppeling te maken tussen een computernaam en het bijbehorende adres IP. Computer A stuurt een speciaal verzoek naar de eerste naamserver in zijn lijst, het zogenaamde DNS-verzoek, waarin dus de naam van de gezochte computer is opgenomen. Als de geraadpleegde server deze naam in zijn tabellen heeft staan, stuurt hij het bijbehorende adres IP naar computer A. Zo niet, dan zal de server in zijn bestanden ook een lijst vinden met naamservers die hij kan raadplegen. Dat zal hij dan doen. Zo wordt een aantal naamservers geraadpleegd, niet op willekeurige wijze, maar op een manier die het aantal verzoeken tot een minimum beperkt. Als de machine uiteindelijk wordt gevonden, komt het antwoord terug naar machine A.

XDR: (eXternal Data Representation)

Dit protocol, ontwikkeld door Sun MicroSystems, specificeert een standaard, machine-onafhankelijke gegevensweergave.

RPC: (Remote Procedure Call)

Ook gedefinieerd door Sun; dit is een communicatieprotocol tussen externe applicaties, onafhankelijk van de transportlaag. Dit protocol is belangrijk: het ontlast de programmeur van de noodzaak om de details van de transportlaag te kennen en maakt applicaties draagbaar. Dit protocol is gebaseerd op het protocol XDR

NFS: Network File System

Dit protocol, eveneens gedefinieerd door Sun, stelt een computer in staat om het bestandssysteem van een andere computer te „zien”. Het is gebaseerd op het voorgaande protocol RPC.

11.1.9. Conclusie

In deze inleiding hebben we enkele hoofdlijnen van de internetprotocollen uiteengezet. Om je verder in dit onderwerp te verdiepen, kun je het uitstekende boek van Douglas Comer lezen:

Titel TCP/IP: Architectuur, protocollen, toepassingen.

Auteur Douglas COMER

Uitgever InterEditions

11.2. De klassen .NET van het adresbeheer IP

Een computer op het internet wordt op unieke wijze gedefinieerd door een IP-adres (Internet Protocol), dat twee vormen kan aannemen:

  • IPv4: gecodeerd op 32 bits en weergegeven door een tekenreeks in de vorm "I1.I2.I3.I4", waarbij In een getal tussen 1 en 254 is. Dit zijn momenteel de meest gangbare IP-adressen.
  • IPv6: gecodeerd op 128 bits en weergegeven als een tekenreeks in de vorm "[I1.I2.I3.I4.I5.I6.I7.I8]", waarbij In een reeks van 4 hexadecimale cijfers is. In dit document zullen we de IPv6-adressen niet gebruiken.

Een machine kan ook worden gedefinieerd door een eveneens unieke naam. Deze naam is niet verplicht; applicaties gebruiken uiteindelijk altijd de IP-adressen van de machines. Ze zijn bedoeld om het leven van de gebruikers te vergemakkelijken. Zo is het met een browser gemakkelijker om de http://www.ibm.com met adres URL op te vragen dan die met adres URL of http://129.42.17.99, hoewel beide methoden mogelijk zijn.

Een apparaat kan meerdere IP-adressen hebben als het fysiek tegelijkertijd met meerdere netwerken is verbonden. Het heeft dan op elk netwerk een IP-adres.

Een adres IP kan op twee manieren worden weergegeven in .NET:

  • in de vorm van een tekenreeks "I1.I2.I3.I4" of "[I1.I2.I3.I4.I5.I6.I7.I8]"
  • in de vorm van een object van het type IPAddress

De klasse IPAddress

Onder de methoden M, eigenschappen P en constanten C van de klasse IPAddress bevinden zich de volgende:

AddressFamily AddressFamily
P
familie van het adres IP. Het type AddressFamily is een opsomming. De twee gangbare waarden zijn:
AddressFamily.InterNetwork: voor een adres IPv4
AddressFamily.InterNetworkV6: voor een adres IPv6
IPAddress Any
C
het adres IP "0.0.0.0". Wanneer een dienst aan dit adres is gekoppeld, betekent dit dat deze clients accepteert op alle adressen IP van de machine waarop deze draait.
IPAddress LoopBack
C
het adres IP "127.0.0.1". Dit wordt een "loopback-adres" genoemd. Wanneer een dienst aan dit adres is gekoppeld, betekent dit dat deze alleen clients accepteert die zich op dezelfde machine bevinden als de dienst zelf.
IPAdress None
C
het adres IP "255.255.255.255". Wanneer een dienst aan dit adres is gekoppeld, betekent dit dat deze geen clients accepteert.
bool TryParse(string ipString, out IPAddress address)
M
probeert het adres IP ipString in de vorm "I1.I2.I3.I4" door te geven als een IPAddress-adresobject. Geeft true terug als de bewerking is geslaagd.
bool IsLoopBack
M
geeft 'true' terug als het adres IP "127.0.0.1" is
string ToString()
M
geeft het adres IP weer in de vorm "I1.I2.I3.I4" of "[I1.I2.I3.I4.I5.I6.I7.I8]"

De koppeling tussen het adres IP en nomMachine wordt verzorgd door een gedistribueerde internetdienst genaamd DNS (Domain Name System). Met de statische methoden van de klasse Dns kan de koppeling tussen de adressen IP en nomMachine worden gemaakt:

GetHostEntry (string hostNameOrdAddress)
levert een adres IPHostEntry op op basis van een adres IP in de vorm van een tekenreeks of op basis van een machinenaam. Er wordt een uitzondering gegenereerd als de machine niet kan worden gevonden.
GetHostEntry (IPAddress ip)
geeft een adres IPHostEntry terug op basis van een adres IP van het type IPAddress. Er wordt een uitzondering gegenereerd als de machine niet kan worden gevonden.
string GetHostName()
geeft de naam weer van de machine waarop het programma draait dat deze instructie uitvoert
IPAddress[] GetHostAddresses(string hostNameOrdAddress)
geeft de adressen IP weer van de machine die wordt geïdentificeerd door haar naam of een van haar adressen IP.

Een instantie IPHostEntry omvat de adressen IP, de aliassen en de naam van een machine. Het type IPHostEntry is als volgt:

IPAddress[] AddressList
P
tabel met de IP-adressen van de machine
String[] Aliases
P
de aliassen van de machine. Dit zijn de namen die overeenkomen met de verschillende adressen van de machine.
string HostName
P
de hoofdhostnaam van de machine

Laten we eens kijken naar het volgende programma dat de naam van de machine weergeeft waarop het wordt uitgevoerd en vervolgens op interactieve wijze de overeenkomsten tussen het adres IP en de machinenaam weergeeft:


using System;
using System.Net;

namespace Chap9 {
    class Program {
        static void Main(string[] args) {
            // geeft de naam van de lokale machine weer
            // geeft vervolgens interactief informatie over de netwerkcomputers
            // die worden geïdentificeerd aan de hand van een naam of een adres IP

            // lokale machine
            Console.WriteLine("Machine Locale= {0}" ,Dns.GetHostName());

            // interactieve vraag-en-antwoordronde
            string machine;
            IPHostEntry ipHostEntry;
            while (true) {
                // invoer van de naam of het adres IP van de gezochte machine
                Console.Write("Machine recherchée (rien pour arrêter) : ");
                machine = Console.ReadLine().Trim().ToLower();
                // klaar?
                if (machine == "") return;
                // uitzonderingsafhandeling
                try {
                    // machine zoeken
                    ipHostEntry = Dns.GetHostEntry(machine);
                    // de naam van de machine
                    Console.WriteLine("Machine : " + ipHostEntry.HostName);
                    // de adressen van de machine IP
                    Console.Write("Adresses IP : {0}" , ipHostEntry.AddressList[0]);
                    for (int i = 1; i < ipHostEntry.AddressList.Length; i++) {
                        Console.Write(", {0}" , ipHostEntry.AddressList[i]);
                    }
                    Console.WriteLine();
                    // de aliassen van de machine
                    if (ipHostEntry.Aliases.Length != 0) {
                        Console.Write("Alias : {0}" , ipHostEntry.Aliases[0]);
                        for (int i = 1; i < ipHostEntry.Aliases.Length; i++) {
                            Console.Write(", {0}" , ipHostEntry.Aliases[i]);
                        }
                        Console.WriteLine();
                    }
                } catch {
                    // de machine bestaat niet
                    Console.WriteLine("Impossible de trouver la machine [{0}]",machine);
                }
            }
        }
    }
}

De uitvoering levert de volgende resultaten op:

Machine Locale= LISA-AUTO2005A
Machine recherchée (rien pour arrêter) : localhost
Machine : LISA-AUTO2005A
Adresses IP : 127.0.0.1
Machine recherchée (rien pour arrêter) : 127.0.0.1
Machine : LISA-AUTO2005A
Adresses IP : 127.0.0.1
Machine recherchée (rien pour arrêter) : istia.univ-angers.fr
Machine : istia.univ-angers.fr
Adresses IP : 193.49.146.171
Machine recherchée (rien pour arrêter) : 193.49.146.171
Machine : istia.istia.univ-angers.fr
Adresses IP : 193.49.146.171
Machine recherchée (rien pour arrêter) : xx
Impossible de trouver la machine [xx]

11.3. De basisprincipes van internetprogrammering

11.3.1. Algemeen

Laten we eens kijken naar de communicatie tussen twee op afstand gelegen computers A en B:

Wanneer een toepassing AppA op computer A wil communiceren met een toepassing AppB op computer B via het internet, moet deze een aantal zaken weten:

  • het adres IP of de naam van machine B
  • het poortnummer waarmee de toepassing AppB werkt. Machine B kan namelijk talrijke toepassingen ondersteunen die via het internet werken. Wanneer deze machine informatie uit het netwerk ontvangt, moet zij weten voor welke toepassing deze informatie bestemd is. De applicaties op machine B hebben toegang tot het netwerk via poorten, ook wel communicatiepoorten genoemd. Deze informatie is opgenomen in het pakket dat door machine B wordt ontvangen, zodat het aan de juiste applicatie kan worden afgeleverd.
  • De communicatieprotocollen die door machine B worden begrepen. In onze studie zullen we uitsluitend de protocollen TCP-IP gebruiken.
  • het dialoogprotocol dat door de toepassing AppB wordt geaccepteerd. Machine A en machine B gaan namelijk met elkaar ‘praten’. Wat ze gaan zeggen, wordt ingekapseld in de protocollen TCP-IP. Wanneer echter aan het einde van de keten de applicatie AppB de door de applicatie AppA verzonden informatie ontvangt, moet zij deze kunnen interpreteren. Dit is vergelijkbaar met de situatie waarin twee personen, A en B, via de telefoon communiceren: hun dialoog wordt via de telefoon overgebracht. De spraak wordt door telefoon A gecodeerd in de vorm van signalen, via telefoonlijnen getransporteerd en komt aan bij telefoon B om daar te worden gedecodeerd. Persoon B hoort dan spraak. Hier komt het begrip dialoogprotocol om de hoek kijken: als A Frans spreekt en B deze taal niet begrijpt, kunnen A en B geen zinvol gesprek voeren.

Daarom moeten de twee communicerende applicaties het eens zijn over het type dialoog dat ze gaan hanteren. De dialoog met een dienst ftp is bijvoorbeeld niet dezelfde als die met een dienst pop: deze twee diensten accepteren niet dezelfde opdrachten. Ze hebben een verschillend dialoogprotocol.

11.3.2. De kenmerken van het protocol TCP

We zullen hier alleen netwerkcommunicatie behandelen die gebruikmaakt van het transportprotocol TCP. Laten we hier nog even de kenmerken ervan in herinnering brengen:

  • Het proces dat gegevens wil verzenden, brengt eerst een verbinding tot stand met het proces dat de informatie zal ontvangen. Deze verbinding wordt tot stand gebracht tussen een poort van de verzendende machine en een poort van de ontvangende machine. Tussen beide poorten ontstaat zo een virtueel pad dat uitsluitend is gereserveerd voor de twee processen die de verbinding tot stand hebben gebracht.
  • Alle pakketten die door het bronproces worden verzonden, volgen dit virtuele pad en komen aan in de volgorde waarin ze zijn verzonden
  • De verzonden informatie heeft een continu karakter. Het verzendende proces verstuurt informatie in zijn eigen tempo. Deze informatie wordt niet noodzakelijkerwijs onmiddellijk verzonden: het protocol TCP wacht tot er voldoende informatie is verzameld om deze te verzenden. De informatie wordt opgeslagen in een structuur die het segment TCP wordt genoemd. Zodra dit segment gevuld is, wordt het doorgestuurd naar de laag IP, waar het wordt ingekapseld in een pakket IP.
  • Elk segment dat via het protocol TCP wordt verzonden, krijgt een nummer. Het ontvangende protocol TCP controleert of het de segmenten in de juiste volgorde ontvangt. Voor elk correct ontvangen segment stuurt het een ontvangstbevestiging naar de afzender.
  • Zodra de afzender deze ontvangstbevestiging ontvangt, geeft hij dit door aan het verzendende proces. Dit proces weet dan dat een segment goed is aangekomen.
  • Als het protocol TCP, dat een segment heeft verzonden, na verloop van tijd geen ontvangstbevestiging ontvangt, verzendt het het betreffende segment opnieuw, waardoor de kwaliteit van de dienst voor het doorsturen van informatie wordt gewaarborgd.
  • De virtuele verbinding die tussen de twee communicerende processen tot stand is gebracht, is full-duplex: dit betekent dat de informatie in beide richtingen kan worden verzonden. Zo kan het bestemmingsproces ontvangstbevestigingen versturen terwijl het bronproces doorgaat met het verzenden van informatie. Hierdoor kan bijvoorbeeld het bronprotocol TCP meerdere segmenten verzenden zonder op een ontvangstbevestiging te wachten. Als het na verloop van tijd vaststelt dat het geen ontvangstbevestiging heeft ontvangen voor een bepaald segment met nummer n, zal het de verzending van de segmenten vanaf dat punt hervatten.

11.3.3. De client-serverrelatie

Vaak is de communicatie via internet asymmetrisch: machine A initieert een verbinding om een dienst aan te vragen bij machine B; hij geeft aan dat hij een verbinding wil openen met de dienst SB1 van machine B. Deze accepteert of weigert het verzoek. Als machine B de verbinding accepteert, kan machine A haar verzoeken naar de dienst SB1 sturen. Deze verzoeken moeten voldoen aan het communicatieprotocol dat door de dienst SB1 wordt ondersteund. Zo ontstaat een vraag-antwoorddialoog tussen machine A, die we de clientmachine noemen, en machine B, die we de servermachine noemen. Een van beide partners zal de verbinding verbreken.

11.3.4. Architectuur van een client

De architectuur van een netwerkprogramma dat gebruikmaakt van de diensten van een servertoepassing ziet er als volgt uit:

ouvrir la connexion avec le service SB1 de la machine B
si réussite alors
    tant que ce n'est pas fini
        préparer une demande
        l'émettre vers la machine B
        attendre et récupérer la réponse
        la traiter
    fin tant que
finsi
fermer la connexion

11.3.5. Architectuur van een server

De architectuur van een programma dat diensten aanbiedt, ziet er als volgt uit:

ouvrir le service sur la machine locale
tant que le service est ouvert
    se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
    lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
fin tant que

Het serverprogramma behandelt het eerste verbindingsverzoek van een klant anders dan zijn latere verzoeken om een dienst te verkrijgen. Het programma levert de dienst zelf niet. Als het dat wel zou doen, zou het gedurende de looptijd van de dienst niet meer luisteren naar verbindingsverzoeken en zouden klanten dan niet bediend worden. Het gaat daarom anders te werk: zodra een verbindingsverzoek wordt ontvangen op de luisterpoort en vervolgens wordt geaccepteerd, maakt de server een taak aan die verantwoordelijk is voor het leveren van de door de client gevraagde dienst. Deze dienst wordt geleverd op een andere poort van de server, de zogenaamde servicepoort. Zo kunnen meerdere clients tegelijkertijd worden bediend.

Een servicetaken heeft de volgende structuur:

tant que le service n'a pas été rendu totalement
        attendre une demande sur le port de service
        lorsqu'il y en a une, élaborer la réponse
        transmettre la réponse via le port de service
fin tant que
libérer le port de service

11.4. Ontdek de communicatieprotocollen van het internet

11.4.1. Inleiding

Wanneer een client verbinding heeft gemaakt met een server, ontstaat er een dialoog tussen beide. De aard van deze dialoog vormt het zogenaamde communicatieprotocol van de server. Tot de meest gangbare protocollen op het internet behoren de volgende:

  • HTTP: HyperText Transfer Protocol – het communicatieprotocol voor een webserver (HTTP-server)
  • SMTP: Simple Mail Transfer Protocol – het protocol voor communicatie met een e-mailverzendingsserver (server SMTP)
  • POP: Post Office Protocol – het protocol voor communicatie met een e-mailopslagserver (server POP). Dit protocol dient voor het ophalen van ontvangen e-mails en niet voor het verzenden ervan.
  • FTP: File Transfer Protocol – het protocol voor communicatie met een bestandsopslagserver (server FTP).

Al deze protocollen hebben als bijzonderheid dat het tekstregelprotocollen zijn: de client en de server wisselen tekstregels uit. Als we een client hebben die in staat is om:

  • een verbinding met een TCP-server tot stand te brengen
  • de tekstregels die de server hem stuurt op de console weer te geven
  • de tekstregels die een gebruiker zou invoeren naar de server te sturen

dan kunnen we communiceren met een TCP-server die een tekstregelprotocol gebruikt, mits we de regels van dat protocol kennen.

Het telnet-programma dat op Unix- of Windows-computers te vinden is, is zo’n client. Op Windows-computers is er ook een tool genaamd putty en die gaan we hier gebruiken. putty kan worden gedownload via het adres [http://www.putty.org/]. Het is een direct bruikbaar uitvoerbaar bestand (.exe). We zullen het als volgt configureren:

  • [1]: het adres IP van de TCP-server waarmee we verbinding willen maken, of de naam ervan
  • [2]: de luisterpoort van de TCP-server
  • [3]: gebruik de modus Raw, die een ruwe TCP-verbinding aangeeft.
  • [4]: de modus Never gebruiken om te voorkomen dat het venster van de client putty wordt gesloten als de server de verbinding verbreekt.
  • [6,7]: aantal kolommen/rijen van de console
  • [5]: het maximale aantal regels dat in het geheugen wordt bewaard. Een HTTP-server kan veel regels verzenden. Het moet mogelijk zijn om hierdoorheen te scrollen.
  • [8,9]: om de vorige instellingen te behouden, geef de configuratie [8] een naam en sla deze op met [9].
  • [11,12]: om een opgeslagen configuratie op te halen, selecteer je [11] en laad je deze in als [12].

Nu deze tool zo is geconfigureerd, gaan we enkele protocollen bekijken: TCP.

11.4.2. Het protocol HTTP (HyperText Transfer Protocol)

Laten we onze client TCP verbinden met de webserver van de machine istia.univ-angers.fr [2], poort 80 [3]:

In de console van putty stellen we de volgende dialoog HTTP samen:

GET / HTTP/1.1
Host: istia.univ-angers.fr:80
Connection: close

HTTP/1.1 200 OK
Date: Sat, 03 May 2008 07:53:47 GMT
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
X-Powered-By: PHP/4.4.4-8+etch4
Set-Cookie: fe_typo_user=0d2e64b317; path=/
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html;charset=iso-8859-1

693f
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"                                                                        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
         <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR">
....
         </html>
0
  • de regels 1-4 zijn het verzoek van de klant, ingevoerd via het toetsenbord
  • de regels 5-19 vormen het antwoord van de server
  • regel 1: syntaxis GET UrlDocument HTTP/1.1 – we vragen de URL /, c.a.d. de root van de website [istia.univ-angers.fr].
  • regel 2: syntaxis Host: machine:poort
  • regel 3: syntaxis Connection: [mode de la connexion]. De modus [close] geeft de server de opdracht de verbinding te verbreken zodra deze zijn antwoord heeft verzonden. De modus [Keep-Alive] vraagt om de verbinding open te laten.
  • regel 4: lege regel. De regels 1-3 worden HTTP-headers genoemd. Er kunnen nog andere headers zijn dan de hier getoonde. Het einde van de HTTP-headers wordt aangegeven met een lege regel.
  • regels 5-13: de HTTP-headers van het antwoord van de server – eindigen daar eveneens met een lege regel.
  • regels 14-19: het door de server verzonden document, in dit geval een HTML-document
  • regel 5: syntaxis HTTP/1.1 msg-code – de code 200 geeft aan dat het opgevraagde document is gevonden.
  • regel 6: de datum en tijd van de server
  • regel 7: identificatie van de software die de webservice verzorgt – hier een Apache-server op een Linux/Debian-systeem
  • regel 8: het document is dynamisch gegenereerd door PHP
  • regel 9: identificatiecookie van de klant – als deze bij zijn volgende verbinding herkend wil worden, moet hij deze cookie in zijn headers HTTP terugsturen.
  • regel 10: geeft aan dat de server de verbinding zal verbreken nadat het gevraagde document is geleverd
  • regel 11: het document wordt in stukjes (chunked) verzonden en niet in één keer.
  • regel 12: aard van het document: in dit geval een document HTML
  • regel 13: de lege regel die het einde aangeeft van de HTTP-headers van de server
  • regel 14: hexadecimaal getal dat het aantal tekens van het eerste blok van het document aangeeft. Wanneer dit getal 0 is (regel 19), weet de client dat hij het volledige document heeft ontvangen.
  • regels 15-18: deel van het ontvangen document.

De verbinding is verbroken en de client putty is inactief. Laten we opnieuw verbinding maken met [1] en het scherm wissen van eerdere weergaven [2,3]:

Het dialoogvenster ziet er deze keer als volgt uit:

GET /inconnu HTTP/1.1
Host: istia.univ-angers.fr:80
Connection: Close

HTTP/1.1 404 Not Found
Date: Sat, 03 May 2008 08:16:02 GMT
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html; charset=iso-8859-1

11a
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
                                                  <HTML><HEAD>
                                                              <TITLE>404 Not Found</TITLE>
                                                                                          </HEAD><BODY>
                                                                                                       <H1>Not Found</H1>
 The requested URL /inconnu was not found on this server.<P>
                                                            <HR>
                                                                <ADDRESS>Apache/1.3.34 Server at www.istia.univ-angers.fr Port 80</ADDRESS>
                   </BODY></HTML>

0
  • regel 1: er is een niet-bestaand document opgevraagd
  • regel 5: de server HTTP heeft gereageerd met de code 404, wat betekent dat het opgevraagde document niet is gevonden.

Als we dit document opvragen met de Firefox-browser:

Image

Als we de broncode van [Affichage/Code source] opvragen:

1
2
3
4
5
6
7
8
9
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<HTML><HEAD>
<TITLE>404 Not Found</TITLE>
</HEAD><BODY>
<H1>Not Found</H1>
The requested URL /inconnu was not found on this server.<P>
<HR>
<ADDRESS>Apache/1.3.34 Server at www.istia.univ-angers.fr Port 80</ADDRESS>
</BODY></HTML>

Dan krijgen we de regels 13-22 te zien die onze client heeft ontvangen: putty. Het voordeel hiervan is dat we bovendien de headers van het antwoord te zien krijgen: HTTP. Het is ook mogelijk om deze met Firefox te bekijken.

11.4.3. Het protocol SMTP (Simple Mail Transfer Protocol)

De SMTP-servers werken doorgaans op poort 25 [2]. We maken verbinding met de [1]-server. Hier moet je meestal een server kiezen

die tot hetzelfde domein IP behoort als de computer, omdat de SMTP-servers meestal zo zijn geconfigureerd dat ze alleen verzoeken accepteren van computers die tot hetzelfde domein behoren als zijzelf. Bovendien zijn de firewalls of antivirusprogramma’s van persoonlijke computers vaak zo geconfigureerd dat ze geen verbindingen via poort 25 met een externe computer toestaan. Het kan dan nodig zijn om deze firewall of dit antivirusprogramma opnieuw te configureren.

Het dialoogvenster SMTP in het clientvenster putty ziet er als volgt uit:

220 neuf-infra-smtp-out-sp604001av.neufgp.fr neuf telecom Service relais mail ready
HELO istia.univ-angers.fr
250 neuf-infra-smtp-out-sp604002av.neufgp.fr hello [84.100.189.193], Banniere OK , pret pour envoyer un mail
mail from: @expéditeur
250 2.1.0 <@expéditeur> sender ok
rcpt to: @destinataire
250 2.1.5 <@destinataire> destinataire ok
data
354 enter mail, end with "." on a line by itself
ligne1
ligne2
.
250 2.0.0 LwiU1Z00V4AoCxw0200000 message ok
quit
221 2.0.0 neuf-infra-smtp-out-sp604002av.neufgp.fr neuf telecom closing connection

Hieronder staat (D) een verzoek van de client en (R) een antwoord van de server.

  • regel 1: (R) welkomstbericht van de server SMTP
  • regel 2: (D) commando HELO om ‘hallo’ te zeggen
  • regel 3: (R) antwoord van de server
  • regel 4: (D) afzenderadres, bijvoorbeeld mail from: someone@gmail.com
  • regel 5: (R) antwoord van de server
  • regel 6: (D) adres van de ontvanger, bijvoorbeeld rcpt to: someoneelse@gmail.com
  • regel 7: (R) antwoord van de server
  • regel 8: (D) geeft het begin van het bericht aan
  • regel 9: (R) antwoord van de server
  • regels 10-12: (D) het te verzenden bericht, afgesloten met een regel die alleen een punt bevat.
  • regel 13: (R) antwoord van de server
  • regel 14: (D) de client geeft aan dat hij klaar is
  • regel 15: (R) antwoord van de server, die vervolgens de verbinding verbreekt

11.4.4. Het protocol POP (Post Office Protocol)

De POP-servers werken doorgaans op poort 110 [2]. We maken verbinding met de server [1]. De dialoog POP in het venster van de client putty ziet er als volgt uit:

+OK Hello there.
user xx
+OK Password required.
pass yy
+OK logged in.
list
+OK POP3 clients that break here, they violate STD53.
1 10105
2 55875
...
64 1717
.
retr 64
+OK 1717 octets follow.
Return-Path: <xx@neuf.fr>
X-Original-To: xx@univ-angers.fr
Delivered-To: xx@univ-angers.fr
....
Date: Sat,  3 May 2008 10:59:25 +0200 (CEST)
From: xx@neuf.fr
To: undisclosed-recipients:;

ligne1
ligne2
.
quit
+OK Bye-bye.
  • regel 1: (R) welkomstbericht van de server POP
  • regel 2: (D) de client geeft zijn gebruikersnaam op POP, c.a.d. de login waarmee hij zijn e-mail leest
  • regel 3: (R) het antwoord van de server
  • regel 4: (D) het wachtwoord van de klant
  • regel 5: (R) het antwoord van de server
  • regel 6: (D) de klant vraagt de lijst met zijn e-mails op
  • regels 7-12: (R) de lijst met berichten in de mailbox van de klant, in de vorm [N° du message taille en octets du message]
  • regel 13: (D) bericht nr. 64 wordt opgevraagd
  • regels 14-25: (R) bericht nr. 64, waarbij regels 15-22 de kopteksten van het bericht bevatten en regels 23-24 de hoofdtekst van het bericht.
  • regel 26: (D) de client geeft aan dat hij klaar is
  • regel 27: (R) antwoord van de server, die vervolgens de verbinding verbreekt.

11.4.5. Het protocol FTP (File Transfer Protocol)

Het protocol FTP is complexer dan de eerder besproken protocollen. Om de tekstregels te bekijken die tussen de client en de server worden uitgewisseld, kan men een tool gebruiken zoals FileZilla [http://www.filezilla.fr/].

Filezilla is een FTP-client met een Windows-interface voor het overzetten van bestanden. De handelingen van de gebruiker in de Windows-interface worden omgezet in FTP-commando's die worden gelogd in [1]. Dit is een goede manier om de commando's van het FTP-protocol te ontdekken.

11.5. De .NET-klassen voor internetprogrammering

11.5.1. De juiste klasse kiezen

Het .NET-framework biedt verschillende klassen om met het netwerk te werken:

  • De Socket-klasse is de klasse die het dichtst bij het netwerk opereert. Hiermee kan de netwerkverbinding nauwkeurig worden beheerd. De term socket verwijst naar een stopcontact. De term is uitgebreid om een softwarematige netwerkverbinding aan te duiden. Bij een TCP-IP-communicatie tussen twee machines A en B communiceren twee sockets'en met elkaar. Een applicatie kan rechtstreeks met de sockets werken. Dit is het geval bij de hierboven genoemde applicatie A. Een socket kan een client- of een serveur-socket zijn.
  • Als men op een minder gedetailleerd niveau wil werken dan dat van de klasse Socket, kan men de klassen
  • TcpClient om een TCP-client
  • TcpListener om een TCP-server te maken

Deze twee klassen bieden de toepassing die ze gebruikt een eenvoudiger beeld van de netwerkcommunicatie door de technische details van het socketbeheer voor haar af te handelen.

  • .NET biedt klassen die specifiek zijn voor bepaalde protocollen:
  • de klasse SmtpClient voor het beheer van het protocol SMTP voor communicatie met een SMTP-server voor het verzenden van e-mails
  • de klasse WebClient voor het beheer van de protocollen HTTP of FTP voor communicatie met een webserver.

Opgemerkt moet worden dat de klasse Socket op zichzelf voldoende is om alle TCP/IP-communicatie te beheren, maar dat we in de eerste plaats zullen streven naar het gebruik van de klassen op een hoger niveau om het schrijven van de TCP/IP-toepassing te vergemakkelijken.

11.5.2. De klasse TcpClient

De klasse TcpClient is in de meeste gevallen de geschikte klasse om de client van een TCP-service te maken. Deze klasse bevat onder andere de volgende constructors C, methoden M en eigenschappen P:

TcpClient(string hostname, int port)
C
maakt een TCP-verbinding met de service die draait op de opgegeven poort (port) van de opgegeven machine (hostname). Bijvoorbeeld: new TcpClient("istia.univ-angers.fr",80) om verbinding te maken met poort 80 van de machine istia.univ-angers.fr
Socket Client
P
de socket die door de client wordt gebruikt om met de server te communiceren.
NetworkStream GetStream()
M
verkrijgt een lees- en schrijfstream naar de server. Deze stream maakt de communicatie tussen client en server mogelijk.
void Close()
M
sluit de verbinding. De socket en de stream NetworkStream worden eveneens gesloten
bool Connected()
P
waar als de verbinding tot stand is gebracht

De klasse NetworkStream vertegenwoordigt de netwerkstroom tussen de client en de server. Deze is afgeleid van de klasse Stream. Veel client-servertoepassingen wisselen tekstregels uit die eindigen met de regeleinde-tekens "\r\n". Daarom is het interessant om de objecten StreamReader en StreamWriter te gebruiken om deze regels in de netwerkstroom te lezen en te schrijven. Als een machine M1 dus een verbinding heeft tot stand gebracht met een machine M2 met behulp van een object TcpClient client1 en zij tekstregels uitwisselen, dan kan deze machine zijn lees- en schrijfstromen als volgt aanmaken:

StreamReader in1=new StreamReader(client1.GetStream());
StreamWriter out1=new StreamWriter(client1.GetStream());
out1.AutoFlush=true;

De instructie

out1.AutoFlush=true;

betekent dat de schrijfstroom van client1 niet via een tussenbuffer verloopt, maar rechtstreeks naar het netwerk gaat. Dit is een belangrijk punt. Over het algemeen verwacht client1 een antwoord wanneer het een tekstregel naar zijn partner verstuurt. Dit antwoord zal nooit komen als de regel in werkelijkheid op de machine M1 in de buffer is opgeslagen en nooit naar de machine M2 is verzonden.

Om een tekstregel naar de machine M2 te verzenden, schrijft men:

client1.WriteLine("un texte");

Om het antwoord van M2 te lezen, schrijf je:

string réponse=client1.ReadLine();

We beschikken nu over de elementen om de basisarchitectuur te ontwerpen van een internetclient met het volgende basiscommunicatieprotocol met de server:

  • de client verstuurt een verzoek dat in één enkele regel staat
  • de server stuurt een antwoord dat in één regel staat

using System;
using System.IO;
using System.Net.Sockets;

namespace ... {
    class ... {
        static void Main(string[] args) {
            ...
            try {
                // er wordt verbinding gemaakt met de service
                using (TcpClient tcpClient = new TcpClient(serveur, port)) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // uitvoerstroom niet gebufferd
                                writer.AutoFlush = true;
                                // verzoek-antwoordlus
                                while (true) {
                                    // het verzoek komt van het toetsenbord
                                    Console.Write("Demande (bye pour arrêter) : ");
                                    demande = Console.ReadLine();
                                    // klaar?
                                    if (demande.Trim().ToLower() == "bye")
                                        break;
                                    // het verzoek wordt naar de server verzonden
                                    writer.WriteLine(demande);
                                    // het antwoord van de server wordt gelezen
                                    réponse = reader.ReadLine();
                                    // het antwoord wordt verwerkt
                                    ...
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // fout
                ...
            }
        }
    }
}
  • regel 11: het tot stand brengen van de verbinding door de client – de clausule using zorgt ervoor dat de bijbehorende bronnen worden vrijgegeven bij het afsluiten van de verbinding (using).
  • regel 12: het openen van de netwerkstroom in een using-clausule
  • regel 13: aanmaken en gebruiken van de leesstroom in een clausule using
  • regel 14: aanmaken en gebruiken van de schrijfstroom in een clausule using
  • regel 16: de uitvoerstroom niet bufferen
  • regels 18-31: de cyclus van verzoek van de client / antwoord van de server
  • regel 26: de client stuurt zijn verzoek naar de server
  • regel 28: de client wacht op het antwoord van de server. Dit is een blokkerende bewerking, net als het lezen vanaf het toetsenbord. Het wachten eindigt bij de ontvangst van een tekenreeks die eindigt op "\n" of bij het einde van de stream. Dit gebeurt als de server de verbinding met de client verbreekt.

11.5.3. De klasse TcpListener

De klasse TcpListener is in de meeste gevallen de geschikte klasse om een TCP-service te maken. Deze klasse bevat onder andere de volgende C-constructors, M-methoden en P-eigenschappen:

TcpListener(int port)
C
maakt een service TCP aan die luistert (listen) naar verzoeken van clients op een als parameter doorgegeven poort (port), de zogenaamde luisterpoort. Als de machine is aangesloten op meerdere netwerken IP, luistert de service op elk van deze netwerken.
TcpListener(IPAddress ip, int port)
C
hetzelfde, maar er wordt alleen geluisterd op het opgegeven IP-adres.
void Start()
M
start het afluisteren van verzoeken van klanten
TcpClient AcceptTcpClient()
M
accepteert het verzoek van een klant. Er wordt vervolgens een nieuwe verbinding met de klant geopend, een zogenaamde serviceverbinding. De poort die aan de serverzijde wordt gebruikt, is willekeurig en wordt door het systeem gekozen. Deze wordt de servicepoort genoemd. AcceptTcpClient retourneert als resultaat het object TcpClient dat aan de serverzijde aan de serviceverbinding is gekoppeld.
void Stop()
M
stopt met het afwachten van verzoeken van klanten
Socket Server
P
de luistersocket van de server

De basisstructuur van een TCP-server die met zijn klanten communiceert volgens het volgende protocol:

  • de klant stuurt een verzoek dat in één enkele regel staat
  • de server stuurt een antwoord dat in één regel staat

zou er als volgt uit kunnen zien:


using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;

namespace ... {
    public class ... {
            ...
            // de luisterservice wordt aangemaakt
            TcpListener ecoute = null;
            try {
                // de service wordt aangemaakt – deze luistert op alle netwerkinterfaces van de machine
                ecoute = new TcpListener(IPAddress.Any, port);
                // de service wordt gestart
                ecoute.Start();
                // servicelus
                TcpClient tcpClient = null;
                // oneindige lus – wordt gestopt met Ctrl-C
                while (true) {
                    // wacht op een client
                    tcpClient = ecoute.AcceptTcpClient();
                    // de service wordt verzorgd door een andere taak
                    ThreadPool.QueueUserWorkItem(Service, tcpClient);
                    // volgende klant
                }
            } catch (Exception ex) {
                // de fout wordt gemeld
                ...
            } finally {
                // einde van de dienst
                ecoute.Stop();
            }
        }

        // -------------------------------------------------------
        // verleent de dienst aan een klant
        public static void Service(Object infos) {
            // de te bedienen klant wordt opgehaald
            Client client = infos as Client;
            // verwerking verbinding TcpClient
            try {
                using (TcpClient tcpClient = client.CanalTcp) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // niet-gebufferde uitgangsstraal
                                writer.AutoFlush = true;
                                // lus voor het lezen van verzoeken en het schrijven van antwoorden
                                bool fini=false;
                                while (! fini) != null) {
                                    // wachten op verzoek van de client – blokkerende bewerking
                                    demande=reader.ReadLine();
                                    // antwoord voorbereiden
                                    réponse=...;
                                    // antwoord naar de klant verzenden
                                    writer.WriteLine(réponse);
                                    // volgend verzoek
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // fout
                ...
            } finally {
                // einde klant
                ...
            }
        }
    }
}
  • regel 14: de luisterdienst wordt aangemaakt voor een bepaalde poort en een bepaald IP-adres. Houd er hierbij rekening mee dat een machine ten minste twee adressen heeft: het adres "127.0.0.1", dat het lokale adres is, en het adres "I1.I2.I3.I4" dat het heeft op het netwerk waarmee het is verbonden. De machine kan nog andere adressen hebben als deze met meerdere netwerken is verbonden. IPAddress.Any verwijst naar alle adressen van een machine.
  • regel 16: de luisterdienst start. Deze was eerder al aangemaakt, maar luisterde nog niet. Luisteren betekent wachten op verzoeken van klanten.
  • regels 20-26: de lus van ‘wachten op een verzoek van een klant’ en ‘de klant bedienen’ wordt voor elke nieuwe klant herhaald
  • regel 22: het verzoek van een klant wordt geaccepteerd. De methode AcceptTcpClient retourneert een zogenaamde service-instantie TcpClient:
    • de klant heeft zijn verzoek ingediend met zijn eigen client-side-instantie TcpClient, die we TcpClientDemande zullen noemen
    • de server accepteert dit verzoek met AcceptTcpClient. Deze methode creëert een server-instantie TcpClient, die we TcpClientService zullen noemen. Er is nu een open TCP-verbinding met aan beide uiteinden de instanties TcpClientDemande <--> TcpClientService.
    • De client-servercommunicatie die vervolgens plaatsvindt, verloopt via deze verbinding. De luisterservice speelt hierbij geen rol meer.
  • regel 24: om ervoor te zorgen dat de server meerdere clients tegelijk kan verwerken, wordt de dienst verzorgd door threads, 1 thread per client.
  • regel 32: de luisterdienst wordt gesloten
  • regel 38: de methode die wordt uitgevoerd door de servicethread voor een klant. Deze ontvangt als parameter de instantie TcpClient die al is verbonden met de klant die moet worden bediend.
  • regels 38-71: hier zien we code die vergelijkbaar is met die van de eerder besproken eenvoudige TCP-client.

11.6. Voorbeelden van TCP-clients/servers

11.6.1. Een echo-server

We gaan een echo-server schrijven die vanuit een venster met de naam DOS wordt gestart met het commando:

ServeurEcho poort

De server draait op de als parameter doorgegeven poort. Hij stuurt de client gewoon het verzoek terug dat deze naar hem heeft gestuurd. Het programma ziet er als volgt uit:


using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;

// oproep: serveurEcho poort
// echo-server
// stuurt de regel die de client heeft verzonden terug naar de client

namespace Chap9 {
    public class ServeurEcho {
        public const string syntaxe = "Syntaxe : [serveurEcho] port";

        // hoofdprogramma
        public static void Main(string[] args) {

            // is er een argument?
            if (args.Length != 1) {
                Console.WriteLine(syntaxe);
                return;
            }
            // dit argument moet een geheel getal >0 zijn
            int port = 0;
            if (!int.TryParse(args[0], out port) || port<=0) {
                Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
                return;
            }
            // de luisterdienst wordt aangemaakt
            TcpListener ecoute = null;
            int numClient = 0;    // volgend klantnummer
            try {
                // de service wordt aangemaakt – deze luistert op alle netwerkinterfaces van de machine
                ecoute = new TcpListener(IPAddress.Any, port);
                // de service wordt gestart
                ecoute.Start();
                // vervolg
                Console.WriteLine("Serveur d'écho lancé sur le port {0}", ecoute.LocalEndpoint);
                // servicethreads
                ThreadPool.SetMinThreads(10, 10);
                ThreadPool.SetMaxThreads(10, 10);
                // servicelus
                TcpClient tcpClient = null;
                // oneindige lus – wordt gestopt met Ctrl-C
                while (true) {
                    // wacht op een client
                    tcpClient = ecoute.AcceptTcpClient();
                    // de service wordt verzorgd door een andere taak
                    ThreadPool.QueueUserWorkItem(Service, new Client() { CanalTcp = tcpClient, NumClient = numClient });
                    // volgende klant
                    numClient++;
                }
            } catch (Exception ex) {
                // de fout wordt gemeld
                Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
            } finally {
                // einde van de dienst
                ecoute.Stop();
            }
        }

        // -------------------------------------------------------
        // verleent de dienst aan een klant van de echo-server
        public static void Service(Object infos) {
            // de te bedienen client wordt opgehaald
            Client client = infos as Client;
            // de dienst aan de client leveren
            Console.WriteLine("Début de service au client {0}", client.NumClient);
            // verbinding in gebruik TcpClient
            try {
                using (TcpClient tcpClient = client.CanalTcp) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // niet-gebufferde uitvoerstroom
                                writer.AutoFlush = true;
                                // lees-/schrijfverzoek-antwoordlus
                                string demande = null;
                                while ((demande = reader.ReadLine()) != null) {
                                    // console-monitoring
                                    Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
                                    // echo van het verzoek naar de client
                                    writer.WriteLine("[{0}]", demande);
                                    // console-log
                                    Console.WriteLine("---> Client {0} : {1}", client.NumClient, demande);
                                    // de service stopt wanneer de client "bye" verstuurt
                                    if (demande.Trim().ToLower() == "bye")
                                        break;
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // fout
                Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
            } finally {
                // einde client
                Console.WriteLine("Fin du service au client {0}", client.NumClient);
            }
        }
    }

    // klantinformatie
    internal class Client {
        public TcpClient CanalTcp { get; set; }        // verbinding met de client
        public int NumClient { get; set; }            // klantnummer
    }
}

De structuur van de echo-server komt overeen met de eerder beschreven basisarchitectuur van TCP-servers. We zullen alleen het gedeelte „dienstverlening aan de client“ toelichten:

  • regel 79: het verzoek van de klant wordt gelezen
  • regel 83: het wordt tussen vierkante haken aan de klant teruggestuurd
  • regel 79: de service stopt wanneer de klant de verbinding verbreekt

In een DOS-venster gebruiken we het uitvoerbare bestand van het C#-project:

...\Chap9\02\bin\Release>dir
 03/05/2008  11:46             7 168 ServeurEcho.exe
...>ServeurEcho 100
Serveur d'écho lancé sur le port 0.0.0.0:100

Vervolgens starten we twee clients putty die we verbinden met poort 100 van de machine localhost:

 

De console-uitvoer van de echo-server ziet er als volgt uit:

1
2
3
Serveur d'écho lancé sur le port 0.0.0.0:100
Début de service au client 0
Début de service au client 1

Eerst client 1 en vervolgens client 0 verzenden de volgende teksten:

  • [1]: client nr. 1
  • [2]: client nr. 0
  • [3]: de console van de echo-server
  • in [4]: client 1 verbreekt de verbinding met het commando bye.
  • in [5]: de server detecteert dit

De server kan worden gestopt met Ctrl-C. Client nr. 0 detecteert dit dan met [6].

11.6.2. Een client voor de echo-server

We schrijven nu een client voor de vorige server. Deze wordt als volgt aangeroepen:

ClientEcho nomServeur poort

Hij maakt verbinding met de machine nomServeur op poort port en stuurt vervolgens tekstregels naar de server, die deze als echo terugstuurt.


using System;
using System.IO;
using System.Net.Sockets;

namespace Chap9 {
    // maakt verbinding met een echo-server
    // elke regel die via het toetsenbord wordt ingevoerd, wordt als echo teruggegeven
    class ClientEcho {
        static void Main(string[] args) {
            // syntaxis
            const string syntaxe = "pg machine port";

            // aantal argumenten
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

            // noteer de naam van de server
            string serveur = args[0];

            // de poort moet een geheel getal >0 zijn
            int port = 0;
            if (!int.TryParse(args[1], out port) || port <= 0) {
                Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
                return;
            }

            // we kunnen aan de slag
            string demande = null;        // verzoek van de client
            string réponse = null;        // antwoord van de server
            try {
                // verbinding maken met de dienst
                using (TcpClient tcpClient = new TcpClient(serveur, port)) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // niet-gebufferde uitvoerstroom
                                writer.AutoFlush = true;
                                // verzoek-antwoordlus
                                while (true) {
                                    // het verzoek komt van het toetsenbord
                                    Console.Write("Demande (bye pour arrêter) : ");
                                    demande = Console.ReadLine();
                                    // klaar?
                                    if (demande.Trim().ToLower() == "bye")
                                        break;
                                    // het verzoek wordt naar de server verzonden
                                    writer.WriteLine(demande);
                                    // het antwoord van de server wordt gelezen
                                    réponse = reader.ReadLine();
                                    // het antwoord wordt verwerkt
                                    Console.WriteLine("Réponse : {0}", réponse);
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // fout
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            }
        }
    }
}

De structuur van deze client komt overeen met de algemene basisarchitectuur die wordt voorgesteld voor Tcp-clients. Hieronder staan de resultaten die zijn verkregen in de volgende configuratie:

  • de server wordt gestart op poort 100 in een DOS-venster
  • op dezelfde machine worden twee clients gestart in twee andere DOS-vensters

In het venster van client A (nr. 0) zien we de volgende weergaven:

1
2
3
4
5
6
...\Chap9\03\bin\Release>ClientEcho localhost 100
Demande (bye pour arrêter) : ligne1A
Réponse : [ligne1A]
Demande (bye pour arrêter) : ligne2A
Réponse : [ligne2A]
Demande (bye pour arrêter) :

In het venster van client B (nr. 1):

1
2
3
4
5
6
...\Chap9\03\bin\Release>ClientEcho localhost 100
Demande (bye pour arrêter) : ligne1B
Réponse : [ligne1B]
Demande (bye pour arrêter) : ligne2B
Réponse : [ligne2B]
Demande (bye pour arrêter) :

In het venster van de server:

...\Chap9\02\bin\Release>ServeurEcho 100
Serveur d'écho lancé sur le port 0.0.0.0:100
Début de service au client 0
<--- Client 0 : ligne1A
---> Client 0 : ligne1A
<--- Client 0 : ligne2A
---> Client 0 : ligne2A
Début de service au client 1
<--- Client 1 : ligne1B
---> Client 1 : ligne1B
<--- Client 1 : ligne2B
---> Client 1 : ligne2B

Klant A nr. 0 verbreekt de verbinding:

1
2
3
4
Demande (bye pour arrêter) : ligne1A
Réponse : [ligne1A]
...
Demande (bye pour arrêter) : bye

De serverconsole:

1
2
3
Serveur d'écho lancé sur le port 0.0.0.0:100
...
Fin du service au client 0

11.6.3. Een generieke TCP-client

We gaan een generieke TCP-client schrijven die als volgt wordt gestart: ClientTcpGenerique serverpoort. Deze werkt op dezelfde manier als de Putty-client, maar heeft een console-interface en biedt geen configuratieopties.

In de vorige toepassing was het communicatieprotocol bekend: de client stuurde één regel en de server antwoordde met één regel. Elke dienst heeft zijn eigen specifieke protocol en er zijn ook de volgende situaties:

  • de client moet meerdere regels tekst verzenden voordat hij een antwoord krijgt
  • het antwoord van een server kan uit meerdere regels tekst bestaan

Daarom is de cyclus van het verzenden van één regel naar de server / het ontvangen van één regel van de server niet altijd geschikt. Om protocollen te verwerken die complexer zijn dan het echo-protocol, zal de generieke TCP-client twee threads hebben:

  • de hoofdthread leest de via het toetsenbord ingevoerde tekstregels en verstuurt deze naar de server.
  • Een secundaire thread werkt parallel en is gewijd aan het lezen van de door de server verzonden tekstregels. Zodra deze er een ontvangt, geeft hij deze weer op de console. De thread stopt pas wanneer de server de verbinding verbreekt. Hij werkt dus continu.

De code is als volgt:


using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;

namespace Chap9 {
    // ontvangt als parameter de kenmerken van een service in de vorm: server poort
    // maakt verbinding met de dienst
    // stuurt elke regel die op het toetsenbord wordt ingevoerd naar de server
    // maakt een thread aan om de door de server verzonden tekstregels continu te lezen
    class ClientTcpGenerique {
        static void Main(string[] args) {
            // syntaxis
            const string syntaxe = "pg serveur port";

            // aantal argumenten
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

            // noteer de naam van de server
            string serveur = args[0];

            // de poort moet een geheel getal >0 zijn
            int port = 0;
            if (!int.TryParse(args[1], out port) || port <= 0) {
                Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
                return;
            }
            // verbinding maken met de dienst
            TcpClient tcpClient = null;
            try {
                tcpClient = new TcpClient(serveur, port);
            } catch (Exception ex) {
                // fout
                Console.WriteLine("Impossible de se connecter au service ({0},{1}) : erreur {2}", serveur, port, ex.Message);
                // einde
                return;
            }

            // er wordt een aparte thread gestart om de door de server verzonden tekstregels te lezen
            ThreadPool.QueueUserWorkItem(Receive, tcpClient);

            // toetsenbordcommando’s worden in de hoofdthread gelezen
            Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
            string demande = null;        // verzoek van de client
            try {
                // de clientverbinding wordt verwerkt
                using (tcpClient) {
                    // er wordt een schrijfstroom naar de server aangemaakt
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamWriter writer = new StreamWriter(networkStream)) {
                            // niet-gebufferde uitvoerstroom
                            writer.AutoFlush = true;
                            // verzoek-antwoordlus
                            while (true) {
                                demande = Console.ReadLine();
                                // klaar?
                                if (demande.Trim().ToLower() == "bye")
                                    break;
                                // het verzoek wordt naar de server verzonden
                                writer.WriteLine(demande);
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // fout
                Console.WriteLine("L'erreur suivante s'est produite dans le thread principal : {0}", e.Message);
            }
        }

        // lees-thread client <-- server
        public static void Receive(object infos) {
            // lokale gegevens
            string réponse = null;    // antwoord van de server
            // invoerstroom aanmaken
            try {
                using (TcpClient tcpClient = infos as TcpClient) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            // lus voor het continu lezen van tekstregels uit de invoerstroom
                            while ((réponse = reader.ReadLine()) != null) {
                                // consoleweergave
                                Console.WriteLine("<-- {0}", réponse);
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                // fout
                Console.WriteLine("Flux de lecture : l'erreur suivante s'est produite : {0}", ex.Message);
            } finally {
                // het einde van de leesthread wordt gemeld
                Console.WriteLine("Fin du thread de lecture des réponses du serveur. Si besoin est, arrêtez le thread de lecture console avec la commande bye.");
            }
        }
    }
}
  • regel 34: de client maakt verbinding met de server
  • regel 43: er wordt een thread gestart om de tekstregels van de server te lezen. Deze moet de methode Receive uit regel 73 uitvoeren. Aan deze methode wordt de instantie TcpClient doorgegeven die verbinding heeft gemaakt met de server.
  • regels 57-64: de lus voor het invoeren van toetsenbordopdrachten en het verzenden ervan naar de server. Het invoeren van toetsenbordopdrachten wordt verzorgd door de hoofdthread.
  • regels 75-98: de methode Receive die wordt uitgevoerd door de thread die de tekstregels leest. Deze methode ontvangt als parameter de instantie TcpClient die verbinding heeft gemaakt met de server.
  • regels 84-87: de doorlopende lus voor het lezen van de door de server verzonden tekstregels. Deze stopt pas wanneer de server de open verbinding met de client verbreekt.

Hier volgen enkele voorbeelden die aansluiten bij de voorbeelden die in paragraaf 11.4 met de client putty zijn gebruikt. De client wordt uitgevoerd in een DOS-console.

Protocol HTTP

...\Chap9\04\bin\Release>ClientTcpGenerique istia.univ-angers.fr 80
Tapez vos commandes (bye pour arrêter) :
GET /inconnu HTTP/1.1
Host: istia.univ-angers.fr:80
Connection: Close

<-- HTTP/1.1 404 Not Found
<-- Date: Sat, 03 May 2008 12:35:11 GMT
<-- Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29

<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html; charset=iso-8859-1
<--
<-- 11a
<-- <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<-- <HTML><HEAD>
<-- <TITLE>404 Not Found</TITLE>
<-- </HEAD><BODY>
<-- <H1>Not Found</H1>
<-- The requested URL /inconnu was not found on this server.<P>
<-- <HR>
<-- <ADDRESS>Apache/1.3.34 Server at www.istia.univ-angers.fr Port 80</ADDRESS>
<-- </BODY></HTML>
<--
<-- 0
<--
[Fin du thread de lecture des réponses du serveur]
bye

...\Chap9\04\bin\Release>

De lezer wordt verzocht de toelichting in paragraaf 11.4.2 nog eens door te lezen. We geven alleen commentaar op wat specifiek betrekking heeft op de toepassing:

  • regel 28: na het verzenden van regel 27 heeft de server HTTP de verbinding verbroken, waardoor de leesthread is beëindigd. De hoofdthread, die de via het toetsenbord ingevoerde opdrachten leest, is nog steeds actief. De opdracht op regel 29, die via het toetsenbord is ingevoerd, stopt deze thread.

Protocol SMTP

...\Chap9\04\bin\Release>ClientTcpGenerique smtp.neuf.fr 25
Tapez vos commandes (bye pour arrêter) :
<-- 220 neuf-infra-smtp-out-sp604002av.neufgp.fr neuf telecom Service relais mail ready
HELO istia.univ-angers.fr
<-- 250 neuf-infra-smtp-out-sp604002av.neufgp.fr hello [84.100.189.193], Banniere OK , pret pour envoyer un mail
mail from: xx@neuf.fr
<-- 250 2.1.0 <xx@neuf.fr> sender ok
rcpt to: yy@univ-angers.fr
<-- 250 2.1.5 <yy@univ-angers.fr> destinataire ok
data
<-- 354 enter mail, end with "." on a line by itself
ligne1
ligne2
.
<-- 250 2.0.0 M0jL1Z0044AoCxw0200000 message ok
quit
<-- 221 2.0.0 neuf-infra-smtp-out-sp604002av.neufgp.fr neuf telecom closing connection
[Fin du thread de lecture des réponses du serveur]
bye

...\Chap9\04\bin\Release>

De lezer wordt verzocht de uitleg in paragraaf 11.4.3 nogmaals door te nemen en de andere voorbeelden te testen met de client putty.

11.6.4. Een generieke TCP-server-

Nu richten we ons op een server

  • die de door zijn clients verzonden commando’s op het scherm weergeeft
  • en als antwoord de tekstregels terugstuurt die door een gebruiker via het toetsenbord zijn ingevoerd. Het is dus de gebruiker die als server fungeert.

Het programma wordt in een DOS-venster gestart met: ServeurTcpGenerique portEcoute, waarbij portEcoute de poort is waarop de clients verbinding moeten maken. De dienstverlening aan de client wordt verzorgd door twee threads:

  • de hoofdthread, die:
    • de clients één voor één verwerkt en niet parallel.
    • de regels leest die de gebruiker via het toetsenbord invoert en deze naar de client verstuurt. De gebruiker geeft met het commando ‘bye’ aan dat hij de verbinding met de client verbreekt. Omdat de console niet voor twee clients tegelijk kan worden gebruikt, verwerkt onze server slechts één client tegelijk.
  • een secundaire thread die zich uitsluitend bezighoudt met het lezen van de door de klant verzonden tekstregels

De server stopt nooit, tenzij de gebruiker op het toetsenbord op Ctrl-C drukt.

Laten we enkele voorbeelden bekijken. De server wordt gestart op poort 100 en we gebruiken de generieke client van paragraphe11.6.3 om met de server te communiceren. Het venster van de client ziet er als volgt uit:

1
2
3
4
5
6
7
...\Chap9\04\bin\Release>ClientTcpGenerique localhost 100
Tapez vos commandes (bye pour arrêter) :
commande 1 du client 1
<-- réponse 1 au client 1
commande 2 du client 1
<-- réponse 2 au client 1
bye

De regels die beginnen met <-- zijn de berichten die van de server naar de client worden verzonden, de andere zijn de berichten van de client naar de server. Het servervenster ziet er als volgt uit:

...\Chap9\05\bin\Release>ServeurTcpGenerique 100
Serveur générique lancé sur le port 0.0.0.0:100
Client 127.0.0.1:4165
Tapez vos commandes (bye pour arrêter) :
<-- commande 1 du client 1
réponse 1 au client 1
<-- commande 2 du client 1
réponse 2 au client 1
[Fin du thread de lecture des demandes du client]
bye

De regels die beginnen met <-- zijn de regels die van de client naar de server zijn verzonden, de overige zijn de regels die door de server naar de client zijn verzonden. Regel 9 geeft aan dat de thread die de verzoeken van de client leest, is gestopt. De hoofdthread van de server wacht nog steeds op commando’s die via het toetsenbord worden ingevoerd om deze naar de client te verzenden. Je moet dan het commando bye uit regel 10 op het toetsenbord invoeren om naar de volgende client over te schakelen. De server is nog steeds actief, terwijl client 1 is voltooid. We starten een tweede client voor dezelfde server:

1
2
3
4
5
...\Chap9\04\bin\Release>ClientTcpGenerique localhost 100
Tapez vos commandes (bye pour arrêter) :
commande 3 du client 2
<-- réponse 3 au client 2
bye

Het servervenster ziet er dan als volgt uit:

1
2
3
4
5
6
Tapez vos commandes (bye pour arrêter) :
Client 127.0.0.1:4166
<-- commande 3 du client 2
réponse 3 au client 2
[Fin du thread de lecture des demandes du client]
bye

Na regel 6 hierboven wacht de server op een nieuwe client. We kunnen hem stoppen met Ctrl-C.

Laten we nu een webserver simuleren door onze generieke server op poort 88 te starten:

1
2
3
...\Chap9\05\bin\Release>ServeurTcpGenerique 88

Serveur générique lancé sur le port 0.0.0.0:88

Laten we nu een browser openen en de pagina URL http://localhost:88/exemple.html opvragen. De browser maakt vervolgens verbinding met poort 88 van de machine localhost en vraagt vervolgens de pagina /exemple.html op:

 

Laten we nu eens kijken naar het venster van onze server:

Serveur générique lancé sur le port 0.0.0.0:88
Client 127.0.0.1:4167
Tapez vos commandes (bye pour arrêter) :
<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/msword, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-appl
ication, application/x-silverlight, */*
<-- Accept-Language: fr,en-US;q=0.7,fr-FR;q=0.3
<-- UA-CPU: x86
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.
4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.590; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

We zien de headers HTTP die door de browser zijn verzonden. Hierdoor kunnen we andere headers HTTP ontdekken dan die we al eerder zijn tegengekomen. Laten we een antwoord voor onze client opstellen. De gebruiker achter het toetsenbord is hier de echte server en kan met de hand een antwoord opstellen. Laten we even terugdenken aan het antwoord dat een webserver in een eerder voorbeeld gaf:

HTTP/1.1 200 OK
Date: Sat, 03 May 2008 07:53:47 GMT
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
X-Powered-By: PHP/4.4.4-8+etch4
Set-Cookie: fe_typo_user=0d2e64b317; path=/
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html;charset=iso-8859-1

693f
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"                                                                        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
         <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR">
....
         </html>
0

Laten we proberen een soortgelijk antwoord te geven door ons tot het strikte minimum te beperken:

HTTP/1.1 200 OK
Server: serveur tcp generique
Connection: close
Content-Type: text/html

<html>
<head><title>Serveur generique</title></head>
<body><h2>Reponse du serveur generique</h2></body>
</html>
bye
Flux de lecture des lignes de texte du client : l'erreur suivante s'est produite : Unable to read data from the transport connection: Une opération de blocage a été interrompue par un appel à WSACancelBlockingCall.
[Fin du thread de lecture des demandes du client]

We hebben ons in ons antwoord beperkt tot de headers HTTP uit de regels 1-4. We geven de grootte van het document dat we gaan verzenden (Content-Length) niet aan, maar vermelden alleen dat we de verbinding zullen verbreken (Connection: close) nadat het is verzonden. Dit is voldoende voor de browser. Zodra de verbinding wordt verbroken, weet de browser dat het antwoord van de server is voltooid en zal hij de pagina HTML weergeven die naar hem is verzonden. Dit is de pagina uit de regels 6-9. De gebruiker sluit vervolgens via het toetsenbord de verbinding met de client door het commando bye in te voeren, regel 10. Bij dit toetsenbordcommando sluit de hoofdthread de verbinding met de client. Dit veroorzaakt de uitzondering in regel 11. De thread die de tekstregels van de client leest, is abrupt onderbroken door het sluiten van de verbinding met de client en heeft een uitzondering gegenereerd. Na regel 12 wacht de server op een nieuwe client.

De browser van de klant geeft nu het volgende weer:

Als we hierboven Affichage/Source invoeren om te zien wat de browser heeft ontvangen, krijgen we [2], dat wil zeggen precies wat we vanaf de generieke server hebben verzonden.

De code van de generieke server TCP is als volgt:


using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace Chap9 {
    public class ServeurTcpGenerique {
        public const string syntaxe = "Syntaxe : ServeurGénérique Port";

        // hoofdprogramma
        public static void Main(string[] args) {

            // is er een argument?
            if (args.Length != 1) {
                Console.WriteLine(syntaxe);
                Environment.Exit(1);
            }
            // dit argument moet een geheel getal >0 zijn
            int port = 0;
            if (!int.TryParse(args[0], out port) || port <= 0) {
                Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
                Environment.Exit(2);
            }
            // de luisterservice wordt aangemaakt
            TcpListener ecoute = null;
            try {
                // de service wordt aangemaakt
                ecoute = new TcpListener(IPAddress.Any, port);
                // de service wordt gestart
                ecoute.Start();
                // vervolg
                Console.WriteLine("Serveur générique lancé sur le port {0}", ecoute.LocalEndpoint);
                while (true) {
                    // wachten op een client
                    Console.WriteLine("Attente du client suivant...");
                    TcpClient tcpClient = ecoute.AcceptTcpClient();
                    Console.WriteLine("Client {0}", tcpClient.Client.RemoteEndPoint);
                    // er wordt een aparte thread gestart om de door de klant verzonden tekstregels te lezen
                    ThreadPool.QueueUserWorkItem(Receive, tcpClient);
                    // het lezen van toetsaanslagen gebeurt in de hoofdthread
                    Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
                    string réponse = null;        // antwoord van de server
                    // de verbinding met de klant wordt verwerkt
                    using (tcpClient) {
                        // er wordt een schrijfstroom naar de client aangemaakt
                        using (NetworkStream networkStream = tcpClient.GetStream()) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // niet-gebufferde uitvoerstroom
                                writer.AutoFlush = true;
                                // lus voor het invoeren van antwoorden via het toetsenbord
                                while (true) {
                                    réponse = Console.ReadLine();
                                    // klaar?
                                    if (réponse.Trim().ToLower() == "bye")
                                        break;
                                    // het verzoek wordt naar de client verzonden
                                    writer.WriteLine(réponse);
                                }
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                // de fout wordt gemeld
                Console.WriteLine("Main : l'erreur suivante s'est produite : {0}", ex.Message);
            } finally {
                // einde van het luisteren
                ecoute.Stop();
            }
        }

        // lees-thread server <-- client
        public static void Receive(object infos) {
            // lokale gegevens
            string demande = null;    // verzoek van de klant
            string idClient=null;    // identiteit van de client

            // verwerking van de verbinding met de client
            try {
                using (TcpClient tcpClient = infos as TcpClient) {
                    // identiteit van de client
                    idClient = tcpClient.Client.RemoteEndPoint.ToString();
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            // lus voor het continu lezen van tekstregels uit de invoerstroom
                            while ((demande = reader.ReadLine()) != null) {
                                // consoleweergave
                                Console.WriteLine("<-- {0}", demande);
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                // fout
                Console.WriteLine("Flux de lecture des lignes de texte du client {1} : l'erreur suivante s'est produite : {0}", ex.Message,idClient);
            } finally {
                // het einde van de leesthread wordt gemeld
                Console.WriteLine("Fin du thread de lecture des lignes de texte du client {0}. Si besoin est, arrêtez le thread de lecture console du serveur pour ce client, avec la commande bye.", idClient);
            }
        }
    }
}
  • regel 29: de luisterservice is aangemaakt maar nog niet gestart. Deze luistert naar alle netwerkinterfaces van de machine.
  • regel 31: de luisterdienst wordt gestart
  • regel 34: oneindige lus om op clients te wachten. De gebruiker zal de server stoppen met Ctrl-C.
  • regel 37: wachten op een klant – blokkerende bewerking. Wanneer de klant arriveert, vertegenwoordigt de instantie TcpClient, die door de methode AcceptTcpClient wordt geretourneerd, de serverzijde van een geopende verbinding met de klant.
  • regel 40: de leesstroom van de verzoeken van de klant wordt toevertrouwd aan een aparte thread.
  • regel 45: gebruik van de verbinding met de klant in een using-clausule om ervoor te zorgen dat deze onder alle omstandigheden wordt gesloten.
  • regel 47: gebruik van de netwerkstroom in een using-clausule
  • regel 48: aanmaken van een schrijfstroom op de netwerkstroom in een using-clausule
  • regel 50: de schrijfstroom wordt niet gebufferd
  • regels 52-59: lus voor het via het toetsenbord invoeren van de naar de client te verzenden commando’s
  • regel 69: einde van de luisterdienst. Deze instructie zal hier nooit worden uitgevoerd, aangezien de server wordt gestopt met Ctrl-C.
  • regel 78: de methode Receive die de door de client verzonden tekstregels continu op de console weergeeft. Hier zien we hetzelfde als bij de generieke client TCP.

11.6.5. Een web -client

In het vorige voorbeeld hebben we enkele van de HTTP-headers gezien die door een browser werden verzonden:

<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/msword, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-appl
ication, application/x-silverlight, */*
<-- Accept-Language: fr,en-US;q=0.7,fr-FR;q=0.3
<-- UA-CPU: x86
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.
4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.590; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

We gaan een webclient schrijven waaraan we een URL als parameter doorgeven en die de door de server verzonden tekst op het scherm weergeeft. We gaan ervan uit dat de server het protocol HTTP 1.1 ondersteunt. Van de voorgaande headers gebruiken we alleen de volgende:

1
2
3
4
<-- GET /exemple.html HTTP/1.1
<-- Host: localhost:88
<-- Connection: close
<--
  • de eerste header geeft het gewenste document aan
  • de tweede de server die wordt opgevraagd
  • de derde dat we willen dat de server de verbinding verbreekt nadat hij ons heeft geantwoord.

Als we in regel 1 hierboven GET vervangen door HEAD, zal de server ons alleen de headers HTTP sturen en niet het in regel 1 gespecificeerde document.

Onze webclient wordt als volgt aangeroepen: ClientWeb URL cmd, waarbij URL hetgewenste URL is en cmd een van de twee trefwoorden GET of HEAD om aan te geven of men alleen de headers (HEAD) of ook de inhoud van de pagina (GET). Laten we eens naar een eerste voorbeeld kijken:

...\Chap9\06\bin\Release>ClientWeb http://istia.univ-angers.fr:80 HEAD
HTTP/1.1 200 OK
Date: Sat, 03 May 2008 14:05:24 GMT
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
X-Powered-By: PHP/4.4.4-8+etch4
Set-Cookie: fe_typo_user=e668408ac1; path=/
Connection: close
Content-Type: text/html;charset=iso-8859-1

...\Chap9\06\bin\Release>
  • regel 1: we vragen alleen de headers HTTP (HEAD)
  • regels 2-9: het antwoord van de server

Als we GET gebruiken in plaats van HEAD in de aanroep naar de webclient, krijgen we hetzelfde resultaat als met HEAD, maar dan met de inhoud van het opgevraagde document erbij.

De code van de webclient is als volgt:


using System;
using System.IO;
using System.Net.Sockets;

namespace Chap9 {
    class ClientWeb {
        static void Main(string[] args) {
            // syntaxis
            const string syntaxe = "pg URI GET/HEAD";

            // aantal argumenten
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

            // de gevraagde URI wordt genoteerd
            string stringURI = args[0];
            string commande = args[1].ToUpper();

            // controle van de geldigheid van de URI
            if(! stringURI.StartsWith("http://")){
                Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
                return;
            }
            Uri uri = null;
            try {
                uri = new Uri(stringURI);
            } catch (Exception ex) {
                // URI is onjuist
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }
            // controle van de bestelling
            if (commande != "GET" && commande != "HEAD") {
                // Onjuiste bestelling
                Console.WriteLine("Le second paramètre doit être GET ou HEAD");
                return;
            }

            try {
                // verbinding maken met de service
                using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // uitvoer niet gebufferd
                                writer.AutoFlush = true;
                                // URL wordt opgevraagd - HTTP-headers worden verzonden
                                writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
                                writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
                                writer.WriteLine("Connection: close");
                                writer.WriteLine();
                                // het antwoord wordt gelezen
                                string réponse = null;
                                while ((réponse = reader.ReadLine()) != null) {
                                    // het antwoord wordt weergegeven op de console
                                    Console.WriteLine(réponse);
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // de uitzondering wordt weergegeven
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            }
        }
    }
}

Het enige nieuwe in dit programma is het gebruik van de klasse Uri. Het programma ontvangt een URL (Uniform Resource Locator) of URI (Uniform Resource Identifier) in de vorm http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... Met de klasse Uri kunnen we de tekenreeks van de URL opsplitsen in de verschillende onderdelen.

  • regels 26-33: er wordt een Uri-object aangemaakt op basis van de als parameter ontvangen tekenreeks stringURI. Als de als parameter ontvangen tekenreeks URI geen geldige URI is (ontbrekend protocol, ontbrekende server, ...), wordt er een uitzondering gegenereerd. Zo kunnen we de geldigheid van de ontvangen parameter controleren. Zodra het object Uri is aangemaakt, hebben we toegang tot de verschillende elementen van deze Uri. Als het object uri uit de vorige code dus is aangemaakt op basis van de tekenreeks http://serveur:port/document?param1=val1&param2=val2;..., krijgen we:
    • uri.Host=serveur,
    • uri.Port=port,
    • uri.Path = document,
    • uri.Query=param1=val1&param2=val2;...,
    • uri.pathAndQuery= cheminPageHTML?param1=val1&param2=val2;...,
    • uri.Scheme=http.

11.6.6. Een webclient die omleidingen verwerkt

De vorige webclient ondersteunt geen eventuele omleiding van de URL die hij heeft aangevraagd. Hier volgt een voorbeeld:

...\Chap9\06\bin\Release>ClientWeb http://www.ibm.com GET
HTTP/1.1 302 Found
Date: Sat, 03 May 2008 14:50:52 GMT
Server: IBM_HTTP_Server
Location: http://www.ibm.com/us/
Content-Length: 206
Kp-eeAlive: timeout=10, max=73
Connection: Keep-Alive
Content-Type: text/html

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>302 Found</title>
</head><body>
<h1>Found</h1>
<p>The document has moved <a href="http://www.ibm.com/us/">here</a>.</p>
</body></html>
  • regel 2: de statuscode 302 Found duidt op een omleiding. Het adres waarnaar de browser moet worden omgeleid, staat in de hoofdtekst van het document, op regel 16.

Een tweede voorbeeld:

...\Chap9\06\bin\Release>ClientWeb http://www.bull.com GET
HTTP/1.1 301 Moved Permanently
Date: Sat, 03 May 2008 14:52:31 GMT
Server: Apache/1.3.33 (Unix) WS_filter/2.1.15 PHP/4.3.4
X-Powered-By: PHP/4.3.4
Location: http://www.bull.com/index.php
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html

0
  • regel 2: de code 301 Moved Permanently duidt op een omleiding. Het adres waarnaar de browser moet worden omgeleid, staat vermeld op regel 6, in de header HTTP Location.

Een derde voorbeeld:

1
2
3
4
5
6
7
...\Chap9\06\bin\Release>ClientWeb http://www.gouv.fr GET
HTTP/1.1 302 Moved Temporarily
Server: AkamaiGHost
Content-Length: 0
Location: http://www.premier-ministre.gouv.fr/nl/
Date: Sat, 03 May 2008 14:56:53 GMT
Connection: close
  • regel 2: de code 302 Moved Temporarily duidt op een omleiding. Het adres waarnaar de browser moet worden omgeleid, staat vermeld op regel 5, in de header HTTP Location.

Een vierde voorbeeld met een lokale server op de machine:

...\istia\Chap9\06\bin\Release>ClientWeb.exe http://localhost HEAD
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.1
Date: Sun, 04 May 2008 10:16:56 GMT
Connection: close
Location: localstart.asp
Content-Length: 121
Content-Type: text/html
Set-Cookie: ASPSESSIONIDQQASDQAB=FDJLADLCOLDHGKGNIPMLHIIA; path=/
Cache-control: private
  • regel 2: de code 302 Object moved duidt op een omleiding. Het adres waarnaar de browser moet worden omgeleid, staat vermeld op regel 5, in de header HTTP Location. Merk op dat, in tegenstelling tot de voorgaande voorbeelden, het omleidingsadres relatief is. Het volledige adres is in feite http://localhost/localstart.asp.

We stellen voor om omleidingen te verwerken wanneer de eerste regel van de headers HTTP het trefwoord moved bevat (hoofdlettergevoelig) en het omleidingsadres in de header HTTP Location staat.

Als we de laatste drie voorbeelden nog eens bekijken, krijgen we de volgende resultaten:

URL: http://www.bull.com

...\Chap9\06B\bin\Release>ClientWebAvecRedirection http://www.bull.com HEAD
HTTP/1.1 301 Moved Permanently
Date: Sun, 04 May 2008 10:22:48 GMT
Server: Apache/1.3.33 (Unix) WS_filter/2.1.15 PHP/4.3.4
X-Powered-By: PHP/4.3.4
Location: http://www.bull.com/index.php
Connection: close
Content-Type: text/html


<--Redirection vers l'URL http://www.bull.com/index.php-->

HTTP/1.1 200 OK
Date: Sun, 04 May 2008 10:22:49 GMT
Server: Apache/1.3.33 (Unix) WS_filter/2.1.15 PHP/4.3.4
X-Powered-By: PHP/4.3.4
Connection: close
Content-Type: text/html
  • regel 11: de omleiding vindt plaats naar het adres op regel 6

URL: http://www.gouv.fr

...\Chap9\06B\bin\Release>ClientWebAvecRedirect
ion http://www.gouv.fr HEAD
HTTP/1.1 302 Moved Temporarily
Server: AkamaiGHost
Content-Length: 0
Location: http://www.premier-ministre.gouv.fr/nl/
Date: Sun, 04 May 2008 10:30:38 GMT
Connection: close


<--Redirection vers l'URL http://www.premier-ministre.gouv.fr/fr/-->

HTTP/1.1 200 OK
Server: Apache
X-Powered-By: PHP/4.4.1
Last-Modified: Sun, 04 May 2008 10:29:48 GMT
Content-Type: text/html
Expires: Sun, 04 May 2008 10:40:38 GMT
Date: Sun, 04 May 2008 10:30:38 GMT
Connection: close
  • regel 11: de omleiding vindt plaats naar het adres van regel 6

URL: http://localhost

...\Chap9\06B\bin\Release>ClientWebAvecRedirection.exe http://localhost HEAD
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.1
Date: Sun, 04 May 2008 10:37:11 GMT
Connection: close
Location: localstart.asp
Content-Length: 121
Content-Type: text/html
Set-Cookie: ASPSESSIONIDQQASDQAB=GDJLADLCJCMPCHFFEJEFPKMK; path=/
Cache-control: private


<--Redirection vers l'URL http://localhost/localstart.asp-->

HTTP/1.1 401 Access Denied
Server: Microsoft-IIS/5.1
Date: Sun, 04 May 2008 10:37:11 GMT
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
WWW-Authenticate: Basic realm="localhost"
Connection: close
Content-Length: 4766
Content-Type: text/html
  • regel 13: de omleiding vindt plaats naar het adres van regel 6
  • regel 15: de toegang tot de pagina http://localhost/localstart.asp werd ons geweigerd.

Het programma dat de omleiding regelt, is het volgende:


using System;
using System.IO;
using System.Net.Sockets;
using System.Text.RegularExpressions;

namespace Chap9 {
    class ClientWebAvecRedirection {
        static void Main(string[] args) {
            // syntaxis
            const string syntaxe = "pg URI GET/HEAD";

            // aantal argumenten
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

            // let op de gevraagde URI
            string stringURI = args[0];
            string commande = args[1].ToUpper();

            // controle van de geldigheid van de URI
            if (!stringURI.StartsWith("http://")) {
                Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
                return;
            }
            Uri uri = null;
            try {
                uri = new Uri(stringURI);
            } catch (Exception ex) {
                // URI is onjuist
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }
            // controle van de bestelling
            if (commande != "GET" && commande != "HEAD") {
                // onjuiste bestelling
                Console.WriteLine("Le second paramètre doit être GET ou HEAD");
                return;
            }

            const int nbRedirsMax = 1;        // er wordt niet meer dan één omleiding geaccepteerd
            int nbRedirs = 0;                            // aantal lopende omleidingen

            // reguliere expressie om een omleiding URL te vinden
            Regex location = new Regex(@"^Location: (.+?)$");
            try {
                // er kunnen meerdere URL worden opgevraagd als er omleidingen zijn
                while (nbRedirs <= nbRedirsMax) {
                    // beheer van omleidingen
                    bool redir = false;
                    bool locationFound = false;
                    string locationString = null;
                    // verbinding maken met de service
                    using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
                        using (StreamReader reader = new StreamReader(tcpClient.GetStream())) {
                            using (StreamWriter writer = new StreamWriter(tcpClient.GetStream())) {
                                // uitvoerstroom zonder buffering
                                writer.AutoFlush = true;
                                // de URL wordt opgevraagd – verzending van de headers HTTP
                                writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
                                writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
                                writer.WriteLine("Connection: close");
                                writer.WriteLine();
                                // de eerste regel van het antwoord wordt gelezen
                                string premièreLigne = reader.ReadLine();
                                // schermweergave
                                Console.WriteLine(premièreLigne);

                                // omleiding?
                                if (Regex.IsMatch(premièreLigne.ToLower(), @"\s+moved\s*")) {
                                    // er is een omleiding
                                    redir = true;
                                    nbRedirs++;
                                }

                                // de volgende HTTP-headers totdat de lege regel wordt gevonden die het einde van de headers aangeeft
                                string réponse = null;
                                while ((réponse = reader.ReadLine()) != "") {
                                    // het antwoord wordt weergegeven
                                    Console.WriteLine(réponse);
                                    // als er een omleiding is, wordt gezocht naar de Location-header
                                    if (redir && !locationFound) {
                                        // de huidige regel wordt vergeleken met de relationele uitdrukking location
                                        Match résultat = location.Match(réponse);
                                        if (résultat.Success) {
                                            // als er iets is gevonden, noteer dan de omleidingscode URL
                                            locationString = résultat.Groups[1].Value;
                                            // er wordt genoteerd dat er iets is gevonden
                                            locationFound = true;
                                        }
                                    }
                                }

                                // de kopteksten HTTP zijn opgebruikt – er wordt een lege regel geschreven
                                Console.WriteLine(réponse);
                                // daarna gaan we verder met de hoofdtekst van het document
                                while ((réponse = reader.ReadLine()) != null) {
                                    Console.WriteLine(réponse);
                                }
                            }
                        }
                    }
                    // zijn we klaar?
                    if (!locationFound || nbRedirs > nbRedirsMax)
                        break;
                    // er moet een omleiding worden uitgevoerd – we stellen de nieuwe Uri samen
                    try {
                        if (locationString.StartsWith("http")) {
                            // volledig http-adres
                            uri = new Uri(locationString);
                        } else {
                            // relatief http-adres ten opzichte van de huidige URI
                            uri = new Uri(uri, locationString);
                        }
                        // console-log
                        Console.WriteLine("\n<--Redirection vers l'URL {0}-->\n", uri);
                    } catch (Exception ex) {
                        // probleem met de URI
                        Console.WriteLine("\n<--L'adresse de redirection {0} n'a pas été comprise : {1} -->\n", locationString, ex.Message);
                    }
                }
            } catch (Exception e) {
                // de uitzondering wordt weergegeven
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            }
        }
    }
}

Ten opzichte van de vorige versie zijn de wijzigingen als volgt:

  • regel 46: de reguliere expressie om het omleidingsadres op te halen uit de header HTTP Location: adres.
  • regel 49: de code die voorheen voor één enkele Uri werd uitgevoerd, kan nu achtereenvolgens voor meerdere Uri's worden uitgevoerd.
  • regel 66: de eerste regel van de door de server verzonden HTTP-headers wordt gelezen. Deze regel bevat het trefwoord moved als het opgevraagde document is verplaatst.
  • regels 71-75: er wordt gecontroleerd of de eerste regel het trefwoord moved bevat. Zo ja, dan wordt dit genoteerd.
  • regels 79-93: de overige headers HTTP worden gelezen totdat de lege regel wordt aangetroffen die het einde ervan aangeeft. Als de eerste regel een omleiding aankondigde, wordt er vervolgens gekeken naar de header HTTP Location: adres om het omleidingsadres op te slaan in locationString.
  • regels 98-100: de rest van het antwoord van de server HTTP wordt op de console weergegeven.
  • regels 105-106: de opgevraagde Uri is volledig verwerkt en weergegeven. Als er geen omleiding nodig is of als het toegestane aantal omleidingen is overschreden, wordt het programma afgesloten.
  • regels 108-122: als er een omleiding is, wordt de nieuwe Uri berekend die moet worden opgevraagd. Er is wat rekenwerk nodig, afhankelijk van of het gevonden omleidingsadres absoluut (regel 111) of relatief (regel 114) was.

11.7. De klassen .NET die gespecialiseerd zijn in een bepaald internetprotocol

In de voorgaande voorbeelden van de webclient werd het protocol HTTP afgehandeld met een client TCP. We moesten dus zelf het gebruikte specifieke communicatieprotocol afhandelen. We hadden op dezelfde manier een client SMTP of POP kunnen bouwen. Het .NET-framework biedt gespecialiseerde klassen voor de protocollen HTTP en SMTP. Deze klassen kennen het communicatieprotocol tussen de client en de server en zorgen ervoor dat de ontwikkelaar zich daar niet mee hoeft bezig te houden. We zullen ze nu voorstellen.

11.7.1. De classeWebClient

Er bestaat een klasse WebClient die met een webserver kan communiceren. Laten we het voorbeeld van de webclient uit paragraaf 11.6.5 bekijken, dat hier wordt behandeld met de klasse WebClient.


using System;
using System.IO;
using System.Net;
namespace Chap9 {
    public class Program {
        public static void Main(string[] args) {
            // syntaxis: [prog] URI
            const string syntaxe = "pg URI";

            // aantal argumenten
            if (args.Length != 1) {
                Console.WriteLine(syntaxe);
                return;
            }

            // de gevraagde URI wordt genoteerd
            string stringURI = args[0];

            // controle van de geldigheid van de URI
            if (!stringURI.StartsWith("http://")) {
                Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
                return;
            }
            Uri uri = null;
            try {
                uri = new Uri(stringURI);
            } catch (Exception ex) {
                // URI is onjuist
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }

            try {
                // webklant aangemaakt
                using (WebClient client = new WebClient()) {
                    // toevoeging van een header HTTP 
                    client.Headers.Add("user-agent", "st");
                    using (Stream stream = client.OpenRead(uri)) {
                        using (StreamReader reader = new StreamReader(stream)) {
                            // weergave van het antwoord van de webserver
                            Console.WriteLine(reader.ReadToEnd());
                            // weergave van de headers van het antwoord van de server
                            Console.WriteLine("---------------------");
                            foreach (string clé in client.ResponseHeaders.Keys) {
                                Console.WriteLine("{0}: {1}", clé, client.ResponseHeaders[clé]);
                            }
                            Console.WriteLine("---------------------");
                        }
                    }
                }
            } catch (WebException e1) {
                Console.WriteLine("L'exception suivante s'est produite : {0}", e1);
            } catch (Exception e2) {
                Console.WriteLine("L'exception suivante s'est produite : {0}", e2);
            }
        }
    }
}
  • regel 35: de webclient is aangemaakt, maar nog niet geconfigureerd
  • regel 37: er wordt een header HTTP toegevoegd aan het verzoek HTTP dat zal worden verzonden. We zullen zien dat er standaard nog andere headers worden verzonden.
  • regel 38: de webclient vraagt de door de gebruiker opgegeven Uri op en leest het verzonden document. [WebClient].OpenRead(Uri) opent de verbinding met Uri en leest het antwoord. Dat is het nut van deze klasse. Deze zorgt voor de communicatie met de webserver. Het resultaat van de methode OpenRead is van het type Stream en vertegenwoordigt het opgevraagde document. De headers HTTP die door de server worden verzonden en die in het antwoord voorafgaan aan het document, maken hier geen deel van uit.
  • regel 39: er wordt een StreamReader gebruikt en op regel 41 de bijbehorende methode ReadToEnd om het volledige antwoord te lezen.
  • regels 44-46: de HTTP-headers van het antwoord van de server worden weergegeven. [WebClient].ResponseHeaders vertegenwoordigt een gewaardeerde verzameling waarvan de sleutels de namen van de headers HTTP zijn en de waarden de tekenreeksen die aan deze headers zijn gekoppeld.
  • regel 51: de uitzonderingen die tijdens een client-serveruitwisseling worden gegenereerd, zijn van het type WebException.

Laten we enkele voorbeelden bekijken.

We starten de generieke server TCP die in paragraaf 6.4.6 is opgebouwd:

...\Chap9\05\bin\Release>ServeurTcpGenerique.exe 88
Serveur générique lancé sur le port 0.0.0.0:88

We starten de vorige webclient als volgt:

...\Chap9\09\bin\Release>09 http://localhost:88

De opgevraagde URI is die van de generieke server. Deze geeft vervolgens de headers HTTP weer die de webclient hem heeft gestuurd:

1
2
3
4
5
6
7
Client 127.0.0.1:1415
Tapez vos commandes (bye pour arrêter) :
<-- GET / HTTP/1.1
<-- User-Agent: st
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

We zien dus:

  • dat de webclient standaard 3 HTTP-headers verstuurt (regels 3, 5, 6)
  • regel 4: de header die we zelf hebben gegenereerd (regel 37 van de code)
  • dat de webclient standaard de methode GET gebruikt (regel 3). Er zijn nog andere methoden, waaronder POST en HEAD.

Laten we nu een niet-bestaande bron opvragen:

1
2
3
4
5
...\Chap9\09\bin\Release>09 http://istia.univ-angers.fr/onbekend
L'exception suivante s'est produite : System.Net.WebException: The remote server returned an error: (404) Not Found.
   at System.Net.WebClient.OpenRead(Uri address)
   at System.Net.WebClient.OpenRead(String address)
   at Chap9.WebClient1.Main(String[] args) in C:\data\2007-2008\c# 2008\poly\istia\Chap9\09\Program.cs:regel 16
  • regel 2: er is een uitzondering van het type WebException opgetreden omdat de server met de statuscode 404 Not Found heeft gereageerd om aan te geven dat de opgevraagde bron niet bestond.

Laten we tot slot een bestaande bron opvragen:

...\istia\Chap9\09\bin\Release>09 http://istia.univ-angers.fr >istia.univ-angers.txt

Het bestand istia.univ-angers.txt dat door het commando wordt gegenereerd, ziet er als volgt uit:

<!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
...
</html>
---------------------
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html;charset=iso-8859-1
Date: Sun, 04 May 2008 14:30:53 GMT
Set-Cookie: fe_typo_user=22eaaf283a; path=/
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
X-Powered-By: PHP/4.4.4-8+etch4
---------------------
  • regel 1: het opgevraagde document HTML.
  • regels 3-10: de headers van het antwoord HTTP in een volgorde die niet noodzakelijkerwijs overeenkomt met de volgorde waarin ze zijn verzonden.

De klasse WebClient beschikt over methoden om een document te ontvangen (methoden DownLoad) of te verzenden (methoden UpLoad):

DownLoadData
om een bron als byte-array te downloaden (bijvoorbeeld een afbeelding)
DownLoadFile
om een bron te downloaden en op te slaan in een lokaal bestand
DownLoadString
om een bron te downloaden en deze op te halen als tekenreeks (bijvoorbeeld een HTML-bestand)
OpenWrite
het tegenstuk van OpenRead, maar dan om gegevens naar de server te verzenden
UpLoadData
de tegenhanger van DownLoadData, maar dan naar de server
UpLoadFile
de tegenhanger van DownLoadFile, maar dan naar de server
UpLoadString
de tegenhanger van DownLoadString, maar dan naar de server
UpLoadValues
om de gegevens van een opdracht POST naar de server te verzenden en de resultaten ervan terug te ontvangen in de vorm van een byte-array. De opdracht POST vraagt om een document en stuurt tegelijkertijd informatie naar de server die nodig is om te bepalen welk document daadwerkelijk moet worden verzonden. Deze informatie wordt als document naar de server verzonden, vandaar de naam UpLoad van de methode. Ze wordt achter de lege regel van de headers HTTP verzonden in de vorm param1=valeur1&param2=valeur2&...:
POST /document HTTP/1.1
...
[ligne vide]
param1=valeur1&param2=valeur2&...
Hetzelfde document kan worden opgevraagd met de methode GET:
GET /document?param1=valeur1&param2=valeur2&...
...
[ligne vide]
Het verschil tussen de twee methoden is dat de browser die de opgevraagde Uri weergeeft, in het geval van POST de tekst /document zal weergeven en in het geval van GET de tekst /document?param1=valeur1&param2=valeur2&....

11.7.2. De klassen WebRequest / WebResponse

Soms is de klasse WebClient niet flexibel genoeg om te doen wat we willen. Laten we het voorbeeld van de webclient met omleiding uit paragraaf 11.6.6 nog eens bekijken. We moeten de header HTTP verzenden:

HEAD /document HTTP/1.1

We hebben gezien dat de HTTP-headers die standaard door de webclient worden verzonden, de volgende waren:

1
2
3
<-- GET / HTTP/1.1
<-- Host: machine:port
<-- Connection: Keep-Alive

We hebben ook gezien dat het mogelijk was om HTTP-headers toe te voegen aan de voorgaande met de collectie [WebClient].Headers. Alleen regel 1 is geen header die tot de verzameling Headers behoort, omdat deze niet de vorm sleutel:waarde heeft. Ik heb niet kunnen achterhalen hoe ik de GET in regel 1 kan wijzigen in HEAD vanuit de klasse WebClient (misschien heb ik niet goed gezocht?). Wanneer de klasse WebClient zijn limieten heeft bereikt, kan men overschakelen naar de klassen WebRequest / WebResponse:

  • WebRequest: vertegenwoordigt het volledige verzoek van de webclient.
  • WebResponse: vertegenwoordigt het volledige antwoord van de webserver

We hebben gezegd dat de klasse WebClient de schema’s http:, https:, ftp: en file: beheert. De verzoeken en antwoorden van deze verschillende protocollen hebben niet dezelfde vorm. Daarom is het noodzakelijk om met het exacte type van deze elementen te werken in plaats van met hun generieke type WebRequest en WebResponse. We zullen dus de volgende klassen gebruiken:

  • HttpWebRequest, HttpWebResponse voor een client HTTP
  • FtpWebRequest, FtpWebResponse voor een klant FTP

We behandelen nu met de klassen HttpWebRequest en HttpWebresponse het voorbeeld van de webclient met omleiding dat in paragraaf 11.6.6 is besproken. De code is als volgt:


using System;
using System.IO;
using System.Net.Sockets;
using System.Net;

namespace Chap9 {
    class WebRequestResponse {
        static void Main(string[] args) {
            // syntaxis
            const string syntaxe = "pg URI GET/HEAD";

            // aantal argumenten
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

            // de gevraagde URI wordt genoteerd
            string stringURI = args[0];
            string commande = args[1].ToUpper();

            // controle van de geldigheid van de URI
            Uri uri = null;
            try {
                uri = new Uri(stringURI);
            } catch (Exception ex) {
                // URI is onjuist
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }
            // controle van de bestelling
            if (commande != "GET" && commande != "HEAD") {
                // onjuiste bestelling
                Console.WriteLine("Le second paramètre doit être GET ou HEAD");
                return;
            }

            try {
                // het verzoek wordt geconfigureerd
                HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
                httpWebRequest.Method = commande;
                httpWebRequest.Proxy = null;
                // wordt uitgevoerd
                HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
                // resultaat
                Console.WriteLine("---------------------");
                Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri,(int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
                // kopteksten HTTP
                Console.WriteLine("---------------------");
                foreach (string clé in httpWebResponse.Headers.Keys) {
                    Console.WriteLine("{0}: {1}", clé, httpWebResponse.Headers[clé]);
                }
                Console.WriteLine("---------------------");
                // document
                using (Stream stream = httpWebResponse.GetResponseStream()) {
                    using (StreamReader reader = new StreamReader(stream)) {
                        // het antwoord wordt op de console weergegeven
                        Console.WriteLine(reader.ReadToEnd());
                    }
                }
            } catch (WebException e1) {
                // het antwoord wordt opgehaald
                HttpWebResponse httpWebResponse = e1.Response as HttpWebResponse;
                Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri, (int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
            } catch (Exception e2) {
                // de uitzondering wordt weergegeven
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e2.Message);
            }
        }
    }
}
  • regel 40: er wordt een object van het type WebRequest aangemaakt met de statische methode WebRequest.Create(Uri uri), waarbij uri de URI is van het document dat moet worden gedownload. Omdat we weten dat het protocol van de Uri HTTP is, wordt het type van het resultaat gewijzigd in HttpWebRequest om toegang te krijgen tot de specifieke elementen van het HTTP-protocol.
  • regel 41: we stellen de methode GET / POST / HEAD in op de eerste regel van de headers HTTP. Hier zal dat GET of HEAD zijn.
  • regel 42: in een privébedrijfsnetwerk is het gebruikelijk dat de computers van het bedrijf om veiligheidsredenen van het internet worden afgeschermd. Hiervoor gebruikt het privébedrijfsnetwerk internetadressen die niet door internetrouters worden doorgestuurd. Het privénetwerk is met het internet verbonden via speciale computers, zogenaamde proxy’s, die zowel met het privénetwerk van het bedrijf als met het internet zijn verbonden. Dit is een voorbeeld van computers met meerdere adressen: IP. Een computer in het privénetwerk kan niet zelf een verbinding tot stand brengen met een server op het internet, bijvoorbeeld een webserver. Hij moet een proxyserver vragen dit voor hem te doen. Een computer met het adres proxy kan servers met het adres proxy voor verschillende protocollen hosten. De term HTTP-proxy verwijst naar de dienst die de HTTP-verzoeken namens de computers in het privénetwerk afhandelt. Als een dergelijke HTTP-proxyserver bestaat, moet deze worden opgegeven in het veld [WebRequest].proxy. Men schrijft bijvoorbeeld:
[WebRequest].proxy=new WebProxy("pproxy.istia.uang:3128");

als de proxy HTTP op poort 3128 van de machine pproxy.istia.uang draait. In het veld [WebRequest].proxy wordt ‘null’ ingevuld als de machine rechtstreeks toegang heeft tot het internet en niet via een proxy hoeft te gaan.

  • regel 44: de methode GetResponse() vraagt het document op dat wordt geïdentificeerd door zijn Uri en retourneert een object WebRequestResponse dat hier wordt omgezet in een object HttpWebResponse. Dit object vertegenwoordigt het antwoord van de server op het verzoek om het document.
  • regel 47:
    • [HttpWebResponse].ResponseUri: is de URI van de server die het document heeft verzonden. In geval van een omleiding kan deze afwijken van de URI van de server die aanvankelijk werd opgevraagd. Merk op dat de code geen omleiding ondersteunt. Dit wordt automatisch afgehandeld door de methode GetResponse. Ook dit is weer het voordeel van klassen op hoog niveau ten opzichte van de basisklassen van het TCP-protocol.
    • [HttpWebResponse].StatusCode, [HttpWebResponse].StatusDescription vertegenwoordigen de eerste regel van het antwoord, bijvoorbeeld: HTTP/1.1 200 OK. StatusCode is 200 en StatusDescription is OK.
  • regel 50: [HttpWebResponse].Headers is de verzameling van de headers HTTP van het antwoord.
  • regel 55: [HttpWebResponse].GetResponseStream: is de stream waarmee het document in het antwoord kan worden opgehaald.
  • regel 61: er kan een uitzondering van het type WebException optreden
  • regel 63: [WebException].Response is het antwoord dat de uitzondering heeft veroorzaakt.

Hier volgt een uitvoervoorbeeld:

...\Chap9\09B\bin\Release>09B http://www.gouv.fr HEAD
---------------------
Le serveur http://www.premier-ministre.gouv.fr/fr/ heeft geantwoord: 200 OK
---------------------
Connection: keep-alive
Content-Type: text/html; charset=iso-8859-1
Date: Mon, 05 May 2008 13:02:29 GMT
Expires: Mon, 05 May 2008 13:07:20 GMT
Last-Modified: Mon, 05 May 2008 12:56:59 GMT
Server: Apache
X-Powered-By: PHP/4.4.1
---------------------
  • regels 1 en 3: de server die heeft geantwoord, is niet dezelfde als de server die werd opgevraagd. Er heeft dus een omleiding plaatsgevonden.
  • regels 5-11: de headers HTTP die door de server zijn verzonden

11.7.3. Toepassing: een proxyclient voor een vertaalwebserver

We laten nu zien hoe we met behulp van de voorgaande klassen gebruik kunnen maken van de bronnen op het web.

11.7.3.1. L'application

Er bestaan vertaalsites op het web. De site die hier wordt gebruikt, is http://trans.voila.fr/traduction_voila.php:

De te vertalen tekst wordt ingevoerd in [1], de vertaalrichting wordt gekozen in [2]. De vertaling wordt aangevraagd via [3] en verkregen in [4].

We gaan een Windows-clienttoepassing schrijven voor de bovenstaande toepassing. Deze zal niets meer doen dan de toepassing op de website [trans.voila.fr]. De interface ziet er als volgt uit:

11.7.3.2. De architectuur van de applicatie

De applicatie zal de volgende tweelaagse architectuur hebben:

11.7.3.3. Het Visual Studio-project

Het Visual Studio-project ziet er als volgt uit:

  • In [1] bestaat de oplossing uit twee projecten,
  • [2]: één voor de laag [dao] en de entiteiten die daarin worden gebruikt,
  • [3]: het andere voor de Windows-interface

11.7.3.4. Het project [dao]

Het project [dao] bestaat uit de volgende onderdelen:

  • IServiceTraduction.cs: de interface die wordt aangeboden aan de laag [ui]
  • ServiceTraduction: de implementatie van deze interface
  • WebTraductionsException: een toepassingsspecifieke uitzondering

De interface IServiceTraduction is als volgt:


using System.Collections.Generic;

namespace dao {
    public interface IServiceTraduction {
        // gebruikte talen
        IDictionary<string, string> LanguesTraduites { get; }
        // vertaling
        string Traduire(string texte, string deQuoiVersQuoi);
    }
}
  • regel 6: de eigenschap LanguesTraduites geeft het woordenboek weer van de talen die door de vertaalserver worden ondersteund. Dit woordenboek bevat vermeldingen in de vorm ["fe","Français-Anglais"], waarbij de waarde een vertaalrichting aangeeft, in dit geval van het Frans naar het Engels, en de sleutel "fe" een code is die wordt gebruikt door de vertaalserver trans.voila.fr.
  • regel 8: de methode Traduire is de vertaalmethode:
    • texte is de te vertalen tekst
    • deQuoiVersQuoi is een van de sleutels uit het woordenboek van de vertaalde talen
    • de methode voert de vertaling van de tekst uit

ServiceTraduction is een implementatieklasse van de interface IServiceTraduction. We gaan hier in het volgende gedeelte dieper op in.

WebTraductionsException is de volgende uitzonderingsklasse:


using System;

namespace entites {
    public class WebTraductionsException : Exception {

        // foutcode
        public int Code { get; set; }

        // fabrikanten
        public WebTraductionsException() {
        }
        public WebTraductionsException(string message)
            : base(message) {
        }
        public WebTraductionsException(string message, Exception e)
            : base(message, e) {
        }
    }
}
  • regel 7: een foutcode

11.7.3.5. De webclient [ServiceTraduction]

Laten we nog eens terugkomen op de architectuur van onze applicatie:

De klasse [ServiceTraduction] die we moeten schrijven, is een client van de vertaalwebservice [trans.voila.fr]. Om deze te schrijven, moeten we begrijpen

  • wat de vertaalserver van zijn client verwacht
  • wat hij terugstuurt naar zijn client

Laten we aan de hand van een voorbeeld eens kijken naar de dialoog tussen client en server die plaatsvindt tijdens een vertaling. Laten we het voorbeeld uit de inleiding van de applicatie nog eens bekijken:

De te vertalen tekst wordt ingevoerd in [1], de vertaalrichting wordt gekozen in [2]. De vertaling wordt aangevraagd via [3] en verkregen in [4].

Om de vertaling [4] te verkrijgen, heeft de browser het volgende verzoek GET verzonden (weergegeven in het adresveld):

http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection=fe&stext=de+hond+is+ziek

Dit is vrij eenvoudig te begrijpen:

  • http://trans.voila.fr/traduction_voila.php is de URL van de vertaaldienst
  • isText=1 lijkt te betekenen dat het om tekst gaat
  • translationDirection geeft de vertaalrichting aan, in dit geval Français-Anglais
  • stext is de te vertalen tekst in een zogenaamde URL-gecodeerde vorm. Bepaalde tekens mogen namelijk niet in een URL voorkomen. Dit geldt bijvoorbeeld voor de spatie, die hier is gecodeerd met een +. Het .NET-framework biedt de statische methode System.Web.HttpUtility.UrlEncode om deze codering uit te voeren.

Hieruit volgt dat onze klasse [ServiceTraduction] de tekenreeks

"http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}"

waarbij de plaatshouders {0} en {1} respectievelijk worden vervangen door de vertaalrichting en de te vertalen tekst.

Hoe weten we welke vertaalrichtingen door de server worden geaccepteerd? In de bovenstaande schermafbeelding staan de vertaalde talen in de vervolgkeuzelijst. Als we in de browser (Weergave / Brontekst) de HTML-code van de pagina bekijken, vinden we het volgende voor de vervolgkeuzelijst:

<select name="translationDirection" class="champs">
    <option selected value='fe'>Fran&ccedil;ais vers Anglais
    <option  value='ef'>Anglais vers Fran&ccedil;ais
    <option  value='fg'>Fran&ccedil;ais vers Allemand
    <option  value='gf'>Allemand vers Fran&ccedil;ais
    <option  value='fs'>Fran&ccedil;ais vers Espagnol
    <option  value='sf'>Espagnol vers Fran&ccedil;ais
    <option  value='fr'>Fran&ccedil;ais vers Russe
    <option  value='rf'>Russe vers Fran&ccedil;ais
    <option  value='es'>Anglais vers Espagnol
    <option  value='se'>Espagnol vers Anglais
    <option  value='eg'>Anglais vers Allemand
    <option  value='ge'>Allemand vers Anglais
    <option  value='ep'>Anglais vers Portugais
    <option  value='pe'>Portugais vers Anglais
    <option  value='ie'>Italien vers Anglais
    <option  value='gs'>Allemand vers Espagnol
    <option  value='sg'>Espagnol vers Allemand
</select>

Dit is geen erg nette HTML-code, aangezien elke <option>-tag normaal gesproken zou moeten worden afgesloten met een </option>-tag. Dat gezegd hebbende, geven de value-attributen ons de lijst met vertaalcodes die naar de server moeten worden verzonden. In het woordenboek LanguesTraduites van de interface IServiceTraduction zijn de sleutels de bovenstaande value-attributen en de waarden de teksten die door de vervolgkeuzelijst worden weergegeven.

Laten we nu eens kijken (Weergave / Broncode) waar in de HTML-pagina de door de vertaalserver teruggestuurde vertaling zich bevindt:

...                                                                
<strong>Texte traduit : </strong><div class="txtTrad">this dog is sick</div> 
...

De vertaling bevindt zich midden op de teruggestuurde HTML-pagina. Hoe kunnen we deze vinden? We kunnen een reguliere expressie gebruiken met de reeks <div class="txtTrad">...</div>, omdat de tag <div class="txtTrad"> alleen op deze plek in de HTML-pagina voorkomt. De reguliere expressie in C# waarmee de vertaalde tekst kan worden opgehaald, is als volgt:

@"<div class=""txtTrad"">(.*?)</div>"

We hebben nu de elementen om de implementatieklasse ServiceTraduction van de interface IServiceTraduction te schrijven:


using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Web;
using entites;

namespace dao {
    public class ServiceTraduction : IServiceTraduction {
        // automatische configuratie-eigenschappen van de dienst
        public IDictionary<string, string> LanguesTraduites { get; set; }
        public string UrlServeurTraduction { get; set; }
        public string ProxyHttp { get; set; }
        public String RegexTraduction { get; set; }

        // vertaling
        public string Traduire(string texte, string deQuoiVersQuoi) {
            // is de gevraagde vertaling mogelijk?
            if (!LanguesTraduites.ContainsKey(deQuoiVersQuoi)) {
                throw new WebTraductionsException(String.Format("Le sens de traduction [{0}] n'est pas reconnu")) { Code = 10 };
            }
            // te vertalen tekst
            string texteATraduire = HttpUtility.UrlEncode(texte);
            // op te vragen URI
            string uri = string.Format(UrlServeurTraduction, deQuoiVersQuoi, texteATraduire);
            // reguliere expressie om de vertaling in het antwoord te vinden
            Regex patternTraduction = new Regex(RegexTraduction);
            // uitzondering
            WebTraductionsException exception = null;
            // vertaling
            string traduction = null;
            try {
                // het verzoek wordt geconfigureerd
                HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
                httpWebRequest.Method = "GET";
                httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
                // het wordt uitgevoerd
                HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
                // document
                using (Stream stream = httpWebResponse.GetResponseStream()) {
                    using (StreamReader reader = new StreamReader(stream)) {
                        bool traductionTrouvée = false;
                        string ligne = null;
                        while (!traductionTrouvée && (ligne = reader.ReadLine()) != null) {
                            // zoek naar vertaling in de huidige regel
                            MatchCollection résultats = patternTraduction.Matches(ligne);
                            // vertaling gevonden?
                            if (résultats.Count != 0) {
                                traduction = résultats[0].Groups[1].Value.Trim();
                                traductionTrouvée = true;
                            }
                        }
                        // vertaling gevonden?
                        if (!traductionTrouvée) {
                            exception = new WebTraductionsException("Le serveur n'a pas renvoyé de réponse") { Code = 12 };
                        }
                    }
                }
            } catch (Exception e) {
                exception = new WebTraductionsException("Erreur rencontrée lors de la traduction", e) { Code = 11 };
            }
            // uitzondering?
            if (exception != null) {
                throw exception;
            } else {
                return traduction;
            }
        }
    }
}
  • regel 12: de eigenschap LanguesTraduites van de interface IServiceTraduction – van buitenaf geïnitialiseerd
  • regel 13: de eigenschap UrlServeurTraduction is de URL die aan de vertaalserver moet worden opgevraagd: http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}, waarbij de markering {0} moet worden vervangen door de vertaalrichting en de markering {1} door de te vertalen tekst - van buitenaf geïnitialiseerd
  • regel 14: de eigenschap ProxyHttp is de eventuele te gebruiken HTTP-proxy, bijvoorbeeld: pproxy.istia.uang:3128 – van buitenaf geïnitialiseerd
  • regel 15: de eigenschap RegexTraduction is de reguliere expressie waarmee de vertaling kan worden opgehaald uit de HTML-stream die door de vertaalserver wordt teruggestuurd, bijvoorbeeld @"<div class=""txtTrad"">(.*?)</div>" – van buitenaf geïnitialiseerd
  • deze vier eigenschappen worden in onze applicatie door Spring geïnitialiseerd.
  • regels 20-22: er wordt gecontroleerd of de gevraagde vertaalrichting daadwerkelijk voorkomt in het woordenboek met vertaalde talen. Als dat niet het geval is, wordt er een uitzondering gegenereerd.
  • regel 24: de te vertalen tekst wordt gecodeerd zodat deze in een URL kan worden opgenomen
  • regel 26: de URI van de vertaaldienst wordt samengesteld. Als de eigenschap UrlServeurTraduction de tekenreeks http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}, dan wordt de markering {0} vervangen door de vertaalrichting en de markering {1} door de te vertalen tekst.
  • regel 28: het patroon voor het zoeken naar de vertaling in het HTML-antwoord van de vertaalserver wordt samengesteld.
  • regels 33, 60: de aanvraag aan de vertaalserver vindt plaats in een try/catch-blok
  • regel 35: het object HttpWebRequest, dat zal worden gebruikt om de vertaalserver te raadplegen, wordt aangemaakt met de Uri van het opgevraagde document.
  • regel 36: de opvraagmethode is GET. Deze instructie zou overbodig kunnen zijn, aangezien GET waarschijnlijk de standaardmethode is van het object HttpWebRequest.
  • regel 37: de eigenschap Proxy van het object HttpWebRequest wordt ingesteld.
  • regel 39: er wordt een verzoek naar de vertaalserver gestuurd en het antwoord, van het type HttpWebResponse, wordt opgehaald.
  • regels 41-42: er wordt een StreamReader gebruikt om elke regel van het HTML-antwoord van de server te lezen.
  • regels 45-53: in elke regel van het antwoord wordt naar de vertaling gezocht. Zodra deze is gevonden, wordt het lezen van het HTML-antwoord gestopt en worden alle geopende streams gesloten.
  • regels 55-57: als er geen vertaling in het HTML-antwoord is gevonden, wordt er een uitzondering van het type WebTraductionsException voorbereid om dit te melden.
  • regels 60-62: als er een uitzondering is opgetreden tijdens de communicatie tussen client en server, wordt deze ingekapseld in een uitzondering van het type WebTraductionsException om dit te melden.
  • regels 64-68: als er een uitzondering is geregistreerd, wordt deze afgegeven; anders wordt de gevonden vertaling geretourneerd.

In ons voorbeeld gaan we ervan uit dat de HTTP-proxy geen authenticatie vereist. Als dat niet het geval zou zijn, zouden we iets schrijven als:


httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
httpWebRequest.Proxy.Credentials=new NetworkCredential("login","password");

We hebben hier WebRequest / WebResponse gebruikt in plaats van WebClient, omdat we niet het volledige HTML-antwoord van de vertaalserver hoeven te verwerken. Zodra de vertaling in dit antwoord is gevonden, hebben we de rest van de regels van het antwoord niet meer nodig. Met de klasse WebClient is dit niet mogelijk.

Hier volgt een testprogramma voor de klasse ServiceTraduction:


using System;
using System.Collections.Generic;
using dao;
using entites;

namespace ui {
    class Program {
        static void Main(string[] args) {
            try {
                // vertaalservice aanmaken
                ServiceTraduction serviceTraduction = new ServiceTraduction();
                // reguliere expressie om de vertaling te vinden
                serviceTraduction.RegexTraduction = @"<div class=""txtTrad"">(.*?)</div>";
                // URL van de vertaalserver
                serviceTraduction.UrlServeurTraduction = "http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}";
                // woordenboek van vertaalde talen
                Dictionary<string, string> languesTraduites = new Dictionary<string, string>();
                languesTraduites["fe"]= "Français-Anglais";
                languesTraduites["fs"]= "Français-Espagnol";
                languesTraduites["ef"]= "Anglais-Français";
                serviceTraduction.LanguesTraduites = languesTraduites;
                // proxy
                //serviceTraduction.ProxyHttp = "pproxy.istia.uang:3128";
                // vertaling
                string texte = "ce chien est perdu";
                string deQuoiVersQuoi = "fe";
                Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
                texte = "l'été sera chaud";
                deQuoiVersQuoi = "fs";
                Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
                texte = "my tailor is rich";
                deQuoiVersQuoi = "ef";
                Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
                texte = "xx";
                deQuoiVersQuoi = "ef";
                Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
            } catch (WebTraductionsException e) {
                // fout
                Console.WriteLine("L'erreur suivante de code {1} s'est produite : {0}", e.Message, e.Code);
            }
        }
    }
}

De verkregen resultaten zijn als volgt:

1
2
3
4
Traduction [Français-Anglais] de [ce chien est perdu] : [this dog is lost]
Traduction [Français-Espagnol] de [l'été sera chaud] : [el verano será caliente]
Traduction [Anglais-Français] de [my tailor is rich] : [mon tailleur est riche]
Traduction [Anglais-Français] de [xx] : [xx]

Het project [dao] van de oplossing wordt gecompileerd tot een DLL HttpTraductions.dll:

 

11.7.3.6. De grafische gebruikersinterface van de applicatie

Laten we nog eens terugkomen op de architectuur van onze applicatie:

We schrijven nu de laag [ui]. Deze vormt het onderwerp van het project [ui] van de oplossing die momenteel in ontwikkeling is:

De map [lib] [3] bevat enkele van de DLL-bestanden waarnaar wordt verwezen door het project [4]:

  • die welke nodig zijn voor Spring: Spring.Core, Common.Logging, antlr.runtime
  • die van de [dao]-laag: HttpTraductions

Het bestand [App.config] bevat de Spring-configuratie:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <description>Traductions sur le web</description>
            <!-- de vertaaldienst -->
            <object name="ServiceTraduction" type="dao.ServiceTraduction, HttpTraductions">
                <property name="UrlServeurTraduction" value="http://trans.voila.fr/traduction_voila.php?isText=1&amp;translationDirection={0}&amp;stext={1}"/>
                <!--
                <property name="ProxyHttp" value="pproxy.istia.uang:3128"/>
                -->
                <property name="RegexTraduction" value="&lt;div class=&quot;txtTrad&quot;&gt;(.*?)&lt;/div&gt;"/>
                <property name="LanguesTraduites">
                    <dictionary key-type="string" value-type="string">
                        <entry key="fe" value="Français-Anglais"/>
                        <entry key="ef" value="Anglais-Français"/>
...
                        <entry key="ei" value="Anglais-Italien"/>
                        <entry key="ie" value="Italien-Anglais"/>
                    </dictionary>
                </property>
            </object>
        </objects>
    </spring>
</configuration>
  • regel 15: de objecten die door Spring moeten worden geïnstantieerd. Er is er slechts één, namelijk die op regel 18, die de vertaalservice instantiëert met de klasse ServiceTraduction, te vinden in DLL HttpTraductions.
  • regel 19: de eigenschap UrlServeurTraduction van de klasse ServiceTraduction. Er is een probleem met het teken & in de URL. Dit teken heeft een betekenis in een XML-bestand. Het moet daarom worden afgeschermd. Dit geldt ook voor andere tekens die we verderop in het bestand zullen tegenkomen. Ze moeten worden vervangen door een reeks [&code;]: & door [&amp;], < door [&lt;], > door [&gt;], " door [&quot;].
  • regel 21: de eigenschap ProxyHttp van de klasse ServiceTraduction. Een niet-geïnitialiseerde eigenschap blijft null. Het niet definiëren van deze eigenschap komt neer op het aangeven dat er geen Http-proxy is.
  • regel 23: de eigenschap RegexTraduction van de klasse ServiceTraduction. In de reguliere expressie moesten de tekens [< > "] worden vervangen door hun beschermde equivalenten.
  • regels 24-33: de eigenschap LanguesTraduites van de klasse ServiceTraduction.

Het programma [Program.cs] wordt uitgevoerd bij het opstarten van de applicatie. De code ervan is als volgt:


using System;
using System.Text;
using System.Windows.Forms;
using dao;
using Spring.Context;
using Spring.Context.Support;

namespace ui {
    static class Program {
        /// <summary>
        /// Het belangrijkste toegangspunt voor de applicatie.
        /// </summary>
        [STAThread]
        static void Main() {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
    
            // --------------- Ontwikkelaarscode
            // instantiëren van de vertaaldienst
            IApplicationContext ctx = null;
            Exception ex = null;
            ServiceTraduction serviceTraduction = null;
            try {
                // Spring-context
                ctx = ContextRegistry.GetContext();
                // een referentie aanvragen voor de vertaaldienst
                serviceTraduction = ctx.GetObject("ServiceTraduction") as ServiceTraduction;
            } catch (Exception e1) {
                // uitzondering opslaan
                ex = e1;
            }
            // weer te geven formulier
            Form form = null;
            // is er een uitzondering opgetreden?
            if (ex != null) {
                // ja – er wordt een foutmelding aangemaakt om weer te geven
                StringBuilder msgErreur = new StringBuilder(String.Format("Chaîne des exceptions : {0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
                Exception e = ex;
                while (e != null) {
                    msgErreur.Append(String.Format("{0}: {1}{2}", e.GetType().FullName, e.Message, Environment.NewLine));
                    msgErreur.Append(String.Format("{0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
                    e = e.InnerException;
                }
                // foutvenster aanmaken waaraan de weer te geven foutmelding wordt doorgegeven
                Form2 form2 = new Form2();
                form2.MsgErreur = msgErreur.ToString();
                // dit wordt het venster dat moet worden weergegeven
                form = form2;
            } else {
                // alles is goed verlopen
                // aanmaken van de grafische interface [Form1] waaraan de verwijzing naar de vertaaldienst wordt doorgegeven
                Form1 form1 = new Form1();
                form1.ServiceTraduction = serviceTraduction;
                // dit wordt het venster dat moet worden weergegeven
                form = form1;
            }
            // venster weergeven
            Application.Run(form);
        }
    }
}

Deze code is al gebruikt in de applicatie Belastingen versie 6, in paragraaf 7.6.2.

  • De vertaaldienst wordt op regel 27 door Spring aangemaakt. Als dit is gelukt, wordt het formulier [Form1] weergegeven (regels 52-55), anders wordt het foutformulier [Form2] weergegeven (regels 36-48).

Het formulier [Form2] wordt gebruikt in de applicatie Impôts versie 6 en is toegelicht in paragraaf 7.6.4.

Formulier [Form1] ziet er als volgt uit:

nr.
type
naam
rol
1
TextBox
textBoxTexteATraduire
invoervak voor de te vertalen tekst
MultiLine=true
2
ComboBox
comboBoxLangues
de lijst met vertaalrichtingen
3
Knop
buttonTraduire
om de vertaling van de tekst [1] in de richting [2] aan te vragen
4
TextBox
textBoxTraduction
de vertaling van de tekst [1]

De code van het formulier [Form1] is als volgt:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using dao;

namespace ui {
    public partial class Form1 : Form {
        // vertaaldienst
        public ServiceTraduction ServiceTraduction { get; set; }
        // taalwoordenboek
        Dictionary<string, string> languesInversées = new Dictionary<string, string>();

        // constructor
        public Form1() {
            InitializeComponent();
        }

        // eerste laden van het formulier
        private void Form1_Load(object sender, EventArgs e) {
            // opbouw van het omgekeerde woordenboek
            foreach (string code in ServiceTraduction.LanguesTraduites.Keys) {
                // talen
                string langues = ServiceTraduction.LanguesTraduites[code];
                // toevoeging (talen, code) aan het omgekeerde woordenboek
                languesInversées[langues] = code;
            }
            // invullen van de keuzelijst in alfabetische volgorde van de talen
            string[] languesCombo = languesInversées.Keys.ToArray();
            Array.Sort<string>(languesCombo);
            foreach (string langue in languesCombo) {
                comboBoxLangues.Items.Add(langue);
            }
            // selectie van de eerste taal
            if (comboBoxLangues.Items.Count != 0) {
                comboBoxLangues.SelectedIndex = 0;
            }
        }

        private void buttonTraduire_Click(object sender, EventArgs e) {
            // iets om te vertalen?
            string texte = textBoxTexteATraduire.Text.Trim();
            if (texte == "") return;
            // vertaling
            try {
                textBoxTraduction.Text = ServiceTraduction.Traduire(texte, languesInversées[comboBoxLangues.SelectedItem.ToString()]);
            } catch (Exception ex) {
                textBoxTraduction.Text = ex.Message;
            }
        }
    }
}
  • regel 10: een verwijzing naar de vertaaldienst. Deze openbare eigenschap is geïnitialiseerd door [Program.cs], regel 53. Wanneer de methoden Form1_Load (regel 20) of buttonTraduire_Click (regel 40) worden uitgevoerd, is dit veld dus al geïnitialiseerd.
  • regel 12: het woordenboek van de vertaalde talen met vermeldingen van het type ["Français-Anglais","fe"], c.a.d. Het omgekeerde van het woordenboek LanguesTraduites dat door de vertaaldienst wordt geretourneerd.
  • regel 20: de methode Form1_Load wordt uitgevoerd bij het laden van het formulier.
  • regels 22-27: het woordenboek serviceTraduction.LanguesTraduites ["fe","Français-Anglais"] wordt gebruikt om het woordenboek languesInversées ["Français-Anglais", "fe"] samen te stellen.
  • regel 29: languesCombo is de sleuteltabel van de woordenboeken languesInversées en c.a.d. een tabel met elementen ["Français-Anglais"]
  • regel 30: deze tabel is gesorteerd om de vertaalrichtingen in de keuzelijst in alfabetische volgorde weer te geven
  • regels 31-33: de keuzelijst met talen is gevuld.
  • regel 40: de methode die wordt uitgevoerd wanneer de gebruiker op de knop [Traduire] klikt
  • regel 46: het volstaat om de methode serviceTraduction.Traduire aan te roepen om de vertaling op te vragen. De eerste parameter is de te vertalen tekst, de tweede de code van de vertaalrichting. Deze code wordt gevonden in het woordenboek languesInversées op basis van het geselecteerde element in de taalkeuzelijst.
  • regel 48: als er een uitzondering is, wordt deze weergegeven in plaats van de vertaling.

11.7.3.7. Conclusion

Deze toepassing heeft aangetoond dat we met de webclients van het .NET-framework gebruik kunnen maken van de bronnen op het web. De werkwijze is telkens vergelijkbaar:

  • bepaal de Uri die moet worden opgevraagd. Deze Uri is meestal geparametriseerd.
  • de URI opvragen
  • in het antwoord van de server vinden wat we zoeken met behulp van reguliere expressies

Deze techniek is willekeurig. Na verloop van tijd kunnen namelijk zowel de opgevraagde Uri als de reguliere expressie waarmee het verwachte resultaat wordt gevonden, veranderen. Het is daarom raadzaam om deze twee gegevens in een configuratiebestand op te nemen. Maar dat kan onvoldoende blijken te zijn. In het volgende hoofdstuk zullen we zien dat er stabielere bronnen op het web bestaan: webservices.

11.7.4. Een SMTP-client (Simple Mail Transport Protocol) met de klasse SmtpClient

Een SMTP-client is een client van een SMTP-server, een server voor het verzenden van e-mail. De klasse .NET SmtpClient omvat alle vereisten van een dergelijke client. De ontwikkelaar hoeft de details van het SMTP-protocol niet te kennen. Wij kennen dit protocol. Het is beschreven in paragraaf 11.4.3.

We presenteren de klasse SmtpClient in het kader van een eenvoudige Windows-toepassing waarmee e-mails met bijlagen kunnen worden verzonden. De toepassing maakt verbinding met poort 25 van een SMTP-server. We wijzen erop dat op de meeste Windows-systemen firewalls of antivirusprogramma’s verbindingen met poort 25 blokkeren. Het is dan ook noodzakelijk om deze beveiliging uit te schakelen om de toepassing te kunnen testen:

De SMTP-client heeft een eenlaagse architectuur:

Het Visual Studio-project ziet er als volgt uit:

  

De grafische interface [SendMailForm.cs] van de applicatie ziet er als volgt uit:

nr.
type
naam
rol
1
TextBox
textBoxServeur
naam van de server SMTP waarmee verbinding moet worden gemaakt
2
NumericUpDown
numericUpDownPort
de poort waarmee verbinding moet worden gemaakt
3
TextBox
textBoxExpediteur
adres van de afzender van het bericht
4
TextBox
textBoxTo
adressen van de ontvangers in de vorm: adres1,adres2, ...
5
TextBox
textBoxCc
adressen van de ontvangers in kopie (CC=Carbon Copy) in de vorm: adres1,adres2, ...
6
TextBox
textBoxBcc
adressen van de ontvangers in blinde kopie (BCC=Blind Carbon Copy) in de vorm: adres1,adres2, ... Alle adressen in deze drie invoervelden ontvangen hetzelfde bericht met dezelfde bijlagen. De ontvangers van het bericht kunnen de adressen in de velden 4 en 5 zien, maar niet die in veld 6. De Bcc is dus een manier om iemand in cc te zetten zonder dat de andere ontvangers van het bericht dit weten.
7
Knop
buttonAjouter
om een bijlage aan de e-mail toe te voegen
8
ListBox
listBoxPiecesJointes
lijst met bijlagen bij de e-mail
9
TextBox
textBoxSujet
onderwerp van de brief
10
TextBox
textBoxMessage
de tekst van het bericht.
MultiLine=true
11
Button
buttonEnvoyer
om het bericht en eventuele bijlagen te verzenden
12
TextBox
textBoxRésultat
geeft een overzicht van het verzonden bericht weer of een foutmelding als er een probleem is opgetreden
13
Knop
buttonEffacer
om te wissen [12]
 
OpenfileDialog
openFileDialog1
niet-visuele controle waarmee een bijlage uit het lokale bestandssysteem kan worden geselecteerd

In het vorige voorbeeld is de samenvatting die wordt weergegeven in [12] als volgt:

Envoi réussi...
Sujet : votre demande
Destinataires : y2000@hotmail.com
Cc : 
Bcc : 
Pièces jointes :
C:\data\travail\2007-2008\recrutements 0809\ing3\documents\ing3.zip
Texte : Bonjour,

Vous trouverez ci-joint le dossier de candidature à l'ISTIA.

Cordialement,

ST

De code van het formulier [SendMailForm.cs] is als volgt:


using System;
using System.Windows.Forms;
using System.Net.Mail;
using System.Text.RegularExpressions;
using System.Text;

namespace Chap9 {
    public partial class SendMailForm : Form {
        public SendMailForm() {
            InitializeComponent();
        }

        // bijlage toevoegen
        private void buttonAjouter_Click(object sender, EventArgs e) {
            // het dialoogvenster instellen openfileDialog1
            openFileDialog1.InitialDirectory = Application.ExecutablePath;
            openFileDialog1.Filter = "Tous les fichiers (*.*)|*.*";
            openFileDialog1.FilterIndex = 0;
            openFileDialog1.FileName = "";
            // het dialoogvenster wordt weergegeven en het resultaat wordt opgehaald
            if (openFileDialog1.ShowDialog() == DialogResult.OK) {
                // de bestandsnaam ophalen
                listBoxPiecesJointes.Items.Add(openFileDialog1.FileName);
            }
        }

        private void textBoxServeur_TextChanged(object sender, EventArgs e) {
            setStatutEnvoyer();
        }

        private void setStatutEnvoyer() {
            buttonEnvoyer.Enabled = textBoxServeur.Text.Trim() != "" && textBoxTo.Text.Trim() != "" && textBoxSujet.Text.Trim() != "";
        }

        // een bijlage verwijderen
        private void buttonRetirer_Click(object sender, EventArgs e) {
            // Bijlage geselecteerd?
            if (listBoxPiecesJointes.SelectedIndex != -1) {
                // de bijlage wordt verwijderd
                listBoxPiecesJointes.Items.RemoveAt(listBoxPiecesJointes.SelectedIndex);
                // de knop ‘Verwijderen’ wordt bijgewerkt
                buttonRetirer.Enabled = listBoxPiecesJointes.Items.Count != 0;
            }
        }

        private void listBoxPiecesJointes_SelectedIndexChanged(object sender, EventArgs e) {
            // Bijlage geselecteerd?
            if (listBoxPiecesJointes.SelectedIndex != -1) {
                // de knop 'Verwijderen' wordt bijgewerkt
                buttonRetirer.Enabled = true;
            }
        }

        // bericht met bijlagen verzenden
        private void buttonEnvoyer_Click(object sender, EventArgs e) {
....
        }

        private void textBoxTo_TextChanged(object sender, EventArgs e) {
            setStatutEnvoyer();
        }

        private void textBoxSujet_TextChanged(object sender, EventArgs e) {
            setStatutEnvoyer();
        }

        private void buttonEffacer_Click(object sender, EventArgs e) {
            textBoxResultat.Text = "";
        }
    }
}

We zullen deze code niet toelichten, aangezien deze geen nieuwe elementen bevat. Om de methode buttonAjouter_Click in regel 14 te begrijpen, wordt de lezer verzocht paragraaf 7.5.1 nogmaals door te nemen.

De methode buttonEnvoyer_Click op regel 55, die de e-mail verstuurt, is als volgt:


private void buttonEnvoyer_Click(object sender, EventArgs e) {
            try {
                // zandloper
                Cursor = Cursors.WaitCursor;
                // de SMTP-client
                SmtpClient smtpClient = new SmtpClient(textBoxServeur.Text.Trim(), (int)numericUpDownPort.Value);
                // het bericht
                MailMessage message = new MailMessage();
                // afzender
                message.Sender = new MailAddress(textBoxExpéditeur.Text.Trim());
                message.From = message.Sender;
                // ontvangers
                Regex marqueur = new Regex("\\s*,\\s*");
                string[] destinataires = marqueur.Split(textBoxTo.Text.Trim());
                foreach (string destinataire in destinataires) {
                    if (destinataire.Trim() != "") {
                        message.To.Add(new MailAddress(destinataire));
                    }
                }
                // CC
                string[] copies = marqueur.Split(textBoxCc.Text.Trim());
                foreach (string copie in copies) {
                    if (copie.Trim() != "") {
                        message.CC.Add(new MailAddress(copie));
                    }
                }
                // BCC
                string[] blindCopies = marqueur.Split(textBoxBcc.Text.Trim());
                foreach (string blindCopie in blindCopies) {
                    if (blindCopie.Trim() != "") {
                        message.Bcc.Add(new MailAddress(blindCopie));
                    }
                }
                // onderwerp
                message.Subject = textBoxSujet.Text.Trim();
                // berichttekst
                message.Body = textBoxMessage.Text;
                // bijlagen
                foreach (string attachement in listBoxPiecesJointes.Items) {
                    message.Attachments.Add(new Attachment(attachement));
                }
                // bericht verzenden
                smtpClient.Send(message);
                // Ok – er wordt een samenvatting weergegeven
                StringBuilder msg = new StringBuilder(String.Format("Envoi réussi...{0}", Environment.NewLine));
                msg.Append(String.Format("Sujet : {0}{1}", textBoxSujet.Text.Trim(), Environment.NewLine));
                textBoxSujet.Clear();
                msg.Append(String.Format("Destinataires : {0}{1}", textBoxTo.Text.Trim(), Environment.NewLine));
                textBoxTo.Clear();
                msg.Append(String.Format("Cc : {0}{1}", textBoxCc.Text.Trim(), Environment.NewLine));
                textBoxCc.Clear();
                msg.Append(String.Format("Bcc : {0}{1}", textBoxBcc.Text.Trim(), Environment.NewLine));
                textBoxBcc.Clear();
                msg.Append(String.Format("Pièces jointes :{0}", Environment.NewLine));
                foreach (string attachement in listBoxPiecesJointes.Items) {
                    msg.Append(String.Format("{0}{1}", attachement, Environment.NewLine));
                }
                msg.Append(String.Format("Texte : {0}{1}", textBoxMessage.Text, Environment.NewLine));
                listBoxPiecesJointes.Items.Clear();
                textBoxResultat.Text = msg.ToString();
            } catch (Exception ex) {
                // de fout wordt weergegeven
                textBoxResultat.Text = String.Format("L'erreur suivante s'est produite {0}", ex);
            }
            // normale cursor
            Cursor = Cursors.Arrow;
        }
  • regel 6: de SMTP-client wordt aangemaakt. Deze heeft twee parameters nodig: de naam van de server SMTP en de poort waarop deze actief is
  • regel 8: er wordt een bericht van het type MailMessage aangemaakt. Dit bericht zal de volledige te verzenden boodschap omvatten.
  • regel 10: het e-mailadres Sender van de afzender wordt ingevuld. Een e-mailadres is een instantie van het type MailAddress, samengesteld uit de tekenreeks "xx@yy.zz". Deze tekenreeks moet de verwachte vorm voor een e-mailadres hebben, anders wordt er een uitzondering gegenereerd. In dat geval wordt deze in het veld textBoxResultat (regel 63) weergegeven in een weinig gebruiksvriendelijke vorm.
  • regels 13-19: de e-mailadressen van de ontvangers worden in de To-lijst van het bericht geplaatst. Deze adressen worden opgehaald uit het veld textBoxTo. Met de reguliere expressie in regel 13 worden de verschillende adressen opgehaald die door een komma van elkaar worden gescheiden.
  • regels 21-26: hetzelfde proces wordt herhaald om het veld CC van het bericht te vullen met de adressen in de kopie van het veld textBoxCc.
  • regels 28-33: hetzelfde proces wordt herhaald om het veld Bcc van het bericht te vullen met de blindkopie-adressen uit het veld textBoxBcc.
  • regel 35: het veld Subject van het bericht wordt gevuld met het onderwerp uit het veld textBoxSujet.
  • regel 37: het veld Body van het bericht wordt gevuld met de tekst van het bericht textBoxMessage.
  • regels 39-41: de bijlagen worden aan het bericht toegevoegd. Elke bijlage wordt als een Attachment-object toegevoegd aan het veld Attachments van het bericht. Een Attachment-object wordt aangemaakt op basis van het volledige pad van de bijlage die moet worden toegevoegd in het lokale bestandssysteem.
  • regel 43: het bericht wordt verzonden met behulp van de methode Send van de SMTP-client.
  • regels 45-60: het verzendoverzicht wordt weggeschreven in het veld textBoxResultat en het formulier wordt gereset.
  • regel 63: weergave van een eventuele fout

11.8. Een generieke asynchrone TCP-client

11.8.1. Inleiding

In alle voorbeelden van dit hoofdstuk vond de communicatie tussen client en server plaats in de blokkerende modus, ook wel de synchrone modus genoemd:

  • wanneer een client verbinding maakt met een server, wacht hij op het antwoord van de server op dit verzoek voordat hij verdergaat.
  • Wanneer een client een door de server verzonden tekstregel leest, wordt hij geblokkeerd zolang de server deze niet heeft verzonden.
  • Aan de serverzijde werken de servicethreads die de client bedienen op dezelfde manier als hierboven beschreven.

In grafische interfaces is het vaak noodzakelijk om de gebruiker niet te blokkeren bij langdurige bewerkingen. Een vaak genoemd voorbeeld is het downloaden van een groot bestand. Tijdens deze download moet de gebruiker de vrijheid hebben om verder te werken met de grafische interface.

We stellen hier voor om de generieke TCP-client uit paragraaf 11.6.3 te herschrijven door de volgende wijzigingen aan te brengen:

  • de interface wordt grafisch
  • het communicatiemiddel met de server is een Socket-object
  • de communicatiemodus is asynchroon:
    • de client zal een verbinding met de server tot stand brengen, maar zal niet geblokkeerd blijven wachten tot deze is gelegd
    • de client zal een verzending naar de server initiëren, maar zal niet geblokkeerd blijven wachten tot deze is voltooid
    • de client zal het ontvangen van gegevens van de server initiëren, maar blijft niet geblokkeerd wachten tot dit is voltooid.

Laten we nog eens bekijken op welk niveau het Socket-object zich bevindt in de TCP-communicatie tussen client en server:

De Socket-klasse is de klasse die het dichtst bij het netwerk opereert. Hiermee kan de netwerkverbinding nauwkeurig worden beheerd. De term socket verwijst naar een stopcontact. De term is uitgebreid om een softwarematige netwerkverbinding aan te duiden. Bij een TCP-IP-communicatie tussen twee machines A en B communiceren twee sockets’en met elkaar. Een applicatie kan rechtstreeks met de sockets werken. Dit is het geval bij de hierboven genoemde applicatie A. Een socket kan een client- of een serveur-socket zijn.

11.8.2. De grafische interface van de asynchrone TCP-client

De Visual Studio-applicatie is als volgt:

  

[ClientTcpAsynchrone.cs] is de grafische interface. Deze ziet er als volgt uit:

nr.
type
naam
rol
1
TextBox
textBoxNomServeur
naam van de TCP-server waarmee verbinding moet worden gemaakt
2
NumericUpDown
numericUpDownPortServeur
de poort waarmee verbinding moet worden gemaakt
3
RadioButton
radioButtonLF
radioButtonRCLF
om aan te geven welk regeleinde-teken de client moet gebruiken: LF "\n" of RCLF "\r\n"
4
Knop
buttonConnexion
om verbinding te maken met poort [2] van de server [1]. De knop heeft de tekst [Connecter] wanneer de client niet is verbonden met een server, en [Déconnecter] wanneer hij wel is verbonden.
5
TextBox
textBoxMsgToServeur
bericht dat naar de server moet worden verzonden zodra de verbinding tot stand is gebracht. Wanneer de gebruiker op de toets [Entrée] drukt, wordt het bericht verzonden met het in [3] gekozen regeleinde-teken
6
ListBox
listBoxEvts
lijst waarin de belangrijkste gebeurtenissen van de verbinding tussen client en server worden weergegeven: verbinding, verbreking, sluiten van de datastroom, communicatiefouten
7
ListBox
listBoxDialogue
lijst waarin de berichten van de dialoog tussen client en server worden weergegeven
8
Knop
buttonRazEvts
om de lijst te wissen [6]
4
Knop
buttonRazDialogue
om de lijst te wissen [7]

De werking van deze interface is als volgt:

  • de gebruiker verbindt zijn grafische TCP-client met een TCP-service via [1, 2, 3, 4].
  • Een asynchrone thread ontvangt continu alle gegevens die door de TCP-server worden verzonden en geeft deze weer in de lijst [7]. Deze thread staat los van de andere activiteiten van de interface.
  • De gebruiker kan op eigen tempo berichten naar de server verzenden via [5]. Elk bericht wordt verzonden door een asynchrone thread. In tegenstelling tot de ontvangstthread, die nooit stopt, wordt de verzendthread beëindigd zodra het bericht is verzonden. Voor het volgende bericht wordt een nieuwe asynchrone thread gebruikt.
  • De communicatie tussen client en server wordt beëindigd wanneer een van beide partijen de verbinding verbreekt. De gebruiker kan dit zelf doen met de knop [4], die, zodra de verbinding tot stand is gebracht, de tekst [Déconnecter] draagt.

Hieronder ziet u een schermafbeelding van een uitvoering:

  • in [1]: verbinding met een dienst POP
  • naar [2]: weergave van de gebeurtenissen die tijdens de verbinding hebben plaatsgevonden
  • in [3]: het bericht dat door de server POP is verzonden na afloop van de verbinding
  • in [4]: de knop [Connecter] is veranderd in de knop [Déconnecter]
  • in [1] is het commando quit naar de server POP verzonden. De server antwoordde met +OK goodbye en sloot de verbinding af
  • in [2] werd deze verbroken verbinding aan de serverzijde gedetecteerd. De client heeft vervolgens de verbinding aan zijn kant verbroken.
  • in [3] is de knop [Déconnecter] weer een knop [Connecter] geworden

11.8.3. Asynchrone verbinding met de server

Als de knop [Connecter] wordt ingedrukt, wordt de volgende methode uitgevoerd:


        private void buttonConnexion_Click(object sender, EventArgs e) {
            // Inloggen of uitloggen?
            if (buttonConnexion.Text == "Déconnecter")
                déconnexion();
            else
                connexion();
}
  • regel 3: de knop kan de tekst [Connecter] of [Déconnecter] hebben.

De verbindingsmethode is als volgt:


using System.Net.Sockets;
...

namespace Chap9 {
    public partial class ClientTcp : Form {
        const int tailleBuffer = 1024;
        private Socket client = null;
        private byte[] data = new byte[tailleBuffer];
        private string réponse = null;
        private string finLigne = "\r\n";

        // gevolmachtigden
        public delegate void writeLog(string log);

        public ClientTcp() {
            InitializeComponent();
        }
....................................
    private void connexion() {
            // gegevenscontrole
            string nomServeur = textBoxNomServeur.Text.Trim();
            if (nomServeur == "") {
                logEvent("indiquez le nom du serveur");
                return;
            }
            // opvolging
            logEvent(String.Format("connexion en cours au serveur {0}", nomServeur));
            try {
                 // socket aanmaken
                client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                // asynchrone verbinding
                client.BeginConnect(Dns.GetHostEntry(nomServeur).AddressList[0],(int)numericUpDownPortServeur.Value, connecté, client);

            } catch (Exception ex) {
                logEvent(String.Format("erreur de connexion : {0}", ex.Message));
                return;
            }
        }

         // de verbinding is tot stand gekomen
        private void connecté(IAsyncResult résultat) {
            // de socket van de client wordt opgehaald
            Socket client = résultat.AsyncState as Socket;
    ...
        }


        // proces volgen
        private void logEvent(string msg) {
....
        }
    }
}
  • regel 1: de klasse Socket maakt deel uit van de naamruimte System.Net.Sockets.

Een aantal gegevens moet worden gedeeld tussen verschillende methoden van het formulier. Dit zijn de volgende:

  • regel 7: client is de socket voor de communicatie met de server
  • regels 6 en 8: de client ontvangt zijn berichten in een byte-array data.
  • regel 9: ‘response’ is het antwoord dat door de server wordt verzonden.
  • regel 10: finLigne is het einde-van-regel-teken dat door de TCP-client wordt gebruikt – dit is standaard ingesteld op RCLF, maar kan door de gebruiker worden gewijzigd met de keuzerondjes [3].

De procedure connexion op regel 19 brengt de verbinding met de TCP-server tot stand:

  • regels 21-25: er wordt gecontroleerd of de servernaam niet leeg is. Als dat niet het geval is, wordt de gebeurtenis gelogd in listBoxEvts via de methode logEvent op regel 49.
  • regel 27: er wordt gemeld dat de verbinding tot stand zal worden gebracht
  • regel 30: het Socket-object dat nodig is voor TCP/IP-communicatie wordt aangemaakt. De constructor accepteert drie parameters:
    • AddressFamily addressFamily: de adresfamilie IP van de client en de server, in dit geval de adressen IPv4 (AddressFamily.InterNetwork)
    • SocketType socketType: het type socket. Het type SocketType.Stream is geschikt voor TCP/IP-verbindingen
    • ProtocolType protocolType: het gebruikte internetprotocol, in dit geval het TCP-protocol
  • regel 32: de verbinding wordt asynchroon tot stand gebracht. De verbinding wordt gestart, maar de uitvoering gaat door zonder te wachten tot deze is voltooid. De methode [Socket].BeginConnect heeft vier parameters:
    • IPAddress ipAddress: het IP-adres van de machine waarop de service draait waarmee verbinding moet worden gemaakt
    • Int32 port: de poort van de service
    • AsyncCallBack asyncCallBack: AsyncCallBack is een delegatietype:
public void AsyncCallBack(IAsyncResult ar);

De methode asyncCallBack, die als derde parameter wordt doorgegeven aan de methode BeginConnect, moet een methode zijn die een type IAsyncCallBack accepteert en geen resultaat retourneert. Dit is de methode die wordt aangeroepen zodra de verbinding tot stand is gebracht. We geven hier als derde parameter de methode connecté uit regel 41 door.

  • (vervolg)
    • Object state: een object dat moet worden doorgegeven aan de methode asyncCallBack. Deze methode ontvangt (zie bovenstaande delegatie) een parameter ar van het type IAsyncResult. Het object state kan worden opgehaald in ar.AsyncState (regel 43). Hier geven we als vierde parameter de socket van de client door.
  • regel 38: de methode is voltooid. De gebruiker kan weer interactie hebben met de grafische interface. De verbinding verloopt op de achtergrond, parallel aan de afhandeling van de gebeurtenissen van de grafische interface. Eveneens parallel hieraan wordt de methode connecté uit regel 41 aangeroepen aan het einde van de verbinding, ongeacht of deze succesvol is of niet.

De code van de methode connecté is als volgt:


// de verbinding is tot stand gekomen
        private void connecté(IAsyncResult résultat) {
            // de socket van de client wordt opgehaald
            Socket client = résultat.AsyncState as Socket;
            try {
                // de asynchrone bewerking wordt beëindigd
                client.EndConnect(résultat);
                // opvolging
                logEvent(String.Format("connecté au service {0}", client.RemoteEndPoint));
                // formulier
                buttonConnexion.Text = "Déconnecter";
                // asynchroon uitlezen van gegevens van de server
                réponse = "";
                client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
            } catch (SocketException e) {
                logEvent(String.Format("erreur de connexion : {0}", e.Message));
                return;
            }
}

        // ontvangst van gegevens
        private void lecture(IAsyncResult résultat) {
            // de socket van de client wordt opgehaald
            Socket client = résultat.AsyncState as Socket;
...
        }

  • regel 4: de socket van de client wordt opgehaald uit de parameter résultat die door de methode wordt ontvangen. Ter herinnering: dit object is het object dat als vierde parameter aan de methode BeginConnect is doorgegeven.
  • regel 7: de verbindingspoging wordt beëindigd door de methode EndConnect, waaraan de parameter résultat moet worden doorgegeven die door de methode is ontvangen.
  • regel 9: de gebeurtenis wordt geregistreerd in de gebeurtenissenlijst
  • regel 11: de knop [Connecter] wordt een knop [Déconnecter], zodat de gebruiker kan vragen om de verbinding te verbreken.
  • regel 13: het antwoord van de server wordt geïnitialiseerd. Dit wordt bijgewerkt door herhaalde aanroepen van de asynchrone methode BeginReceive.
  • regel 14: eerste aanroep van de asynchrone methode BeginReceive. Deze wordt aangeroepen met de volgende parameters:
    • byte[] buffer: de buffer waarin de te ontvangen gegevens moeten worden geplaatst – hier is de buffer data
    • int offset: vanaf welke positie in de buffer de te ontvangen gegevens moeten worden geplaatst – hier is de offset 0, c.a.d, wat betekent dat de gegevens vanaf het eerste byte van de buffer worden geplaatst.
    • int size: de grootte in bytes van de buffer – hier is de grootte tailleBuffer.
    • SocketFlags socketFlags: configuratie van de socket – hier is er geen configuratie
    • AsyncCallBack asyncCallBack: de callback-methode die moet worden aangeroepen wanneer het ontvangen is voltooid. Dit is het geval wanneer de buffer gegevens heeft ontvangen of wanneer de verbinding is verbroken. Hier is de callback-methode de methode lecture uit regel 22.
    • Object state: het object dat moet worden doorgegeven aan de callback-methode asyncCallBack. Hier wordt opnieuw de socket van de client doorgegeven.

Merk op dat dit alles plaatsvindt zonder enige actie van de gebruiker, afgezien van het initiële verzoek om verbinding via de knop [Connecter]. Aan het einde van de methode connecté wordt op de achtergrond een andere methode uitgevoerd: de methode lecture, die we nu gaan bekijken.


// gegevens ontvangen
        private void lecture(IAsyncResult résultat) {
            // de socket van de client wordt opgehaald
            Socket client = résultat.AsyncState as Socket;
            int nbOctetsReçus = 0;
            bool erreur = false;
            try {
                // aantal ontvangen bytes
                nbOctetsReçus = client.EndReceive(résultat);
                if (nbOctetsReçus == 0) {
                    // de server reageert niet meer
                    logEvent("le serveur a fermé la connexion");
                }
            } catch (Exception e) {
                // er is een ontvangstprobleem opgetreden
                logEvent(String.Format("erreur de réception : {0}", e.Message));
                erreur = true;
            }
            // voltooid?
            if (nbOctetsReçus == 0 || erreur) {
                // de client wordt indien nodig losgekoppeld
                déconnexion();
                // het einde van het antwoord wordt weergegeven
                afficherRéponseServeur(réponse, true);
                // einde van het lezen
                return;
            }
            // de ontvangen gegevens worden opgehaald
            string données = Encoding.UTF8.GetString(data, 0, nbOctetsReçus);
            // deze worden toegevoegd aan de reeds ontvangen gegevens
            réponse += données;
            // het antwoord wordt weergegeven
            afficherRéponseServeur(réponse, false);
            // we gaan verder met lezen
            client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
        }
  • regel 2: de methode lecture wordt op de achtergrond geactiveerd wanneer de buffer data gegevens heeft ontvangen of wanneer de verbinding door de server is verbroken.
  • regel 9: het asynchrone leesverzoek wordt beëindigd door EndReceive. Ook hier moet deze methode worden aangeroepen met de parameter die door de callback-functie is ontvangen. De methode EndReceive retourneert het aantal bytes dat in de leesbuffer is ontvangen.
  • regel 10: als het aantal bytes nul is, betekent dit dat de verbinding door de server is verbroken.
  • regel 12: de gebeurtenis wordt genoteerd in de gebeurtenissenlijst
  • regel 14: een eventuele uitzondering wordt afgehandeld
  • regels 16-17: de gebeurtenis wordt in de gebeurtenissenlijst vastgelegd en de fout wordt genoteerd
  • regel 20: er wordt gecontroleerd of de verbinding moet worden gesloten
  • regel 22: de verbinding wordt aan de clientzijde verbroken met de methode déconnexion, die we later zullen bespreken.
  • regel 24: het antwoord van de server, c.a.d. De globale variabele réponse wordt weergegeven in de dialooglijst listBoxDialogue met behulp van een privémethode afficherRéponseServeur.
  • regel 26: einde van de asynchrone methode lecture
  • regel 29: de ontvangen bytes worden in een tekenreeks in het formaat UTF8 geplaatst.
  • regel 31: ze worden toegevoegd aan het antwoord dat momenteel wordt opgebouwd
  • regel 33: het antwoord wordt weergegeven in de lijst listBoxDialogue.
  • regel 35: er wordt opnieuw gewacht op gegevens van de server

Uiteindelijk stopt de asynchrone methode lecture nooit. Ze leest continu de gegevens van de server en laat deze weergeven in de lijst listBoxDialogue. Ze stopt pas wanneer de verbinding wordt verbroken, hetzij door de server, hetzij door de gebruiker zelf.

11.8.4. Verbinding met de server verbreken

Als je op de knop [Déconnecter] drukt, wordt de volgende methode uitgevoerd:


        private void buttonConnexion_Click(object sender, EventArgs e) {
            // verbinding maken of verbreken?
            if (buttonConnexion.Text == "Déconnecter")
                déconnexion();
            else
                connexion();
}
  • regel 3: de knop kan de tekst [Connecter] of [Déconnecter] hebben.

De methode déconnexion zorgt ervoor dat de verbinding van de client wordt verbroken:


private void déconnexion() {
            // socket sluiten
            if (client != null && client.Connected) {
                try {
                    // volgen
                    logEvent(String.Format("déconnexion du service {0}", client.RemoteEndPoint));
                    // verbinding verbreken
                    client.Shutdown(SocketShutdown.Both);
                    client.Close();
                    // formulier
                    buttonConnexion.Text = "Connecter";
                } catch (Exception ex) {
                    // opvolging
                    logEvent(String.Format("erreur de lors de la déconnexion : {0}", ex.Message));
                }
            }
        }
  • regel 3: als de klant bestaat en is ingelogd
  • regel 6: de verbroken verbinding wordt aangekondigd in listBoxEvts. De eigenschap client.RemoteEndPoint geeft het paar (IP-adres, poort) van het andere uiteinde van de verbinding weer, in dit geval c.a.d van de server.
  • regel 8: de gegevensstroom van de socket wordt gesloten met de methode ShutDown. De gegevensstroom van een socket is bidirectioneel: de socket verzendt en ontvangt gegevens. De parameter van de methode ShutDown kan zijn: ShutDown.Receive om de ontvangststroom te sluiten, Shutdonw.Send om de verzendstroom te sluiten of ShutDown.Both om beide stromen te sluiten.
  • regel 9: de aan de socket gekoppelde bronnen worden vrijgegeven
  • regel 11: de knop [Déconnecter] wordt de knop [Connecter]
  • regels 12-15: afhandeling van een eventuele uitzondering

11.8.5. Asynchroon verzenden van gegevens naar de server

Wanneer de gebruiker het bericht in het veld textBoxMsgToServeur bevestigt, wordt de volgende methode uitgevoerd:


        private void textBoxMsgToServeur_KeyPress(object sender, KeyPressEventArgs e) {
            // toets [Entrée] ?
            if (e.KeyChar == 13 && client.Connected) {
                envoyerMessage();
            }
}
  • regels 3-5: als de gebruiker op de toets [Entrée] heeft gedrukt en de client-socket is verbonden, dan wordt het bericht van het veld textBoxMsgToServeur verzonden met de methode envoyerMessage.

De methode envoyerMessage is als volgt:


        private void envoyerMessage() {
            // een bericht asynchroon verzenden
            // het bericht
            byte[] message = Encoding.UTF8.GetBytes(textBoxMsgToServeur.Text.Trim() + finLigne);
            // het is verzonden
            client.BeginSend(message, 0, message.Length, SocketFlags.None, écriture, client);
            // dialoog
            logDialogue("--> " + textBoxMsgToServeur.Text.Trim());
            // bericht wissen
            textBoxMsgToServeur.Clear();
}
  • regel 4: het einde-van-regel-teken van de klant wordt aan het bericht toegevoegd en in de byte-array message geplaatst.
  • regel 6: er wordt een asynchrone verzending gestart met de methode BeginSend. De parameters van BeginSend zijn identiek aan die van de methode BeginReceive. Aan het einde van de asynchrone verzending van het bericht wordt de methode écriture aangeroepen.
  • regel 8: het verzonden bericht wordt toegevoegd aan de lijst listBoxDialogue om de dialoog tussen client en server te kunnen volgen
  • regel 10: het verzonden bericht wordt uit de grafische interface verwijderd

De callback-methode écriture is als volgt:


        private void écriture(IAsyncResult résultat) {
            // resultaat van het verzenden van een bericht
            Socket client = résultat.AsyncState as Socket;
            try {
                client.EndSend(résultat);
            } catch (Exception e) {
                // er is een verzendingsprobleem opgetreden
                logEvent(String.Format("erreur d'émission : {0}", e.Message));
            }
}
  • regel 4: de callback-methode écriture ontvangt een resultaatparameter van het type IAsyncResult.
  • regel 3: in de parameter résultat wordt de socket van de client opgehaald. Deze socket was de 5e parameter van de methode BeginSend.
  • regel 5: de asynchrone verzendbewerking wordt beëindigd.

Er wordt niet gewacht tot het verzenden van een bericht is voltooid om de controle weer aan de gebruiker over te dragen. De gebruiker kan dus een tweede bericht verzenden terwijl het verzenden van het eerste bericht nog niet is voltooid.

11.8.6. Weergave van gebeurtenissen en de client/server-dialoog

De gebeurtenissen worden weergegeven door de methode logEvents:


        // procesopvolging
        private void logEvent(string msg) {
            listBoxEvts.Invoke(new writeLog(logEventCallBack), msg);
        }

        private void logEventCallBack(string msg) {
            // weergave bericht
            msg = msg.Replace(finLigne, " ");
            listBoxEvts.Items.Insert(0, String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}
  • regel 2: de methode logEvents ontvangt als parameter het bericht dat moet worden toegevoegd aan de lijst listBoxEvts.
  • regel 3: de component listBoxEvents kan niet rechtstreeks worden gebruikt. De methode logEvents wordt namelijk aangeroepen door twee soorten threads:
    • de hoofdthread die eigenaar is van de grafische interface, bijvoorbeeld wanneer deze aangeeft dat er een verbindingspoging gaande is
    • een secundaire thread die een asynchrone bewerking uitvoert. Dit type thread is geen eigenaar van de componenten en zijn toegang tot een C-component moet worden gecontroleerd door een bewerking van het type C.Invoke. Deze bewerking geeft aan de C-component door dat een thread een bewerking op deze component wil uitvoeren. De methode Invoke accepteert twee parameters:
      • een callback-functie van het type delegate. Deze callback-functie wordt uitgevoerd door de thread die eigenaar is van de grafische interface en niet door de thread die de methode C.Invoke uitvoert.
      • een object dat aan de callback-functie wordt doorgegeven.

Hier is de eerste parameter die aan de methode Invoke wordt doorgegeven een instantie van de volgende delegate:


        public delegate void writeLog(string log);

De delegatie writeLog heeft een parameter van het type string en retourneert geen resultaat. De parameter is het bericht dat moet worden opgeslagen in listBoxEvts.

Regel 3: de eerste parameter die wordt doorgegeven aan de methode Invoke is de methode logEventCallBack uit regel 6. Deze komt inderdaad overeen met de handtekening van de delegatie writeLog. De tweede parameter die aan de methode Invoke wordt doorgegeven, is het bericht dat als parameter aan de methode logEventCallBack zal worden doorgegeven.

De bewerking Invoke is een synchrone bewerking. De uitvoering van de secundaire thread wordt geblokkeerd totdat de thread die eigenaar is van het besturingselement de callback-methode uitvoert.

  • regel 6: de callback-methode die wordt uitgevoerd door de thread van de grafische interface ontvangt het bericht dat moet worden weergegeven in het besturingselement listBoxEvts.
  • regel 9: de gebeurtenis wordt op de eerste plaats in de lijst geregistreerd, zodat de meest recente gebeurtenissen bovenaan de lijst staan.

De berichten van de client-serverdialoog worden weergegeven via de methode logDialogue:


        // opvolging van de dialoog
        private void logDialogue(string msg) {
            listBoxDialogue.Invoke(new writeLog(logDialogueCallBack), msg);
        }
        private void logDialogueCallBack(string msg) {
            // bericht weergeven
            msg = msg.Replace(finLigne, " ");
            listBoxDialogue.Items.Add(String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}

Het principe is hetzelfde als bij de methode logEvent.

De door de client ontvangen berichten worden weergegeven via de methode afficherRéponseServeur:


        private void afficherRéponseServeur(String msg, bool dernièreLigne) {
...
}

De eerste parameter is het weer te geven bericht. Dit bericht kan uit meerdere regels bestaan. De client leest de gegevens van de server namelijk in blokken van tailleBuffer (1024) bytes. In deze 1024 bytes kunnen verschillende regels voorkomen, die herkenbaar zijn aan hun regeleinde-teken "\n". De laatste regel kan onvolledig zijn, omdat het einde-van-regel-teken zich in de volgende 1024 bytes bevindt. De methode zoekt in het bericht de regels die eindigen op "\n" en vraagt vervolgens aan logDialogue om deze weer te geven. De tweede parameter van de methode geeft aan of de laatst gevonden regel moet worden weergegeven of in de buffer moet blijven om door het volgende bericht te worden aangevuld. De code is vrij complex en is hier niet van belang. Daarom wordt er geen uitleg bij gegeven.

11.8.7. Conclusie

Hetzelfde voorbeeld zou met synchrone bewerkingen kunnen worden afgehandeld. In dit geval voegt het asynchrone aspect van de grafische interface weinig toe voor de gebruiker. Als de gebruiker echter verbinding maakt en vervolgens merkt dat de server „niet meer reageert”, kan hij de verbinding verbreken omdat de grafische interface tijdens de uitvoering van de asynchrone bewerkingen blijft reageren op gebeurtenissen. Dit vrij complexe voorbeeld heeft ons in staat gesteld om nieuwe begrippen te introduceren:

  • het gebruik van sockets
  • het gebruik van asynchrone methoden. Wat we hebben gezien, maakt deel uit van een standaard. Er bestaan nog andere asynchrone methoden die volgens hetzelfde model werken.
  • het bijwerken van besturingselementen van een grafische interface door secundaire threads.

Asynchrone TCP/IP-communicatie biedt voor een server grotere voordelen dan die welke in het vorige voorbeeld werden getoond. We weten dat de server zijn clients bedient met behulp van secundaire threads. Als de threadpool N threads bevat, betekent dit dat de server slechts N clients tegelijkertijd kan bedienen. Als de N threads allemaal een blokkerende (synchrone) bewerking uitvoeren, zijn er geen threads meer beschikbaar voor een nieuwe klant totdat een van de blokkerende bewerkingen is voltooid en een thread vrijgeeft. Als er op de threads asynchrone in plaats van synchrone bewerkingen worden uitgevoerd, wordt een thread nooit geblokkeerd en kan deze snel worden hergebruikt voor nieuwe klanten.

11.9. Voorbeeldtoepassing, versie 8: Server voor belastingberekening

11.9.1. De architectuur van de nieuwe versie

We nemen de toepassing voor belastingberekening weer op, die al in verschillende vormen is behandeld. Laten we even terugkijken naar de laatste versie, die van versie 7 uit paragraaf 9.8.

De gegevens stonden in een database en de laag [ui] was een grafische interface:

 

We gaan deze architectuur overnemen en over twee machines verdelen:

  • één machine, [serveur], zal de lagen [metier] en [dao] van versie 7 hosten. Er zal een TCP/IP-laag [serveur] [1] worden opgezet om internetgebruikers in staat te stellen de belastingberekeningsdienst te raadplegen.
  • Een machine [client] zal de laag [ui] van versie 7 hosten. Er zal een TCP/IP-laag [client] [2] worden gebouwd, zodat de laag [ui] de belastingberekeningsdienst kan raadplegen.

De architectuur verandert hier ingrijpend. Versie 7 was een Windows-applicatie voor één werkstation. Versie 8 wordt een client/server-applicatie voor het internet. De server kan meerdere clients tegelijkertijd bedienen.

We gaan eerst het gedeelte [serveur] van de applicatie schrijven.

11.9.2. De server voor de belastingberekening

11.9.2.1. Het Visual Studio-project

Het Visual Studio-project ziet er als volgt uit:

  • in [1], het project. Daarin bevinden zich de volgende elementen:
  • [ServeurImpot.cs]: de TCP/IP-server voor de belastingberekening in de vorm van een console-applicatie.
  • [dbimpots.sdf]: de SQL Compact Server-database van versie 7, zoals beschreven in paragraaf 9.8.5.
  • [App.config]: het configuratiebestand van de applicatie.
  • In [2] bevat de map [lib] de bestanden DLL die nodig zijn voor het project:
    • [ImpotsV7-dao]: de laag [dao] van versie 7
    • [ImpotsV7-metier]: de laag [metier] van versie 7
    • [antlr.runtime, CommonLogging, Spring.Core] voor Spring
  • in [3], de projectreferenties

11.9.2.2. Configuratie van de applicatie

Het bestand [App.config] wordt gebruikt door Spring. De inhoud ervan is als volgt:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
                <constructor-arg index="0" value="System.Data.SqlServerCe.3.5"/>
                <constructor-arg index="1" value="Data Source=|DataDirectory|\dbimpots.sdf;" />
                <constructor-arg index="2" value="select data1, data2, data3 from data"/>
            </object>
            <object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
                <constructor-arg index="0" ref="dao"/>
            </object>
        </objects>
    </spring>
</configuration>
  • regels 16-20: configuratie van de laag [dao] gekoppeld aan de database SQL Server compact
  • regels 21-23: configuratie van de laag [metier].

Dit is het configuratiebestand dat wordt gebruikt in de laag [ui] van versie 7. Het is beschreven in paragraaf 9.8.4.

11.9.2.3. Werking van de server

Bij het opstarten van de server maakt de servertoepassing de lagen [metier] en [dao] aan en geeft vervolgens een beheerconsole weer:

  

De beheerconsole accepteert de volgende opdrachten:

start port
om de service op een bepaalde poort te starten
stop
om de dienst te stoppen. Deze kan vervolgens opnieuw worden gestart op dezelfde of een andere poort.
echo start
om de echo van de client/server-dialoog op de console in te schakelen
echo stop
om de echo uit te schakelen
status
om de actieve/inactieve status van de service weer te geven
quit
om de applicatie af te sluiten

Laten we de server starten:

1
2
3
Serveur de calcul d'impôt >start 27
Serveur de calcul d'impôt lancé sur le port 27
Serveur de calcul d'impôt >

Laten we nu de asynchrone grafische TCP-client starten die we eerder in paragraaf 11.8 hebben besproken.

Image

De client is verbonden. Hij kan de volgende opdrachten naar de belastingberekeningsserver sturen:

aide
om de lijst met toegestane commando's op te vragen
impot marié nbEnfants salaireAnnuel
om de belasting te berekenen van iemand met nbEnfants kinderen en een salaris van salaireAnnuel euro. marié is gelijk aan o als de persoon getrouwd is, en n anders.
aurevoir
om de verbinding met de server te verbreken

Hier volgt een voorbeeld van een dialoog:

Aan de serverzijde geeft de console het volgende weer:

1
2
3
4
Serveur de calcul d'impôt >start 27
Serveur de calcul d'impôt >Serveur de calcul d'impôt lancé sur le port 27
Début du service au client 0
Fin du service au client 0

Laten we de echo inschakelen en een nieuwe dialoog starten vanuit de grafische client:

 

De beheerconsole geeft dan het volgende weer:

1
2
3
4
5
6
7
echo start
Serveur de calcul d'impôt >Début du service au client 1
<--- Client 1 : aide
---> Client 1 : Commandes acceptées
1-aide
2-impot marié(O/N) nbEnfants salaireAnnuel
3-aurevoir
  • regel 1: de echo van de client/server-dialoog is ingeschakeld
  • regel 2: er is een client binnengekomen
  • regel 3: deze heeft het commando [aide] verzonden
  • regels 4-7: het antwoord van de server over 4 regels.

Laten we de service stoppen:

1
2
3
stop
L'erreur suivante s'est produite sur le serveur : Une opération de blocage a été interrompue par un appel à WSACancelBlockingCall
Serveur de calcul d'impôt >
  • regel 1: er wordt gevraagd om de dienst te stoppen (niet de applicatie zelf)
  • regel 2: een uitzondering omdat de server, die vastzat in het wachten op een client, abrupt werd onderbroken door het afsluiten van de luisterservice.
  • regel 3: de dienst kan nu opnieuw worden gestart via start port of worden gestopt via quit.

Voordat de luisterservice werd gestopt, werd een client via een andere verbinding bediend. Deze verbinding wordt niet gesloten door het sluiten van de luistersocket. De client kan doorgaan met het verzenden van opdrachten: de servicethread die vóór het sluiten van de luisterservice aan de client was toegewezen, blijft de client antwoorden:

Image

11.9.3. De code van de TCP-server voor belastingberekening

1
  

De code van de server [ServeurImpot.cs] is als volgt:


...
namespace Chap9 {
    public class ServeurImpot {

        // gegevens gedeeld tussen threads en methoden
        private static IImpotMetier metier = null;
        private static int port;
        private static TcpListener service;
        private static bool actif = false;
        private static bool echo = false;

        // hoofdprogramma
        public static void Main(string[] args) {
            // instanties van de lagen [metier] en [dao]
            IApplicationContext ctx = null;
            metier = null;
            try {
                // Spring-context
                ctx = ContextRegistry.GetContext();
                // er wordt een referentie opgevraagd op de laag [metier]
                metier = (IImpotMetier)ctx.GetObject("metier");

                // configuratie van de threadpool
                ThreadPool.SetMinThreads(10, 10);
                ThreadPool.SetMaxThreads(10, 10);

                // leest de via het toetsenbord ingevoerde beheeropdrachten voor de server in een eindeloze lus
                string commande = null;
                string[] champs = null;
                while (true) {
                    // prompt
                    Console.Write("Serveur de calcul d'impôt >");
                    // commando lezen
                    commande = Console.ReadLine().Trim().ToLower();
                    champs = Regex.Split(commande, @"\s+");
                    // commando uitvoeren
                    switch (champs[0]) {
                        case "start":
                            // actief?
                            if (actif) {
                                //fout
                                Console.WriteLine("Le serveur est déjà actif");
                            } else {
                                // poortcontrole
                                if (champs.Length != 2 || !int.TryParse(champs[1], out port) || port <= 0) {
                                    Console.WriteLine("Syntaxe : start port. Port incorrect");
                                } else {
                                    // luisterdienst wordt gestart
                                    ThreadPool.QueueUserWorkItem(doEcoute, null);
                                }
                            }
                            break;
                        case "echo":
                            // echo start / stop
                            if (champs.Length != 2 || (champs[1] != "start" && champs[1] != "stop")) {
                                Console.WriteLine("Syntaxe : echo start / stop");
                            } else {
                                echo = champs[1] == "start";
                            }
                            break;
                        case "stop":
                            // einde van de dienst
                            if (actif) {
                                service.Stop();
                                actif = false;
                            }
                            break;
                        case "status":
                            // serverstatus
                            if (actif) {
                                Console.WriteLine("Le service est lancé sur le port {0}", port);
                            } else {
                                Console.WriteLine("Le service n'est pas lancé}");
                            }
                            break;
                        case "quit":
                            // de applicatie wordt afgesloten
                            Console.WriteLine("Fin du service");
                            Environment.Exit(0);
                            break;
                        default:
                            // ongeldige opdracht
                            Console.WriteLine("Commande incorrecte. Utilisez (start,stop,echo, status, quit)");
                            break;
                    }
                }
            } catch (Exception e1) {
                // uitzondering weergeven
                Console.WriteLine("L'erreur suivante s'est produite à l'initialisation de l'application : {0}", e1.Message);
                return;
            }
        }


        private static void doEcoute(Object data) {
...
        }

....
    }
}
  • regels 18-21: de lagen [metier] en [dao] worden geïnstantieerd door Spring, geconfigureerd door [App.config]. De globale variabele metier op regel 6 wordt vervolgens geïnitialiseerd.
  • regels 24-25: de threadpool van de applicatie wordt geconfigureerd met minimaal en maximaal 10 threads.
  • regels 30-86: de lus voor het invoeren van de beheeropdrachten van de service (start, stop, quit, echo, status).
  • regel 32: serverprompt voor elke nieuwe opdracht
  • regel 34: het beheerderscommando wordt ingelezen
  • regel 35: het commando wordt in velden opgesplitst om te worden geanalyseerd
  • regels 38-52: het start-port-commando, bedoeld om de luisterservice te starten
    • regel 40: als de dienst al actief is, hoeft er niets te gebeuren
    • regel 45: er wordt gecontroleerd of de poort aanwezig en correct is. Zo ja, dan wordt de globale variabele port uit regel 7 ingesteld.
    • regel 49: de luisterdienst wordt beheerd door een secundaire thread, zodat de hoofdthread de commando’s van de console kan blijven uitvoeren. Als de methode doEcoute de verbinding tot stand brengt, worden de globale variabelen service uit regel 8 en actif uit regel 9 geïnitialiseerd.
  • regels 53-60: het commando echo start / stop dat de echo van de client-serverdialoog op de console in- of uitschakelt
    • regel 58: de globale variabele echo uit regel 7 wordt ingesteld
  • regels 61-67: het commando stop dat de luisterservice stopt.
    • regel 64: de luisterdienst wordt gestopt
  • regels 68-75: het commando status dat de actieve/inactieve status van de dienst weergeeft
  • regels 76-80: de opdracht quit die alles stopt.

De thread die verantwoordelijk is voor het afluisteren van verzoeken van clients voert de volgende methode doEcoute uit:


        private static void doEcoute(Object data) {
            // thread voor het afluisteren van verzoeken van klanten
            try {
                // de service wordt aangemaakt
                service = new TcpListener(IPAddress.Any, port);
                // de service wordt gestart
                service.Start();
                // de server is actief
                actif = true;
                // monitoring
                Console.WriteLine("Serveur de calcul d'impôt lancé sur le port {0}", port);
                // lus voor klantenservice
                TcpClient tcpClient = null;
                // klantnummer
                int numClient = 0;
                // eindeloze lus
                while (true) {
                    // wachten op een klant
                    tcpClient = service.AcceptTcpClient();
                    // de dienst wordt verzorgd door een andere taak
                    ThreadPool.QueueUserWorkItem(doService, new Client() { CanalTcp = tcpClient, NumClient = numClient });
                    // volgende klant
                    numClient++;
                }
            } catch (Exception ex) {
                // de fout wordt gemeld
                Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
            }
        }

        // klantgegevens
        internal class Client {
            public TcpClient CanalTcp { get; set; }        // verbinding met de klant
            public int NumClient { get; set; }            // klantnummer
}

Dit is code die vergelijkbaar is met die van de echo-server die in paragraaf 11.6.1 is besproken. We lichten alleen de verschillen toe:

  • regel 7: de luisterdienst is van start gegaan
  • regel 9: er wordt vermeld dat de dienst nu actief is

Lijn 21: klanten worden bediend door servicethreads die de volgende methode doService uitvoeren:


private static void doService(Object infos) {
            // de te bedienen klant wordt opgehaald
            Client client = infos as Client;
            // de klant wordt bediend
            Console.WriteLine("Début du service au client {0}", client.NumClient);
            // verwerking van de verbinding TcpClient
            try {
                using (TcpClient tcpClient = client.CanalTcp) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // niet-gebufferde uitvoerstroom
                                writer.AutoFlush = true;
                                // verzending van een welkomstbericht naar de klant
                                writer.WriteLine("Bienvenue sur le serveur de calcul de l'impôt");
                                // lus voor het lezen van verzoeken en het schrijven van antwoorden
                                string demande = null;
                                bool serviceFini = false;
                                while (!serviceFini && (demande = reader.ReadLine()) != null) {
                                    // console-monitoring
                                    if (echo) {
                                        Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
                                    }
                                    // verzoek analyseren
                                    demande = demande.Trim().ToLower();
                                    // Lege verzoek?
                                    if (demande.Length == 0) {
                                        // Onjuist verzoek
                                        writeClient(writer,client.NumClient,"Commande non reconnue. Utilisez la commande aide.");
                                        return;
                                    }

                                    // het verzoek wordt opgesplitst in velden
                                    string[] champs = Regex.Split(demande, @"\s+");
                                    // analyse
                                    switch (champs[0].ToLower()) {
                                        case "aide":
                                            writeClient(writer, client.NumClient, "Commandes acceptées\n1-aide\n2-impot marié(O/N) nbEnfants salaireAnnuel\n3-aurevoir");
                                            break;
                                        case "impot":
                                            // de belasting wordt berekend
                                            writeClient(writer, client.NumClient, calculImpot(writer, client.NumClient, champs));
                                            break;
                                        case "aurevoir":
                                            serviceFini = true;
                                            writeClient(writer, client.NumClient, "Au revoir...");
                                            break;
                                        default:
                                            writeClient(writer, client.NumClient, "Commande non reconnue. Utilisez la commande aide.");
                                            break;
                                    }
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // fout
                Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
            } finally {
                Console.WriteLine("Fin du service au client {0}", client.NumClient);
            }
        }

        private static void writeClient(StreamWriter writer, int numClient, string message) {
            // console-uitvoer?
            if (echo) {
                Console.WriteLine("---> Client {0} : {1}", numClient, message);
            }
            // bericht naar de klant verzenden
            writer.WriteLine(message);
}

Ook hier hebben we weer code die vergelijkbaar is met die van de echo-server die in paragraaf 11.6.1 is besproken. We bespreken alleen wat er anders is:

  • regel 15: zodra de klant is ingelogd, stuurt de server hem een welkomstbericht.
  • regels 19-52: de lus voor het lezen van de opdrachten van de client. De lus stopt wanneer de client de opdracht "aurevoir" verstuurt.
  • regel 27: geval van een leeg commando
  • regel 34: het verzoek wordt opgesplitst in velden om te worden geanalyseerd
  • regel 37: commando aide: de klant vraagt de lijst met toegestane commando’s op
  • regel 40: opdracht impot: de klant vraagt om een belastingberekening. Er wordt geantwoord met het bericht dat wordt teruggestuurd door de methode calculImpot, die we straks nader zullen toelichten.
  • regel 44: opdracht aurevoir: de klant geeft aan dat hij klaar is.
  • regel 45: we bereiden ons voor om de lus voor het lezen van de verzoeken van de klant te verlaten (regels 19-52)
  • regel 46: we sturen de klant een afscheidsbericht
  • regel 48: een onjuist commando. We sturen de klant een foutmelding.

De verwerking van de opdracht impot wordt verzorgd door de volgende methode calculImpot:


private static string calculImpot(StreamWriter writer, int numClient, string[] champs) {
            // vraag of gehuwd (J/N) nbEnfants salaireAnnuel
            // er zijn 4 velden nodig
            if (champs.Length != 4) {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // velden [1]
            string marié = champs[1];
            if (marié != "o" && marié != "n") {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // velden [2]
            int nbEnfants;
            if (!int.TryParse(champs[2], out nbEnfants)) {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // velden [3]
            int salaireAnnuel;
            if (!int.TryParse(champs[3], out salaireAnnuel)) {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // oké – we berekenen de belasting
            int impot = 0;
            try {
                impot = metier.CalculerImpot(marié == "o", nbEnfants, salaireAnnuel);
                return impot.ToString();
            } catch (Exception ex) {
                return ex.Message;
            }
        }
  • regel 1: de methode ontvangt als derde parameter de veldtabel van de opdracht impot. Als deze correct is opgesteld, heeft deze de vorm: inkomend gehuwd nbEnfants salaireAnnuel. De methode retourneert als resultaat het antwoord dat naar de klant moet worden verzonden.
  • regel 4: er wordt gecontroleerd of de opdracht 4 velden bevat
  • regel 8: er wordt gecontroleerd of het veld marié geldig is
  • regel 14: er wordt gecontroleerd of het veld nbEnfants geldig is
  • regel 19: er wordt gecontroleerd of het veld salaireAnnuel geldig is
  • regel 25: de belasting wordt berekend met behulp van de methode CalculerImpot van de laag [metier]. Ter herinnering: deze laag is ingekapseld in een DLL.
  • regel 26: als de laag [metier] een resultaat heeft opgeleverd, wordt dit aan de klant teruggestuurd.
  • regel 28: als de laag [metier] een uitzondering heeft gegenereerd, wordt het bijbehorende bericht aan de client teruggestuurd.

11.9.4. De grafische client van de TCP-server voor belastingberekening

11.9.4.1. Het Visual Studio-project „

Het Visual Studio-project van de grafische client ziet er als volgt uit:

  • in [1], de twee projecten van de oplossing, één voor elk van de twee lagen van de applicatie
  • in [2], de TCP-client die fungeert als laag [metier] voor de laag [ui]. We zullen dus beide termen gebruiken.
  • in [3], de laag [ui] van versie 7, op één detail na waarover we het nog zullen hebben

11.9.4.2. De laag [metier]

De interface IImpotMetier is niet veranderd. Het is nog steeds die van versie 7:


namespace Metier {
    public interface IImpotMetier {
        int CalculerImpot(bool marié, int nbEnfants, int salaire);
    }
}

De implementatie van deze interface is de volgende klasse [ImpotMetierTcp]:


using System.Net.Sockets;
using System.IO;
namespace Metier {
    public class ImpotMetierTcp : IImpotMetier {

        // informatie [serveur]
        private string Serveur { get; set; }
        private int Port { get; set; }

        // belastingberekening
        public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
                // verbinding maken met de dienst
                using (TcpClient tcpClient = new TcpClient(Serveur, Port)) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // uitvoer zonder buffer
                                writer.AutoFlush = true;
                                // het welkomstbericht overslaan
                                reader.ReadLine();
                                // verzoek
                                writer.WriteLine(string.Format("impot {0} {1} {2}",marié ? "o" : "n",nbEnfants, salaire));
                                // antwoord
                                return int.Parse(reader.ReadLine());
                            }
                        }
                    }
                }
            }
        }
    }
  • regel 7: de naam of het IP-adres van de TCP-server voor belastingberekening
  • regel 8: de luisterpoort van deze server
  • deze twee eigenschappen worden door Spring geïnitialiseerd bij het instantiëren van de klasse [ImpotMetierTcp].
  • regel 11: de methode voor de belastingberekening. Wanneer deze wordt uitgevoerd, zijn de eigenschappen Serveur en Port al geïnitialiseerd. In de code zien we de klassieke aanpak van een TCP-client
  • regel 13: de verbinding met de server wordt geopend
  • regels 14-16: we halen (regel 14) de netwerkstroom op die bij deze verbinding hoort, waaruit we een leesstroom (regel 15) en een schrijfstroom (regel 16) afleiden.
  • regel 18: de schrijfstream moet niet-gebufferd zijn
  • regel 20: hier moet men bedenken dat de server bij het openen van de verbinding een eerste regel naar de client stuurt, namelijk het welkomstbericht „Welkom op de server voor belastingberekening”. Dit bericht wordt gelezen en genegeerd.
  • regel 22: we sturen het volgende commando naar de server: „impot o 2 60000” om de server te vragen de belasting te berekenen voor een gehuwde persoon met twee kinderen en een jaarsalaris van 60.000 euro.
  • regel 24: de server antwoordt met het belastingbedrag in de vorm "4282" of met een foutmelding als het commando onjuist was opgesteld (dat zal hier niet gebeuren) of als er een probleem is opgetreden bij de berekening van de belasting. Dit laatste geval wordt hier niet afgehandeld, maar het zou zeker "netter" zijn geweest om dat wel te doen. Als de gelezen regel namelijk een foutmelding is, wordt er een uitzondering gegenereerd omdat de conversie naar een geheel getal mislukt. De uitzondering die door de grafische interface wordt opgevangen, is dan een conversiefout, terwijl de oorspronkelijke uitzondering van een heel andere aard is. De lezer wordt uitgenodigd om deze code te verbeteren.
  • regels 25-28: vrijgeven van alle bronnen die met een „using“-clausule zijn gebruikt.

De laag [metier] wordt gecompileerd in de DLL ImpotsV8-metier.dll:

Image

11.9.4.3. De laag [ui]

De laag [ui] [1,3] is dezelfde als die welke in versie 7 in paragraaf 9.8.4 wordt besproken, op drie details na:

  • de configuratie van de laag [metier] in [App.config] is anders omdat de implementatie ervan is gewijzigd
  • de grafische interface [Form1.cs] is aangepast om een eventuele uitzondering weer te geven
  • de laag [metier] bevindt zich in de DLL [ImpotsV8-metier.dll].

Het bestand [App.config] ziet er als volgt uit:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <object name="metier" type="Metier.ImpotMetierTcp, ImpotsV8-metier">
                <property name="Serveur" value="localhost"/>
                <property name="Port" value="27"/>
            </object>
        </objects>
    </spring>
</configuration>
  • regel 16: instantiëring van de laag [metier] met de klasse Metier.ImpotMetierTcp uit de DLL ImpotsV8-metier.dll
  • regels 17-18: de eigenschappen Server en Poort van de klasse Metier.ImpotMetierTcp worden geïnitialiseerd. De server staat op de machine localhost en draait op poort 27.

De grafische interface die aan de gebruiker wordt getoond, ziet er als volgt uit:

  • In [1] is een TextBox toegevoegd om een eventuele uitzondering weer te geven. Dit veld bestond niet in de vorige versie.

Afgezien van dit detail is de code van het formulier dezelfde als die welke reeds in paragraaf 6.4.3 is besproken. De lezer wordt verwezen naar die paragraaf. In [2] zien we een uitvoervoorbeeld dat is verkregen met een server die als volgt is gestart:

1
2
3
4
5
6
7
8
9
Serveur de calcul d'impôt >start 27
Serveur de calcul d'impôt lancé sur le port 27
Serveur de calcul d'impôt >echo start
Serveur de calcul d'impôt >
...
Début du service au client 9
<--- Client 9 : impot o 2 60000
---> Client 9 : 4282
Fin du service au client 9

De schermafbeelding [2] van de client komt overeen met de regels van client 9 hierboven.

11.9.5. Conclusie

Opnieuw konden we bestaande code hergebruiken, zonder aanpassingen (serverlagen [metier], [dao]) of met zeer weinig aanpassingen (clientlaag [ui]). Dit werd mogelijk gemaakt door ons systematische gebruik van interfaces en het instantiëren daarvan met Spring. Als we in versie 7 de bedrijfslogica rechtstreeks in de gebeurtenishandlers van de grafische interface hadden geplaatst, zou deze bedrijfslogica niet herbruikbaar zijn geweest. Dit is het grootste nadeel van 1-laagsarchitecturen.

Tot slot valt op te merken dat de laag [ui] geen weet heeft van het feit dat een externe server het belastingbedrag voor haar berekent.