Skip to content

11. Programming Internet

11.1. General

11.1.1. The Internet protocols

We give ici an introduction to the Internet communication protocols, also known as the protocol suite TCP/IP (Transfer Control Protocol / Internet Protocol), named after the two main protocols. It may be useful for the reader to have a general understanding of how networks work, and in particular of the TCP/IP protocols, before tackling the construction of distributed applications. The following text is a partial translation of a text found in the document "Lan Workplace for Dos - Administrator's Guide" from NOVELL, document from the early 90s.


The general concept of creating a network of heterogeneous computers comes from research carried out by the DARPA (Defense Advanced Research Projects Agency) in the USA. The DARPA developed the protocol suite known as TCP/IP, which enables heterogeneous machines to communicate with each other. These protocols have been tested on a network called ARPAnet, network, which later became the INTERNET. The TCP/IP protocols define transmission and reception formats and rules that are independent of network organization and the hardware used.

The network designed by DARPA and managed by the TCP/IP protocols is a DARPA network packet switching. Such a network transmits information over the network, in small chunks called packages. So, if a computer transmits a large file, it will be broken down into smaller pieces, which will be sent over the network to be recomposed at their destination. TCP/IP defines the format of these packets, i.e. :

  • package origin
  • destination
  • length
  • type

11.1.2. The OSI model

The TCP/IP protocols roughly follow the open network model called OSI (Open Systems Interconnection Reference Model) defined by theISO (International Standards Organization). This model describes an ideal network where communication between machines can be represented by a seven-layer model:

Each layer receives services from the lower layer and offers its own to the higher layer. Suppose two applications on different machines A and B want to communicate: they do so at layer Application. They do not need to know all the details of how the network works: each application hands over the information it wishes to transmit to the layer below: the Presentation. The application only needs to know the rules for interfacing with the Presentation.

Once the information in the Presentation, it has been transferred to the Session and so on, until the information arrives on the physical medium and is physically transmitted to the destination machine. There, it undergoes the reverse treatment to that which it underwent on the sending machine.

At each layer, the sending process responsible for sending the information sends it to a receiving process on the other machine belonging to the same layer as itself. It does so according to certain rules known as the protocol layer. This gives us the following final communication diagram:

The role of the different layers is as follows:

Physique
Ensures the transmission of bits on a physical medium. This layer includes data processing terminal equipment (E.T.T.D.) such as a terminal or computer, as well as data circuit termination equipment (E.T.C.D.) such as a modulator/demodulator, multiplexer or concentrator. Points of interest at this level are :
  • choice of information coding (analog or digital)
  • choice of transmission mode (synchronous or asynchronous).
Liaison de données
Masks the physical characteristics of the physical layer. Detects and corrects transmission errors.
Réseau
Manages the path taken by information sent over the network. This is called the routing : determine the route an item of information must take to reach its destination.
Transport
Allows communication between two applications, whereas previous layers only allowed machine-to-machine communication. One service provided by this layer is multiplexing: the transport layer can use the same network connection (from machine to machine) to transmit information belonging to several applications.
Session
This layer contains services enabling an application to open and maintain a work session on a remote machine.
Présentation
Its aim is to standardize the representation of data on different machines. In this way, data from machine A will be "dressed" by the Presentation from machine A, in a standard format, before being sent over the network. Once they reach the Presentation machine B, which will recognize them thanks to their standard format, they will be dressed in another way so that the machine B application can recognize them.
Application
At this level, we find applications that are generally close to the user, such as e-mail and file transfer.

11.1.3. The TCP/IP model

The OSI model is an ideal model that has never yet been realized. The TCP/IP protocol suite approaches it in the following form:

Physical layer

For local area networks, we generally use Ethernet or Token-Ring. We present ici Ethernet technology only.

Ethernet

This is the name given to a packet-switched LAN technology invented at PARC Xerox in the early 1970s and standardized by Xerox, Intel and Digital Equipment in 1978. The network physically consists of a coaxial cable with a diameter of around 1.27 cm and a maximum length of 500 m. It can be extended by means of repeaters, no two machines can be separated by more than two repeaters. The cable is passive: all active elements are on the machines connected to the cable. Each machine is connected to the cable by a network access card comprising :

  • a transmitter (transceiver) which detects the presence of signals on the cable and converts analog signals into digital and vice versa.
  • a coupler that receives digital signals from the transmitter and transmits them to the computer for processing, or vice versa.

The main features of Ethernet technology are as follows:

  • 10 Megabits/second capacity.
  • Bus topology: all machines are connected to the same cable
  • Broadcasting network - A transmitting machine transfers information over the cable with the address of the destination machine. All connected machines then receive this information, and only the one for which it is intended retains it.
  • The access method is as follows: the transmitter wishing to transmit listens to the cable - it then detects the presence or absence of a carrier wave, which would mean that a transmission is in progress. This is the CSMA (Carrier Sense Multiple Access). In the absence of a carrier, a transmitter may decide to transmit in turn. Several transmitters may take this decision. The transmitted signals are mixed: we say there is collision. The transmitter detects this situation: at the same time as it transmits on the cable, it listens to what is actually passing over it. If it detects that the information transiting on the cable is not the one it has transmitted, it deduces that there is a collision and stops transmitting. The other transmitters will do the same. Each will resume transmission after a random time depending on each transmitter. This technique is called CD (Collision Detect). The access method is called CSMA/CD.
  • 48-bit addressing. Each machine has an address, called ici physical address, which is written on the card that connects it to the cable. This address is called the Ethernet of the machine.

Network layer

This layer includes the IP, ICMP, ARP and RARP protocols.

IP (Internet Protocol)
Delivers packets between two network nodes
ICMP 
(Internet Control Message Protocol)
ICMP enables communication between the IP protocol program of one machine and that of another. It is therefore a message exchange protocol within the IP protocol.
ARP
(Address Resolution Protocol)
maps Internet machine address to physical machine address
RARP
(Reverse Address Resolution Protocol)
maps physical machine address to Internet machine address

Transport/Session layers

This layer includes the following protocols:

TCP (Transmission Control Protocol)
Ensures reliable information transfer between two customers
UDP (User Datagram Protocol)
Ensures unreliable delivery of information between two customers

Application/Presentation/Session layers

There are ici various protocols:

TELNET
Terminal emulator allowing machine A to connect to machine B as a terminal
FTP (File Transfer Protocol)
enables file transfers
TFTP (Trivial File Transfer Protocol)
enables file transfers
SMTP (Simple Mail Transfer protocol)
allows messages to be exchanged between network users
DNS (Domain Name System)
transforms a machine name into a Internet machine address
XDR (eXternal Data Representation)
created by sun MicroSystems, it specifies a standard, machine-independent data representation
RPC(Remote Procedures Call)
also defined by Sun, is a communication protocol between remote applications, independent of the transport layer. This protocol is important: it relieves the programmer of knowledge of transport layer details and makes applications portable. This protocol is based on the XDR protocol
NFS (Network File System)
still defined by Sun, this protocol allows one machine to "see" the file system of another. It is based on the previous RPC protocol

11.1.4. How Internet protocols work

Applications developed in the TCP/IP environment generally use several of this environment's protocols. An application program communicates with the highest protocol layer. This passes the information to the layer below, and so on until it reaches the physical medium. Here, the information is physically transferred to the machine where it passes through the same layers again, this time in the opposite direction, until it reaches the application to which the information has been sent. The following diagram shows the information path:

Let's take an example: the FTP application, defined in the Application which enables file transfers between machines.

  • The application delivers a sequence of bytes to be transmitted to the transport.
  • Layer transport cuts this sequence of bytes into segments TCP, and adds the segment number to the beginning of each segment. Segments are passed to the Network layer, governed by the IP.
  • The IP layer creates a packet encapsulating the received TCP segment. At the head of this packet, it places the Internet addresses of the source and destination machines. It also determines the physical address of the destination machine. All this is passed on to the Data link & Physical link, i.e. the network card that connects the machine to the physical network.
  • Here, the IP package is in turn encapsulated in a frame and sent to its recipient over the cable.
  • On the receiving machine, the Data link & Physical link does the opposite: it decapsulates the IP packet from the physical frame and passes it to the IP layer.
  • Layer IP checks that the packet is correct: it calculates a sum based on the bits received (checksum), which must be found in the packet header. If this is not the case, the packet is rejected.
  • If the packet is declared correct, the IP layer unencapsulates the TCP segment contained in it and passes it on to the IP layer transport.
  • Layer transport, layer TCP in our example, examines the segment number to restore the correct segment order.
  • It also calculates a checksum for the TCP segment. If it is found to be correct, the TCP layer sends an acknowledgement to the source machine, otherwise the TCP segment is rejected.
  • All that's left for the TCP layer is to transmit the data part of the segment to the destination application in the layer above.

11.1.5. Addressing problems in Internet

A node of a network can be a computer, a smart printer, a file server, in fact anything that can communicate using the TCP/IP protocols. Each node has a physical address whose format depends on the type of network. On an Ethernet network, the physical address is encoded in 6 bytes. An X25 network address is a 14-digit number.

L'address Internet of a node is an address logic : it is independent of the hardware and network used. It's a 4-byte address identifying both a local network and a node on that network. The Internet address is usually represented as 4 numbers, the values of the 4 bytes, separated by a dot. For example, the address of the Lagaffe machine at the Faculty of Science in Angers is 193.49.144.1, and that of the Liny machine is 193.49.144.9. We deduce that the Internet address of the local network is 193.49.144.0. We can have up to 254 nodes on this network.

Because Internet or IP addresses are network-independent, a machine on network A can communicate with a machine on network B regardless of the type of network it's on: it just needs to know its IP address. The IP protocol on each network takes care of the IP <--> physical address conversion, in both directions.

Addresses IP must all be different. In France, INRIA is responsible for assigning IP addresses. In fact, this organization issues an address for your local network, e.g. 193.49.144.0 for the network of the Faculty of Science in Angers. The network administrator can then assign the IP addresses 193.49.144.1 to 193.49.144.254 as he sees fit. This address is usually written to a special file on each machine connected to the network.

11.1.5.1. IP address classes

A IP address is a sequence of 4 bytes, often referred to as I1.I2.I3.I4, which actually contains two addresses:

  • network address
  • the address of a node in this network

Depending on the size of these two fields, IP addresses are divided into 3 classes: classes A, B and C.

Class A

The address IP : I1.I2.I3.I4 has the form R1.N1.N2.N3 where

R1 is the network address

N1.N2.N3 is the address of a machine in this network

More precisely, the form of a class A IP address is as follows:

The network address is 7 bits and the node address is 24 bits. We can therefore have 127 Class A networks, each with up to 224 knots.

Class B

Ici, address IP : I1.I2.I3.I4 has the form R1.R2.N1.N2 where

R1.R2 is the network address

N1.N2 is the address of a machine in this network

More precisely, the form of a class B IP address is as follows:

The network address is on 2 bytes (14 bits exactly), as is the node address. We can therefore have 214 class B networks, each comprising up to 216 knots.

Class C

In this class, the address IP : I1.I2.I3.I4 has the form R1.R2.R3.N1 where

R1.R2.R3 is the network address

N1 is the address of a machine in this network

More precisely, the form of a class C IP address is as follows:

The network address is 3 bytes (minus 3 bits) and the node address is 1 byte. We can therefore have 221 class C networks with up to 256 nodes.

Machine address Lagaffe of the Faculty of Science in Angers being 193.49.144.1, we can see that the most significant byte is 193, i.e. in binary 11000001. This means that the network is class C.

Reserved addresses

  • Some IP addresses are network addresses rather than addresses of nodes in the network. These are addresses where the node address is set to 0. For example, address 193.49.144.0 is the IP address of the Faculté des Sciences d'Angers network. Consequently, no node on a network can have address zero.
  • When the node address in a IP address contains only 1's, we have a broadcast address: this address designates all network nodes.
  • In a class C network, theoretically allowing 28=256 nodes, if we remove the two prohibited addresses, we're left with 254 authorized addresses.

11.1.5.2. Conversion protocols Address Internet <--> Physical address

We have seen that when information is transmitted from one machine to another, it is encapsulated in packets as it passes through the IP layer. These take the following form:

The IP packet therefore contains the Internet addresses of the source and destination machines. When this packet is transmitted to the layer responsible for sending it on the physical network, other information is added to it to form the physical frame that will finally be sent on the network. For example, the format of a frame on an Ethernet network is as follows:

The final frame contains the physical addresses of the source and destination machines. How are they obtained?

The sending machine, knowing the IP address of the machine it wishes to communicate with, obtains the latter's physical address using a special protocol called ARP (Address Resolution Protocol).

  • It sends a special type of packet called a ARP packet, containing the IP address of the machine whose physical address we're looking for. It has also taken care to include its own IP address, as well as its physical address.
  • This packet is sent to all network nodes.
  • These recognize the special nature of the packet. The node that recognizes its IP address in the packet responds by sending the sender of the packet its physical address. How can it do this? It has found the sender's IP and physical addresses in the packet.
  • The sender receives the physical address he was looking for. He stores it in memory so that he can use it later if he needs to send other packets to the same recipient.

A machine's IP address is normally recorded in one of its files, which it can consult to find out what it is. This address can be changed by editing the file. The physical address, on the other hand, is stored in a memory on the network card and cannot be changed.

When an administrator wishes to organize his network differently, he may have to change the IP addresses of all the nodes and therefore edit the various configuration files for the different nodes. This can be tedious and error-prone if there are a lot of machines. One method consists in not assigning a IP address to machines: a special code is written to the file in which the machine should find its IP address. Discovering that it doesn't have a IP address, the machine requests it using a protocol called RARP (Reverse Address Resolution Protocol). It then sends a special packet on a network, called a RARP packet, analogous to the ARP packet above, in which it puts its physical address. This packet is sent to all nodes, which then recognize a RARP packet. One of them, called server RARP, has a file containing the physical address <--> IP address of all nodes. It then replies to the sender of the RARP packet, sending back his IP address. An administrator wishing to reconfigure his network simply needs to edit the RARP server's correspondence file. This should normally have a fixed IP address, which he should be able to find out without having to use the RARP protocol himself.

11.1.6. The IP network layer of the internet

The IP protocol (Internet Protocol) defines the form that packets should take and how they should be handled when sent or received. This particular type of packet is called a datagram IP. We have already presented :

The important thing is that, in addition to the data to be transmitted, the IP datagram contains the Internet addresses of the source and destination machines. In this way, the receiving machine knows who is sending it a message.

Unlike a network frame, whose length is determined by the physical characteristics of the network over which it travels, the length of datagram IP is fixed by the software and will therefore be the same on different physical networks. As we have seen, the IP datagram is encapsulated in a physical frame as it descends from the network layer to the physical layer. We have given the example of the physical frame of an Ethernet network:

Physical frames travel from node to node towards their destination, which may not be on the same physical network as the sending machine. The IP packet may therefore be successively encapsulated in different physical frames at the nodes that link two different types of network. It is also possible that the IP packet is too large to be encapsulated in a physical frame. The IP software of the node where this problem occurs, then breaks down the IP packet into fragments according to precise rules, each of which is then sent over the physical network. They are not reassembled until they reach their final destination.

11.1.6.1. Routing

Routing is the method of routing IP packets to their destination. There are two methods: direct routing and indirect routing.

Direct routing

Direct routing refers to the routing of a IP packet directly from the sender to the recipient within the same network:

  • The machine sending a IP datagram has the IP address of the recipient.
  • It obtains the latter's physical address via the ARP protocol or from its tables, if this address has already been obtained.
  • It sends the packet over the network to this physical address.

Indirect routing

Indirect routing refers to the routing of a IP packet to a destination on a network other than that to which the sender belongs. In this case, the network address parts of the IP addresses of the source and destination machines are different. The source machine recognizes this point. It then sends the packet to a special node called router (router), the node that connects a local network to other networks and whose IP address it finds in its tables, an address initially obtained either in a file or in permanent memory, or via information circulating on the network.

A router is attached to two networks and has a IP address within these two networks.

In our example above :

. Network #1 has address Internet 193.49.144.0 and network #2 has address 193.49.145.0.

. Within network no. 1, the router has address 193.49.144.6, and within network no. 2, it has address 193.49.145.3.

The router's role is to put the IP packet it receives, which is contained in a physical frame typical of network no. 1, into a physical frame that can circulate on network no. 2. If the IP address of the packet's recipient is in network no. 2, the router will send the packet directly to it, otherwise it will send it to another router, connecting network no. 2 to network no. 3, and so on.

11.1.6.2. Error and control messages

Still in the network layer, at the same level as the IP protocol, there is the ICMP (Internet Control Message Protocol). It is used to send messages about the internal workings of the network: nodes down, congestion at a router, etc ... Messages ICMP are encapsulated in IP packets and sent over the network. The IP layers of the various nodes take appropriate action according to the ICMP messages they receive. In this way, an application itself never sees these network-specific problems.

A node will use ICMP information to update its routing tables.

11.1.7. The transport layer: UDP and TCP protocols

11.1.7.1. The UDP protocol: User Datagram Protocol

The UDP protocol enables an unreliable exchange of data between two points, i.e. the correct routing of a packet to its destination is not guaranteed. The application can manage this itself, for example by waiting for an acknowledgement of receipt after sending a message, before sending the next one.

For the moment, at network level, we've been talking about IP machine addresses. on a machine, different processes can coexist at the same time, and all of them can communicate. When sending a message, you must therefore specify not only the IP address of the destination machine, but also the "name" of the destination process. This name is actually a number, called port number. Some numbers are reserved for standard applications: port 69 for the tftp (trivial file transfer protocol) for example.

Packets managed by the UDP protocol are also called datagrams. They take the following form:

These datagrams are encapsulated in IP packets, then in physical frames.

11.1.7.2. The TCP protocol: Transfer Control Protocol

For secure communications, the UDP protocol is insufficient: the application developer must develop his own protocol to detect the correct routing of packets. The protocol TCP (Transfer Control Protocol) avoids these problems. Its features are as follows:

  • The process that wishes to issue first establishes a connection with the process receiving the information it is going to send. This connection is made between a port on the sending machine and a port on the receiving machine. A virtual path is thus created between the two ports, which will be reserved for the two processes that have made the connection.
  • All packets sent by the source process follow this virtual path and arrive in the order in which they were sent, which was not guaranteed in the UDP protocol, since packets could follow different paths.
  • The information sent is continuous. The transmitting process sends information at its own pace. This information is not necessarily sent immediately: the TCP protocol waits until it has enough information to send it. They are stored in a structure called segment TCP. Once completed, this segment is transmitted to the IP layer, where it is encapsulated in a IP packet.
  • Each segment sent by the TCP protocol is numbered. The receiving TCP protocol checks that it has received the segments in sequence. For each segment correctly received, it sends an acknowledgement to the sender.
  • When the latter receives it, it informs the sending process. This means that the sending process knows that a segment has arrived safely, which was not possible with the UDP protocol.
  • If, after a certain time, the TCP protocol that transmitted a segment does not receive an acknowledgement, it retransmits the segment in question, thus guaranteeing the quality of the information routing service.
  • The virtual circuit established between the two communicating processes is full-duplex : this means that information can flow in both directions. In this way, the destination process can send acknowledgements while the source process continues to send information. This allows the TCP source protocol, for example, to send several segments without waiting for an acknowledgement. If it realizes after a certain time that it has not received acknowledgement of a certain segment no n, it will resume segment broadcasting at this point.

11.1.8. The Applications layer

Above the UDP and TCP protocols, there are various standard protocols:

TELNET

This protocol allows a user on machine A of the network to connect to machine B (often called the host machine). TELNET emulates a universal terminal on machine A. The user then behaves as if he or she had a terminal connected to machine B. Telnet is based on the TCP protocol.

FTP : (File Transfer protocol)

This protocol enables files to be exchanged between two remote machines, as well as file manipulations such as directory creation. It is based on the TCP protocol.

TFTP: (Trivial File Transfer Control)

This protocol is a variant of FTP. It is based on the UDP protocol and is less sophisticated than FTP.

DNS : (Domain Name System)

When a user wishes to exchange files with a remote machine, by FTP for example, he needs to know the Internet address of this machine. For example, to perform FTP on the Lagaffe machine at the University of Angers, you would need to run FTP as follows: FTP 193.49.144.1

This would require a directory mapping machine &lt;--&gt; address IP. In this directory, machines would probably be designated by symbolic names such as :

machine DPX2/320 from Angers University

sun machine from ISERPA in Angers

Clearly, it would be nicer to refer to a machine by name rather than by its IP address. Then there&#x27;s the problem of name uniqueness: there are millions of interconnected machines. We could imagine a centralized body assigning names. This would undoubtedly be rather cumbersome. The control of names has in fact been distributed in **fields**. Each domain is managed by a very small organization, which is free to choose its own machine names. For example, machines in France belong to the **en**, domain managed by the Inria in Paris. To keep things simple, we distribute control even further: domains are created within the **en**. The University of Angers belongs to the **univ-Angers**. The department managing this domain is free to name the machines on the Université d&#x27;Angers network. For the moment, this domain has not been subdivided. But in a large university with many networked machines, it could be.

The DPX2/320 machine at the University of Angers was named *Lagaffe* while a PC 486DX50 was named *liny*. How do you reference these machines from the outside? By specifying the domain hierarchy to which they belong. For example, the full name of the Lagaffe machine would be :

    **Lagaffe.univ-Angers.fr**

Within domains, relative names can be used. Thus, within the **en** and outside the field **univ-Angers**, the Lagaffe machine can be referenced by

    **Lagaffe.univ-Angers**

Finally, within the *univ-Angers*, it can be referenced simply by

    **Lagaffe**

An application can therefore reference a machine by name. At the end of the day, you still need to obtain the machine&#x27;s Internet address. How is this achieved? Suppose you want to communicate from machine A to machine B.
  • if machine B belongs to the same domain as machine A, we'll probably find its IP address in a file on machine A.
  • otherwise, machine A will find a list of some name servers with their IP addresses. A name server is responsible for mapping a machine name to its IP address. Machine A will send a special request to the first name server in its list, called a DNS request, including the name of the machine it's looking for. If the queried server has this name in its tablets, it will send machine A the corresponding IP address. If not, the server will also find a list of name servers in its files, which it will send to machine A can question. It will then do so. In this way, a number of name servers will be queried, not anarchically, but in such a way as to minimize the number of queries. If the machine is finally found, the answer will go back down to machine A.

XDR : (eXternal Data Representation)

Created by sun MicroSystems, this protocol specifies a standard, machine-independent representation of data.

RPC : (Remote Procedure Call)

Also defined by sun, this is a communication protocol between remote applications, independent of the transport layer. This protocol is important: it relieves the programmer of knowledge of transport layer details and makes applications portable. This protocol is based on the XDR protocol

NFS : Network File System

Still defined by Sun, this protocol allows one machine to &quot;see&quot; the file system of another. It is based on the previous RPC protocol.

11.1.9. Conclusion

In this introduction, we've presented a few outlines of the Internet protocols. For a more in-depth look at this field, read the excellent book by Douglas Comer :

Title TCP/IP: Architecture, Protocols, Applications.

Author Douglas COMER

Publisher InterEditions

11.2. The .NET classes for IP address management

A machine on the Internet network is uniquely defined by a IP (Internet Protocol) address, which can take two forms:

  • IPv4 : coded on 32 bits and represented by a string of the form "I1.I2.I3.I4" where In is a number between 1 and 254. These are currently the most common IP addresses.
  • IPv6: coded on 128 bits and represented by a string of the form "[I1.I2.I3.I4.I5.I6.I7.I8]" where In is a string of 4 hexadecimal digits. In this document, we won't use IPv6 addresses.

A machine can also be defined by an equally unique name. This name is not mandatory, as applications always end up using the IP addresses of the machines. It's easier, for example, to request the URL from a browser http://www.ibm.com that the URL http://129.42.17.99 although both methods are possible.

A machine can have several IP addresses if it is physically connected to several networks at the same time. It then has a IP address on each network.

An address IP can be represented in two ways in .NET :

  • as string "I1.I2.I3.I4" or "[I1.I2.I3.I4.I5.I6.I7.I8]"
  • in the form of a IPAddress

The IPAddress class

Among the M methods, P properties and C constants of the IPAddress, are the following:

AddressFamily AddressFamily
P
address family IP. The type AddressFamily is an enumeration. The two most common values are :
AddressFamily.InterNetwork : for a IPv4 address
AddressFamily.InterNetworkV6 : for a IPv6 address
IPAddress Any
C
address IP "0.0.0.0". When a service is associated with this address, this means it accepts clients on all IP addresses of the machine on which it operates.
IPAddress LoopBack
C
address IP "127.0.0.1". Known as the "loopback address". When a service is associated with this address, it means that it only accepts clients that are on the same machine as it.
IPAdress None
C
address IP "255.255.255.255". When a service is associated with this address, this means that it accepts no customers.
bool TryParse(string ipString, out IPAddress address)
M
tries to pass address IP ipString form "I1.I2.I3.I4" in the form of a IPAddress address. Makes true if the operation was successful.
bool IsLoopBack
M
returns true if address IP is "127.0.0.1"
string ToString()
M
renders address IP as "I1.I2.I3.I4" or "[I1.I2.I3.I4.I5.I6.I7.I8]"

The association address IP <--> nomMachine is provided by a distributed internet service called DNS (Domain Name System) class. The static methods of the Dns make the association address IP <--> nomMachine :

GetHostEntry (string hostNameOrdAddress)
returns an address IPHostEntry from a IP address as a string or from a machine name. Throws an exception if the machine cannot be found.
GetHostEntry (IPAddress ip)
returns an address IPHostEntry from a IP address of type IPAddress. Throws an exception if the machine cannot be found.
string GetHostName()
returns the name of the machine running the program that plays this instruction
IPAddress[] GetHostAddresses(string hostNameOrdAddress)
returns the IP addresses of the machine identified by its name or one of its IP addresses.

An instance IPHostEntry encapsulates IP addresses, aliases and machine names. The type IPHostEntry is as follows:

IPAddress[] AddressList
P
table of machine IP addresses
String[] Aliases
P
the machine's DNS aliases. These are the names corresponding to the machine's various IP addresses.
string HostName
P
machine's main host name

Consider the following program which displays the name of the machine on which it is running, and then interactively provides address correspondences IP <--> machine name :


using System;
using System.Net;
 
namespace Chap9 {
    class Program {
        static void Main(string[] args) {
             // displays the name of the local machine
             // then interactively provides information on network machines
             // identified by name or address IP
 
             // local machine
            Console.WriteLine("Machine Locale= {0}" ,Dns.GetHostName());
 
             // interactive Q&A
            string machine;
            IPHostEntry ipHostEntry;
            while (true) {
                 // enter the name or IP address of the machine you are looking for
                Console.Write("Machine recherchée (rien pour arrêter) : ");
                machine = Console.ReadLine().Trim().ToLower();
                 // finished?
                if (machine == "") return;
                 // management exception
                try {
                     // machine search
                    ipHostEntry = Dns.GetHostEntry(machine);
                     // machine name
                    Console.WriteLine("Machine : " + ipHostEntry.HostName);
                     // the machine's IP addresses
                    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();
                     // machine aliases
                    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 {
                     // the machine doesn't exist
                    Console.WriteLine("Impossible de trouver la machine [{0}]",machine);
                }
            }
        }
    }
}

Execution gives the following results:

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. Programming basics internet

11.3.1. General

Consider the communication between two remote machines A and B :

When an application AppA of a machine A wants communicate with an application AppB machine B of the Internet, it needs to know several things:

  • address IP or machine name B
  • the port number with which the application works AppB. Machine B can support a large number of applications working on the Internet. When it receives information from the network, it needs to know which application the information is intended for. Machine B's applications have access to the network via windows, also known as communication ports. This information is contained in the packet received by machine B so that it can be delivered to the right application.
  • communication protocols understood by machine B. In our study, we'll be using TCP-IP protocols only.
  • the dialog protocol accepted by the application AppB. In effect, machines A and B are going to "talk" to each other. What they say is encapsulated in TCP-IP protocols. However, at the end of the chain, the application AppB will receive the information sent by the AppA, it must be able to interpret it. This is analogous to the situation where two people, A and B, communicate by telephone: their dialogue is carried by the telephone. The speech is encoded in signal form by telephone A, transported over telephone lines, and arrives at telephone B to be decoded. Person B then hears speech. This is where the notion of dialogue protocol comes into play: if A speaks French and B doesn't understand the language, A and B won't be able to dialogue effectively.

The two communicating applications must therefore agree on the type of dialogue they will adopt. For example, dialogue with a ftp is not the same as with a service pop : these two services do not accept the same orders. They have a different dialogue protocol.

11.3.2. Characteristics of the TCP protocol

We will only study ici network communications using the TCP transport protocol. Let's recall ici, its characteristics:

  • The process that wishes to issue first establishes a connection with the process receiving the information it is going to send. This connection is made between a port on the sending machine and a port on the receiving machine. A virtual path is thus created between the two ports, which will be reserved for the two processes that have made the connection.
  • All packets sent by the source process follow this virtual path and arrive in the order in which they were sent
  • The information sent is continuous. The transmitting process sends information at its own pace. This information is not necessarily sent immediately: the TCP protocol waits until it has enough information to send it. They are stored in a structure called segment TCP. Once completed, this segment is transmitted to the IP layer, where it is encapsulated in a IP packet.
  • Each segment sent by the TCP protocol is numbered. The receiving TCP protocol checks that it has received the segments in sequence. For each segment correctly received, it sends an acknowledgement to the sender.
  • When the latter receives it, it indicates this to the sending process. This means that the sending process knows that a segment has arrived safely.
  • If, after a certain time, the TCP protocol that transmitted a segment does not receive an acknowledgement, it retransmits the segment in question, thus guaranteeing the quality of the information routing service.
  • The virtual circuit established between the two communicating processes is full-duplex : this means that information can flow in both directions. In this way, the destination process can send acknowledgements while the source process continues to send information. This allows the TCP source protocol, for example, to send several segments without waiting for an acknowledgement. If, after a certain period of time, it realizes that it has not received an acknowledgement for a certain segment, n° n, it will resume segment broadcasting at this point.

11.3.3. The client-server relationship

Often, communication on Internet is asymmetrical: machine A initiates a connection to request a service from machine B: it specifies that it wants to open a connection with machine B's SB1 service. Machine B accepts or refuses. If it accepts, machine A can send its requests to the SB1 service. These must conform to the dialog protocol understood by the SB1 service. A request-response dialogue is thus established between machine A, which we call machine customer and machine B, which we call server. One of the two partners will close the connection.

11.3.4. Customer architecture

The architecture of a network program requesting the services of a server application will be as follows:

        ```text
        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. Server architecture

The architecture of a program offering services will be as follows:

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

The server program handles a client's initial connection request differently from its subsequent requests for service. The program does not provide the service itself. If it did, during the service period it would no longer be listening to connection requests, and customers would not be served. It therefore proceeds differently: as soon as a connection request is received on the listening port and accepted, the server creates a task responsible for rendering the service requested by the client. This service is rendered on another port on the server machine, called service port. This means you can serve several customers at the same time.

A service task will have the following structure:

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. Discover the communication protocols of the internet

11.4.1. Introduction

Once a client has connected to a server, a dialogue is established between them. The nature of this dialogue forms what is known as the server's communication protocol. Among the most common internet protocols are the following:

  • HTTP : HyperText Transfer Protocol - the protocol for dialogue with a web server (HTTP server)
  • SMTP: Simple Mail Transfer Protocol - the protocol for communicating with an e-mail server (SMTP server)
  • POP : Post Office Protocol - the protocol for dialog with an e-mail storage server (POP server). The purpose is to retrieve incoming e-mails, not to send them.
  • FTP : File Transfer Protocol - the protocol used to communicate with a file storage server (FTP server).

All these protocols have the distinctive feature of being text-line protocols: the client and server exchange lines of text. If we have a client capable of :

  • create a connection with a Tcp server
  • display text lines sent by the server to the console
  • send text lines entered by a user to the server

then you'll be able to communicate with a Tcp server using a text line protocol, provided you know the rules of this protocol.

The program telnet found on Unix or Windows machines is such a client. On Windows machines, there is also a tool called putty and we're going to use ici. putty can be downloaded at [http://www.putty.org/]. It is a directly usable executable (.exe). We'll configure it as follows:

  • [1]: the IP address of the Tcp server you want to connect to, or its name
  • [2]: server listening port Tcp
  • [3]: take mode Raw which designates a raw Tcp connection.
  • [4]: take mode Never to prevent the client window putty to close if the server closes the connection.
  • [6,7]: number of console columns/rows
  • [5]: maximum number of lines stored in memory. A HTTP server can send a lot of lines. You need to be able to "scroll" over them.
  • [8,9]: to retain previous settings, name the configuration [8] and save it [9].
  • [11,12]: to retrieve a saved configuration, select it [11] and load it [12].

With this tool thus configured, let's take a look at some TCP protocols.

11.4.2. The HTTP protocol (HyperText Transfer Protocol)

Let's connect [1] our client TCP to the server web on the machine istia.univ-angers.fr [2], port 80 [3] :

In the putty, we build dialogue HTTP next :

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
  • lines 1-4 are the customer's request, typed on the keyboard
  • lines 5-19 are the server response
  • line 1: syntax GET UrlDocument HTTP/1.1 - we request the Url /, c.a.d. site root web [istia.univ-angers.fr].
  • line 2: syntax Host: machine:port
  • line 3: syntax Connection: [connection mode]. The [close] mode tells the server to close the connection once it has sent its response. Keep-Alive] mode tells the server to leave the connection open.
  • line 4: empty line. Lines 1-3 are called headers HTTP. There may be others than those shown ici. The end of HTTP headers is indicated by an empty line.
  • lines 5-13: HTTP headers in the server response - again ending with an empty line.
  • lines 14-19: the document sent by the server, ici a document HTML
  • line 5: syntax HTTP/1.1 msg code - code 200 indicates that the requested document has been found.
  • line 6: server date and time
  • line 7: software identification for service web - ici an Apache server on Linux / Debian
  • line 8: the document was dynamically generated by PHP
  • line 9: customer identification cookie - if the customer wants to be recognized the next time he connects, he must return this cookie in his HTTP headers.
  • line 10: indicates that after serving the requested document, the server will close the connection
  • line 11: the document will be transmitted in pieces (chunked) rather than as a single block.
  • line 12: type of document : ici a document HTML
  • line 13: the blank line that signals the end of the server's HTTP headers
  • line 14: hexadecimal number indicating the number of characters in the 1st block of the document. When this number equals 0 (line 19), the customer will know that he has received the entire document.
  • lines 15-18: part of the document received.

The connection has been closed and the customer putty is inactive. Let's reconnect [1] and clear the screen of previous displays [2,3] :

The dialogue this time is as follows:

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
  • line 1: a non-existent document has been requested
  • line 5: server HTTP responded with code 404, meaning that the requested document was not found.

If you request this document with a Firefox browser :

Image

If we ask to see the source code [Display/Source code] :

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>

We obtain lines 13-22 received by our customer putty. The advantage of this is that it also shows us the HTTP headers of the response. It's also possible to get these with Firefox.

11.4.3. The protocol SMTP (Simple Mail Transfer Protocol)

SMTP servers generally operate on port 25 [2]. We connect to server [1]. for Ici servers, you'll generally need a

belonging to the same IP domain as the machine, as most SMTP servers are configured to accept requests only from machines belonging to the same domain as themselves. Quite often, firewalls or anti-virus software on personal machines are configured not to accept connections to port 25 from an external machine. It may then be necessary to reconfigure [3] this firewall or antivirus.

The SMTP dialog in the client window putty is as follows:

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

Below (D) is a client request, (R) a server response.

  • line 1: (R) server greeting SMTP
  • line 2: (D) command HELO to say hello
  • line 3: (R) server response
  • line 4: (D) sender address, e.g mail from: someone@gmail.com
  • line 5: (R) server response
  • line 6: (D) recipient address, e.g rcpt to: someoneelse@gmail.com
  • line 7: (R) server response
  • line 8: (D) marks the beginning of the message
  • line 9: (R) server response
  • lines 10-12: (D) the message to be sent terminated by a line containing a period only.
  • line 13: (R) server response
  • line 14: (D) the customer signals that he is finished
  • line 15: (R) response from the server, which then closes the connection

11.4.4. The POP protocol (Post Office Protocol)

POP servers generally operate on port 110 [2]. We connect to server [1]. The POP dialog in the client window putty is as follows:

+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.
  • line 1: (R) server welcome message POP
  • line 2: (D) the customer gives his identifier POP, c.a.d. the login with which he reads his mail
  • line 3: (R) the server response
  • line 4: (D) customer password
  • line 5: (R) the server response
  • line 6: (D) the customer asks for a list of his letters
  • lines 7-12: (R) list of messages in the customer's mailbox, in the form [message no. message size in bytes]
  • line 13: (D) message no. 64 is requested
  • lines 14-25: (R) message no. 64 with lines 15-22, the message headers, and lines 23-24 the message body.
  • line 26: (D) the customer says he's finished
  • line 27: (R) response from the server, which then closes the connection.

11.4.5. The FTP protocol (File Transfer Protocol)

The FTP protocol is more complex than those described above. To discover the lines of text exchanged between client and server, you can use a tool such as FileZilla [http://www.filezilla.fr/].

Filezilla is a FTP client offering a windows interface for file transfers. User actions on the windows interface are translated into FTP commands, which are logged in [1]. This is a good way to discover the FTP protocol commands.

11.5. The .NET classes of internet programming

11.5.1. Choosing the right class

The .NET framework offers various classes for working with the :

  • the class Socket is the one that operates closest to the network. It enables fine-tuned management of the network connection. The term socket refers to a power socket. The term has been extended to designate a software network socket. In a TCP-IP communication between two machines A and B, these are two sockets that communicate with each other. An application can work directly with sockets. This is the case for application A above. A socket can be a customer or server.
  • if you wish to work at a lower level than that of the class Socket, you can use the
  • TcpClient to create a Tcp customer
  • TcpListener to create a Tcp server

These two classes offer the application that uses them a simpler view of network communication, managing the technical details of socket management on its behalf.

  • .NET offers specific classes for certain protocols :
  • the class SmtpClient to manage the SMTP protocol for communication with a SMTP server for sending e-mails
  • the class WebClient to manage HTTP or FTP protocols for communication with a web server.

The Socket is sufficient on its own to handle all tcp-ip communication, but we'll focus on using the higher-level classes to make writing the tcp-ip application easier.

11.5.2. The TcpClient class

The TcpClient is the class most suitable for creating the client of a TCP service. Its C constructors, M methods and P properties include the following:

TcpClient(string hostname, int port)
C
creates a tcp link with the service operating on the specified port (port) of the indicated machine (hostname). For example new TcpClient("istia.univ-angers.fr",80) to connect to the machine's port 80 istia.univ-angers.fr
Socket Client
P
the socket used by the client to communicate with the server.
NetworkStream GetStream()
M
gets a read/write flow to the server. It is this flow that enables client-server exchanges.
void Close()
M
closes the connection. Socket and flow NetworkStream are also closed
bool Connected()
P
true if the connection has been established

The class NetworkStream represents the network flow between client and server. She is derived from the Stream. Many client-server applications exchange lines of text terminated by the end-of-line characters "\r\n". This is why it's a good idea to use StreamReader and StreamWriter to read and write these lines in the network stream. So if a machine M1 has established a link with a machine M2 using a TcpClient object customer1 and they exchange lines of text, it can create its read and write flows as follows:

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

Instruction

out1.AutoFlush=true;

means that the customer1 will not pass through an intermediate buffer, but will go directly to the network. This is an important point. In general, when customer1 sends a line of text to its partner, it expects a response. The response will never come if the line was actually buffered on machine M1 and never sent to machine M2.

To send a line of text to machine M2, write :

client1.WriteLine("un texte");

To read M2's answer, write :

string réponse=client1.ReadLine();

We now have the elements to write the basic architecture of a internet client with the following basic communication protocol with the server:

  • the customer sends a request contained in a single line
  • the server sends a response contained in a single line

using System;
using System.IO;
using System.Net.Sockets;
 
namespace ... {
    class ... {
        static void Main(string[] args) {
            ...
            try {
                 // connect to the 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)) {
                                 // unbuffered output stream
                                writer.AutoFlush = true;
                                 // request-response loop
                                while (true) {
                                     // demand comes from the keyboard
                                    Console.Write("Demande (bye pour arrêter) : ");
                                    demande = Console.ReadLine();
                                     // finished?
                                    if (demande.Trim().ToLower() == "bye")
                                        break;
                                     // send the request to the server
                                    writer.WriteLine(demande);
                                     // we read the server response
                                    réponse = reader.ReadLine();
                                     // the answer is processed
                                    ...
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                 // error
                ...
            }
        }
    }
}
  • line 11: create customer login - the clause using ensures that the related resources will be released when the using.
  • line 12: opening the network flow in a clause using
  • line 13: creation and operation of the read flow in a clause using
  • line 14: creation and operation of writing flow in a clause using
  • line 16: do not buffer the output stream
  • lines 18-31: the client request/server response cycle
  • line 26: the client sends its request to the server
  • line 28: the client waits for the server's response. This is a blocking operation, like reading from the keyboard. The wait ends with the arrival of a string terminated by "\n" or by an end of flow. The latter occurs if the server closes the connection it has opened with the client.

11.5.3. The TcpListener class

The class TcpListener is the most suitable class for creating a TCP service. Its C constructors, M methods and P properties include the following:

TcpListener(int port)
C
creates a TCP service that will wait (listen) for client requests on a port passed as a parameter (port) called listening port. If the machine is connected to several IP networks, the service listens on each network.
TcpListener(IPAddress ip, int port)
C
ditto, but listening only takes place on the ip address specified.
void Start()
M
listens to customer requests
TcpClient AcceptTcpClient()
M
accepts a client's request. It then opens a new connection with the client, called a service connection. The port used on the server side is random and chosen by the system. It's called the service port. AcceptTcpClient results in the object TcpClient associated with the service connection on the server side.
void Stop()
M
stops listening to customer requests
Socket Server
P
the server's listening socket

The basic structure of a TCP server exchanging data with its clients using the following protocol :

  • the customer sends a request contained in a single line
  • the server sends a response contained in a single line

might look something like this:


using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;
 
namespace ... {
    public class ... {
            ...
             // we create the listening service
            TcpListener ecoute = null;
            try {
                 // create the service - it will listen on all the machine's network interfaces
                ecoute = new TcpListener(IPAddress.Any, port);
                 // launch it
                ecoute.Start();
                 // service loop
                TcpClient tcpClient = null;
                 // infinite loop - will be stopped by Ctrl-C
                while (true) {
                     // waiting for a customer
                    tcpClient = ecoute.AcceptTcpClient();
                     // the service is provided by another task
                    ThreadPool.QueueUserWorkItem(Service, tcpClient);
                     // next customer
                }
            } catch (Exception ex) {
                // on signale l'erreur
                ...
            } finally {
                 // end of service
                ecoute.Stop();
            }
        }
 
        // -------------------------------------------------------
         // provides service to a customer
        public static void Service(Object infos) {
             // the customer is picked up and served
            Client client = infos as Client;
             // operation link TcpClient
            try {
                using (TcpClient tcpClient = client.CanalTcp) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                 // unbuffered output stream
                                writer.AutoFlush = true;
                                 // loop read request/write response
                                bool fini=false;
                                while (! fini) != null) {
                                     // waiting for customer request - blocking operation
                                    demande=reader.ReadLine();
                                     // response preparation
                                    réponse=...;
                                     // reply to customer
                                    writer.WriteLine(réponse);
                                     // next request
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                 // error
                ...
            } finally {
                 // end customer
                ...
            }
        }
    }
}
  • line 14: the listening service is created for a given port and a given IP address. Remember ici that a machine has at least two IP addresses: the "127.0.0.1" address, which is its loopback address on itself, and the "I1.I2.I3.I4" address it has on the network to which it is connected. It can have other IP addresses if it is connected to several IP networks. IPAddress.Any designates all IP addresses of a machine.
  • line 16: the listening service starts. Previously, it had been created but was not yet listening. Listening means waiting for customer requests.
  • lines 20-26: the waiting for customer request / customer service loop repeated for each new customer
  • line 22: a customer's request is accepted. The AcceptTcpClient makes an instance TcpClient dite de service :
    • the customer has made his request with his own authority TcpClient on the customer's side, which we'll call TcpClientDemande
    • the server accepts this request with AcceptTcpClient. This method creates an instance of TcpClient on the server side, which we'll call TcpClientService. We then have a Tcp connection open with authorities at both ends TcpClientDemande <--> TcpClientService.
    • the subsequent client/server communication takes place over this connection. The listening service is no longer involved.
  • line 24: so that the server can handle several clients at once, the service is provided by threads, 1 thread per client.
  • line 32: listening service closed
  • line 38: the method executed by the client service thread. It receives the instance TcpClient already connected to the customer to be served.
  • lines 38-71: code similar to that of the basic Tcp client studied above.

11.6. Examples of clients / servers TCP

11.6.1. An echo server

We propose to write an echo server that will be launched from a DOS window with the command :

ServeurEcho port

The server operates on the port passed as a parameter. It simply sends the request back to the client. The program is as follows:


using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;
 
// call: serveurEcho port
// echo server
// returns the line sent to the customer
 
namespace Chap9 {
    public class ServeurEcho {
        public const string syntaxe = "Syntaxe : [serveurEcho] port";
 
         // main program
        public static void Main(string[] args) {
 
             // is there an argument?
            if (args.Length != 1) {
                Console.WriteLine(syntaxe);
                return;
            }
             // this argument must be integer >0
            int port = 0;
            if (!int.TryParse(args[0], out port) || port<=0) {
                Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
                return;
            }
             // we create the listening service
            TcpListener ecoute = null;
             int numClient =     0; // next customer no
            try {
                 // create the service - it will listen on all the machine's network interfaces
                ecoute = new TcpListener(IPAddress.Any, port);
                 // launch it
                ecoute.Start();
                 // follow-up
                Console.WriteLine("Serveur d'écho lancé sur le port {0}", ecoute.LocalEndpoint);
                 // service threads
                ThreadPool.SetMinThreads(10, 10);
                ThreadPool.SetMaxThreads(10, 10);
                 // service loop
                TcpClient tcpClient = null;
                 // infinite loop - will be stopped by Ctrl-C
                while (true) {
                     // waiting for a customer
                    tcpClient = ecoute.AcceptTcpClient();
                     // the service is provided by another task
                    ThreadPool.QueueUserWorkItem(Service, new Client() { CanalTcp = tcpClient, NumClient = numClient });
                     // next customer
                    numClient++;
                }
            } catch (Exception ex) {
                // on signale l'erreur
                Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
            } finally {
                 // end of service
                ecoute.Stop();
            }
        }
 
        // -------------------------------------------------------
         // provides service to an echo server client
        public static void Service(Object infos) {
             // the customer is picked up and served
            Client client = infos as Client;
             // renders service to the customer
            Console.WriteLine("Début de service au client {0}", client.NumClient);
             // operation link TcpClient
            try {
                using (TcpClient tcpClient = client.CanalTcp) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                 // unbuffered output stream
                                writer.AutoFlush = true;
                                 // loop read request/write response
                                string demande = null;
                                while ((demande = reader.ReadLine()) != null) {
                                     // console monitoring
                                    Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
                                     // echo from demand to customer
                                    writer.WriteLine("[{0}]", demande);
                                     // console monitoring
                                    Console.WriteLine("---> Client {0} : {1}", client.NumClient, demande);
                                     // service stops when customer sends "bye
                                    if (demande.Trim().ToLower() == "bye")
                                        break;
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                 // error
                Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
            } finally {
                 // end customer
                Console.WriteLine("Fin du service au client {0}", client.NumClient);
            }
        }
    }
 
     // customer info
    internal class Client {
         public TcpClient CanalTcp { get; se        t; } // customer liaison
         public int NumClient { get; se            t; } // customer no
    }
}

The structure of the echo server is in line with the basic Tcp server architecture described above. We will only comment on the "customer service" part:

  • line 79: the customer's request is read out
  • line 83: it is returned to the customer surrounded by square brackets
  • line 79: service stops when the client closes the connection

In a Dos window, we use the C# project executable :

...\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

We then launch two customers putty which we connect to the machine's port 100 localhost :

 

The echo server console display changes to :

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

Client 1 and then client 0 send the following texts:

  • [1]: customer no. 1
  • [2]: customer no. 0
  • [3]: the echo server console
  • in [4]: client 1 disconnects with the command bye.
  • in [5]: the server detects it

The server can be stopped by pressing Ctrl-C. Client 0 then detects it [6].

11.6.2. A client for the echo server

We now write a client for the previous server. It will be called as follows:

ClientEcho nomServeur port

It connects to the machine nomServeur on the port port then sends lines of text to the server, which echoes them back.


using System;
using System.IO;
using System.Net.Sockets;
 
namespace Chap9 {
     // connects to an echo server
     // any line typed on the keyboard is received as an echo
    class ClientEcho {
        static void Main(string[] args) {
             // syntax
            const string syntaxe = "pg machine port";
 
             // number of arguments
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }
 
             // note the server name
            string serveur = args[0];
 
             // port must be integer >0
            int port = 0;
            if (!int.TryParse(args[1], out port) || port <= 0) {
                Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
                return;
            }
 
            // on peut travailler
             string demande = nu        ll; // customer request
             string réponse =         nu ll; // server response
            try {
                 // connect to the 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)) {
                                 // unbuffered output stream
                                writer.AutoFlush = true;
                                 // request-response loop
                                while (true) {
                                     // demand comes from the keyboard
                                    Console.Write("Demande (bye pour arrêter) : ");
                                    demande = Console.ReadLine();
                                     // finished?
                                    if (demande.Trim().ToLower() == "bye")
                                        break;
                                     // send the request to the server
                                    writer.WriteLine(demande);
                                     // we read the server response
                                    réponse = reader.ReadLine();
                                     // the answer is processed
                                    Console.WriteLine("Réponse : {0}", réponse);
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                 // error
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            }
        }
    }
}

The structure of this client conforms to the basic general architecture proposed for Tcp. Here are the results obtained with the following configuration :

  • the server is launched on port 100 in a Dos window
  • on the same machine, two clients are launched in two different windows Dos

The window for customer A (n° 0) displays the following information:

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 customer B's (n° 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) :

On the 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

Customer A n° 0 disconnects:

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

The server console :

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

11.6.3. A generic TCP client

We will write a generic Tcp client that will be launched as follows: ClientTcpGenerique server port. It will operate in a similar way to the putty client, but will have a console interface and no option configuration.

In the previous application, the dialog protocol was known: the client sent a single line and the server responded with a single line. Each service has its own particular protocol, and the following situations can also be found:

  • the client has to send several lines of text before getting a response
  • a server response may include several lines of text

So the cycle of sending a single line to the server and receiving a single line from the server is not always appropriate. To handle protocols more complex than the echo protocol, the generic Tcp client will have two threads:

  • the main thread reads the lines of text typed on the keyboard and sends them to the server.
  • a secondary thread will work in parallel, reading lines of text sent by the server. As soon as it receives one, it displays it on the console. The thread doesn't stop until the server closes the connection. It therefore works continuously.

The code is as follows:


using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
 
namespace Chap9 {
     // receives the characteristics of a service as a parameter in the form: server port
     // connects to the service
     // sends each line typed on the keyboard to the server
     // creates a thread to continuously read text lines sent by the server
    class ClientTcpGenerique {
        static void Main(string[] args) {
             // syntax
            const string syntaxe = "pg serveur port";
 
             // number of arguments
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }
 
             // note the server name
            string serveur = args[0];
 
             // port must be integer >0
            int port = 0;
            if (!int.TryParse(args[1], out port) || port <= 0) {
                Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
                return;
            }
             // connect to the service
            TcpClient tcpClient = null;
            try {
                tcpClient = new TcpClient(serveur, port);
            } catch (Exception ex) {
                 // error
                Console.WriteLine("Impossible de se connecter au service ({0},{1}) : erreur {2}", serveur, port, ex.Message);
                 // end
                return;
            }
 
             // launch a separate thread to read the text lines sent by the server
            ThreadPool.QueueUserWorkItem(Receive, tcpClient);
 
             // keyboard commands are read in the main thread
            Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
             string demande = nu        ll; // customer request
            try {
                 // operate the customer connection
                using (tcpClient) {
                     // create a write stream to the server
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamWriter writer = new StreamWriter(networkStream)) {
                             // unbuffered output stream
                            writer.AutoFlush = true;
                             // request-response loop
                            while (true) {
                                demande = Console.ReadLine();
                                 // finished?
                                if (demande.Trim().ToLower() == "bye")
                                    break;
                                 // send the request to the server
                                writer.WriteLine(demande);
                            }
                        }
                    }
                }
            } catch (Exception e) {
                 // error
                Console.WriteLine("L'erreur suivante s'est produite dans le thread principal : {0}", e.Message);
            }
        }
 
         // client read thread <-- server
        public static void Receive(object infos) {
             // local data
             string réponse =     nu ll; // server response
             // input flow creation
            try {
                using (TcpClient tcpClient = infos as TcpClient) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                             // loop continuous reading of text lines in the input stream
                            while ((réponse = reader.ReadLine()) != null) {
                                 // console display
                                Console.WriteLine("<-- {0}", réponse);
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                 // error
                Console.WriteLine("Flux de lecture : l'erreur suivante s'est produite : {0}", ex.Message);
            } finally {
                 // signals the end of the read thread
                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.");
            }
        }
    }
}
  • line 34: client connects to server
  • line 43: a thread is started to read text lines from the server. It must execute the Receive on line 73. We pass the instance TcpClient which has been connected to the server.
  • lines 57-64: the keyboard command input / send command to server loop. Keyboard command input is handled by the main thread.
  • lines 75-98 : the method Receive executed by the text line reading thread. This method receives the instance TcpClient which has been connected to the server.
  • lines 84-87: the continuous loop for reading text lines sent by the server. It stops only when the server closes the open connection with the client.

Here are a few examples based on those used with the customer putty in paragraph 11.4. The client is run in a 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>

The reader is invited to reread the explanations given in paragraph 11.4.2. We only comment on what is specific to the application:

  • line 28: after sending line 27, the HTTP server closed the connection, thus terminating the read thread. The main thread reading keyboard commands is still active. The command on line 29, typed from the keyboard, stops it.

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>

The reader is invited to reread the explanations given in paragraph 11.4.3 and test other examples used with the customer putty.

11.6.4. A server Tcp generic

We are now interested in a server

  • which displays on-screen orders sent by its customers
  • sends them the text lines typed by a user. The user acts as the server.

The program is launched in a Dos window by : ServeurTcpGenerique portEcoute, where portEcoute is the port on which clients should connect. Service to the client will be provided by two threads:

  • the main thread which :
    • will process customers one after the other, not in parallel.
    • which will read the lines typed by the user and send them to the client. The user will send the command bye that it closes the connection with the client. Because the console cannot be used for two clients simultaneously, our server handles only one client at a time.
  • a secondary thread dedicated exclusively to reading lines of text sent by the client

The server never stops, except when the user types Ctrl-C on the keyboard.

Let's look at a few examples. The server is launched on port 100 and we use the generic client in paragraph11.6.3, to talk to it. The customer's window is as follows:

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

Lines beginning with <-- are those sent from the server to the client, the others from the client to the server. The server window is as follows:

...\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

Lines beginning with <-- are those sent from the client to the server, the others those sent from the server to the client. Line 9 indicates that the client's request reading thread has stopped. The main server thread is still waiting for keyboard commands to be sent to the client. To do this, type the command bye from line 10 to move on to the next client. The server is still active, while client 1 is finished. We launch a second client for the same 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

The server window then looks like this:

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

After line 6 above, the server is waiting for a new client. It can be stopped by pressing Ctrl-C.

Let's now simulate a web server by launching our generic server on port 88 :

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

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

Let's take a browser and ask for the URL http://localhost:88/exemple.html. The browser will then connect to the 88 of the machine localhost then request the page /exemple.html :

 

Let's take a look at our server window:

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 discover the HTTP headers sent by the browser. This allows us to discover other HTTP headers than those already encountered. Let's draw up a response to our client. The user at the keyboard is ici the real server, and he can draw up a response by hand. Recall the response made by a Web server in a previous example:

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

Let's try to give an analogous answer, keeping to the bare minimum:

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]

In our response, we have limited ourselves to the HTTP headers on lines 1-4. We do not give the size of the document we are going to send (Content-Length) but simply say that we are going to close the connection (Connection: close) after sending it. This is enough for the browser. When it sees the closed connection, it will know that the server's response is complete and will display the HTML page that was sent to it. This is the page shown on lines 6-9. The keyboard user then closes the connection to the client by typing the command bye, line 10. On this keyboard command, the main thread closes the connection with the client. This causes the exception in line 11. The thread reading the client's text lines was abruptly interrupted by the closing of the connection with the client and threw an exception. After line 12, the server waits for a new client.

The client browser now displays the following:

If above, we make Display/Source to see what the browser has received, we get [2], i.e. exactly what we sent from the generic server.

The generic TCP server code is as follows:


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";
 
         // main program
        public static void Main(string[] args) {
 
             // is there an argument?
            if (args.Length != 1) {
                Console.WriteLine(syntaxe);
                Environment.Exit(1);
            }
             // this argument must be integer >0
            int port = 0;
            if (!int.TryParse(args[0], out port) || port <= 0) {
                Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
                Environment.Exit(2);
            }
             // we create the listening service
            TcpListener ecoute = null;
            try {
                 // create the service
                ecoute = new TcpListener(IPAddress.Any, port);
                 // launch it
                ecoute.Start();
                 // follow-up
                Console.WriteLine("Serveur générique lancé sur le port {0}", ecoute.LocalEndpoint);
                while (true) {
                     // waiting for a customer
                    Console.WriteLine("Attente du client suivant...");
                    TcpClient tcpClient = ecoute.AcceptTcpClient();
                    Console.WriteLine("Client {0}", tcpClient.Client.RemoteEndPoint);
                     // launch a separate thread to read the lines of text sent by the client
                    ThreadPool.QueueUserWorkItem(Receive, tcpClient);
                     // keyboard commands are read in the main thread
                    Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
                     string répon        se = null; // server response
                     // operate the customer connection
                    using (tcpClient) {
                         // create a write flow to the client
                        using (NetworkStream networkStream = tcpClient.GetStream()) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                 // unbuffered output stream
                                writer.AutoFlush = true;
                                 // keyboard response loop
                                while (true) {
                                    réponse = Console.ReadLine();
                                     // finished?
                                    if (réponse.Trim().ToLower() == "bye")
                                        break;
                                     // we send the request to the customer
                                    writer.WriteLine(réponse);
                                }
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                // on signale l'erreur
                Console.WriteLine("Main : l'erreur suivante s'est produite : {0}", ex.Message);
            } finally {
                 // end of listening
                ecoute.Stop();
            }
        }
 
         // read thread server <-- client
        public static void Receive(object infos) {
             // local data
             string demande = nu    ll; // customer request
             string idClient    =nu ll; // customer identity
 
             // operation customer connection
            try {
                using (TcpClient tcpClient = infos as TcpClient) {
                     // customer identity
                    idClient = tcpClient.Client.RemoteEndPoint.ToString();
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                             // loop continuous reading of text lines in the input stream
                            while ((demande = reader.ReadLine()) != null) {
                                 // console display
                                Console.WriteLine("<-- {0}", demande);
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                 // error
                Console.WriteLine("Flux de lecture des lignes de texte du client {1} : l'erreur suivante s'est produite : {0}", ex.Message,idClient);
            } finally {
                 // signals the end of the read thread
                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);
            }
        }
    }
}
  • line 29: the listening service is created but not started. It listens to all the machine's network interfaces.
  • line 31: listening service is started
  • line 34: infinite customer waiting loop. The user stops the server with Ctrl-C.
  • line 37: waiting for a customer - blocking operation. When the customer arrives, the TcpClient rendered by the AcceptTcpClient represents the server side of an open connection with the client.
  • line 40: customer requests are read by a separate thread.
  • line 45: use of client connection in a clause using to make sure it's closed, whatever happens.
  • line 47: using the network flow in a clause using
  • line 48: creation in a clause using from a write stream to the network stream
  • line 50: the write stream will be unbuffered
  • lines 52-59: keyboard input loop for orders to be sent to the customer
  • line 69: end of listening service. This instruction will never be executed ici since the server is stopped by Ctrl-C.
  • line 78: the method Receive which continuously displays lines of text sent by the client on the console. This is the same as for the generic TCP client.

11.6.5. A customer Web

In the previous example, we saw some of the HTTP headers sent by a :

<-- 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're going to write a Web client, to which we'll pass a URL as a parameter, and which will display on screen the text sent by the server. We'll assume that the server supports the HTTP 1.1 protocol. Of the above headers, we'll use only the following:

1
2
3
4
<-- GET /exemple.html HTTP/1.1
<-- Host: localhost:88
<-- Connection: close
<--
  • the first header indicates the desired document
  • the second is the queried server
  • the third that we want the server to close the connection after responding to us.

If we replace GET with HEAD above line 1, the server will only send us HTTP headers and not the document specified on line 1.

Our client web will be called as follows: ClientWeb URL cmd, where URL is the desired URL and cmd one of the two keywords GET or HEAD to indicate whether only the headers are required (HEAD) or also the page content (GET). Let's look at a first example:

...\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>
  • line 1, we only request HTTP (HEAD) headers
  • lines 2-9: server response

If we use GET instead of HEAD in the Web client call, we get the same result as with HEAD, plus the requested document body.

The customer code web is as follows:


using System;
using System.IO;
using System.Net.Sockets;
 
namespace Chap9 {
    class ClientWeb {
        static void Main(string[] args) {
             // syntax
            const string syntaxe = "pg URI GET/HEAD";
 
             // number of arguments
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }
 
             // note the URI required
            string stringURI = args[0];
            string commande = args[1].ToUpper();
 
             // URI validity check
            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 incorrect
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }
             // order verification
            if (commande != "GET" && commande != "HEAD") {
                 // incorrect order
                Console.WriteLine("Le second paramètre doit être GET ou HEAD");
                return;
            }
 
            try {
                 // connect to the 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)) {
                                 // unbuffered output stream
                                writer.AutoFlush = true;
                                 // request URL - send HTTP headers
                                writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
                                writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
                                writer.WriteLine("Connection: close");
                                writer.WriteLine();
                                 // we read the answer
                                string réponse = null;
                                while ((réponse = reader.ReadLine()) != null) {
                                     // the response is displayed on the console
                                    Console.WriteLine(réponse);
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // on affiche l'exception
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            }
        }
    }
}

The only new feature in this program is the use of the Uri. The program receives a URL (Uniform Resource Locator) or URI (Uniform Resource Identifier) of the form http://server:port/cheminPageHTML?param1=val1;param2=val2;.... The class Uri allows us to break down the URL chain into its individual elements.

  • lines 26-33: an object Uri is built from the string stringURI received as parameter. If the URI string received as a parameter is not a valid URI (absence of protocol, server, etc.), an exception is thrown. This allows us to check the validity of the parameter received. Once the Uri constructed, we have access to the various elements of this Uri. So if the uri in the previous code was built from the string http://server:port/document?param1=val1&param2=val2;... we have :
    • uri.Host=server,
    • 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. A Web client to manage redirects

The previous Web client does not handle any redirection of the URL it has requested. Here's an example:

...\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>
  • line 2: the code 302 Found indicates a redirection. The address to which the browser should redirect is in the body of the document, line 16.

A second example:

...\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
  • line 2: the code 301 Moved Permanently indicates a redirection. The address to which the browser must redirect is indicated on line 6, in the HTTP header Rental.

A third example:

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/fr/
Date: Sat, 03 May 2008 14:56:53 GMT
Connection: close
  • line 2: the code 302 Moved Temporarily indicates a redirection. The address to which the browser must redirect is indicated on line 5, in the HTTP header Rental.

A fourth example with a IIS server local to the :

...\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
  • line 2: the code 302 Object moved indicates a redirection. The address to which the browser must redirect is indicated on line 5, in the HTTP header Rental. Note that unlike the previous examples, the redirection address is relative. The full address is in fact http://localhost/localstart.asp.

We propose to manage redirects when the first line of HTTP headers contains the keyword moved (case insensitive) and that the redirection address is in the HTTP header Rental.

If we take the last three examples, we have the following results:

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
  • line 11: redirection to the address on line 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/fr/
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
  • line 11: redirection to the address on line 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
  • line 13: redirection to the address on line 6
  • ligne 15: access to the page http://localhost/localstart.asp was refused.

The program handling the redirection is as follows:


using System;
using System.IO;
using System.Net.Sockets;
using System.Text.RegularExpressions;
 
namespace Chap9 {
    class ClientWebAvecRedirection {
        static void Main(string[] args) {
             // syntax
            const string syntaxe = "pg URI GET/HEAD";
 
             // number of arguments
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }
 
             // note the URI required
            string stringURI = args[0];
            string commande = args[1].ToUpper();
 
             // URI validity check
            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 incorrect
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }
             // order verification
            if (commande != "GET" && commande != "HEAD") {
                 // incorrect order
                Console.WriteLine("Le second paramètre doit être GET ou HEAD");
                return;
            }
 
             const int nbRedirsMa        x = 1; // no more than one redirection accepted
             int nbRedirs =                             0; // number of redirects in progress
 
             // regular expression to find a URL redirect
            Regex location = new Regex(@"^Location: (.+?)$");
            try {
                 // you may have several URL to request if there are redirections
                while (nbRedirs <= nbRedirsMax) {
                     // redirection management
                    bool redir = false;
                    bool locationFound = false;
                    string locationString = null;
                     // connect to the service
                    using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
                        using (StreamReader reader = new StreamReader(tcpClient.GetStream())) {
                            using (StreamWriter writer = new StreamWriter(tcpClient.GetStream())) {
                                 // unbuffered output stream
                                writer.AutoFlush = true;
                                 // request URL - send HTTP headers
                                writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
                                writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
                                writer.WriteLine("Connection: close");
                                writer.WriteLine();
                                 // read the first line of the answer
                                string premièreLigne = reader.ReadLine();
                                 // screen echo
                                Console.WriteLine(premièreLigne);
 
                                 // redirection?
                                if (Regex.IsMatch(premièreLigne.ToLower(), @"\s+moved\s*")) {
                                     // there is a redirection
                                    redir = true;
                                    nbRedirs++;
                                }
 
                                 // next HTTP headers until you find the empty line signalling the end of the headers
                                string réponse = null;
                                while ((réponse = reader.ReadLine()) != "") {
                                     // the answer is displayed
                                    Console.WriteLine(réponse);
                                     // if there is a redirection, we search for the Location header
                                    if (redir && !locationFound) {
                                         // compare the current line with the relational expression location
                                        Match résultat = location.Match(réponse);
                                        if (résultat.Success) {
                                             // if found, note the URL of redirection
                                            locationString = résultat.Groups[1].Value;
                                             // we note that we found
                                            locationFound = true;
                                        }
                                    }
                                }
 
                                 // the HTTP headers have been used up - write the empty line
                                Console.WriteLine(réponse);
                                 // then move on to the body of the document
                                while ((réponse = reader.ReadLine()) != null) {
                                    Console.WriteLine(réponse);
                                }
                            }
                        }
                    }
                    // a-t-on fini ?
                    if (!locationFound || nbRedirs > nbRedirsMax)
                        break;
                     // there is a redirection to be made - we build the new Uri
                    try {
                        if (locationString.StartsWith("http")) {
                             // full http address
                            uri = new Uri(locationString);
                        } else {
                             // http address relative to current uri
                            uri = new Uri(uri, locationString);
                        }
                         // log console
                        Console.WriteLine("\n<--Redirection vers l'URL {0}-->\n", uri);
                    } catch (Exception ex) {
                         // pb with Uri
                        Console.WriteLine("\n<--L'adresse de redirection {0} n'a pas été comprise : {1} -->\n", locationString, ex.Message);
                    }
                }
            } catch (Exception e) {
                // on affiche l'exception
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            }
        }
    }
}

Compared with the previous version, the changes are as follows:

  • line 46: the regular expression to retrieve the redirection address in the HTTP header Location: address.
  • line 49: code previously executed for a single Uri can now be executed successively for several Uri.
  • line 66: reads the 1st line of HTTP headers sent by the server. It contains the keyword moved if the requested document has been moved.
  • lines 71-75: check whether the 1st line contains the keyword moved. If so, we'll make a note of it.
  • lines 79-93: read the other HTTP headers until you reach the empty line that signals their end. If the 1st line announced a redirection, we then focus on the HTTP header Location: address to store the redirection address in locationString.
  • lines 98-100: the rest of the HTTP server response is displayed at the console.
  • lines 105-106: the Uri requested has been fully evaluated and displayed. If there are no redirections to be made, or if the number of redirections allowed has been exceeded, the program is exited.
  • lines 108-122: if there's a redirection, we calculate the new Uri to request. There's a bit of gymnastics involved, depending on whether the redirection address found was absolute (line 111) or relative (line 114).

11.7. .NET classes specialized in a particular internet protocol

In the previous examples of the web client, the HTTP protocol was managed with a TCP client. We therefore had to manage the particular communication protocol ourselves. Similarly, we could have built a SMTP or POP client. The .NET framework offers specialized classes for the HTTP and SMTP protocols. These classes know the communication protocol between client and server, and save the developer the trouble of having to manage them. We now present them.

11.7.1. The classeWebClient

There is a WebClient that can communicate with a web server. Let's consider the example of the web client in paragraph 11.6.5, processed ici with the class WebClient.


using System;
using System.IO;
using System.Net;
namespace Chap9 {
    public class Program {
        public static void Main(string[] args) {
             // syntax: [prog] Uri
            const string syntaxe = "pg URI";
 
             // number of arguments
            if (args.Length != 1) {
                Console.WriteLine(syntaxe);
                return;
            }
 
             // note the URI required
            string stringURI = args[0];
 
             // URI validity check
            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 incorrect
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }
 
            try {
                 // web client creation
                using (WebClient client = new WebClient()) {
                     // added HTTP header 
                    client.Headers.Add("user-agent", "st");
                    using (Stream stream = client.OpenRead(uri)) {
                        using (StreamReader reader = new StreamReader(stream)) {
                             // display web server response
                            Console.WriteLine(reader.ReadToEnd());
                             // display headers server response
                            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);
            }
        }
    }
}
  • line 35: client web is created but not yet configured
  • line 37: a HTTP header is added to the HTTP request. We'll discover that other headers will be sent by default.
  • line 38: the web client requests the Uri given by the user and reads the document sent. [WebClient].OpenRead(Uri) opens the connection with Uri and reads the answer. This is where the class comes in. It handles the dialog with the web server. The result is the method OpenRead type is Stream and represents the requested document. The HTTP headers sent by the server and preceding the document in the response are not part of it.
  • line 39: a StreamReader and line 41, its method ReadToEnd to read the full answer.
  • lines 44-46: HTTP headers are displayed in the server response. [WebClient].ResponseHeaders represents a valuated collection whose keys are the names of HTTP headers, and whose values are the strings associated with these headers.
  • line 51: exceptions raised during a client/server exchange are of type WebException.

Let's look at a few examples.

The generic TCP server built in paragraph 6.4.6 :

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

The previous web client is launched as follows:

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

The Uri requested is that of the generic server. The generic server then displays the HTTP headers sent to it by the web client:

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
<--

This shows :

  • customer web sends 3 HTTP headers by default (lines 3, 5, 6)
  • line 4: the header we generated ourselves (line 37 of the code)
  • that client web defaults to method GET (line 3). Other methods include POST and HEAD.

Now let's ask for a non-existent resource:

1
2
3
4
5
...\Chap9\09\bin\Release>09 http://istia.univ-angers.fr/inconnu
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:line 16
  • line 2: we had an exception of type WebException because the server responded with the code 404 Not Found to indicate that the requested resource did not exist.

Finally, let's request an existing resource:

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

The file istia.univ-angers.txt produced by the order is as follows:

<!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
---------------------
  • line 1: the HTML document requested.
  • lines 3-10: HTTP response headers in an order that is not necessarily that in which they were sent.

The class WebClient has methods for receiving a document (methods DownLoad) or to send them ( UpLoad) :

DownLoadData
to download a resource as an array of bytes (image, for example)
DownLoadFile
to download a resource and save it as a local file
DownLoadString
to download a resource and retrieve it as a string (e.g. html file)
OpenWrite
the counterpart ofOpenRead but to send data to the server
UpLoadData
the counterpart of DownLoadData but to the server
UpLoadFile
the counterpart of DownLoadFile but to the server
UpLoadString
the counterpart of DownLoadString but to the server
UpLoadValues
to send the data from a POST command to the server and retrieve the results in the form of an array of bytes. The POST command requests a document, while at the same time transmitting to the server the information it needs to determine the actual document to send. This information is sent as a document to the server, hence the name UpLoad of the method. They are sent behind the empty HTTP header line in the form param1=value1&param2=value2&... :
POST /document HTTP/1.1
...
[ligne vide]
param1=valeur1&param2=valeur2&...
The same document could be requested using the GET method:
GET /document?param1=valeur1&param2=valeur2&...
...
[ligne vide]
The difference between the two methods is that the browser displaying the requested Uri will display /document in the case of POST and /document?param1=value1&param2=value2&... in the case of GET.

11.7.2. The WebRequest / WebResponse classes

Sometimes the class WebClient is not flexible enough to do what you want. Let's take the example of the web client with redirection studied in paragraph 11.6.6. We need to send the HTTP header:

HEAD /document HTTP/1.1

We have seen that the HTTP headers sent by default by the web client are as follows:

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

We've also seen that it's possible to add HTTP headers to previous ones using the [WebClient].Headers. Only line 1 is not a header belonging to the Headers because it doesn't have the shape key: value. I can't figure out how to change the GET to HEAD in line 1 from class WebClient (maybe I looked it up wrong?). When the class WebClient has reached its limits, we can move on to the WebRequest / WebResponse :

  • WebRequest : represents the entire Web customer request.
  • WebResponse : represents the entire server response Web

We said that the WebClient managed schematics http:, https:, ftp:, file:. The requests and responses of these different protocols do not have the same form. It is therefore necessary to manipulate the exact type of these elements rather than their generic type WebRequest and WebResponse. We will therefore use the :

  • HttpWebRequest, HttpWebResponse for a HTTP customer
  • FtpWebRequest, FtpWebResponse for a FTP customer

We now deal with the HttpWebRequest and HttpWebresponse the example of the web client with redirection studied in paragraph 11.6.6. The code is as follows:


using System;
using System.IO;
using System.Net.Sockets;
using System.Net;
 
namespace Chap9 {
    class WebRequestResponse {
        static void Main(string[] args) {
             // syntax
            const string syntaxe = "pg URI GET/HEAD";
 
             // number of arguments
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }
 
             // note the URI required
            string stringURI = args[0];
            string commande = args[1].ToUpper();
 
             // URI validity check
            Uri uri = null;
            try {
                uri = new Uri(stringURI);
            } catch (Exception ex) {
                 // URI incorrect
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }
             // order verification
            if (commande != "GET" && commande != "HEAD") {
                 // incorrect order
                Console.WriteLine("Le second paramètre doit être GET ou HEAD");
                return;
            }
 
            try {
                 // configure the query
                HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
                httpWebRequest.Method = commande;
                httpWebRequest.Proxy = null;
                 // it is executed
                HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
                 // result
                Console.WriteLine("---------------------");
                Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri,(int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
                 // headers 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)) {
                         // the response is displayed on the console
                        Console.WriteLine(reader.ReadToEnd());
                    }
                }
            } catch (WebException e1) {
                 // the answer is retrieved
                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) {
                // on affiche l'exception
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e2.Message);
            }
        }
    }
}
  • line 40: an object of type WebRequest is created with the static method WebRequest.Create(Uri uri) where uri is the uri of the document to be downloaded. Because we know that the protocol of Uri is HTTP, the type of the result is changed to HttpWebRequest to access specific elements of the Http protocol.
  • line 41: we set the GET / POST / HEAD method for the 1st line of HTTP headers. Ici will be GET or HEAD.
  • line 42: in a private corporate network, company machines are often isolated from the internet for security reasons. To achieve this, the private network uses internet addresses that the internet routers do not route. The private network is connected to internet by special machines called proxy which are connected to both the company's private network and the internet. This is an example of machines with multiple IP addresses. A machine on the private network cannot itself establish a connection with a server on the internet, a web server for example. It must ask a proxy machine to do this for it. A proxy machine proxy can house servers proxy for different protocols. We speak of proxy HTTP to designate the service responsible for making HTTP requests on behalf of machines on the private network. If such a proxy server HTTP exists, it must be indicated in the field [WebRequest].proxy. For example, write :
[WebRequest].proxy=new WebProxy("pproxy.istia.uang:3128");

if the HTTP proxy operates on the machine's port 3128 pproxy.istia.uang. We put null in the field [WebRequest].proxy if the machine has direct access to the internet and doesn't have to go through a proxy.

  • line 44: the method GetResponse() requests the document identified by its Uri and returns an object WebRequestResponse transform ici into an object HttpWebResponse. This object represents the server's response to the document request.
  • line 47:
    • [HttpWebResponse].ResponseUri : is the Uri of the server that sent the document. In the case of redirection, this may be different from the Uri of the server initially queried. Note that the code does not manage redirection. It is handled automatically by the GetResponse. Once again, this is the advantage of high-level classes over basic classes in the Tcp protocol.
    • [HttpWebResponse].StatusCode, [HttpWebResponse].StatusDescription represent the 1st line of the answer, for example : HTTP/1.1 200 OK. StatusCode is 200 and StatusDescription is OK.
  • line 50: [HttpWebResponse].Headers is the collection of HTTP headers in the response.
  • line 55: [HttpWebResponse].GetResponseStream : is the stream used to obtain the document contained in the response.
  • line 61: an exception of type WebException
  • line 63: [WebException].Response is the response that caused the exception to be raised.

Here's an example:

...\Chap9\09B\bin\Release>09B http://www.gouv.fr HEAD
---------------------
Le serveur http://www.premier-ministre.gouv.fr/fr/ a répondu : 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
---------------------
  • lines 1 and 3: the server that responded is not the same as the one that was queried. There has therefore been a redirection.
  • lines 5-11: HTTP headers sent by the server

11.7.3. Application: a proxy client for a web translation server

We'll now show how the preceding classes allow us to exploit the resources of web.

11.7.3.1. The application

There are several translation sites on web. The one that will be used ici is the site http://trans.voila.fr/traduction_voila.php :

The text to be translated is inserted in [1], the translation direction is chosen in [2]. The translation is requested in [3] and obtained in [4].

We're going to write a Windows application that is a client of the above application. It will do nothing more than the [trans.voila.fr] site application. Its interface will be as follows:

11.7.3.2. Application architecture

The application will have the following 2-layer architecture:

11.7.3.3. The Visual studio project

The Visual studio project will be as follows:

  • in [1], the solution consists of two projects,
  • [2]: one for the [dao] layer and the entities it uses,
  • [3]: the other for the Windows interface

11.7.3.4. The [dao] project

The [dao] project consists of the following elements:

  • IServiceTraduction.cs : the interface presented to layer [ui]
  • ServiceTraduction : implementation of this interface
  • WebTraductionsException : an application-specific exception

The interface IServiceTraduction is as follows:


using System.Collections.Generic;
 
namespace dao {
    public interface IServiceTraduction {
         // languages used
        IDictionary<string, string> LanguesTraduites { get; }
         // translation
        string Traduire(string texte, string deQuoiVersQuoi);
    }
}
  • line 6: property LanguesTraduites returns the dictionary of languages accepted by the translation server. This dictionary has entries of the form ["fe", "French-English"] where the value designates a translation direction, ici from French to English, and the "fe" key is a code used by the translation server trans.voila.fr.
  • line 8: the method Translate is the translation method:
    • text is the text to be translated
    • deQuoiVersQuoi is one of the keys to the dictionary of translated languages
    • the method translates the text

ServiceTraduction is an implementation class of the IServiceTraduction. We describe it in detail in the following section.

WebTraductionsException is the following exception class:


using System;
 
namespace entites {
    public class WebTraductionsException : Exception {
 
         // error code
        public int Code { get; set; }
 
         // manufacturers
        public WebTraductionsException() {
        }
        public WebTraductionsException(string message)
            : base(message) {
        }
        public WebTraductionsException(string message, Exception e)
            : base(message, e) {
        }
    }
}
  • line 7: an error code

11.7.3.5. The customer web [ServiceTraduction]

Let's go back to the architecture of our application:

The class [ServiceTraduction] we need to write is a client of the web translation service [trans.voila.fr]. To write it, we need to understand

  • what the translation server expects from its client
  • what he sends back to his customer

Let's take a look at the client/server dialogue involved in translation. Let's take the example presented in the introduction to the application:

The text to be translated is inserted in [1], the translation direction is chosen in [2]. The translation is requested in [3] and obtained in [4].

To obtain the translation [4], the browser sent the following GET request (displayed in its address field):

http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection=fe&stext=ce+chien+est+malade

It's pretty simple to understand:

  • http://trans.voila.fr/traduction_voila.php is the Url of the translation service
  • isText=1 semble vouloir dire qu'on a affaire à du texte
  • translationDirection refers to the meaning of the translation, ici French-English
  • stext is the text to be translated in a form we call Url encoded. Some characters cannot appear in a Url. This is the case, for example, of the space which has been ici encoded by a +. The .Net framework offers the static method System.Web.HttpUtility.UrlEncode to do this encoding work.

We conclude that to query the translation server, our [ServiceTraduction] class can use the string

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

where {0} and {1} will be replaced by the translation direction and the text to be translated respectively.

How do I know which translation directions are accepted by the server? In the screenshot above, the translated languages are in the drop-down list. If we look in the browser (View / source) at the Html code of the page, we find this for the drop-down list:

<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>

This isn't very clean Html code, insofar as each <option> tag should normally be closed by a </option> tag. That said, the value give us the list of translation codes to be sent to the server. In the dictionary LanguesTraduites interface IServiceTraduction, the keys will be the attributes value above and the values and texts displayed by the drop-down list.

Now let's take a look (View / Source) at where the translation returned by the translation server is located in the Html page:

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

The translation is right in the middle of the returned Html page. How do I find it? You can use a regular expression with the sequence <div class="txtTrad">...</div> because the <div class="txtTrad"> is only present at this point on the Html page. The C# regular expression used to retrieve the translated text is :

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

We now have the elements we need to write the implementation class ServiceTraduction interface IServiceTraduction :


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 {
         // automatic service configuration properties
        public IDictionary<string, string> LanguesTraduites { get; set; }
        public string UrlServeurTraduction { get; set; }
        public string ProxyHttp { get; set; }
        public String RegexTraduction { get; set; }
 
         // translation
        public string Traduire(string texte, string deQuoiVersQuoi) {
             // is the requested translation possible?
            if (!LanguesTraduites.ContainsKey(deQuoiVersQuoi)) {
                throw new WebTraductionsException(String.Format("Le sens de traduction [{0}] n'est pas reconnu")) { Code = 10 };
            }
             // text to translate
            string texteATraduire = HttpUtility.UrlEncode(texte);
             // uri to request
            string uri = string.Format(UrlServeurTraduction, deQuoiVersQuoi, texteATraduire);
             // regular expression to find the translation in the answer
            Regex patternTraduction = new Regex(RegexTraduction);
             // exception
            WebTraductionsException exception = null;
             // translation
            string traduction = null;
            try {
                 // configure the query
                HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
                httpWebRequest.Method = "GET";
                httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
                 // it is executed
                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) {
                             // search for translation in current line
                            MatchCollection résultats = patternTraduction.Matches(ligne);
                             // translation found?
                            if (résultats.Count != 0) {
                                traduction = résultats[0].Groups[1].Value.Trim();
                                traductionTrouvée = true;
                            }
                        }
                         // translation found?
                        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 };
            }
             // exception?
            if (exception != null) {
                throw exception;
            } else {
                return traduction;
            }
        }
    }
}
  • line 12: property LanguesTraduites interface IServiceTraduction - externally initialized
  • line 13: property UrlServeurTraduction is the Url to request from the translation server: http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1} where the {0} marker must be replaced by the translation direction and the {1} marker by the text to be translated - externally initialized
  • line 14: the property ProxyHttp is the Http proxy to be used, for example : pproxy.istia.uang:3128 - externally initialized
  • line 15 : the property RegexTraduction is the regular expression used to retrieve the translation from the Html stream returned by the translation server, for example @"<div class=""txtTrad"">(.*?)</div>" - externally initialized
  • in our application, these four properties will be initialized by Spring.
  • lines 20-22: check that the requested translation direction exists in the dictionary of translated languages. If not, an exception is thrown.
  • line 24: the text to be translated is encoded to form part of a Url
  • line 26: the Uri of the translation service is built. If the UrlServeurTraduction is the chain http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}, the {0} marker is replaced by the translation direction and the {1} marker by the text to be translated.
  • line 28: the translation search model in the html response from the translation server is built.
  • lines 33, 60: the translation server query operation takes place in a try / catch mode
  • line 35: the object HttpWebRequest which will be used to query the translation server is built with the Uri of the requested document.
  • line 36: the query method is GET. This instruction could be dispensed with, as GET is probably the default method for the HttpWebRequest.
  • line 37: we set the property Proxy object HttpWebRequest.
  • line 39: the request to the translation server is made and its response is retrieved HttpWebResponse.
  • lines 41-42: a StreamReader to read each line of the server's html response.
  • lines 45-53: look for the translation in each line of the answer. When we've found it, we stop reading the Html response and close all the streams we've opened.
  • lines 55-57: if no translation has been found in the html response, prepare an exception of type WebTraductionsException to say it.
  • lines 60-62: if an exception has occurred during the client/server exchange, it is encapsulated in an exception of type WebTraductionsException to say it.
  • lines 64-68: if an exception has been registered, it is thrown, otherwise the translation found is returned.

Our example assumes that the Http proxy does not require authentication. If this were not the case, we would write something like :


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

We used ici WebRequest / WebResponse rather than WebClient because we don't have to exploit the entire Html response from the translation server. Once we've found the translation in this response, we don't need the rest of the lines in the response. The class WebClient doesn't allow you to do that.

Here is a test program for the ServiceTraduction :


using System;
using System.Collections.Generic;
using dao;
using entites;
 
namespace ui {
    class Program {
        static void Main(string[] args) {
            try {
                 // creation translation service
                ServiceTraduction serviceTraduction = new ServiceTraduction();
                 // regular expression to find the translation
                serviceTraduction.RegexTraduction = @"<div class=""txtTrad"">(.*?)</div>";
                 // url translation server
                serviceTraduction.UrlServeurTraduction = "http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}";
                 // dictionary of translated languages
                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";
                 // translation
                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) {
                 // error
                Console.WriteLine("L'erreur suivante de code {1} s'est produite : {0}", e.Message, e.Code);
            }
        }
    }
}

The results are as follows:

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]

The solution project [dao] is compiled into a DLL HttpTraductions.dll :

 

11.7.3.6. The application's graphical interface

Let's go back to the architecture of our application:

We now write the [ui] layer. This is the subject of project [ui] in the solution under construction:

The [lib] folder [3] contains some of the DLL referenced by the [4] project:

  • those required for Spring : Spring.Core, Common.Logging, antlr.runtime
  • layer [dao] : HttpTraductions

The [App.config] file contains the Spring configuration:


<?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>
             <!-- translation service -->
            <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>
  • line 15: objects to be instantiated by Spring. There will be only one, the one on line 18, which instantiates the translation service with the class ServiceTraduction found in DLL HttpTraductions.
  • line 19: property UrlServeurTraduction class ServiceTraduction. There is a problem with the & character in Url. This character has a meaning in a Xml file. It must therefore be protected. This is also the case for other characters we'll come across in the rest of the file. They must be replaced by a sequence [&code;] : & by [&amp;], < by [&lt;] > by [&gt;], " by [&quot;].
  • line 21: property ProxyHttp class ServiceTraduction. An uninitialized property remains null. Not setting this property means that there is no Http proxy.
  • line 23: property RegexTraduction class ServiceTraduction. In the regular expression, we had to replace the [< > "] characters with their protected equivalents.
  • lines 24-33: ownership LanguesTraduites class ServiceTraduction.

The [Program.cs] program is executed when the application is started. Its code is as follows:


using System;
using System.Text;
using System.Windows.Forms;
using dao;
using Spring.Context;
using Spring.Context.Support;
 
namespace ui {
    static class Program {
         /// <summary>
        /// The main entry point for the application.
         /// </summary>
        [STAThread]
        static void Main() {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
 
             // --------------- Developer code
             // instantiation translation service
            IApplicationContext ctx = null;
            Exception ex = null;
            ServiceTraduction serviceTraduction = null;
            try {
                 // spring context
                ctx = ContextRegistry.GetContext();
                 // request a reference for the translation service
                serviceTraduction = ctx.GetObject("ServiceTraduction") as ServiceTraduction;
            } catch (Exception e1) {
                 // memory exception
                ex = e1;
            }
             // form to display
            Form form = null;
             // was there an exception?
            if (ex != null) {
                 // yes - create the error message to be displayed
                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;
                }
                 // creation of an error window to which the error message to be displayed is passed
                Form2 form2 = new Form2();
                form2.MsgErreur = msgErreur.ToString();
                 // this will be the window to display
                form = form2;
            } else {
                 // all went well
                 // creation of a graphical interface [Form1] to which we pass the reference on the translation service
                Form1 form1 = new Form1();
                form1.ServiceTraduction = serviceTraduction;
                 // this will be the window to display
                form = form1;
            }
             // window display
            Application.Run(form);
        }
    }
}

This code has already been used in Impôts version 6, in paragraph 7.6.2.

  • the translation service is created on line 27 by Spring. If this creation was successful, the form [Form1] will be displayed (lines 52-55), otherwise the error form [Form2] will be displayed (lines 36-48).

The [Form2] form is the one used in Impôts version 6 and was explained in paragraph 7.6.4.

The form [Form1] is as follows:

type
name
role
1
TextBox
textBoxTexteATraduire
input box for text to be translated
MultiLine=true
2
ComboBox
comboBoxLangues
list of translation directions
3
Button
buttonTraduire
to request the translation of text [1] in the sense [2]
4
TextBox
textBoxTraduction
translation of the text [1]

The form code [Form1] is as follows:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using dao;
 
namespace ui {
    public partial class Form1 : Form {
         // translation service
        public ServiceTraduction ServiceTraduction { get; set; }
         // language dictionary
        Dictionary<string, string> languesInversées = new Dictionary<string, string>();
 
         // manufacturer
        public Form1() {
            InitializeComponent();
        }
 
         // initial form loading
        private void Form1_Load(object sender, EventArgs e) {
             // building an inverted language dictionary
            foreach (string code in ServiceTraduction.LanguesTraduites.Keys) {
                 // languages
                string langues = ServiceTraduction.LanguesTraduites[code];
                // add (languages, code) to the inverted dictionary
                languesInversées[langues] = code;
            }
            // filling combo in alphabetical language order
            string[] languesCombo = languesInversées.Keys.ToArray();
            Array.Sort<string>(languesCombo);
            foreach (string langue in languesCombo) {
                comboBoxLangues.Items.Add(langue);
            }
             // 1st language selection
            if (comboBoxLangues.Items.Count != 0) {
                comboBoxLangues.SelectedIndex = 0;
            }
        }
 
        private void buttonTraduire_Click(object sender, EventArgs e) {
             // something to translate?
            string texte = textBoxTexteATraduire.Text.Trim();
            if (texte == "") return;
             // translation
            try {
                textBoxTraduction.Text = ServiceTraduction.Traduire(texte, languesInversées[comboBoxLangues.SelectedItem.ToString()]);
            } catch (Exception ex) {
                textBoxTraduction.Text = ex.Message;
            }
        }
    }
}
  • line 10: a reference to the translation service. This public property has been initialized by [Program.cs], line 53. When the Form1_Load (line 20) or buttonTraduire_Click (line 40) are executed, so this field is already initialized.
  • line 12: the dictionary of translated languages with entries of type ["French-English", "fe"], c.a.d. the opposite of the dictionary LanguesTraduites rendered by the translation service.
  • line 20: the method Form1_Load is executed when the form is loaded.
  • lines 22-27: using the dictionary serviceTraduction.LanguesTraduites ["fe", "Français-Anglais"] to build the dictionary languagesInversées ["French-English", "fe"].
  • line 29: languesCombo is the array of dictionary keys languagesInversées, c.a.d. an array of elements ["French-English"]
  • line 30: this table is sorted to present the translation directions in alphabetical order in the combo
  • lines 31-33: the language combo is completed.
  • line 40: the method executed when the user clicks on the [Translate] button
  • line 46: simply call the serviceTraduction.Traduire to request a translation. The 1st parameter is the text to be translated, the second is the translation direction code. This code is found in the languagesInversées from the item selected in the language combo.
  • line 48: if there is an exception, it is displayed instead of the translation.

11.7.3.7. Conclusion

This application showed that the web clients of the .NET framework enabled us to exploit the resources of web. The technique is similar each time:

  • determine the Uri to query. This Uri is the plupart of time set.
  • question him
  • find what you're looking for in the server response using regular expressions

This technique is random. Over time, the Uri queried or the regular expression used to find the expected result may change. It's therefore a good idea to place both these items of information in a configuration file. But this may not be enough. We'll see in the next chapter that there are more stable resources on web: web services.

11.7.4. A SMTP (Simple Mail Tranport Protocol) client with SmtpClient class

A SMTP client is a client of a SMTP mail server. The .NET class SmtpClient fully encapsulates the needs of such a client. The developer doesn't need to know the details of the SMTP protocol. We are familiar with it. It was presented in paragraph 11.4.3.

We present the SmtpClient as part of a basic Windows application for sending e-mails with attachments. The application will connect to port 25 of a SMTP server. Remember that on most PC windows, firewalls or other antivirus software block connections to port 25, so it's necessary to disable this protection to test the application:

The Smtp client will have a single-layer architecture:

The Visual studio project is as follows:

  

The application's graphical interface [SendMailForm.cs] is as follows:

type
name
role
1
TextBox
textBoxServeur
server name SMTP to connect to
2
NumericUpDown
numericUpDownPort
the port to connect to
3
TextBox
textBoxExpediteur
message sender's address
4
TextBox
textBoxTo
recipient addresses in the form: address1,address2, ...
5
TextBox
textBoxCc
addresses of copied recipients (CC=Carbon Copy) in the form: address1,address2, ...
6
TextBox
textBoxBcc
blind copy recipient addresses (BCC=Blind Carbon Copy) in the form: address1,address2, ... All addresses in these three input fields will receive the same message with the same attachments. The recipients of the message will know the addresses in fields 4 and 5, but not those in field 6. The Bcc is therefore a way of copying someone without the other recipients of the message knowing.
7
Button
buttonAjouter
to add an attachment to the mail
8
ListBox
listBoxPiecesJointes
list of attachments
9
TextBox
textBoxSujet
mail subject
10
TextBox
textBoxMessage
the message text.
MultiLine=true
11
Button
buttonEnvoyer
to send the message and any attachments
12
TextBox
textBoxResult
displays a summary of the message sent, or an error message if a problem has been encountered
13
Button
buttonEffacer
to delete [12]
 
OpenfileDialog
openFileDialog1
non-visual control to select an attachment in the local file system

In the previous example, the summary displayed in [12] is as follows:

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

The form code [SendMailForm.cs] is as follows:


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();
        }
 
         // add an attachment
        private void buttonAjouter_Click(object sender, EventArgs e) {
            // set the openfileDialog1 dialog box
            openFileDialog1.InitialDirectory = Application.ExecutablePath;
            openFileDialog1.Filter = "Tous les fichiers (*.*)|*.*";
            openFileDialog1.FilterIndex = 0;
            openFileDialog1.FileName = "";
            // display the dialog box and retrieve the result
            if (openFileDialog1.ShowDialog() == DialogResult.OK) {
                // retrieve the file name
                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() != "";
        }
 
         // remove an attachment
        private void buttonRetirer_Click(object sender, EventArgs e) {
             // selected attachment?
            if (listBoxPiecesJointes.SelectedIndex != -1) {
                 // remove it
                listBoxPiecesJointes.Items.RemoveAt(listBoxPiecesJointes.SelectedIndex);
                // update the Remove button
                buttonRetirer.Enabled = listBoxPiecesJointes.Items.Count != 0;
            }
        }
 
        private void listBoxPiecesJointes_SelectedIndexChanged(object sender, EventArgs e) {
             // selected attachment?
            if (listBoxPiecesJointes.SelectedIndex != -1) {
                // update the Remove button
                buttonRetirer.Enabled = true;
            }
        }
 
        // sending the message with attachments
        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 won't be commenting on this code, which doesn't present any new features. To understand the method buttonAjouter_Click from line 14, the reader is invited to reread paragraph 7.5.1.

The method buttonEnvoyer_Click of line 55, which sends the mail, is as follows:


private void buttonEnvoyer_Click(object sender, EventArgs e) {
            try {
                 // hourglass
                Cursor = Cursors.WaitCursor;
                // the customer Smtp
                SmtpClient smtpClient = new SmtpClient(textBoxServeur.Text.Trim(), (int)numericUpDownPort.Value);
                // the message
                MailMessage message = new MailMessage();
                 // sender
                message.Sender = new MailAddress(textBoxExpéditeur.Text.Trim());
                message.From = message.Sender;
                 // recipients
                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));
                    }
                }
                 // subject
                message.Subject = textBoxSujet.Text.Trim();
                 // message text
                message.Body = textBoxMessage.Text;
                 // attachments
                foreach (string attachement in listBoxPiecesJointes.Items) {
                    message.Attachments.Add(new Attachment(attachement));
                }
                // sending the message
                smtpClient.Send(message);
                // Ok - a summary is displayed
                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) {
                // error is displayed
                textBoxResultat.Text = String.Format("L'erreur suivante s'est produite {0}", ex);
            }
             // normal slider
            Cursor = Cursors.Arrow;
        }
  • line 6: client Smtp is created. It needs two parameters: the server name SMTP and the port on which the server operates
  • line 8: a MailMessage is created. It encapsulates the entire message to be sent.
  • line 10: e-mail address Sender of the sender is filled in. An e-mail address is an instance of type MailAddress constructed from the string "xx@yy.zz". This string must have the expected form for an e-mail address, otherwise an exception is thrown. In this case, it will be displayed in the textBoxResultat (line 63) in an unfriendly form.
  • lines 13-19: recipients' e-mail addresses are placed in the list To of the message. These addresses are retrieved from the textBoxTo. The regular expression on line 13 retrieves the various addresses, separated by commas.
  • lines 21-26: repeat the same process to initialize the field CC messaging with addresses copied from the textBoxCc.
  • lines 28-33: repeat the same process to initialize the field Bcc messaging with blind copy addresses in the textBoxBcc.
  • line 35: the field Subject is initialized with the subject of the field textBoxSujet.
  • line 37: the field Body is initialized with the message text textBoxMessage.
  • lines 39-41: attachments are attached to the message. Each attachment is added as an object Attachment in the field Attachments of the message. An object Attachment is instantiated from the full path of the part to be attached in the local file system.
  • line 43: the message is sent using the Send customer Smtp.
  • lines 45-60: write shipment summary in field textBoxResultat and reset the form.
  • line 63: error display

11.8. A generic asynchronous Tcp client

11.8.1. Presentation

In all the examples in this chapter, client/server communication was in blocking mode, also known as synchronous mode:

  • when a client connects to a server, it waits for the server's response to this request before continuing.
  • when a client reads a line of text sent by the server, it is blocked until the server has sent it.
  • on the server side, the service threads that provide service to the client operate in the same way as above.

In graphical user interfaces, it is often necessary to avoid blocking the user during long operations. The case often cited is that of downloading a large file. While the file is being downloaded, the user must be free to continue interacting with the graphical interface.

We propose ici to rewrite the generic Tcp client of the paragraph 11.6.3 with the following changes:

  • the interface will be graphical
  • the communication tool with the server will be a Socket
  • the communication mode will be asynchronous:
    • the client will initiate a connection to the server, but won't be stuck waiting for it to be established
    • the customer will initiate a shipment to the server but won't get stuck waiting for it to finish
    • the client will initiate the reception of data from the server, but will not remain blocked waiting for the data to be received.

Let's recall at what level the object is located Socket in client/server communication Tcp :

The class Socket is the one that operates closest to the network. It enables fine-tuned management of the network connection. The term socket refers to a power socket. The term has been extended to designate a software network socket. In TCP-IP communication between two machines A and B, these are two sockets that communicate with each other. An application can work directly with sockets. This is the case for application A above. A socket can be a customer or server.

11.8.2. Asynchronous Tcp client GUI

The Visual Studio application is as follows:

  

[ClientTcpAsynchrone.cs] is the graphical interface. It is as follows:

type
name
role
1
TextBox
textBoxNomServeur
server name Tcp to connect to
2
NumericUpDown
numericUpDownPortServeur
the port to connect to
3
RadioButton
radioButtonLF
radioButtonRCLF
to indicate the end-of-line mark to be used by the client: LF "\n" or RCLF "\r\n"
4
Button
buttonConnexion
to connect to port [2] of server [1]. The button is labelled [Connect] when the client is not connected to a server, [Disconnect] when connected.
5
TextBox
textBoxMsgToServeur
message to be sent to the server once the connection has been made. When the user presses [Enter], the message is sent with the end-of-line mark selected in [3]
6
ListBox
listBoxEvts
list displaying the main client/server link events: connection, disconnection, flow closure, communication errors, etc
7
ListBox
listBoxDialogue
list displaying client/server dialog messages
8
Button
buttonRazEvts
to clear the list [6]
4
Button
buttonRazDialogue
to clear the list [7]

The operating principles of this interface are as follows:

  • the user connects his Tcp graphical client to a Tcp service via [1, 2, 3, 4].
  • an asynchronous thread continuously accepts all data sent by the Tcp server and displays it in the list [7]. This thread is dissociated from other interface activities.
  • users can send messages to the server at their own pace, thanks to [5]. Each message is sent by an asynchronous thread. Unlike the receive thread, which never stops, the send thread is terminated as soon as the message has been sent. A new asynchronous thread will be used for the next message.
  • client/server communication ends when one of the partners closes the connection. The user can take this initiative with button [4] which, once the connection has been established, is labelled [Disconnect].

Here's a screenshot of an execution:

  • in [1]: connection to a POP service
  • in [2]: display of events that occurred during connection
  • in [3]: the message sent by server POP at the end of the connection
  • in [4]: the [Connect] button has become the [Disconnect] button
  • in [1], we sent the command quit to server POP. The server replied +OK goodbye and closed the connection
  • in [2], this server-side closure was detected. The client then closed the connection on its side.
  • in [3], the [Disconnect] button has reverted to a [Connect] button

11.8.3. Asynchronous server connection

Pressing the [Connect] button executes the following method:


        private void buttonConnexion_Click(object sender, EventArgs e) {
            // connection or disconnection?
            if (buttonConnexion.Text == "Déconnecter")
                déconnexion();
            else
                connexion();
}
  • line 3: the button can be labelled [Connect] or [Disconnect].

The connection method is as follows:


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";
 
         // delegates
        public delegate void writeLog(string log);
 
        public ClientTcp() {
            InitializeComponent();
        }
....................................
    private void connexion() {
             // data checks
            string nomServeur = textBoxNomServeur.Text.Trim();
            if (nomServeur == "") {
                logEvent("indiquez le nom du serveur");
                return;
            }
             // follow-up
            logEvent(String.Format("connexion en cours au serveur {0}", nomServeur));
            try {
                 // socket creation
                client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                 // asynchronous connection
                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;
            }
        }
 
        // the connection has been made
        private void connecté(IAsyncResult résultat) {
            // retrieve the client socket
            Socket client = résultat.AsyncState as Socket;
    ...
        }
 
 
         // process monitoring
        private void logEvent(string msg) {
....
        }
    }
}
  • line 1: the classroom Socket is part of the System.Net.Sockets.

A certain amount of data must be shared between several form methods. These are as follows:

  • line 7: customer is the communication socket with the server
  • lines 6 and 8: the client will receive messages in an array of bytes data.
  • line 9: answer is the response sent by the server.
  • line 10: finLigne is the end-of-line mark used by the client Tcp - is initialized by default to RCLF but can be modified by the user using radio buttons [3].

The procedure connection in line 19 connects to the Tcp server:

  • lines 21-25: check that the server name is not empty. If this is not the case, the event is logged in listBoxEvts method logEvent line 49.
  • line 27: signals that the connection is about to take place
  • line 30: create object Socket required for Tcp-Ip communication. The manufacturer admits three parameters:
    • AddressFamily addressFamily : the family of IP client and server addresses, ici IPv4 addresses (AddressFamily.InterNetwork)
    • SocketType socketType : socket type. The socket type SocketType.Stream is suitable for Tcp-Ip connections
    • ProtocolType protocolType : the type of internet protocol used, ici the Tcp protocol
  • line 32: the connection is made asynchronously. The connection is launched, but execution continues without waiting for its end. The [Socket].BeginConnect has four parameters :
    • IPAddress ipAddress : the Ip address of the machine running the service to be connected to
    • Int32 port : wearing the service
    • AsyncCallBack asyncCallBack : AsyncCallBack is a type delegate :
public void AsyncCallBack(IAsyncResult ar);

The method asyncCallBack passed as the 3rd parameter of the BeginConnect must be a method accepting a IAsyncCallBack and return no results. This is the method that will be called when the connection has been made. We pass ici as the 3rd parameter, the method connected on line 41.

  • (continued)
    • Object state : an object to pass to the asyncCallBack. This method receives (see delegate above) a parameter ar type IAsyncResult. The object state can be retrieved from ar.AsyncState (line 43). We pass ici as the 4th parameter, the client socket.
  • line 38: the method is terminated. The user can interact with the GUI again. The connection takes place in the background, in parallel with GUI event handling. Also in parallel, the connected of line 41 will be called at the end of the connection, whether it ends well or badly.

The method code connected is as follows:


// the connection has been made
        private void connecté(IAsyncResult résultat) {
            // retrieve the client socket
            Socket client = résultat.AsyncState as Socket;
            try {
                 // end asynchronous operation
                client.EndConnect(résultat);
                 // follow-up
                logEvent(String.Format("connecté au service {0}", client.RemoteEndPoint));
                 // form
                buttonConnexion.Text = "Déconnecter";
                // asynchronous reading of data from the 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;
            }
}
 
         // data reception
        private void lecture(IAsyncResult résultat) {
            // retrieve the client socket
            Socket client = résultat.AsyncState as Socket;
...
        }
 
  • line 4: the client socket is retrieved from the parameter result received by the method. Remember that this object is the one passed as the 4th parameter to the BeginConnect.
  • line 7: the connection attempt is terminated by the EndConnect to which the parameter result received by the method.
  • line 9: the event is logged in the event list
  • line 11: the [Connect] button becomes a [Disconnect] button so that the user can request disconnection.
  • line 13: the server response is initialized. It will be updated by repeated calls to the asynchronous method BeginReceive.
  • line 14: 1st call to asynchronous method BeginReceive. It is called with the following parameters :
    • byte[] buffer : the buffer in which to place the data to be received - ici the buffer is data
    • int offset : from which buffer position to place data the data to be received - ici the offset is 0, c.a.d. that data is placed from the 1st byte of the buffer.
    • int size : buffer size in bytes - ici size is tailleBuffer.
    • SocketFlags socketFlags : socket configuration - ici no configuration
    • AsyncCallBack asyncCallBack : the method to be called when reception is complete. This will be the case either because the buffer has received data or because the connection has been closed. in the case of Ici, the callback method is the reading on line 22.
    • Object state : the object to be passed to the callback method asyncCallBack. Ici, the client socket is passed again.

Note that all this takes place without any action on the part of the user, other than the initial connection request via the [Connect] button. At the end of the connected, another method is executed in the background: the reading which we are now examining.


// data reception
        private void lecture(IAsyncResult résultat) {
            // retrieve the client socket
            Socket client = résultat.AsyncState as Socket;
            int nbOctetsReçus = 0;
            bool erreur = false;
            try {
                // number of bytes received
                nbOctetsReçus = client.EndReceive(résultat);
                if (nbOctetsReçus == 0) {
                     // server no longer responds
                    logEvent("le serveur a fermé la connexion");
                }
            } catch (Exception e) {
                 // we had a reception problem
                logEvent(String.Format("erreur de réception : {0}", e.Message));
                erreur = true;
            }
             // finished?
            if (nbOctetsReçus == 0 || erreur) {
                // the customer is disconnected as required
                déconnexion();
                // the end of the answer is displayed
                afficherRéponseServeur(réponse, true);
                 // end reading
                return;
            }
            // retrieve the data received
            string données = Encoding.UTF8.GetString(data, 0, nbOctetsReçus);
            // we add them to the data already received
            réponse += données;
            // the answer is displayed
            afficherRéponseServeur(réponse, false);
            // we read on
            client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
        }
  • line 2: the method reading is triggered in the background when the data has received data or the connection has been closed by the server.
  • line 9: the asynchronous read request is terminated by EndReceive. Again, this method must be called with the parameter received by the callback function. The EndReceive returns the number of bytes received in the read buffer.
  • line 10: if the number of bytes is zero, the connection has been closed by the server.
  • line 12: the event is noted in the event list
  • line 14: an exception is processed
  • lines 16-17: note the event in the event list and record the error
  • line 20: check whether to close the connection
  • line 22: close the client-side connection with a disconnect which we'll look at later.
  • line 24: server response, c.a.d. global variable answer is displayed in the listBoxDialogue using a private method displayServerResponse.
  • line 26: end of asynchronous method reading
  • line 29: the bytes received are put into a string in UTF8 format.
  • line 31: they are added to the answer under construction
  • line 33: the answer is displayed in the list listBoxDialogue.
  • line 35: we go back to waiting for data from the server

Ultimately, the asynchronous method reading never stops. It continuously reads data from the server and displays it in the listBoxDialogue. It only stops when the connection is closed by either the server or the user himself.

11.8.4. Server disconnection

Pressing the [Disconnect] button executes the following method:


        private void buttonConnexion_Click(object sender, EventArgs e) {
            // connection or disconnection?
            if (buttonConnexion.Text == "Déconnecter")
                déconnexion();
            else
                connexion();
}
  • line 3: the button can be labelled [Connect] or [Disconnect].

The method disconnect ensures customer disconnection:


private void déconnexion() {
             // socket closure
            if (client != null && client.Connected) {
                try {
                     // follow-up
                    logEvent(String.Format("déconnexion du service {0}", client.RemoteEndPoint));
                     // disconnect
                    client.Shutdown(SocketShutdown.Both);
                    client.Close();
                     // form
                    buttonConnexion.Text = "Connecter";
                } catch (Exception ex) {
                     // follow-up
                    logEvent(String.Format("erreur de lors de la déconnexion : {0}", ex.Message));
                }
            }
        }
  • line 3: if the customer exists and is connected
  • line 6: disconnection is announced in listBoxEvts. The property client.RemoteEndPoint gives the pair (Address Ip, port) of the other end of the connection, c.a.d ici of the server.
  • line 8: the socket data stream is closed with the ShutDown. A socket's data flow is bidirectional: the socket transmits and receives data. The the method ShutDown can be : ShutDown.Receive to close the receive stream, Shutdonw.Send to close the emission flow or ShutDown.Both to close the two streams.
  • line 9: release socket resources
  • line 11: the [Disconnect] button becomes the [Connect] button
  • lines 12-15: exception handling

11.8.5. Asynchronous data transfer to the server

When the user validates the message in the textBoxMsgToServeur, the following method is executed:


        private void textBoxMsgToServeur_KeyPress(object sender, KeyPressEventArgs e) {
             // enter] key ?
            if (e.KeyChar == 13 && client.Connected) {
                envoyerMessage();
            }
}
  • lines 3-5: if the user has pressed the [Enter] key and the client socket is connected, then the message in the textBoxMsgToServeur is sent with the envoyerMessage.

Visit method envoyerMessage is as follows:


        private void envoyerMessage() {
             // send a message asynchronously
            // the message
            byte[] message = Encoding.UTF8.GetBytes(textBoxMsgToServeur.Text.Trim() + finLigne);
            // it is sent
            client.BeginSend(message, 0, message.Length, SocketFlags.None, écriture, client);
             // dialogue
            logDialogue("--> " + textBoxMsgToServeur.Text.Trim());
             // raz message
            textBoxMsgToServeur.Clear();
}
  • line 4: the customer's end-of-line mark is added to the message and placed in the byte array message.
  • line 6: an asynchronous transmission is started using the BeginSend. The parameters of BeginSend are identical to those of the BeginReceive. At the end of the asynchronous message transmission operation, the writing will be called.
  • line 8: the message sent is added to the list listBoxDialogue to monitor the client/server dialog
  • line 10: the message sent is deleted from the graphical interface

The recall method writing is as follows:


        private void écriture(IAsyncResult résultat) {
             // result of message transmission
            Socket client = résultat.AsyncState as Socket;
            try {
                client.EndSend(résultat);
            } catch (Exception e) {
                 // we had an emission problem
                logEvent(String.Format("erreur d'émission : {0}", e.Message));
            }
}
  • line 4: the recall method writing receives a result parameter of type IAsyncResult.
  • line 3: in parameter result, retrieve the client socket. This socket was the 5th parameter of the BeginSend.
  • line 5: the asynchronous send operation is terminated.

You don't have to wait for a message to be sent before giving it back to the user. This means that the user can send a second message even though the first message has not yet been sent.

11.8.6. Display of events and client/server dialog

Events are displayed using the logEvents :


         // process monitoring
        private void logEvent(string msg) {
            listBoxEvts.Invoke(new writeLog(logEventCallBack), msg);
        }
 
        private void logEventCallBack(string msg) {
             // message display
            msg = msg.Replace(finLigne, " ");
            listBoxEvts.Items.Insert(0, String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}
  • line 2: the method logEvents receives the message to be added to the list as a parameter listBoxEvts.
  • line 3: the component cannot be used directly listBoxEvents. Indeed, the logEvents is called by two types of threads :
    • the main thread owning the GUI, for example when it signals that a connection attempt is in progress
    • a secondary thread for asynchronous operation. This type of thread does not own components, and its access to a C component must be controlled by a C.Invoke. This operation tells control C that a thread wants to perform an operation on it. The Invoke has two parameters :
      • a delegate. This callback function will be executed by the GUI owner thread, not by the thread running the C.Invoke.
      • an object to be passed to the callback function.

Ici the first parameter passed to the Invoke is an instance of the following delegate :


        public delegate void writeLog(string log);

The delegate writeLog has a parameter of type string and returns no result. The parameter will be the message to be entered in listBoxEvts.

Line 3, the first parameter passed to the Invoke is the logEventCallBack on line 6. It corresponds to the delegate's signature writeLog. The second parameter passed to the Invoke is the message that will be passed as a parameter to the logEventCallBack.

The operation Invoke is a synchronous operation. Execution of the secondary thread is blocked until the thread owning the control executes the callback method.

  • line 6: the callback method executed by the GUI thread receives the message to be displayed in the control listBoxEvts.
  • line 9: the event is logged in 1st position in the list, so that the most recent events are at the top of the list.

Client/server dialog messages are displayed using the logDialogue :


         // dialogue follow-up
        private void logDialogue(string msg) {
            listBoxDialogue.Invoke(new writeLog(logDialogueCallBack), msg);
        }
        private void logDialogueCallBack(string msg) {
             // message display
            msg = msg.Replace(finLigne, " ");
            listBoxDialogue.Items.Add(String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}

The principle is the same as in the logEvent.

Messages received by the client are displayed using the displayServerResponse :


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

The first parameter is the message to be displayed. This message can be a series of lines. In fact, the client reads data from the server in blocks of tailleBuffer (1024) bytes. Within these 1024 bytes, various lines can be found, identified by their "\n" end-of-line mark. The last line may be incomplete, its end-of-line mark being in the 1024 bytes that follow. The method finds the lines ending in "\n" in the message and then asks logDialogue to display them. The method's second parameter indicates whether to display the last line found or leave it in the buffer to be completed by the next message. The code is rather complex and of no interest ici. It will therefore not be commented on.

11.8.7. Conclusion

The same example could be treated with synchronous operations. Ici the asynchronous aspect of the graphical interface does little for the user. However, if he logs in and then realizes that the server is "no longer responding", he has the option of disconnecting, thanks to the fact that the GUI continues to respond to events during the execution of asynchronous operations. This rather complex example enabled us to introduce some new notions:

  • the use of sockets
  • the use of asynchronous methods. What we have seen is part of a standard. Other asynchronous methods exist and operate on the same model.
  • the updating of GUI controls by secondary threads.

Asynchronous Tcp / Ip communication offers more serious advantages for a server than those shown by the previous example. We know that the server serves its clients using secondary threads. If its thread pool has N threads, this means it can only serve N clients simultaneously. If all N threads perform a blocking (synchronous) operation, there are no more threads available for a new client until one of the blocking operations completes and frees a thread. If asynchronous rather than synchronous operations are performed on threads, a thread is never blocked and can be quickly recycled for new clients.

11.9. Sample application, version 8: Tax calculation server

11.9.1. The architecture of the new version

We return to the tax calculation application that has already been covered in various forms. Let's recall its latest version, version 7 of paragraph 9.8.

The data was in a database and the [ui] layer was a graphical user interface:

 

We are going to reproduce this architecture and distribute it on two machines:

  • a [server] machine will host the [metier] and [dao] layers of version 7. A Tcp/Ip [server] [1] layer will be built to allow internet clients to query the tax calculation service.
  • a [client] machine will host the [ui] layer of version 7. A Tcp/Ip [client] [2] layer will be built to enable the [ui] layer to query the tax calculation service.

The architecture changes profoundly ici. Version 7 was a single-user Windows application. Version 8 becomes a internet client/server application. The server will be able to serve several clients simultaneously.

First, we'll write the [server] part of the application.

11.9.2. Tax calculation server

11.9.2.1. The Visual Studio project

The Visual studio project will be as follows:

  • in [1], the project. It includes the following elements:
  • [ServeurImpot.cs]: the Tcp/Ip tax calculation server in the form of a console application.
  • [dbimpots.sdf]: the SQL Server compact database from version 7 described in paragraph 9.8.5.
  • [App.config]: application configuration file.
  • in [2], the [lib] folder contains the DLL needed for the project:
    • [ImpotsV7-dao]: the [dao] layer in version 7
    • [ImpotsV7-metier]: the [metier] layer in version 7
    • [antlr.runtime, CommonLogging, Spring.Core] for Spring
  • in [3], the project references

11.9.2.2. Application configuration

File [App.config] is operated by Spring. Its contents are as follows:


<?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>
  • lines 16-20: configuration of the [dao] layer associated with the SQL Server compact database
  • lines 21-23: [metier] layer configuration.

This is the configuration file used in version 7's [ui] layer. It was presented in paragraph 9.8.4.

11.9.2.3. Server operation

On server startup, the server application instantiates the [metier] and [dao] layers, then displays an administration console interface:

  

The administration console accepts the following commands:

start port
to start the service on a given port
stop
to stop the service. It can then be restarted on the same or another port.
echo start
to activate the client/server dialog echo on the console
echo stop
to deactivate echo
status
to display the active/inactive status of the service
quit
to quit the application

Let's start the server:

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 >

Let's now run the asynchronous graphical Tcp client studied earlier in this section 11.8.

Image

The customer is logged in. He can send the following commands to the tax calculation server:

aide
for a list of authorized commands
impot marié nbEnfants salaireAnnuel
to calculate the tax liability of someone with nbEnfants children and a salary of salaireAnnuel euros. married is o if married, n or else.
aurevoir
to close the connection with the server

Here is an example of a dialogue:

On the server side, the console displays the following:

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

Let's switch on the echo and start a new dialog from the graphics client:

 

The admin console then displays the following:

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
  • line 1: client/server dialog echo enabled
  • line 2: a customer has arrived
  • line 3: he sent the [help] command
  • lines 4-7: server response on 4 lines.

Stop the service:

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 >
  • line 1: service stop requested (not the application itself)
  • line 2: an exception due to the fact that the server blocked on a client expectation was abruptly interrupted by the closure of the listening service.
  • line 3: service can now be restarted by start port or stopped by quit.

Before the listening service was stopped, a client was being served on another connection. This connection is not closed when the listening socket is closed. The client can continue to issue commands: the service thread associated with it before the listening service was closed continues to respond:

Image

11.9.3. Server code Tcp for tax calculation

1
  

The server code [ServeurImpot.cs] is as follows:


...
namespace Chap9 {
    public class ServeurImpot {
 
        // data shared between threads and methods
        private static IImpotMetier metier = null;
        private static int port;
        private static TcpListener service;
        private static bool actif = false;
        private static bool echo = false;
 
         // main program
        public static void Main(string[] args) {
            // instantiations layers [metier] and [dao]
            IApplicationContext ctx = null;
            metier = null;
            try {
                 // context Spring
                ctx = ContextRegistry.GetContext();
                // a reference is requested on the [metier] layer
                metier = (IImpotMetier)ctx.GetObject("metier");
 
                 // thread pool configuration
                ThreadPool.SetMinThreads(10, 10);
                ThreadPool.SetMaxThreads(10, 10);
 
                // reads server administration commands typed on the keyboard in an endless loop
                string commande = null;
                string[] champs = null;
                while (true) {
                     // invite
                    Console.Write("Serveur de calcul d'impôt >");
                    // read command
                    commande = Console.ReadLine().Trim().ToLower();
                    champs = Regex.Split(commande, @"\s+");
                     // order execution
                    switch (champs[0]) {
                        case "start":
                             // active?
                            if (actif) {
                                //error
                                Console.WriteLine("Le serveur est déjà actif");
                            } else {
                                 // port check
                                if (champs.Length != 2 || !int.TryParse(champs[1], out port) || port <= 0) {
                                    Console.WriteLine("Syntaxe : start port. Port incorrect");
                                } else {
                                    // we launch the listening service
                                    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":
                             // end of service
                            if (actif) {
                                service.Stop();
                                actif = false;
                            }
                            break;
                        case "status":
                             // server status
                            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":
                            // quit the application
                            Console.WriteLine("Fin du service");
                            Environment.Exit(0);
                            break;
                        default:
                             // incorrect order
                            Console.WriteLine("Commande incorrecte. Utilisez (start,stop,echo, status, quit)");
                            break;
                    }
                }
            } catch (Exception e1) {
                 // exception display
                Console.WriteLine("L'erreur suivante s'est produite à l'initialisation de l'application : {0}", e1.Message);
                return;
            }
        }
 
 
        private static void doEcoute(Object data) {
...
        }
 
....
    }
}
  • lines 18-21: the [metier] and [dao] layers are instantiated by Spring configured by [App.config]. The global variable job of line 6 is then initialized.
  • lines 24-25: configure the application's thread pool with 10 minimum and maximum threads.
  • lines 30-86: loop for entering service administration commands (start, stop, quit, echo, status).
  • line 32: server prompt for each new command
  • line 34: read administrator command
  • line 35: the order is broken down into fields for analysis
  • lines 38-52: the order start port to launch the listening service
    • line 40: if the service is already active, there's nothing to do
    • line 45: check that the port is present and correct. If so, the global variable port of line 7 is positioned.
    • line 49: the listening service will be managed by a secondary thread so that the main thread can continue executing console commands. If the doEcoute successful connection, the global variables service line 8 and assets on line 9 are initialized.
  • lines 53-60: the order echo start / stop enables/disables echo of client/server dialog on console
    • line 58: the global variable echo of line 7 is positioned
  • lines 61-67: the order stop which stops the listening service.
    • line 64: stop listening service
  • lines 68-75: the order status which displays the service's active/inactive status
  • lines 76-80: the order quit that stops everything.

The thread responsible for listening to customer requests executes the doEcoute next :


        private static void doEcoute(Object data) {
            // thread for listening to customer requests
            try {
                // create the service
                service = new TcpListener(IPAddress.Any, port);
                 // launch it
                service.Start();
                // the server is active
                actif = true;
                 // follow-up
                Console.WriteLine("Serveur de calcul d'impôt lancé sur le port {0}", port);
                 // customer service loop
                TcpClient tcpClient = null;
                 // customer no
                int numClient = 0;
                 // endless loop
                while (true) {
                    // waiting for a customer
                    tcpClient = service.AcceptTcpClient();
                    // the service is provided by another task
                    ThreadPool.QueueUserWorkItem(doService, new Client() { CanalTcp = tcpClient, NumClient = numClient });
                     // next customer
                    numClient++;
                }
            } catch (Exception ex) {
                // we report the error
                Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
            }
        }
 
         // customer info
        internal class Client {
             public TcpClient CanalTcp { get; set        ; } // customer liaison
             public int NumClient { get; set            ; } // customer no
}

This code is similar to that of the echo server studied in paragraph 11.6.1. We only comment on what is different:

  • line 7: listening service launched
  • line 9: notes that the service is now active

Line 21, customers are served by service threads running the doService next :


private static void doService(Object infos) {
            // the customer is picked up and served
            Client client = infos as Client;
            // renders service to the customer
            Console.WriteLine("Début du service au client {0}", client.NumClient);
             // operation link TcpClient
            try {
                using (TcpClient tcpClient = client.CanalTcp) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                 // unbuffered output stream
                                writer.AutoFlush = true;
                                // send a welcome message to the customer
                                writer.WriteLine("Bienvenue sur le serveur de calcul de l'impôt");
                                // loop read request/write response
                                string demande = null;
                                bool serviceFini = false;
                                while (!serviceFini && (demande = reader.ReadLine()) != null) {
                                     // console monitoring
                                    if (echo) {
                                        Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
                                    }
                                     // demand analysis
                                    demande = demande.Trim().ToLower();
                                    // empty request?
                                    if (demande.Length == 0) {
                                        // erroneous request
                                        writeClient(writer,client.NumClient,"Commande non reconnue. Utilisez la commande aide.");
                                        return;
                                    }
 
                                    // demand is broken down into fields
                                    string[] champs = Regex.Split(demande, @"\s+");
                                     // analysis
                                    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":
                                            // tax calculation
                                            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) {
                // error
                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) {
             // echo console ?
            if (echo) {
                Console.WriteLine("---> Client {0} : {1}", numClient, message);
            }
             // send msg to customer
            writer.WriteLine(message);
}

Once again, the code is similar to that of the echo server studied in paragraph 11.6.1 We only comment on what is different:

  • line 15: once the client is connected, the server sends a welcome message.
  • lines 19-52: the loop for reading customer commands. The loop stops when the customer sends the "goodbye".
  • line 27: case of an empty order
  • line 34: the request is broken down into fields for analysis
  • line 37: order help : the customer requests a list of authorized orders
  • line 40: order tax : the client requests a tax calculation. We respond with the message returned by the calculImpot which we'll be detailing shortly.
  • line 44: order goodbye : the customer indicates that he is finished.
  • line 45: we're getting ready to leave the reading loop of customer requests (lines 19-52)
  • line 46: we reply to the customer with a good-bye message
  • line 48: an incorrect order. An error message is sent to the customer.

Order processing tax is ensured by the calculImpot next :


private static string calculImpot(StreamWriter writer, int numClient, string[] champs) {
             // request calculation married(Y/N) nbEnfants salaireAnnuel
             // 4 fields are required
            if (champs.Length != 4) {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
             // fields [1]
            string marié = champs[1];
            if (marié != "o" && marié != "n") {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
             // fields [2]
            int nbEnfants;
            if (!int.TryParse(champs[2], out nbEnfants)) {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
             // fields [3]
            int salaireAnnuel;
            if (!int.TryParse(champs[3], out salaireAnnuel)) {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
             // that's it - tax calculation
            int impot = 0;
            try {
                impot = metier.CalculerImpot(marié == "o", nbEnfants, salaireAnnuel);
                return impot.ToString();
            } catch (Exception ex) {
                return ex.Message;
            }
        }
  • line 1: the method receives the array of command fields as the 3rd parameter tax. If correctly formulated, it is of the form married tax nbEnfants salaireAnnuel. The result of the method is the response to be sent to the client.
  • line 4: check that the command has 4 fields
  • line 8: check that the married is valid
  • line 14: check that the nbEnfants is valid
  • line 19: check that the salaireAnnuel is valid
  • line 25: tax is calculated using the CalculerImpot of the [metier] layer. Remember that this layer is encapsulated in a DLL.
  • line 26: if the [metier] layer has returned a result, it is returned to the customer.
  • line 28: if the [metier] layer has thrown an exception, the exception message is returned to the client.

11.9.4. The Tcp tax calculation server graphics client

11.9.4.1. The project Visual Studio

The Visual Studio project for the graphics client will be as follows:

  • in [1], the two solution projects, one for each of the two application layers
  • in [2], the Tcp client, which acts as a [metier] layer for the [ui] layer. We'll use both terms here.
  • layer in [3], the [ui] layer in version 7, with one detail that we will discuss later

11.9.4.2. The diaper [metier]

The interface IImpotMetier has not changed. It is still the same as in version 7:


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

This interface is implemented by the following [ImpotMetierTcp] class:


using System.Net.Sockets;
using System.IO;
namespace Metier {
    public class ImpotMetierTcp : IImpotMetier {
 
         // information [server]
        private string Serveur { get; set; }
        private int Port { get; set; }
 
         // tAX CALCULATION
        public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
                 // connect to the 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)) {
                                 // unbuffered output stream
                                writer.AutoFlush = true;
                                 // skip the welcome message
                                reader.ReadLine();
                                 // request
                                writer.WriteLine(string.Format("impot {0} {1} {2}",marié ? "o" : "n",nbEnfants, salaire));
                                 // answer
                                return int.Parse(reader.ReadLine());
                            }
                        }
                    }
                }
            }
        }
    }
  • line 7: name or address Ip of tax calculation server Tcp
  • line 8: this server's listening port
  • these two properties will be initialized by Spring when the [ImpotMetierTcp] class is instantiated.
  • line 11: tax calculation method. When executed, the properties Server and Port are already initialized. The code follows the classic Tcp client approach
  • line 13: the connection to the server is open
  • lines 14-16: we retrieve (line 14) the network stream associated with this connection, from which we derive a read stream (line 15) and a write stream (line 16).
  • line 18: the write stream must be unbuffered
  • line 20: ici, remember that when the connection is opened, the server sends the client a 1st line, which is the "welcome" messageWelcome to the tax calculation server". This message is read and ignored.
  • line 22: send a command like : impot o 2 60000 to calculate the tax liability of a married person with 2 children and an annual salary of 60,000 euros.
  • line 24: the server responds with the tax amount in the form "4282" or with an error message if the command was badly formed (this won't happen ici) or if there was a problem calculating the tax. Ici, the latter case is not handled, but it would certainly have been "cleaner" to do so. In fact, if the line read is an error message, an exception will be thrown because the conversion to an integer will fail. The exception retrieved by the GUI will be a conversion error, whereas the original exception is of a completely different nature. The reader is invited to improve this code.
  • lines 25-28: release all used resources with a "using" clause.

The [metier] layer is compiled in the DLL ImpotsV8-metier.dll :

Image

11.9.4.3. The [ui] layer

The [ui] [1,3] layer is the one studied in version 7 in paragraph 9.8.4, except for three details:

  • the configuration of the [metier] layer in [App.config] is different because its implementation has changed
  • the [Form1.cs] GUI has been modified to display a possible exception
  • the [metier] layer is in the DLL [ImpotsV8-metier.dll].

The [App.config] file is as follows:


<?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>
  • line 16: instantiation of layer [metier] with class Metier.ImpotMetierTcp from DLL ImpotsV8-metier.dll
  • lines 17-18: properties Server and Port class Metier.ImpotMetierTcp are initialized. The server will be on the localhost and will operate on port 27.

The graphical interface presented to the user is as follows:

  • in [1], we added a TextBox to display a possible exception. This field did not exist in the previous version.

Apart from this detail, the form code is the same as that described in paragraph 6.4.3. The reader is invited to refer to it. In [2], you can see an example of execution obtained with a server launched as follows :

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

Customer screenshot [2] corresponds to the lines in customer 9 above.

11.9.5. Conclusion

Once again, we were able to reuse existing code, either without modification (server layers [metier], [dao]) or with very little modification (client layer [ui]). This was made possible by our systematic use of interfaces and their instantiation with Spring. If, in version 7, we had put the business code directly into the GUI event handlers, this business code would not have been reusable. This is the major drawback of 1-layer architectures.

Finally, note that the [ui] layer has no knowledge of the fact that a remote server is calculating the tax amount for it.