Guest post: Comunicare con una socket in una Universal Windows app
Questo post è stato scritto da Matteo Pagani, Windows AppConsult Engineer in Microsoft
Le socket sono uno dei meccanismi di comunicazione più diffusi quando si ha la necessità di far parlare tra di loro due applicazioni all'interno della stessa rete. Le socket vengono spesso usate in contesti in cui non è disponibile un accesso ad Internet oppure è necessario scambiare pacchetti e informazioni in formati personalizzati: è possibile sfruttare, infatti, lo stesso protocollo utilizzato nel mondo web (TCP) ma senza dover necessariamente comunicare tramite uno dei protocolli standard come HTTP, FTP, ecc.
Una socket è, fondamentalmente, un’accoppiata tra un indirizzo IP e una porta, che può essere utilizzata per dare originale ad un canale di comunicazione tra due endpoint. Una volta stabilita la comunicazione, entrambi possono scambiarsi pacchetti contenenti dati di qualsiasi tipo: messaggi di testo, file binari, immagini, ecc. Al giorno d'oggi, le socket sono sicuramente meno diffuse che in passato, soprattutto in ambito mobile: dato che parliamo di dispositivi che vivono sempre connessi ad Internet, per la maggior parte dei casi si preferisce affidarsi ai meccanismi di comunicazione consentiti dal protocollo HTTP e dai vari comandi POST, GET, ecc.
Esistono però ancora alcuni scenari per cui la connessione tramite socket ha grande importanza:
- La comunicazione diretta tra due dispositivi collegati in rete: per diversi scenari (ad esempio, la comunicazione tra due pc o tra un dispositivo mobile ed un pc) l'utilizzo di Internet non è necessario, anzi, aggiungerebbe un overhead inutile perché sarebbe un terzo attore che dovrebbe fare da tramite nella comunicazione. Ad esempio, ipotizziamo di avere un'applicazione mobile che permetta di controllare in remoto un computer collegato alla stessa rete: in tal caso, l'utilizzo di Internet come canale di comunicazione potrebbe essere un requisito di troppo, perché non è detto che entrambi i dispositivi abbiano connettività in quel momento.
- La mancanza di connettività ad Internet: si tratta di uno scenario piuttosto frequente in scenari enterprise, in cui si hanno in dotazione dispositivi che, per motivi di sicurezza, non sono collegati ad Internet ma comunicano con un server solamente tramite reti private. Pensiamo ad esempio ad un'applicazione di messaggistica (stile WhatsApp) interna, che consenta di scambiare messaggi tra i vari dispositivi aziendali, anche senza la presenza di una connessione ad Internet.
Nel corso di questo post andremo a vedere come collegarci ad una socket in un'applicazione per Windows 10. La maggior parte delle API che andremo ad utilizzare, in realtà, erano disponibili già in 8.1; la novità principale di 10 è la possibilità di avere un background task legato ad una socket, in modo da poter ricevere messaggi anche quando questa non è in esecuzione.
Il progetto
Nel corso di questo post realizzeremo un progetto composto da tre attori:
- Un'applicazione server, che si farà carico di creare la socket e inviare messaggi verso il client. Nel nostro caso vogliamo simulare una socket "reale" (quindi indipendente dalla Universal Windows Platform di Windows 10): il server sarà perciò realizzato con un'applicazione WPF, sfruttando le API del framework .NET che avremmo potuto utilizzare anche in una soluzione web. A dimostrazione di questa "indipendenza", alla fine del post troverete anche un breve esempio su come realizzare il server sfruttando Node.js, una tecnologia completamente slegata dal mondo Microsoft e .NET.
- Un'applicazione client, che si farà carico di collegarsi alla socket e scambiare messaggi con il server. È il contesto in cui ci interessa mettere alla prova la Universal Windows Platform e sarà sviluppata, perciò, come Universal Windows app per Windows 10.
- Un background task, che sarà collegato all'applicazione client e che utilizzeremo per ricevere i messaggi spediti dal server anche quando l'applicazione non è in esecuzione.
Quella che andremo a realizzare è la simulazione di un'applicazione di messaggistica: quando il client Windows 10 riceverà un messaggio dal server mentre è in esecuzione lo mostrerà semplicemente nell'app stessa. Quando, invece, l'app non è in esecuzione, il messaggio sarà intercettato dal background task e lo mostrerà sotto forma di notifica toast all'utente.
Iniziamo, c'è parecchio lavoro da fare
L'applicazione server
Come anticipato, andremo a simulare il server con un'applicazione desktop sviluppata in WPF. Possiamo creare un nuovo progetto di questo tipo in Visual Studio, scegliendo la categoria Classic Desktop e, infine, la voce WPF Application. WPF, esattamente come la Universal Windows Platform, sfrutta lo XAML come tecnologia per definire il layout di un'applicazione. Il nostro layout sarà molto semplice:
- Un pulsante, che si occuperà di creare la socket.
- Un casella di testo, dove inserire il messaggio che vogliamo inviare sul canale.
- Un altro pulsante, che invierà il messaggio al client.
<Window x:Class="SocketServer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="Socket Server" FontSize="24" HorizontalAlignment="Center" />
<Button Content="Create socket" Click="OnCreateSocketClicked" Width="200" Margin="0, 20, 0, 0" />
<TextBlock Text="Enter a message:" Margin="0, 20, 0, 0" />
<StackPanel Orientation="Horizontal" >
<TextBox x:Name="Message" Width="300" />
<Button Content="Send text" Click="OnSendTextClicked" />
</StackPanel>
</StackPanel>
</Window>
Come vedete, il codice XAML è piuttosto semplice da interpretare. Andiamo subito perciò al sodo e vediamo come poter creare una socket utilizzando le API offerte dal framework .NET. Ecco come appare il metodo di creazione della socket:
private void OnCreateSocketClicked(object sender, RoutedEventArgs e)
{
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ipAddress = IPAddress.Loopback;
IPEndPoint endPoint = new IPEndPoint(ipAddress, 1983);
socket.Bind(endPoint);
socket.Listen(10);
socket.BeginAccept(AcceptCallback, socket);
}
Il punto di partenza è la classe Socket, appartenente al namespace System.Net.Sockets, che richiede nel costruttore una serie di parametri di configurazione. In questo esempio, andremo a creare una socket basata sul protocollo TCP, in grado di comunicare tra i dispositivi sfruttando gli indirizzi IP delle rispettive schede di rete. Per questo motivo, specifichiamo:
- Come primo parametro, AddressFamiliy.InterNetwork, per specificare che vogliamo utilizzare il protocollo IPv4 per la comunicazione. L'enumeratore può assumere molti altri valori, legati ad altre tipologie di protocollo (AppleTalk, IPv6, ecc.)
- Come secondo parametro, la tipologia di socket con cui vogliamo lavorare. In questo caso, decidiamo di scambiare i dati sul canale sfruttando degli stream di dati, tramite il valore SocketType.Stream.
- Come terzo parametro, il protocollo da utilizzare. Come già anticipato, vogliamo utilizzare il protocollo TCP, passiamo perciò il valore ProtocolType.Tcp. Un altro protocollo piuttosto diffuso per le socket è UDP, più veloce ma meno affidabile, in quanto non si ha la garanzia che ogni pacchetto venga consegnato con successo o nello stesso esatto ordine in cui lo abbiamo inviato, al contrario di TCP.
Una socket è legata a doppio filo ad un endpoint, ovvero il punto di accesso a cui i client si collegheranno per stabilire la comunicazione. Nel nostro caso (ovvero una socket basata sul protocollo TCP/IP), l'endpoint è costituito da un indirizzo IP e da una porta. Per il nostro esempio, sfrutteremo l'indirizzo locale della macchina (il canonico localhost), tramite il valore Loopback della classe IPAddress. Con un oggetto di tipo IPAddress siamo in grado di creare un'istanza di un IPEndPoint, a cui dobbiamo passare anche il numero di porta che vogliamo utilizzare. Nel nostro esempio, abbiamo creato una socket che risponderà all'indirizzo localhost:1983.
Ora che abbiamo definito l'endpoint, possiamo aprire il canale di comunicazione vera e proprio: lo facciamo chiamando il metodo Bind() e, successivamente, Listen() , specificando il numero di connessione contemporanee che vogliamo poter accettare.
Da questo momento in poi il canale è stato creato: ora dobbiamo prepararci a ricevere le connessioni in ingresso dai client. Lo facciamo sfruttando i metodi asincroni offerti dal framework .NET, che però utilizzano un approccio diverso da quello basato su async e await che siamo abituati ad utilizzare nelle Universal Windows app. Questo perché la comunicazione con le socket è asincrona non solo in quanto non vogliamo bloccare il thread della UI mentre il canale è aperto, ma anche e soprattutto perchè non sappiamo quando avverrà tale comunicazione: il client potrebbe collegarsi al server dopo 1 secondo, 10 minuti od un'ora che il canale è stato creato.
Ecco qual è lo scopo del metodo BeginAccept() : il canale rimarrà in ascolto di connessioni da parte dei client e, appena ne intercetterà una in arrivo, eseguirà il metodo di callback che abbiamo passato come primo parametro (nel nostro caso, si chiama AcceptCallback). Il secondo parametro è un semplice riferimento all'oggetto stesso di tipo Socket con cui abbiamo creato il canale.
Ecco come appare la definizione del metodo AcceptCallback.
private void AcceptCallback(IAsyncResult ar)
{
Socket socketChannel = socket.EndAccept(ar);
}
Quello che vedete è un classico esempio di gestione di operazioni asincrone sfruttando le callback e i prefissi Begin() e End(). Tale approccio prevede la presenza di un metodo preceduto dal prefisso Begin (nel nostro caso, BeginAccept() ) che dà avvio all'operazione. Nel momento in cui questa è terminata e siamo pronti per elaborare i risultati, viene invocato il metodo di callback, all'interno del quale possiamo chiamare il metodo preceduto dal prefisso End (nel nostro caso, EndAccept), passando come parametro il risultato di tipo IAsyncResult che fa parte dei parametri della callback. La chiamata a tale metodo ci restituisce il risultato vero e proprio dell'elaborazione, che nel nostro caso è un altro oggetto di tipo Socket, questa volta però collegato al canale di comunicazione attivo e aperto verso il client. Nell'applicazione WPF di esempio, tale canale sarà memorizzato in una variabile globale della pagina: ci servirà successivamente, infatti, quando vorremo inviare dei dati.
Come inviamo dei dati sul canale? In realtà, le socket non hanno il concetto di "tipo di dati": sono semplicemente dei bocchettoni, in cui far transitare dei pacchetti, che poi il ricevente si farà carico di interpretare. Ecco perciò che se, per il nostro scenario, vogliamo inviare dei messaggi testuali, dobbiamo prima convertirli in un array di byte, come nell'esempio seguente:
private void OnSendTextClicked(object sender, RoutedEventArgs e)
{
if (socketChannel != null)
{
string message = Message.Text;
byte[] bytes = Encoding.ASCII.GetBytes(message);
socketChannel.Send(bytes);
}
}
Questo è il metodo collegato alla pressione del pulsante nello XAML di invio di un messaggio: si fa carico di recuperare il testo inserito dall'utente, di convertirlo in un array di byte tramite il metodo GetBytes() della classe statica Encoding.ASCII e di inviarlo sfruttando il metodo Send() della classe Socket. Come potete notare, per l'invio sfruttiamo il riferimento alla socket che abbiamo ottenuto nella callback AcceptCallback() . In questo modo, siamo sicuri di inviare il messaggio verso un canale attivo a cui è collegato un client.
L'applicazione server, per il momento, è terminata: possiamo lanciarla e premere il pulsante Create socket per creare il nostro canale di comunicazione. Potremmo anche provare ad inviare dei messaggi ma, ovviamente, non succederà nulla dato che, al momento, non c'è alcun client collegato.
L'applicazione client
L'applicazione client, che si collegherà alla socket appena creata, sarà una vera e propria Universal Windows app per Windows 10, in grado di funzionare, perciò, su molteplici dispositivi (pc, tablet, telefoni, ecc.). La differenza principale tra un'applicazione WPF e una Universal Windows app è la tecnologia su cui sono basate: da una parte il framework .NET tradizionale, dall'altra la nuova Universal Windows Platform, costruita come evoluzione del Windows Runtime introdotto in Windows 8. Come conseguenza, per l'applicazione client dovremo utilizzare delle API differenti da quelle che abbiamo visto per l'applicazione server: nella Universal Windows Platform, infatti, non troviamo il namespace System.Net.Sockets e classi come Socket, IPEndPoint, ecc, ma un nuovo set di API incluso nel namespace Windows.Networking.Sockets.
Per iniziare dobbiamo perciò creare un nuovo progetto di tipo Windows Universal.
Iniziamo a vedere come utilizzarle per collegarci alla nostra socket. L'operazione base è molto semplice:
private async void OnForegroundSocketClicked(object sender, RoutedEventArgs e)
{
StreamSocket socket = new StreamSocket();
HostName host = new HostName("localhost");
try
{
await socket.ConnectAsync(host, "1983");
}
catch (Exception exc)
{
MessageDialog dialog = new MessageDialog("Error connecting to the socket");
await dialog.ShowAsync();
}
}
L'oggetto che rappresenta il nostro canale è di tipo StreamSocket ed espone i vari metodi per interagire con la socket. Per collegarsi al canale è necessario utilizzare il metodo ConnectAsync() che richiede, come parametri, un oggetto di tipo HostName con l'indirizzo del canale (nel nostro caso, localhost, dato che la socket gira in locale) e la porta (nel nostro caso, 1983). A questo punto, se tutto è andato a buon fine, la connessione è attiva e possiamo iniziare a metterci in ascolto di messaggi in arrivo dal server. Come citato in precedenza, però, la socket è fondamentalmente un bocchettone sempre aperto, nel quale i dati possono arrivare in qualsiasi momento. Dobbiamo, perciò, adottare lo stesso approccio per la ricezione: dobbiamo mantenere il canale di lettura sempre attivo e risvegliarlo solo nel momento in cui arrivano effettivamente dei dati.
Ecco come appare l'operazione completa di connessione:
private async void OnForegroundSocketClicked(object sender, RoutedEventArgs e)
{
socket = new StreamSocket();
HostName host = new HostName("localhost");
try
{
await socket.ConnectAsync(host, "1983");
isConnected = true;
while (isConnected)
{
try
{
DataReader reader;
using (reader = new DataReader(socket.InputStream))
{
// Set the DataReader to only wait for available data (so that we don't have to know the data size)
reader.InputStreamOptions = InputStreamOptions.Partial;
// The encoding and byte order need to match the settings of the writer we previously used.
reader.UnicodeEncoding = UnicodeEncoding.Utf8;
reader.ByteOrder = ByteOrder.LittleEndian;
// Send the contents of the writer to the backing stream.
// Get the size of the buffer that has not been read.
await reader.LoadAsync(256);
// Keep reading until we consume the complete stream.
while (reader.UnconsumedBufferLength > 0)
{
string readString = reader.ReadString(reader.UnconsumedBufferLength);
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
messages.Add(readString);
});
Debug.WriteLine(readString);
await reader.LoadAsync(256);
}
reader.DetachStream();
}
}
catch (Exception exc)
{
MessageDialog dialog = new MessageDialog("Error reading the data");
await dialog.ShowAsync();
}
}
}
catch (Exception exc)
{
MessageDialog dialog = new MessageDialog("Error connecting to the socket");
await dialog.ShowAsync();
}
}
La lettura dei dati viene inclusa all'interno di un ciclo while, in cui viene valutato il valore di una variabile booleana: fintanto che questa è a true, il "bocchettone" rimane aperto. Nello specifico, dato che si tratta di un'operazione di lettura, il bocchettone è rappresentato dalla proprietà InputStream dell'oggetto StreamSocket: è al suo interno che troveremo i dati spediti dal server. La lettura viene effettuata utilizzando le API del Windows Runtime dedicate alla manipolazione di stream di dati: se avete esperienza con la scrittura e la lettura di stream di file all'interno dello storage locale, noterete che il codice è molto simile.
Il cuore è la classe DataReader, che permette di leggere i dati contenuti all'interno di uno stream, nel nostro caso l'InputStream della socket. Dopodichè, prima di iniziare la lettura vera e propria, dobbiamo configurare in maniera opportuna l'oggetto DataReader: nello specifico, la proprietà più importante è InputStreamOptions, che deve essere impostata su Partial. Questo perché, trattandosi di un canale sempre aperto, non possiamo sapere a priori quanto sarà grande il dato che ci arriverà: questa opzione ci permette di leggere stream anche parziali, senza dover specificare necessariamente la dimensione totale. Per lo stesso motivo, quando iniziamo l'operazione di lettura vera e propria ci viene in aiuto la proprietà UnconsumedBufferLength del DataReader per sapere se, all'interno dello stream, ci sono dei dati ancora non letti: andremo a leggere il dato solo se, effettivamente, la dimensione del buffer è maggiore di 0.
Uno dei punti di forza della classe DataReader è che espone diversi metodi che semplificano la lettura dello stream, dandoci la possibilità di ottenere direttamente il tipo di dato atteso. Nel nostro caso, dato che l'applicazione server aveva inviato un messaggio testuale, lo possiamo leggere grazie al metodo ReadString() , che si farà carico in automatico di convertire i byte in stringa. Finalmente abbiamo recuperato il messaggio inviato dal server: sta a noi, in base alla logica della nostra applicazione, farne buon uso. Nell'esempio di codice la stringa viene aggiunta ad una collezione di tipo ObservableCollection<string> , che è collegata ad un controllo ListView presente nella pagina XAML: in questo modo, ogni volta che l'applicazione WPF invierà un messaggio, lo vedremo comparire all'interno della pagina della nostra applicazione Windows 10.
E se volessimo terminare la connessione alla socket e chiudere il canale? È sufficiente demandare, ad un altro pulsante, la valorizzazione della proprietà boolana isConnected to false: in questo modo, si interromperà il ciclo while definito in precedenza e la classe DataReader smetterà di attendere l'arrivo di nuovi dati. Se vogliamo chiudere completamente la connessione, possiamo anche invocare il metodo CancelIOAsync() (per cancellare eventuali operazioni in corso) e poi chiamare il Dispose() sull'oggetto StreamSocket. Ecco un esempio completo:
private async void OnCloseConnectionClicked(object sender, RoutedEventArgs e)
{
isConnected = false;
await socket.CancelIOAsync();
socket.Dispose();
}
Ricevere i messaggi in background
Ora che abbiamo visto la ricezione dati in foreground (ovvero mentre l'applicazione è in esecuzione), introduciamo un nuovo scenario supportato specificatamente da Windows 10: la ricezione di messaggi in background, ovvero anche quando l'applicazione non è in esecuzione. Per farlo dobbiamo utilizzare il meccanismo dei background task, che dovremmo conoscere già se abbiamo esperienza di sviluppo applicazioni Windows: si tratta di progetti di tipo Windows Runtime Component, separati dall'applicazione vera e propria ma appartenenti alla stessa soluzione di Visual Studio, che contengono il codice che vogliamo eseguire quando il task viene avviato. Potete approfondire l'argomento grazie alla documentazione ufficiale all'indirizzo https://msdn.microsoft.com/en-us/library/windows/apps/mt299103.aspx. I task sono legati ai trigger, ovvero gli eventi che possono scatenarne l'esecuzione anche quando l'applicazione principale non è in uso da parte dell'utente. Windows 10 ha introdotto un nuovo trigger chiamato SocketActivityTrigger, che permette di trasferire la connessione ad una socket dall'applicazione in foreground al task in background: ogni volta sarà rilevata la presenza di un nuovo messaggio nel canale, il task si risveglierà e potrà riceverlo ed elaborarlo.
Il primo passo è quello di creare il background task vero e proprio: creiamo, nella nostra soluzione, un nuovo progetto di tipo Windows Runtime Component e, al suo interno, creiamo una nuova classe, a cui possiamo dare un nome a piacimento. E' fondamentale, però, che questa erediti dall'interfaccia IBackgroundTask: ciò ci costringerà ad implementare il metodo Run(), che è quello che viene invocato quando il task viene eseguito. All'interno di tale metodo potremo capire per quale motivo la socket è stata attivata e, in base allo scenario, leggere il dato ricevuto sul canale. Ecco come appare la definizione del nostro task:
namespace SocketTask
{
public sealed class ReceiveMessagesTask: IBackgroundTask
{
public async void Run(IBackgroundTaskInstance taskInstance)
{
var deferral = taskInstance.GetDeferral();
var details = taskInstance.TriggerDetails as SocketActivityTriggerDetails;
var socketInformation = details.SocketInformation;
if (details.Reason == SocketActivityTriggerReason.SocketActivity)
{
StreamSocket socket = socketInformation.StreamSocket;
DataReader reader = new DataReader(socket.InputStream);
reader.InputStreamOptions = InputStreamOptions.Partial;
await reader.LoadAsync(250);
var dataString = reader.ReadString(reader.UnconsumedBufferLength);
ShowToast(dataString);
socket.TransferOwnership(socketInformation.Id);
}
deferral.Complete();
}
public void ShowToast(string text)
{
string xml =
$@"
<toast activationType='foreground' launch='args'>
<visual>
<binding template='ToastGeneric'>
<text>Message from the socket</text>
<text>{text}</text>
</binding>
</visual>
</toast>";
XmlDocument doc = new XmlDocument();
doc.LoadXml(xml);
ToastNotification notification = new ToastNotification(doc);
ToastNotifier notifier = ToastNotificationManager.CreateToastNotifier();
notifier.Show(notification);
}
}
}
Il metodo Run() contiene un parametro di tipo IBackgroundTaskInstance, che include le principali informazioni sul task. Nello specifico, espone una proprietà di nome TriggerDetails, che viene valorizzata di volta in volta con un oggetto specifico in base al tipo di trigger a cui è collegato il task. Nel nostro caso, dato che stiamo gestendo un trigger di tipo SocketActivityTrigger, tale oggetto sarà di tipo SocketActivityTriggerDetails e ci permetterà di accedere alla socket vera e propria. Sono due le informazioni essenziali che ci servono:
- La proprietà SocketInformation , checontiene un riferimento alla socket e include l'oggetto di tipo StreamSocket che ci serve per leggere e scrivere dati.
- La proprietà Reason, che contiene l'informazione su quale evento legato alla socket ha scatenato l'attivazione del task. Nel nostro caso, ci limitiamo a gestire il valore SocketActivity dell'enumeratore SocketActivityTriggerReason: significa che sono arrivati dei dati sul canale, che possiamo leggere. Altri valori possibili sono ConnectionAccepted (quando il client si è collegato con successo al server), SocketClosed (quando il canale di comunicazione è stato chiuso), ecc.
A questo punto il codice che andiamo a scrivere è molto simile a quello che abbiamo visto nell'applicazione vera e propria: usando la classe DataReader andiamo a leggere il contenuto dell'InputStream e lo convertiamo in stringa tramite il metodo ReadString() . Dato che ci troviamo in un task in background, non abbiamo accesso diretto al thread della UI, perchè l'applicazione potrebbe non essere in esecuzione. Se vogliamo interagire con l'utente dobbiamo, perciò, usare altri meccanismi, come l'uso di notifiche: è quello che facciamo con il metodo ShowToast() , che mostra il contenuto del messaggio sotto forma di notifica toast., usando il meccanismo di Windows 10 delle Adaptive Toast (che possiamo approfondire all'indirizzo http://blogs.msdn.com/b/tiles_and_toasts/archive/2015/07/02/adaptive-and-interactive-toast-notifications-for-windows-10.aspx). Una volta terminata l'operazione di lettura, restituiamo il controllo della socket al sistema operativo tramite il metodo TransferOnwership() : Windows 10 include, infatti, un servizio che si fa carico di tenere sotto controllo la socket e di attivare il task nel momento in cui si è verificata qualche attività.
Ora che il background task è pronto, dobbiamo opportunamente registrarlo nell'applicazione principale. La procedura è quella standard per i background task:
- Si aggiunge, nella sezione Declarations del file di manifest, un oggetto di tipo Background Tasks, specificando:
- Come Supported task types, la voce System event.
- Nella casella Entry point, occorre specificare la firma completa della classe che implementa l'interfaccia IBackgroundTask, composta da namespace più il nome della classe. Nell'esempio precedente, è SocketTask.ReceiveMessageTask.
- Si registra il task all'avvio dell'applicazione, tramite la classe BackgroundTaskBuilder, come nell'esempio seguente:
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
var registration = BackgroundTaskRegistration.AllTasks.FirstOrDefault(x => x.Value.Name == TaskName);
if (registration.Value == null)
{
var socketTaskBuilder = new BackgroundTaskBuilder();
socketTaskBuilder.Name = TaskName;
socketTaskBuilder.TaskEntryPoint = "SocketTask.ReceiveMessagesTask";
var trigger = new SocketActivityTrigger();
socketTaskBuilder.SetTrigger(trigger);
var status = await BackgroundExecutionManager.RequestAccessAsync();
if (status != BackgroundAccessStatus.Denied)
{
task = socketTaskBuilder.Register();
}
}
else
{
task = registration.Value;
}
}
Innanzitutto si verifica che non esista già un background task registrato con lo stesso nome, per evitare registrazioni multiple dello stesso task. Solo in caso non esista già, procediamo a registrarlo specificandone un nome univoco (la proprietà Name) e l'entry point (la proprietà TaskEntryPoint, occorre valorizzarla con lo stesso dato inserito nel file di manifest, ovvero la firma completa della classe che implementa il task). Con il metodo SetTrigger() configuriamo quale trigger vogliamo associare a questo task: nel nostro caso, si tratta di un SocketActivityTrigger. Infine verifichiamo tramite il metodo RequestAccessAsync() se siamo autorizzati a registrare il task (su un dispositivo con poca memoria, infatti, potrebbe essere già stato raggiunto il numero massimo di task registrabili) e, in caso affermativo, chiamiamo il metodo Register() . Possiamo notare come manteniamo, a livello di pagina, un riferimento al task registrato: questo perché ci servirà l'identificativo assegnato dal sistema operativo nel momento in cui dovremo registrare la socket. L'ultimo passaggio è aggiungere, all'applicazione, un riferimento al Windows Runtime Component che contiene il task: facciamo clic con il tasto destro in Visual Studio sul progetto dell'applicazione, scegliamo Add reference e, alla voce Projects, andiamo a scegliere il Windows Runtime Component.
Ora che il background task è stato registrato correttamente, possiamo procedere a collegarci alla socket e a trasferire il controllo al sistema operativo: non sarà più, infatti, la nostra applicazione a rimanere in ascolto di eventuali comunicazioni in arrivo sul canale, ma lo farà Windows per noi, il quale attiverà il nostro task se necessario. Ecco il codice da eseguire:
private async void OnBackgroundSocketClicked(object sender, RoutedEventArgs e)
{
StreamSocket socket = new StreamSocket();
HostName host = new HostName("localhost");
await socket.ConnectAsync(host, "1983");
socket.EnableTransferOwnership(task.TaskId, SocketActivityConnectedStandbyAction.Wake);
socket.TransferOwnership("SampleSocket");
}
La prima parte è la medesima vista in precedenza: creiamo un nuovo oggetto di tipo StreamSocket e ci colleghiamo al canale legato all'indirizzo localhost e alla porta 1983. Dopodichè abilitiamo il trasferimento del controllo al sistema tramite il metodo EnableTransferOwnership() : il primo parametro da passare è l'identificativo del background task che abbiamo registrato, da cui la necessità di memorizzare il risultato dell'operazione Register() in una variabile globale. L'identificativo è memorizzato all'interno della proprietà TaskId. Il secondo parametro è legato, invece, al concetto di Connected Standby, ovvero quando il device si trova in uno stato di "non utilizzo" (ad esempio, telefono bloccato in tasca o PC in stand-by): tramite questo parametro possiamo specificare se vogliamo che il sistema operativo attivi il task anche in questo scenario, tramite uno dei valori dell'enumeratore SocketActivityConnectedStandbyAction. Nel nostro caso, vogliamo che il sistema operativo sia in grado di intercettare dati dalla socket in qualsiasi momento, perché l'utente potrebbe ricevere un messaggio anche quando non sta attivamente usando il telefono: sfruttiamo, perciò, il valore Wake.
Infine, trasferiamo il controllo della socket al sistema operativo tramite il metodo TransferOnwnership() , passando come parametro una stringa che identifica in maniera univoca la socket. Il gioco è fatto: se ora sospendessimo l'app e provassimo ad inviare un messaggio dall'applicazione WPF, lo vedremo arrivare sotto forma di notifica toast, in quanto sarà intercettato dal background task. Il task è una classe a tutti gli effetti, che possiamo debuggare esattamente come l'applicazione vera e propria: se vogliamo vedere cosa sta succedendo in dettaglio, non dobbiamo far altro che mantenere il debugger di Visual Studio collegato e mettere dei breakpoint all'interno del metodo Run() .
E se volessimo rispondere al messaggio?
Nel nostro esempio abbiamo identificato un server (l'applicazione WPF) e un client (l'applicazione Windows 10), ma questo non significa che solo la prima sia in grado di inviare messaggi verso la seconda. Anche il client, a sua volta, può inviare dei dati sul canale, che saranno ricevuti dal server. Dal punto di vista dell'applicazione Windows 10, come possiamo vedere si tratta di un'operazione piuttosto semplice:
private async void OnSendMessageClicked(object sender, RoutedEventArgs e)
{
DataWriter writer = new DataWriter(socket.OutputStream);
writer.WriteString("This is a sample message");
await writer.StoreAsync();
await writer.FlushAsync();
}
Utilizziamo la classe DataWriter, che è il corrispettivo di DataReader per la scrittura di dati all'interno di uno stream. Dato che, in questo caso, non si tratta di un'operazione di lettura ma di scrittura non dovremo più usare l'InputStream dell'oggetto StreamSocket, ma l'OutputStream. In questo caso, il riferimento all'oggetto di tipo SocketStream è quello che abbiamo acquisito in precedenza quando abbiamo stabilito la connessione con il metodo ConnectAsync() . Analogamente a DataReader, anche la classe DataWriter espone diversi metodi per scrivere i tipi di dati più comuni: dato che stiamo scambiando messaggi testuali, utilizziamo il metodo WriteString() . Infine, scriviamo il dato sullo stream con il metodo StoreAsync() e lo liberiamo tramite il metodo FlushAsync() .
Spostiamoci ora sul server, ovvero sull'applicazione WPF: in questo caso, dobbiamo scrivere un po' di codice in più, in quanto dobbiamo sfruttare l'approccio asincrono basato su Begin() e End() che abbiamo imparato a conoscere all'inizio del post. Innanzitutto, ci serve definire un buffer con una dimensione prefissata, all'interno del quale immagazzinare i dati in arrivo: si tratta di un array di byte.
private const int BufferSize = 1024;
private byte[] readBuffer;
Ora dobbiamo, una volta stabilita la connessione con il client, metterci in ascolto sul canale. Ecco come cambia la callback che abbiamo definito in precedenza per accettare l'arrivo di una nuova connessione:
private void AcceptCallback(IAsyncResult ar)
{
socketChannel = socket.EndAccept(ar);
readBuffer = new byte[BufferSize];
socketChannel.BeginReceive(readBuffer, 0, BufferSize, 0, ReadCallback, null);
}
Oltre ad accettare la connessione (con il metodo EndAccept() ) questa volta andiamo anche ad inizializzare il buffer e a chiamare il metodo BeginReceive() , che ci permette di attivare la ricezione di eventuali dati in arrivo. Come parametri, passiamo il riferimento al buffer dove salvare questi dati e una callback (in questo caso, ReadCallback), ovvero il metodo che sarà invocato nel momento in cui la ricezione dei dati è stata completata e siamo pronti per elaborarli. Ecco come appare la definizione della callback:
private async void ReadCallback(IAsyncResult ar)
{
int received = socketChannel.EndReceive(ar);
if (received > 0)
{
string result = Encoding.ASCII.GetString(readBuffer, 0, received);
await Dispatcher.InvokeAsync(() =>
{
messages.Add(result);
});
}
socketChannel.BeginReceive(readBuffer, 0, BufferSize, 0, ReadCallback, null);
}
Come potete vedere, l'approccio è il medesimo visto in precedenza per l'utilizzo delle callback: chiamiamo il metodo EndReceive() passando come parametro l'oggetto di tipo IAsyncResult, così da ricevere la dimensione in byte del dato che è stato letto. Se tale dimensione è maggiore di zero, vuol dire che è arrivato qualcosa sul canale: il nostro buffer contiene dei dati, che possiamo elaborare. Dato che si tratta di un testo, riutilizziamo la classe Encoding.ASCII, questa volta per effettuare l'operazione inversa, ovvero convertire l'array di byte in una stringa tramite il metodo GetString() . Ora che abbiamo il contenuto del messaggio, possiamo elaborarlo a piacimento: in questo caso, adottiamo un approccio simile a quello dell'applicazione Windows 10, ovvero lo aggiungiamo ad una collezione di tipo ObservableCollection<string>, che viene poi mostrata nella finestra tramite un controllo ListBox. L'operazione viene eseguita tramite il Dispatcher: questo perché la callback viene eseguita su un thread secondario e, di conseguenza, non ha accesso al thread della UI.
Possiamo notare come, alla fine dell'operazione di lettura, chiamiamo nuovamente il metodo BeginReceive() della classe Socket. Questo perchè il meccanismo delle callback non è basato ad eventi, che vengono scatenati ogni qualvolta si verifica una determinata condizione. Una volta che il metodo BeginReceive() è stato invocato e ha concluso la sua esecuzione, la socket cessa di rimanere in ascolto di nuovi messaggi. Richiamandolo alla fine della callback di lettura, perciò, ci assicuriamo di poter ricevere eventuali nuovi messaggi che saranno spediti successivamente.
Un'applicazione server alternativa: Node.js
Nell'esempio precedente abbiamo creato l'applicazione server utilizzando WPF per questione di semplicità: se siamo sviluppatori di Universal Windows app, C# e il mondo .NET ci saranno sicuramente più famigliari rispetto ad altre tecnologie. La socket, però, è un canale di comunicazione agnostico e non è legato alla tecnologia con cui è sviluppato. A dimostrazione, vediamo ora brevemente come sostituire l'applicazione server in WPF con una realizzata in Node.js, la popolare tecnologia per creare applicazioni server sfruttando Javascript. Per eseguirla, avrete bisogno del runtime di Node.js attivo e funzionante sul vostro computer: potete fare riferimento a questa guida o, in alternativa, sfruttare l'installazione personalizzata di Visual Studio 2015 che, tra le altre cose, include anche tutto il necessario per sviluppare applicazioni basate su Node.js. Visual Studio stesso vi permette la creazione e il debugging di progetti Node.js, tramite l'apposita estensione che potete installare da https://github.com/Microsoft/nodejstools In alternativa, l'ottimo Visual Studio Code offre il supporto integrato per il debugging di progetti Javascript.
Ecco come appare il codice Javascript della nostra applicazione server:
// Load the TCP Library
var net = require('net');
// Keep track of the clients
var clients = [];
// Start a TCP Server
net.createServer(function (socket) {
// Identify this client
socket.name = socket.remoteAddress + ":" + socket.remotePort
// Put this new client in the list
clients.push(socket);
// Send a nice welcome message and announce
socket.write("Welcome " + socket.name + "\n");
// Remove the client from the list when it leaves
socket.on('end', function () {
clients.splice(clients.indexOf(socket), 1);
});
}).listen(1983);
// Put a friendly message on the terminal of the server.
console.log("Chat server running at port 1983\n");
Con il metodo require('net') otteniamo un riferimento alla libreria di Node.js per operare con le connessioni di rete. Nello specifico, tale libreria espone un metodo di nome createServer() , che ci permette di creare una socket su una specifica porta (che specifichiamo come parametro del metodo listen() ). Il metodo createServer() accetta, come parametro, una funzione, che viene richiamata ogni volta un client si collega al canale. All'interno di tale funzione andiamo a:
- Aggiungere il riferimento al client (contenuto nell'oggetto socket) ad una collezione, che utilizziamo per mantenere l'elenco di tutti i client attivi.
- Inviare un messaggio al client con il nome della socket, chiamando il medoto write() sull'oggetto socket
- Tramite il metodo on() rimaniamo in ascolto dell'evento di terminazione della connessione: in tal caso, rimuoviamo il client dalla lista dei client attivi.
Se ora lanciassimo la nostra applicazione Windows 10 sviluppata in precedenza, vedremmo come questa riesca a collegarsi correttamente alla socket anche se il server è stato creato con una tecnologia differente: nel momento in cui premiamo il pulsante per stabilire il collegamento, riceveremo dal server Node.Js il messaggio di benvenuto con il nome della socket. Da questo punto in poi è possibile espandere ulteriormente l'applicazione Node.js ed aggiungere, ad esempio, la possibilità di effettuare una chiamata HTTP tramite browser per inviare nuovi messaggi sulla socket. Nel progetto di esempio legato a questo post troverete un esempio completo.
Complimenti!
Se avete seguito il post fino a qui, complimenti per la tenacia e la pazienza Spero questo lungo viaggio vi sia servito per capire al meglio come poter sfruttare la potenza delle socket all'interno di una Universal Windows app per Windows 10. Potete scaricare il progetto di esempio utilizzato nel corso dell'articolo da GitHub all'indirizzo https://github.com/qmatteoq/SocketSample-UWP Happy coding!
Comments
Anonymous
February 07, 2016
Attenzione ai termini però, perché un socket è semplicemente una coppia di IP + porta. Il nome delle classi inganna, perché il socket non è la connessione tra due host, ma soltanto il proprio endpoint. Il socket è poi associato a una connessione di qualsiasi protocollo. Scrivere sul socket significa usare quel socket come mittente per la trasmissione :)Anonymous
February 07, 2016
Grazie per la precisazione, provvedo a far correggere l'introduzione.Anonymous
July 06, 2016
è possibile che il client trasmetta files al server tramite questo sistema?