Il framework .NET fornisce una serie di classi che costituiscono un’interfaccia alle Winsock API: IPAddress e IPEndPoint, appartenenti al namespace System.Net, permettono ad esempio di gestire differenti tipi di informazioni relative agli indirizzi IP.
La classe IPAddress è utilizzata per rappresentare un singolo indirizzo IP: il costruttore di default di tale classe prevede come argomento un tipo long invece della classica rappresentazione "dotted" degli indirizzo IP; questo costituisce il motivo per il quale tale costruttore viene difficilmente utilizzato.
public IPAddress(long address)
E’ molto più comodo infatti fare affidamento al metodo Parse() che permette di creare un’istanza della classe IPAddress a partire dalla rappresentazione standard degli indirizzi IP:
IPAddress indirizzo = IPAddress.Parse("189.32.1.1");
IPAddress fornisce oltre ad alcuni metodi, quattro campi di sola lettura (readonly) che rappresentano degli indirizzi speciali:
- Any: rappresenta qualsiasi indirizzo IP disponibile sul sistema locale
- Broadcast: rappresenta l’indirizzo di broadcast per la rete locale
- Loopback: rappresenta l’indirizzo di loopback del sistema (tipicamente 127.0.0.1)
- None: indica che non deve essere utilizzata alcuna interfaccia di rete
La classe IPEndPoint invece rappresenta una combinazione indirizzo/porta e viene utilizzata per collegare un socket ad un indirizzo lcoale o remoto: i due costruttori di default prevedono infatti l’indicazione di un indirizzo IP sotto forma di long (poco utilizzato) o come istanza della classe IPAddress e l’indicazione della relativa porta.
IPEndPoint(long address, int port)
IPEndPoint(IPAddress address, int port)
Il namespace System.Net fornisce anche una versione serializzata (SocketAddress) della classe IPEndPoint che rispetta il seguente formato:
- un byte per rappresentare l’AddressFamily dell’oggetto
- un byte per rappresentarne la dimensione
- due byte per rappresentare il numero di porta
- i restanti per rappresentare l’indirizzo IP dell’oggetto
Oltre ai metodi la classe IPEndPoint contiene le proprietà:
- Address: permette di recuperare o impostare l’indirizzo IP
- AddressFamily: permette di ottenere l’AddressFamily
- Port: consente di recuperare o impostare il numero della porta
I valori massimi e minimi che possono essere utilizzati come numero di porta sono contenuti nei campi MaxPort e MinPort.
Socket
La classe Socket del namespace System.Net.Sockets costituisce l’implementazione managed dell’API Winsock.
Il suo costruttore presenta la seguente firma:
Socket(AddressFamily af, SocketType st, ProtocolType pt)
in cui:
- AddressFamily è un’enumerazione che definisce il tipo di rete (nelle normali comunicazioni su rete si utilizza il valore InterNetwork)
- SocketType definisce il tipo della connessione dati
- ProtocolType definisce il protocollo di rete
SocketType è un’enumerazione che può assumere i seguenti valori:
- Dgram: utilizza ProtocolType.Udp per comunicazioni connectionless
- Stream: utilizza ProtocolType.Tcp per comunicazioni connection-oriented
- Raw: usa ProtocolType.Icmp (Internet Control Message Protocol) o Raw (Plain IP Packet Communication)
Socket miosocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
Per configurare un socket si utilizza il metodo SetSocketOption() la cui firma:
SetSocketOption(SocketOptionLevel sl, SocketOptionName sn, object valore)
permette di specificare:
- il tipo di opzioni del socket da settare (Ip, Socket, Tcp, Udp)
- il nome della specifica opzione che si dovrà settare
- il valore il cui formato differisce in funzione della specifica opzione da settare
Server e Client Connection-Oriented
Per implementare un server è necessario una volta creato un socket effettuarne il binding ad un oggetto IPEndPoint (ovvero indirizzo locale e porta) e quindi utilizzare il metodo Listen() per porlo in ascolto di eventuali connessioni entranti:
IPHostEntry locale = Dns.GetHostByName(Dns.GetHostName());
IPEndPoint mio_ipendpoint = new IPEndPoint(locale.AddressList[0], 80);
Socket mioserver = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
mioserver.Bind(mio_ipendpoint);
mioserver.Listen(5);
Il metodo Listener() permette di specificare il numero di connessioni che possono essere messe in coda in attesa di essere "servite".
Quindi col metodo Accept si ottiene un Socket per la connessione entrante i cui metodi Send() e Receive() potranno essere utilizzati per scambiare dati con il client.
Socket mioclient = mioserver.Accept();
Per creare una connessione di tipo Client si utilizza al posto del metodo Bind() il metodo Connect() specificando un oggetto IPEndPoint rappresentativo dell’indirizzo remoto del server al quale vogliamo connetterci e della porta sulla quale questo è in ascolto.
Il metodo Connect() attende finchè la connessione non viene stabilita producendo un’eccezione in caso di complicazioni: una volta stabilita la connessione è possibile utilizzare i metodi Send() e Receive per le comunicazioni con il server.
IPAddress host_remoto = IPAddress.Parse("189.32.1.1");
IPEndPoint mio_ipendpoint = new IPEndPoint(host_remoto, 80);
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Connect(mio_ipendpoint);
Una volta conclusa la comunicazione è possibile chiudere il socket medainte il metodo close(): è buona norma far precedere all’invocazione di tale metodo il metodo Shutdown() che utilizza come argomento uno fra i seguenti valori dell’enumerazione SocketShutdown:
- Both: disabilita le operazioni di ricezione ed invio dati
- Receive: disabilita l’operazione di ricezione di nuovi dati
- Send: disabilita l’operazione di invio di nuovi dati
socket.Shutdown(SocketShutdown.Both);
socket.Close();
Server e Client Connectionless
Per creare un server o un client UDP non è necessario utilizzare i metodi Listen() e Connect() tantomeno è possibile utilizzare i metodi Send() e Receive() dal momento che non esiste una connessione per la comunicazione.
Si utilizzano invece i metodi ReceiveFrom() e SendTo() le cui firme:
ReceiveFrom(byte[] b, ref EndPoint iep)
SendTo(byte[] b, ref EndPoint iep)
prevedono a differenza dei metodi Receive() e Send() anche l’indicazione di un oggetto IPEndPoint passato per riferimento ed indicante l’indirizzo remoto al quale inviare i dati nel caso del metodo SendTo() e l’indirizzo remoto dal quale provengono i dati nel caso di ReceiveFrom().
Classi di utilità
Per semplificare la programmazione dei socket il framework .NET mette a disposizione tre classi di utilità:
- TcpClient: per creare client su protocollo TCP e comunicazioni connection-oriented
- TcpListener: per creare server su protocollo TCP e comunicazioni connection-oriented
- UdpClient: per applicazioni che richiedono socket connectionless
La classe TcpClient possiede tre costruttori:
TcpClient client = new TcpClient();
client.Connect("www.miohost.net", 80);
crea un oggetto TcpClient e ne effettua il binding ad un indirizzo e porta locali, mediante il metodo Connect() viene effettuata la connessione all’indirizzo remoto ed alla porta indicati.
IPAddress local = Dns.GetHostByName(Dns.GetHostName()).AddressList[0];
IPEndPoint iep = new IPEndPoint(local, 80);
TcpClient client = new TcpClient(iep);
client.Connect("www.miohost.net", 80);
crea un oggetto TcpClient utilizzando un oggetto IPEndPoint per indicare l’indirizzo e la porta locali sui quali effettuare il binding, il metodo Connect() permette come in precedenza di effettuare la connessione ad un indirizzo e porta remoti.
TcpClient client = new TcpClient("www.miohost.net", 80);
in quest’ultimo caso viene creato un oggetto TcpClient, ne viene effettuato il binding su qualsiasi porta locale disponibile e viene effettuata la connessione all’indirizzo remoto ed alla porta indicati.
Una volta creato un oggetto TcpClient è possibile mediante il metodo GetStream() recuperare un oggetto NetworkStream mediante il quale gestire la comunicazione (utilizzando ad esempio i classici metodi Receive() e Send()).
TcpClient client = new TcpClient("www.miohost.net", 80);
NetworkStream ns = client.GetStream();
Così come la classe TcpClient semplifica l’implementazione di client allo stesso modo la classe TcpListener semplifica la creazione di server basati su protocollo TCP.
La classe TcpListener prevede tre possibili costruttori:
- TcpListener(int porta) permette di indicare la porta sulla quale il server sarà in ascolto
- TcpListener(IPEndPoint iep) permette di indicare attraverso un oggetto IPEndPoint l’indirizzo locale e la porta sulla quale il server sarà in ascolto
- TcpListener(IPAddress ia, int porta) permette di specificare l’indirizzo locale IPAddress e la porta sulla quale il server sarà in ascolto.
Una volta creata un’istanza della classe è possibile metterla in attesa di nuove connessioni mediante il metodo Start() e quindi utilizzare i metodi AcceptSocket() o AcceptTcpClient() per ottenere rispettivamente un oggetto Socket o un oggetto TcpClient da utilizzare per comunicare con il client remoto che di volta in volta si connette.
TcpListener server = new TcpListener(9050);
server.Start();
TcpClient client = server.AcceptTcpClient();
NetworkStream ns = client.GetStream();
Il processo di comunicazione per quanto riguarda i Socket o la classe TcpClient è già stato visto in precedenza.
Infine la classe UdpCleint fornisce una semplice interfaccia per la creazione di socket UDP: non esiste un UdpListener perchè si utilizza una comunicazione connectionless.
Per questa ragione i metodi Send() e Receive della classe UdpClient prevedono in maniera del tutto analoga a quanto visto per i socket udp l’indicazione di un oggetto IPEndPoint rappresentativo dell’indirizzo/porta remoti ai quale inviare i dati (per il metodo Send()) e dal quale ricevere i dati (per il metodo Receive()).