Press "Enter" to skip to content

1 – Un sistema di messaggistica per molteplici usi – HTTP Sender e HTTP Listener – le librerie di base

Introduzione

Un servizio windows, per definizione non è dotato di interfaccia utente, è un po’ come una scatola nera con dei plug che riceve input e produce output ma senza alcuna interazione con l’utente.

Un servizio semplice, potrebbe avere bisogno esclusivamente di una console, come quella da noi costruita nei post precedenti, per configurarlo e per verificare che parte e si ferma. Potremmo anche aggiungere alla console creata un sistema veloce per consultare i log del servizio e fermarci qui.

Se il servizio è più complesso di quello che scrive dati su un file di testo che fin qui abbiamo sviluppato, l’amministratore potrebbe aver bisogno anche di valutarne il carico di lavoro, la funzionalità e quindi profilarne il funzionamento per potere ad esempio rispondere alla domanda: “Ma perché è così lento.” facendo un fine tuning della parametrizzazione del servizio.

Per fare questo abbiamo bisogno che il servizio sia in grado di essere monitorato in tempo reale, usando il log verboso che abbiamo introdotto nel primo sviluppo del servizio potremmo usare un file system watcher e monitorare il file di log, ma è un approccio un po’ troglodita, soprattutto se il log è massiccio.

A questo punto entrano in gioco gli oggetti che abbiamo inserito nel titolo di questo post. Un servizio può essere dotato di una interfaccia per comunicare con il mondo (in questo caso con il PC che lo ospita) utilizzando il protocollo HTTP, l’interfaccia può essere bidirezionale, quindi il servizio potrebbe ricevere dei comandi da un programma esterno e potrebbe trasmettere dati e richieste ad un programma esterno.

Ecco perché abbiamo deciso di sviluppare le classi necessarie a creare questo tipo di comunicazione.

Che cosa abbiamo inserito nei nostri progetti di libreria?

Per prima cosa, per parlare con un servizio HTTP abbiamo bisogno di una stringa di connessione, quindi come abbiamo fatto per SQL Server, andiamo a creare una classe per contenere e archiviare le informazioni relative ad una stringa di connessione HTTP. Sia l’HTTP sender che l’HTTP listener hanno bisogno di sapere dove spedire le richieste e i dati e dove ricevere le richieste e i dati. Pertanto per gestire questo tipo di informazioni abbiamo creato le seguenti classi:

  • HttpConnectionInfo
  • HttpConnectionInfosCollection

Che replicano per HTTP quello che abbiamo fatto per SQL Server.

Abbiamo poi sviluppato una classe che fa da HTTP Sender, quindi è in grado di spedire dei messaggi di tipo POST o di tipo GET via protocollo HTTP. Questa classe funziona in modalità single threaded, quindi spedisce un messaggio e attende risposta, perché se volessimo spedire messaggi in multithreading siamo noi che dobbiamo creare questo tipo di gestione nell’applicazione, ma ogni messaggio trasmesso deve per forza attendere risposta. la classe che implementa tutto questo è:

  • SenderHTTP

Il terzo elemento necessario a quanto vogliammo fare è una classe HTTP Listener, in questo caso, la classe funzionerà in Multithreading, in quanto una volta ricevuto il messaggio dal sender dovrà semplicemente passarlo a chi si è registrato per ricevere i messaggi che provvederà quindi alla decodifica ed allo smistamento. Per realizzare il Listener abbiamo le seguenti classi:

  • ListenerHTTP
  • HTTPDataReceivedEventArgs
  • DecodeFromContext

L’ultima delle tre classi è quella che elabora l’HTTP context ricevuto dal listener estraendo i dati che contiene per fornirli a chi li elaborerà.

Il progetto e le modifiche alla soluzione DnwLibraries

http_solution_01[5]

Il nuovo progetto aggiunto a DnwLibraries con le classi che abbiamo illustrato.

 http_solution_02[5]

La configurazione dell’applicazione, con il namespace ed il nome alla libreria secondo il pattern standard Dotnetwork.

http_solution_03[5]

Il Post Build Event per rendere disponibile la DLL a tutti i progetti da un unico punto.

La classe HttpConnectionInfo

[DataContract(Name = "HTTPCONN")]
public class HttpConnectionInfo : ConnectionInfoBase
{
	public HttpConnectionInfo()
	{
		mAddress = string.Empty;
		mTimeout = 100000;
		mPort = 0;
	}

La nostra stringa di connessione HTTP, abbiamo già predisposto il necessario affinché sia serializzabile in JSON perché come per le stringhe di connessione a SQLServer dovremo salvarla su un file di parametrizzazione.

[DataMember(Name = "ADD", EmitDefaultValue = false)]
public string Address
{
	get
	{
		return mAddress;
	}
	set
	{
		mAddress = value;
		OnPropertyChanged(FLD_Address);
		OnPropertyChanged(FLD_ConnectionString);
	}
}

[DataMember(Name = "PT", EmitDefaultValue = false)]
public int Port
{
	get
	{
		return mPort;
	}
	set
	{
		mPort = value;
		OnPropertyChanged(FLD_Port);
		OnPropertyChanged(FLD_ConnectionString);
	}
}

public override string ConnectionString
{
	get
	{
		StringBuilder sb = new StringBuilder();
		sb.Append(PROTOCOL_HEADER);
		sb.Append(Address);

		if (Port >= 1 && Port <= 65535)
		{
			sb.AppendFormat(":{0}", Port);
		}
		return (sb.ToString());
	}
}

[DataMember(Name = "TO", EmitDefaultValue = false)]
public int Timeout
{
	get
	{
		return mTimeout;
	}
	set
	{
		mTimeout = value;
		OnPropertyChanged(FLD_Timeout);
		OnPropertyChanged(FLD_ConnectionString);
	}
}

Le property della connessione, per semplicità abbiamo omesso la parte di dichiarazione delle costanti e delle variabili member che mappano le property. Abbiamo predisposto le 3 property che compongono i dati della connessione per la serializzazione JSON, in seguito costruiremo un componente equivalente a quello che abbiamo fatto per le connessioni SQL Server per generare e salvare su file anche questo tipo di connessione. Abbiamo anche predisposto l’interfaccia INotifyPropertyChanged per poter lavorare senza problemi sulla UI di WPF.

public HttpConnectionInfo Clone()
{
	HttpConnectionInfo item = new HttpConnectionInfo();
	this.CopyTo(item);
	return (item);
}

public void CopyTo(HttpConnectionInfo newItem)
{
	//From Connection base
	base.CopyTo(newItem);

	newItem.Timeout = this.Timeout;
	newItem.Address = this.Address;
	newItem.Port = this.Port;
}

Aggiungiamo i metodi per clonare la classe della connessione che potranno essere utili nella user interface.

La classe HttpConnectionInfosCollection

public class HttpConnectionInfosCollection : ObservableCollection<HttpConnectionInfo>
{

La collezione tipizzata per le nostre HttpConnectionInfo, che abbiamo creato utilizzando l’ObservableCollection perché andremo ad utilizzarla sulla User Interface.

public HttpConnectionInfo this[string ID]
{
	get
	{
		return (this.FirstOrDefault(c => c.ConnectionID == ID));
	}
}

L’indexer per recuperare i dati della collection tramite il loro ID.

public override string ToString()
{
	StringBuilder sb = new StringBuilder();
	for (int i = 0; i < this.Count; i++)
	{
		sb.AppendLine(this[i].ToString());
	}
	return (sb.ToString());
}

public string Serialize()
{
	return (JsonHelper.Serialize(typeof(HttpConnectionInfosCollection), this));
}

public void Serialize(string fileName)
{
	JsonHelper.Serialize(typeof(HttpConnectionInfosCollection), this, fileName);
}

public static HttpConnectionInfosCollection Deserialize(string fileOrData, bool isFile = true)
{
	return ((HttpConnectionInfosCollection)JsonHelper.Deserialize(typeof(HttpConnectionInfosCollection), fileOrData, isFile));
}

public HttpConnectionInfosCollection Clone()
{
	HttpConnectionInfosCollection newData = new HttpConnectionInfosCollection();
	foreach (HttpConnectionInfo item in this)
	{
		newData.Add(item.Clone());
	}
	return (newData);
}

I metodi di lavoro per la collection, il ToString utile ai fini del debug, i metodi di Serializzazione e Deserializzazione JSON, ed il metodo Clone, che può essere utile quando svilupperemo la UI.

La classe DecodeFromContext

internal class DecodeFromContext
{

La classe che si occupa di fare il parse dei messaggi ricevuti dal listener e restituirli a chi sa che cosa farne. Ha una peculiarità, quella di avere il modificatore internal, che indica che non è visibile all’esterno della Dll che la contiene.

public DecodeFromContext(HttpListenerContext context, bool buildResponse, RequestReceivedDelegate requestReceived)
{
	try
	{
		mContext = context;
		mBuildResponse = buildResponse;
		mFieldsCollection = new NameValueCollection();
		mRequestReceived = requestReceived;
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
}

Il costruttore della classe, che oltre al contesto ricevuto dal listener prevede un flag per indicare se è necessaria una risposta e un Delegate, che fornisce il metodo da eseguire al termine della decodifica dei dati.

public void ProcessRequest()
{
	try
	{
		if (mRequestReceived != null)
		{
			HttpListenerRequest request = mContext.Request;

			DateTime? txDate = null;
			string d = request.Headers["date"];
			if (!d.XDwIsNullOrTrimEmpty())
			{
				DateTime d1 = DateTime.Now;
				if (DateTime.TryParse(d, out d1))
				{
					txDate = d1;
				}
			}

			mContentType = request.ContentType;
			long contentLength = request.ContentLength64;
			string httpMethod = request.HttpMethod.ToUpper();
			string requestedURL = request.Url.ToString();

			bool isManagedMethod = false;

			if (httpMethod == "GET")
			{
				isManagedMethod = true;
				ParseGET(request);
			}
			else if (httpMethod == "POST")
			{
				isManagedMethod = true;
				ParsePOST(request);
			}

			if (isManagedMethod)
			{
				// I'm receiving, so the sender is the remote IP and the receiver is the local IP
				HTTPDataReceivedEventArgs args = new HTTPDataReceivedEventArgs(requestedURL,
					txDate, request.RemoteEndPoint, request.LocalEndPoint,
					mReadData, httpMethod, mContentType, contentLength, mFieldsCollection);
				string responseForRequest = mRequestReceived(args);

				if (mBuildResponse)
				{
					try
					{
						if (responseForRequest == null)
						{
							responseForRequest = string.Empty;
						}

						byte[] b = Encoding.UTF8.GetBytes(responseForRequest);
						mContext.Response.ContentLength64 = b.Length;
						mContext.Response.OutputStream.Write(b, 0, b.Length);
						mContext.Response.OutputStream.Close();
					}
					catch (Exception responseEx)
					{
						EventLogger.SendMsg(responseEx);
					}
				}
			}
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
}

Il metodo ProcessRequest, che permette di elaborare i dati ricevuti e costruire la risposta quando necessario. Vediamo cosa fa:

  • Se non è stato definito un metodo delegate per l’elaborazione dei dati la richiesta viene ignorata.
  • Viene estratta la Request del context e vengono estratti una serie di dati relativi alla forma della request ed il suo contenuto.
  • Viene verificato se il metodo http della request è fra quelli gestiti (GET o POST)  se è uno dei due viene eseguita la funzione che effettua il Parse del contenuto della request.
  • Se il parse è andato a buon fine, costruiamo la classe contenente il risultato dell’elaborazione e chiamiamo il metodo delegate definito inviandogli tutti i dati.
  • Se è necessario avere una Response alla chiamata HTTP (ad esempio per una GET), costruiamo una response con il valore restituito dal metodo di elaborazione dei dati e lo rimettiamo nell’ HTTP context arrivato dal Listener in modo che possa essere inviato al sender.
private void ParseGET(HttpListenerRequest request)
{
	mReadData = request.Url.ToString();

	foreach (string qsKey in request.QueryString.AllKeys)
	{
		mFieldsCollection.Add(qsKey, HttpUtility.UrlDecode(request.QueryString[qsKey]));
	}
}

Il metodo parser per la GET, in questo caso, l’unica cosa che ci interessa è la QueryString, dove ci sono i parametri inviati dal sender, che vengono inseriti in una NameValueCollection che poi verrà passata al metodo di elaborazione dei dati.

private void ParsePOST(HttpListenerRequest request)
{
	mReadData = null;

	using (MemoryStream body = new MemoryStream())
	{
		int bytes = 0;
		byte[] temp = new byte[4096];
		while ((bytes = request.InputStream.Read(temp, 0, temp.Length)) > 0)
		{
			body.Write(temp, 0, bytes);
		}
		body.Seek(0, SeekOrigin.Begin);

		Encoding encoding = request.ContentEncoding;
		using (StreamReader reader = new StreamReader(body, encoding))
		{
			if (mContentType.IndexOf("multipart/form-data", StringComparison.InvariantCultureIgnoreCase) > -1)
			{
				Dictionary<string, string> items = ParseContentLine(mContentType);

				string boundary = string.Empty;
				if (items.ContainsKey("boundary"))
				{
					boundary = items["boundary"];
				}

				bool endOfLines = false;

				string row = string.Empty;

				while (row == string.Empty)
				{
					row = reader.ReadLine();
					if (row.IndexOf(boundary) > -1)
					{
						endOfLines = row.EndsWith("--"); // se è boundary + -- sono alla fine
						row = string.Empty;
					}
				}

				while (!endOfLines)
				{
					if (row.ToLower().Contains("content-disposition: form-data;"))
					{
						Dictionary<string, string> p = ParseContentLine(row);
						if (p.ContainsKey("name"))
						{
							string fieldKey = p["name"];

							StringBuilder fieldValue = new StringBuilder();

							row = reader.ReadLine();
							if (row.XDwIsNullOrTrimEmpty())
							{
								bool stop = false;
								while (!stop)
								{
									if (!reader.EndOfStream)
									{
										row = reader.ReadLine();
										if (row.IndexOf(boundary) > -1)
										{
											endOfLines = row.EndsWith("--");  stop = true;
										}
										else
										{
											row = row.Replace("\r\n", "");
											fieldValue.AppendLine(row);

										}
									}
									else
									{
										endOfLines = true;
										stop = true;
									}
								}
							}
							mFieldsCollection.Add(fieldKey, fieldValue.ToString());
						}
					}
					if (!endOfLines)
					{
						if (reader.EndOfStream)
						{
							endOfLines = true;
						}
						else
						{
							row = reader.ReadLine();
						}
					}

					if (!endOfLines)
					{
						endOfLines = (reader.EndOfStream);
					}
				}
			}

			reader.Close();
		}
		body.Close();
	}
}

Il metodo parser per una chiamata di tipo POST, che possiamo notare è molto più complesso di quello relativo alla GET, vediamo di capire perché:

Un messaggio POST deve iniziare con una riga che contiene la stringa “multipart/form-data” seguita da una stringa di tipo “boundary=—————UniqueID”, e da una serie di gruppi di righe che contengono i dati passati in con questa sintassi:

————————-UniqueID–
Content-Disposition: form-data; Name=”nome del parametro”
Valore del parametro
Anche su più righe
————————-UniqueID–

La funzione parser prende i dati in questo formato ed estrae quanto contenuto restituendo una lista Name Value contenente tutto quello che è stato passato. Vediamo gli step che effettua il parser:

  • Crea un MemoryStream
  • Vi inserisce all’interno l’InputStream della request e si posiziona all’inizio dei dati letti.
  • Richiede alla request l’Encoding e genera uno StreamReader per leggere il contenuto della richiesta dal memory stream utilizzando l’encoding.
  • Verifica che il messaggio sia valido, quindi la sua prima riga contenga la stringa “multipart/form-data”.
  • Effettua il parse dell’header del messaggio salvando il valore dello UniqueID che stabilisce il Boundary di ogni elemento del messaggio.
  • Inizia un ciclo per leggere gli elementi contenenti i dati leggendo la prima riga ed aspettandosi di trovare un “content-disposition form-data;”
  • Trovato il primo inizia a leggerne il contenuto e compone una serie di  elementi Name Value per la collezione dei dati risultanti.
  • Al  termine della lettura del messaggio chiude gli stream e ritorna
private Dictionary<string, string> ParseContentLine(string line)
{
	Dictionary<string, string> output = new Dictionary<string, string>();

	try
	{
		string[] contentLinePieces = line.Split(";".ToCharArray());
		for (int i = 1; i < contentLinePieces.Length; i++)
		{
			string[] pieceParts = contentLinePieces[i].Split("=".ToCharArray());
			if (pieceParts.Length > 1)
			{
				string name = HttpUtility.UrlDecode(pieceParts[0].TrimStart(" ".ToCharArray()));
				string value = HttpUtility.UrlDecode(pieceParts[1].TrimStart(" \"".ToCharArray()).TrimEnd(" \"".ToCharArray()));

				output.Add(name, value);
			}
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}

	return output;
}

Il metodo ParseContentLine, è usato sia per leggere la GET che la POST e crea un dizionario di key value effettuando due split sulla riga di testo passata, uno sul punto e virgola che identifica le coppie di valori, l’altro sull’ uguale, che identifica le stringhe key e quelle value.

La classe HTTPDataReceivedEventArgs

public delegate string RequestReceivedDelegate(HTTPDataReceivedEventArgs httpDataReceived);

public class HTTPDataReceivedEventArgs : EventArgs
{

	public HTTPDataReceivedEventArgs(string requestedURL, DateTime? txDate, IPEndPoint senderEndPoint, IPEndPoint destinationEndPoint, string dataReceived, string httpMethod, string contentType, long contentLength, NameValueCollection fieldsCollection)
	{
		RequestedURL = requestedURL;
		SenderEndPoint = senderEndPoint;
		DestinationEndPoint = destinationEndPoint;
		DataReceived = dataReceived;
		HttpMethod = httpMethod;
		ContentType = contentType;
		ContentLength = contentLength;
		FieldsCollection = fieldsCollection;
		TxDate = txDate;
	}

	public long ContentLength
	{
		get;
		private set;
	}

	public string ContentType
	{
		get;
		private set;
	}

	public string DataReceived
	{
		get;
		private set;
	}

	public IPEndPoint DestinationEndPoint
	{
		get;
		private set;
	}

	public NameValueCollection FieldsCollection
	{
		get;
		private set;
	}

	public string HttpMethod
	{
		get;
		private set;
	}

	public string RequestedURL
	{
		get;
		private set;
	}

	public IPEndPoint SenderEndPoint
	{
		get;
		private set;
	}

	public DateTime? TxDate
	{
		get;
		private set;
	}

}

Questa classe contiene il delegate per il metodo da eseguire quando il listener riceve i dati e fornisce tutti i dati ricevuti a chi li dovrà utilizzare. Non necessita di discussione.

La classe SenderHTTP

Si tratta di una classe helper, in quanto fornisce solo i metodi Get e Post pertanto è stata definita come static, vediamo come è stata definita:

public static string HTTPGet(HttpConnectionInfo connectionData, NameValueCollection parametrizerDataToSend)
{
	string result = string.Empty;
	HttpWebRequest txRequest = null;
	try
	{
		StringBuilder sbParams = new StringBuilder(connectionData.ConnectionString);
		if ((parametrizerDataToSend != null) && (parametrizerDataToSend.Count > 0))
		{
			string paramSeparator = "?";
			for (int i = 0; i < parametrizerDataToSend.Count; i++)
			{
				sbParams.Append(paramSeparator);
				sbParams.Append(parametrizerDataToSend.GetKey(i));
				sbParams.Append("=");
				sbParams.Append(HttpUtility.UrlEncode(parametrizerDataToSend.Get(i)));
				paramSeparator = "&";
			}
		}

		txRequest = (HttpWebRequest)WebRequest.Create(sbParams.ToString());
		txRequest.Method = "GET";
		txRequest.Credentials = System.Net.CredentialCache.DefaultCredentials;
		txRequest.Timeout = connectionData.Timeout;
		txRequest.Date = DateTime.Now.ToUniversalTime();

		result = GetResponse(txRequest);
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
	finally
	{
		if (txRequest != null)
		{
			try
			{
				txRequest.Abort();
			}
			catch { }

			txRequest = null;
		}
	}

	return (result);
}

Il metodo HTTPGet è in grado di mandare una richiesta di tipo GET ad un HTTP listener ed attenderne la risposta, restituendo tale risposta al chiamante. vediamo come:

  • Accetta come parametri una connessione http ed una collezione di parametri di tipo Name Value.
  • Crea una stringa nel formato richiesto dalla request di una GET componendo i parametri in forma ?name=value&name=value
  • Crea una request  per effettuare la chiamata
  • Spedisce la request e attende risposta restituendola al chiamante.
private static string GetResponse(HttpWebRequest txRequest)
{
	string result = string.Empty;

	using (WebResponse response = txRequest.GetResponse())
	{
		using (Stream responseStream = response.GetResponseStream())
		{
			using (StreamReader responseStreamReader = new StreamReader(responseStream))
			{
				result = responseStreamReader.ReadToEnd();
				responseStreamReader.Close();
			}
			responseStream.Close();
		}
		response.Close();
	}

	return result;
}

Il metodo GetResponse, che fisicamente fa la chiamata HTTP al listener e attende la sua risposta restituendo lo stream inviato al chiamante.

public static string HTTPPost(HttpConnectionInfo connectionData, NameValueCollection parametrizerDataToSend, bool waitResponse = true)
{
	string result = string.Empty;
	HttpWebRequest txRequest = null;

	try
	{
		if (parametrizerDataToSend != null)
		{
			string boundary = "----------------------------" + DateTime.Now.Ticks.ToString("x");

			ServicePointManager.Expect100Continue = false;

			txRequest = (HttpWebRequest)WebRequest.Create(connectionData.ConnectionString);
			txRequest.ContentType = "multipart/form-data; boundary=" + boundary;
			txRequest.Method = "POST";
			txRequest.KeepAlive = waitResponse;
			txRequest.Credentials = System.Net.CredentialCache.DefaultCredentials;
			txRequest.Timeout = connectionData.Timeout;
			txRequest.Date = DateTime.Now.ToUniversalTime();

			using (Stream memStream = new MemoryStream())
			{
				byte[] boundarybytes = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "\r\n");

				string formdataTemplate = "\r\n--" + boundary + "\r\nContent-Disposition: form-data; name=\"{0}\"\r\n\r\n{1}";

				foreach (string key in parametrizerDataToSend.Keys)
				{
					string formitem = string.Format(formdataTemplate, key, parametrizerDataToSend[key]);
					byte[] formitembytes = System.Text.Encoding.UTF8.GetBytes(formitem);
					memStream.Write(formitembytes, 0, formitembytes.Length);
				}
				memStream.Write(boundarybytes, 0, boundarybytes.Length);

				string footer = "\r\n--" + boundary + "--\r\n";
				byte[] footerbytes = System.Text.Encoding.UTF8.GetBytes(footer);
				memStream.Write(footerbytes, 0, footerbytes.Length);

				txRequest.ContentLength = memStream.Length;

				using (Stream requestStream = txRequest.GetRequestStream())
				{
					memStream.Position = 0;
					byte[] tempBuffer = new byte[memStream.Length];
					memStream.Read(tempBuffer, 0, tempBuffer.Length);
					requestStream.Write(tempBuffer, 0, tempBuffer.Length);
					requestStream.Flush();
					requestStream.Close();
				}
				memStream.Close();
			}

			if (waitResponse)
			{
				result = GetResponse(txRequest);
			}
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
	finally
	{
		if (txRequest != null)
		{
			try
			{
				txRequest.Abort();
			}
			catch { }

			txRequest = null;
		}
	}

	return (result);
}

Il metodo HTTPPost è costruito per inviare una chiamata di tipo POST ad un HTTP Listener, eventualmente attende una risposta che inoltra al chiamante. Vediamo come la esegue:

  • Accetta in input una stringa di connessione HTTP, una lista di tipo Name Value contenente i parametri da inviare, un boolean che indica se attendere o meno risposta.
  • Crea uno UniqueID nel formato previsto dal protocollo per il POST quindi ———————–UniqueID, nel nostro caso oltre ai trattini utilizziamo la conversione in stringa esadecimale dei Ticks dell’orologio di sistema.
  • Indica al ServicePointManager di non utilizzare la modalità 100-continue che prevede che il listener effettui una procedura di autenticazione, nel nostro caso il listener non ha questo tipo di funzionalità.
  • Creiamo una WebRequest di tipo POST e gli predisponiamo i dati dell’header, il tipo di contenuto e gli altri dati necessari.
  • Creiamo un memory stream in cui componiamo il messaggio nel formato richiesto dal post e lo scriviamo nello stream della WebRequest che abbiamo predisposto. e spediamo la nostra Request quando facciamo la Flush dello stream.
  • Se necessario, andiamo ad attendere la risposta al nostro POST usando il GetResponse.

La classe ListenerHTTP

Al contrario del Sender che è disegnato come un Helper, il Listener è una classe che deve essere instanziata per effettuare il suo lavoro, vediamo dunque come lo abbiamo implementato:

public class ListenerHTTP
{
	private HttpListener mListener;
	
	private Thread mListenerThread;
	
	public RequestReceivedDelegate RequestReceived;
	
	public ListenerHTTP(HttpConnectionInfo connectionToListen, 
	  ThreadPriority priority, bool isBackgroundThread = true, 
	bool sendResponseToRequest = true, bool autoStart = true)
	{
		try
		{
			IsListening = false;
			ConnectionToListen = connectionToListen;
			SendResponseToRequest = sendResponseToRequest;
	
			if (autoStart)
			{
				Start(priority, isBackgroundThread: isBackgroundThread);
			}
		}
		catch (Exception ex)
		{
			EventLogger.SendMsg(ex);
			throw;
		}
	}

La classe Listener contiene al suo interno un HTTPListener, un thread, ed un delegate per il metodo di elaborazione delle risposte ricevute. Il suo costruttore prevede che gli vengano inviate la connessione HTTP su cui ascoltare, la priorità per il thread di ascolto, se deve essere utilizzato un background thread, se inviare risposta alle richieste, se effettuare l’auto start del listener.

Il costruttore instanzia e se richiesto avvia il listener.

public HttpConnectionInfo ConnectionToListen
{
	get;
	private set;
}

public bool IsListening
{
	get;
	private set;
}

public bool SendResponseToRequest
{
	get;
	set;
}

Le property utilizzate dalla classe per il suo lavoro.

public void Start(ThreadPriority priority, bool isBackgroundThread = true)
{
	try
	{
		DestroyListener();

		IsListening = true;

		mListener = new HttpListener();
		string cn = ConnectionToListen.ConnectionString;
		if (!cn.EndsWith("/"))
		{
			cn = cn + "/";
		}
		mListener.Prefixes.Add(cn);
		mListener.Start();

		mListenerThread = new Thread(OnDataReceived)
		{
			Priority = priority,
			IsBackground = isBackgroundThread
		};
		mListenerThread.Start();
	}
	catch (Exception ex)
	{
		DestroyListener();

		EventLogger.SendMsg(ex);
		throw;
	}
}

Il metodo start del listener, che istanzia l’HTTPListener  e il thread di elaborazione dei dati.

public void Stop()
{
	try
	{
		DestroyListener();
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
}
private void DestroyListener()
{
	try
	{
		IsListening = false;
		if (mListener != null)
		{
			mListener.Close();
			mListener = null;
		}
	}
	catch { }
}

Il metodo Stop del Listener, ed il metodo che lo elimina e provoca la chiusura del Thread di elaborazione delle risposte.

private void OnDataReceived()
{
	try
	{
		while ((mListener != null) && (mListener.IsListening))
		{
			IAsyncResult context = mListener.BeginGetContext(new AsyncCallback(ListenerCallback), mListener);
			context.AsyncWaitHandle.WaitOne();
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
}

Il metodo che gestisce il thread di ascolto ed utilizza un gli strumenti asincroni forniti dal Listener per agganciare il metodo di Callback e andare in attesa fino all’arrivo di un messaggio per poi ripartire con il ciclo. Quando viene effettuato il Close del listener dal metodo Stop, automaticamente il listener esce dal Wait e chiude il ciclo.

private void ListenerCallback(IAsyncResult result)
{

	try
	{
		HttpListener listener = (HttpListener)result.AsyncState;
		if ((listener != null) && (listener.IsListening))
		{
			HttpListenerContext context = listener.EndGetContext(result);

			DecodeFromContext wrk = new DecodeFromContext(context, SendResponseToRequest, this.RequestReceived);
			Thread th = new Thread(wrk.ProcessRequest);
			th.Start();
			th.Join();
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
}

Il metodo di callback del listener, che utilizza la classe Decoder per elaborare il messaggio utilizzando il delegate che sarà fornito dall’applicazione che istanzia il listener.

Conclusioni

In questo articolo abbiamo creato una nuova libreria che fornisce alle nostre applicazioni la possibilità di comunicare utilizzando il protocollo HTTP, si tratta di un sistema di comunicazione molto semplice, che non prevede autenticazione, ma è abbastanza flessibile da permetterci di fare delle cose interessanti. Nel prossimo post costruiremo un programma che testa il sender ed il receiver  e poi vedremo come utilizzarlo in una applicazione reale.

 

Il codice delle librerie comuni ove si trovano le classi discusse in questo articolo sono disponibili al link seguente:

Per qualsiasi domanda, curiosità , approfondimento, potete usare il link alla form di contatto in cima alla pagina.