Per consentire un controllo più approfondito della rete, Silverlight fornisce un'implementazione managed dell'interfaccia di rete Socket, diversa a seconda dell'ambiente in cui gira l'applicazione, in Windows l'implementazione è basata sulle Windows Sockets (Winsock) mentre su Mac OS X è basata su Berkeley Software Distribution (BSD) UNIX.
L'implementazione è rappresentata dalla classe Socket
contenuta nel namespace System.Net.Sockets
dell'assembly System.Net
, questo namespace contiene anche il resto delle classi necessarie per lo sviluppo di una applicazione basata su Socket
.
Silverlight supporta sia IPv4 che IPv6 con TCP come canale di trasporto, a patto che sul computer locale sia abilitato il supporto per IPv4 e IPv6. Sempre a livello di trasporto UDP non è supportato.
Altra importante restrizione, cui fare attenzione con Silverlight, è quella sul numero di porta, gli unici numeri supportati sono quelli compresi nel range 4502 e 4534.
Un ruolo importante viene ricoperto anche dalla classe SocketAsyncEventArgs utilizzata per tutto il ciclo di vita della comunicazione via Socket
, vediamo quali sono i passi da seguire per connettersi ad un ipotetico Chat Server raggiungibile sul solito dominio dell'applicazione alla porta 4530.
Il comportamento del Chat Server è molto semplice, accetta connessioni in ingresso sulla suddetta porta e notifica a tutti i client connessi il testo inviato da uno di essi. La nostra applicazione permetterà di inviare messaggi di testo al server e di visualizzare quelli ricevuti.
Tipicamente, ma non necessariamente, l'operazione di connessione con il server avviene all'avvio dell'applicazione, vediamo tutto il codice necessario, dopodiché lo commentiamo:
void Page_Loaded(object sender, RoutedEventArgs e) { endPoint = new DnsEndPoint(Application.Current.Host.Source.DnsSafeHost, 4530); socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); SocketAsyncEventArgs args = new SocketAsyncEventArgs(); args.UserToken = socket; args.RemoteEndPoint = endPoint; args.Completed += new EventHandler<SocketAsyncEventArgs>(OnSocketConnectCompleted); socket.ConnectAsync(args); }
Anzitutto creiamo un'istanza di DnsEndPoint
, che rappresenta gli estremi di collegamento al server: la proprietà Application.Current.Host.Source.DnsSafeHost
restituisce il nome dell'host da cui è stata scaricata la pagina Web contenente l'applicazione Silverlight.
Il passo successivo è la creazione del Socket
, dove possiamo scegliere il livello di internetworking (in questo IPv4).
Adesso tocca alla classe SocketAsyncEventArgs
dove impostiamo l'EndPont ed il metodo di callback per l'evento Completed
, scatenato al termine di ogni operazione eseguita sul Socket, che in questa fase rappresenta Connect Completed.
L'istanza della classe SocketAsyncEventArgs
passerà attraverso tutte le fasi della trasmissione dati, perciò utilizziamo la proprietà UserToken
come contenitore per l'istanza della classe Socket
.
Per dare il via alla connessione non resta che invocare il metodo ConnectAsync della classe Socket
passando l'istanza di SocketAsyncEventArgs
. Se il metodo asincrono restituisce true
, l'operazione di I/O è in sospeso, altrimenti sarà eseguita in maniera sincrona.
Al completamento dell'operazione verrà scatenato l'evento Completed
sull'oggetto SocketAsyncEventArgs
passato come parametro al metodo di callback, dove imposteremo le proprietà per le operazioni successive.
Ricezione dei dati
Vediamo le operazioni da eseguire nel metodo OnSocketConnectCompleted
per ricevere dati server:
private void OnSocketConnectCompleted(object sender, SocketAsyncEventArgs e) { Socket innerSocket = (Socket)e.UserToken; string data = string.Empty; if (!innerSocket.Connected) { data = "Connessione con il ChatServer fallita. n"; } else { data = "Connessione effettuata con il ChatServer."; byte[] response = new byte[1024]; e.SetBuffer(response, 0, response.Length); e.Completed -= new EventHandler<SocketAsyncEventArgs>(OnSocketConnectCompleted); e.Completed += new EventHandler<SocketAsyncEventArgs>(OnSocketReceive); innerSocket.ReceiveAsync(e); } this.Dispatcher.BeginInvoke(new Action<string>(ShowReveicedText), new object[] { data }); }
In questo frammento di codice otteniamo l'istanza di Socket
utilizzata per la connessione e verifichiamo che la connessione sia avvenuta, grazie alla proprietà Connected
. In caso negativo informiamo l'utente con un apposito messaggio ed il ciclo conclude, altrimenti instaziamo un array di byte che fungerà da buffer e lo impostiamo nell'istanza della classe SocketAsyncEventArgs
passata come parametro, rimuoviamo il metodo di callback per l'operazione di connessione completata ed impostiamo quello per l'operazione di ricezione completata, infine invochiamo il metodo ReceiveAsync
utilizzando sempre la solita nell'istanza della classe SocketAsyncEventArgs
.
A questo punto il meccanimo dovrebbe apparire più chiaro: ad ogni operazione asincrona invocata sul Socket
sarà richiamato l'handler dell'evento Completed
, spetta a noi sapere e/o capire in che fase siamo e reagire di conseguenza.
Un aiuto ci viene sicuramente fornito dalla proprietà LastOperation della classe SocketAsyncEventArgs
, che è un enumeratore impostato sull'ultima operazione invocata sul Socket
, i possibili valori sono:
None
Connect
Receive
Send
L'ultima riga di codice del frammento di codice non fa altro che visualizzare all'utente il messaggio di avvenuta o meno connessione sfruttando la classe Dispatcher
dato che il metodo di callback gira in un Thread
diverso da quello dell'interfaccia utente. Proseguiamo analizzando il metodo OnSocketReceive
:
private void OnSocketReceive(object sender, SocketAsyncEventArgs e) { string data = Encoding.UTF8.GetString(e.Buffer, e.Offset, e.BytesTransferred); this.Dispatcher.BeginInvoke(new Action<string>(ShowReveicedText), new object[] { data }); Socket socket = (Socket)e.UserToken; socket.ReceiveAsync(e); }
Tramite il metodo GetString
della classe Encoding
convertiamo l'array di byte in una strnga e con il solito meccanismo aggiorniamo l'interfaccia utente mostrando il messaggio ricevuto. Dopodiché ci rimettiamo in ricezione, quindi non dobbiamo far altro che ottenere la solita istanza di Socket
ed invocare il metodo ReceiveAsync
.
Invio dei dati
La parte di ricezione dati è conclusa, passiamo ad implementare la parte di invio dati al servser. L'invio avviene alla pressione di un Button
da parte dell'utente, quindi la logica di invio sarà contenuta nell'handler dell'evento Click
:
private void Button_Click(object sender, RoutedEventArgs e) { if (socket.Connected) { byte[] data = Encoding.UTF8.GetBytes(NameTextBox.Text + ": " + InputTextBox.Text + ">"); List> listData = new List >(); listData.Add(new ArraySegment (data)); InputTextBox.Text = string.Empty; sendEventArgs = new SocketAsyncEventArgs(); sendEventArgs.RemoteEndPoint = endPoint; sendEventArgs.BufferList = listData; socket.SendAsync(sendEventArgs); } else { this.ChatConsole.Text += "Nessuna connessione con il ChatServer. n"; } }
Come primo passo verifichiamo che il Socket
sia connesso, se non lo è notifichiamo all'utente. Poi, sempre grazie alla classe Encoding
, convertiamo il testo da inviare in un array di byte e lo incapsuliamo in una List di ArraySegment
, come richiesto dalla proprietà BufferList
della classe SocketAsyncEventArgs
.
In questo caso dato che non dobbiamo effettuare nessuna operazione dopo l'invio generiamo e impostiamo una nuova istanza di SocketAsyncEventArgs
senza callback per l'evento Completed
ed adibita al solo invio dei dati. Infine invochiamo il metodo SendAsync
della classe Socket
.