Share via


Il presente articolo è stato tradotto automaticamente.

ASP.NET

Creazione di una semplice applicazione Comet in Microsoft .NET Framework

Derrick Lau

Scarica il codice di esempio

 

Cometa è una tecnica per spingere il contenuto da un server Web a un browser senza un'esplicita richiesta, utilizzando connessioni AJAX longevi.Permette una UX più interattivo e meno larghezza di banda rispetto l'andata e ritorno server tipico innescato da un postback della pagina viene utilizzato per recuperare i dati più.Anche se ci sono un sacco di implementazioni di cometa disponibili, la maggior parte sono Java-based.In questo articolo mi concentrerò sulla costruzione di un servizio c# basato sull'esempio di codice cometbox disponibile presso code.google.com/p/cometbox.

Ci sono nuovi metodi per attuare lo stesso comportamento utilizzando funzionalità HTML5 come WebSockets e gli eventi sul lato server, ma questi sono disponibili solo nelle ultime versioni di browser.Se è necessario supportare browser meno recenti, Comet è la soluzione più compatibile.Tuttavia, il browser deve supportare AJAX implementando l'oggetto xmlHttpRequest; in caso contrario non sarà in grado di supportare la comunicazione di cometa-stile.

L'architettura di alto livello

Figura 1 Mostra comunicazione cometa-stile di base, mentre Figura 2 descrive l'architettura del mio esempio.Cometa utilizza oggetto xmlHttpRequest del browser, che è essenziale per la comunicazione AJAX, per stabilire una connessione HTTP longeva a un server.Il server tiene aperta la connessione e spinge il contenuto nel browser quando disponibile.

Comet-Style CommunicationFigura 1 cometa-stile comunicazione

Architecture of the Comet ApplicationFigura 2 architettura dell'applicazione cometa

Tra il browser e il server è una pagina proxy, che risiede nel percorso dell'applicazione Web stesso come la pagina Web che contiene il codice client e non fa nulla tranne inoltrare i messaggi dal browser al server e dal server al browser.Perché avete bisogno di una pagina proxy?Ti spiego un po '.

Il primo passo è quello di scegliere un formato per i messaggi scambiati tra il browser e il server — JSON, XML o un formato personalizzato.Per semplicità, ho scelto JSON perché è naturalmente supportato in JavaScript, jQuery e il Microsoft .NET Framework e può trasmettere la stessa quantità di dati XML utilizzando un minor numero di byte e, quindi, meno larghezza di banda.

Per impostare la comunicazione cometa-style, si apre una connessione di AJAX al server.Il modo più semplice per farlo è di utilizzare jQuery perché supporta più browser e fornisce alcune funzioni wrapper bella come $Ajax.Questa funzione è essenzialmente un wrapper dell'oggetto xmlHttpRequest di ogni browser e ordinatamente fornisce i gestori di eventi che possono essere implementati per elaborare i messaggi in arrivo dal server.

Prima di iniziare la connessione, si crea un'istanza del messaggio da inviare.Per fare questo, dichiarare una variabile e utilizzare JSON. stringify per formattare i dati come un messaggio JSON, come mostrato Figura 3.

Figura 3 formato dei dati come un messaggio JSON

function getResponse() {
  var currentDate = new Date();
  var sendMessage = JSON.stringify({
    SendTimestamp: currentDate,
    Message: "Message 1"
  });
  $.ajaxSetup({
    url: "CometProxy.aspx",
    type: "POST",
    async: true,
    global: true,
    timeout: 600000
  });

Successivamente, inizializzare la funzione con l'URL a cui connettersi, metodo di comunicazione da utilizzare, lo stile di comunicazione e il parametro timeout della connessione HTTP. JQuery fornisce questa funzionalità in una libreria chiamata denominata ajaxSetup. Impostare il timeout in questo esempio per 10 minuti perché sto costruendo solo una prova di soluzione concetto qui; è possibile modificare l'impostazione di timeout per tutto quello che vuoi.

Ora aprire una connessione al server utilizzando il metodo di Ajax jQuery $, con la definizione del gestore di eventi di successo come unico parametro:

$.ajax({
  success: function (msg) {
    // Alert("ajax.success().");
    if (msg == null || msg.Message == null) {
      getResponse();
      return;
    }

Il gestore verifica l'oggetto messaggio restituito affinché che esso contiene informazioni valide prima di analisi; Questo è necessario perché se viene restituito un codice di errore, jQuery avrà esito negativo e visualizzare un messaggio non definito per l'utente. Su di un messaggio null, il gestore deve chiamata ricorsivamente l'AJAX funzionano ancora e ritorno; Ho trovato che aggiungendo il ritorno si ferma il codice da continue. Se il messaggio è OK, basta leggere il messaggio e scrivere il contenuto della pagina:

$("#_receivedMsgLabel").append(msg.Message + "<br/>");
getResponse();
return;
    }
  });

Questo crea un semplice client che illustra come funziona comunicazione cometa-stile, oltre a fornire un mezzo per l'esecuzione di test di scalabilità e prestazioni. Per il mio esempio, ho messo la getResponse codice JavaScript in un controllo utente Web e registrata nel codebehind così la connessione AJAX apre immediatamente quando il controllo viene caricato sulla pagina ASP.NET :

public partial class JqueryJsonCometClientControl :
  System.Web.UI.UserControl
{
  protected void Page_Load(object sender, EventArgs e)
  {
    string getResponseScript =
      @"<script type=text/javascript>getResponse();</script>";
    Page.ClientScript.RegisterStartupScript(GetType(),
      "GetResponseKey", getResponseScript);
  }
}

Il Server

Ora che ho un cliente che può inviare e ricevere messaggi, costruirò un servizio che può ricevere e rispondere a loro.

Ho cercato di attuare diverse tecniche diverse per la comunicazione di cometa-stile, compreso l'uso di pagine ASP.NET e gestori HTTP, nessuno dei quali ebbero successo. Ciò che non potevo sembro fare era ottenere un singolo messaggio a trasmettere a più client. Fortunatamente, dopo un sacco di ricerca sono imbattuto il progetto cometbox e trovato ad essere l'approccio più semplice. Ho fatto alcuni ritocchi per farla funzionare come un servizio Windows quindi sarebbe più facile da usare, quindi dato la capacità di tenere una connessione longeva e spingere il contenuto nel browser. (Purtroppo, in questo modo, distrutto alcuni della compatibilità cross-platform.) Infine, ho aggiunto il supporto per JSON e mio tipi di messaggio contenuto HTTP.

Per iniziare, creare un progetto servizio Windows in soluzione Visual Studio e aggiungere un componente di installazione servizio (troverai le istruzioni su bit.ly/TrHQ8O) così è possibile trasformare il vostro servizio e si spegne nell'applet Servizi di strumenti di amministrazione nel pannello di controllo. Una volta fatto questo, è necessario creare due discussioni: quello che verrà associare alla porta TCP e ricevere così come trasmettere messaggi; e uno che si bloccherà su una coda di messaggi per garantire che il contenuto viene trasmesso solo quando viene ricevuto un messaggio.

In primo luogo, è necessario creare una classe che è in ascolto sulla porta TCP per i nuovi messaggi e trasmette le risposte. Ora, ci sono diversi stili di comunicazione di cometa che possono essere attuate, e in attuazione c'è una classe Server (vedere il file di codice Comet_Win_Service HTTP\Server.cs nel codice di esempio) per astrarre queste. Per semplicità, tuttavia, mi concentrerò su ciò che ha richiesto di fare una molto semplice ricevere un messaggio JSON su HTTP, e per tenere la connessione fino di là di contenuto a spingere indietro.

Nella classe Server, creerò alcuni membri protetti per contenere oggetti che avrete bisogno di accedere dall'oggetto Server. Questi includono il thread che si legano a e ascolta sulla porta TCP per connessioni HTTP, alcuni semafori ed un elenco di oggetti client, ciascuno dei quali rappresenterà una singola connessione al server. Di importanza è _isListenerShutDown, che sarà esposto come una proprietà pubblica così può essere modificato in caso di Stop del servizio.

Successivamente, nel costruttore, avrai istanziare l'oggetto Listener TCP contro la porta, impostarlo per l'uso esclusivo del porto e quindi avviarlo. Quindi inizierò un thread per ricevere e gestire i client che si connettono al listener TCP.

Il thread che ascolti per le connessioni client contiene un po ' un ciclo che reimposta continuamente un flag che indica se è stato generato l'evento di arresto del servizio (vedi Figura 4). La prima parte di questo ciclo impostato un mutex per bloccare su tutti i thread in ascoltanti per verificare se è stato generato l'evento di arresto del servizio. Se è così, la proprietà _isListenerShutDown è vera. Quando il controllo viene completata, il mutex viene rilasciato e se il servizio è ancora in esecuzione, chiamare il TcpListener.Accept­TcpClient, che restituirà un oggetto TcpClient. Facoltativamente, controllare TcpClients esistenti per garantire di che non aggiungere un client esistente. Tuttavia, a seconda del numero dei clienti che si aspettano, che si potrebbe desiderare di sostituirlo con un sistema dove il servizio genera un ID univoco e lo invia al client browser, che ricorda e invia nuovamente l'ID ogni volta che comunica con il server affinché che essa detiene solo una singola connessione. Questo può diventare problematico, però, se il servizio non riesce; Reimposta il contatore di ID e potrebbe dare nuovi clienti IDs già utilizzato.

Figura 4 ascolto per le connessioni Client

private void Loop()
{
  try
  {
    while (true)
    {
      TcpClient client = null;
      bool isServerStopped = false;
      _listenerMutex.WaitOne();
      isServerStopped = _isListenerShutDown;
      _listenerMutex.ReleaseMutex();
      if (!isServerStopped)
      {
        client = listener.AcceptTcpClient();
      }
    else
    {
      continue;
    }
    Trace.WriteLineIf(_traceSwitch.TraceInfo, "TCP client accepted.",
      "COMET Server");
    bool addClientFlag = true;
    Client dc = new Client(client, this, authconfig, _currentClientId);
    _currentClientId++;
    foreach (Client currentClient in clients)
    {
      if (dc.TCPClient == currentClient.TCPClient)
      {
        lock (_lockObj)
        {
          addClientFlag = false;
        }
      }
    }
    if (addClientFlag)
    {
      lock (_lockObj)
      {
        clients.Add(dc);
      }
    }

Infine, il filo passa attraverso l'elenco dei clienti e rimuove qualsiasi che non sono più vivi. Per semplicità, ho messo questo codice nel metodo che viene chiamato quando il listener TCP accetta una connessione client, ma questo può influenzare le prestazioni quando si ottiene il numero di client in centinaia di migliaia. Se si intende usare questo nelle applicazioni Web Web pubblico, vi suggerisco di aggiungere un timer che spara ogni tanto e fare la pulizia in quanto.

Quando nel ciclo metodo della classe Server viene restituito un oggetto TcpClient, esso viene utilizzato per creare un oggetto client che rappresenta il client del browser. Perché ogni oggetto client viene creato in un unico thread, come con il costruttore di server, il costruttore della classe client deve attendere un mutex affinché che il client non è stato chiuso prima di continuare. In seguito, controllare il flusso TCP e comincio a leggerlo e avviare un gestore di callback deve essere eseguito una volta completata la lettura. Al gestore di callback, semplicemente leggere i byte e analizzare tramite il metodo ParseInput, che si può vedere nel codice di esempio fornito con questo articolo.

Nel metodo ParseInput della classe Client, costruire un oggetto di richiesta con i membri che corrispondono alle diverse parti del messaggio HTTP tipico e popolano quei membri in modo appropriato. In primo luogo, analizzare le informazioni di intestazione cercando i token caratteri, ad esempio "\r\n," determinare i pezzi di informazioni di intestazione dal formato dell'intestazione HTTP. Quindi chiamare il metodo ParseRequestContent per ottenere il corpo del messaggio HTTP. Il primo passo di ParseInput è quello di determinare il metodo di comunicazione HTTP utilizzato e fu inviato l'URL della richiesta. Successivamente, le intestazioni del messaggio HTTP sono estratti e archiviate nell'oggetto richiesta proprietà Headers, che è un dizionario dei tipi di intestazione e valori. Ancora una volta, dare un'occhiata al codice di esempio scaricabile per vedere come questo è fatto. Infine, caricare il contenuto della richiesta nella proprietà Body dell'oggetto di richiesta, che è solo una stringa variabile contenente tutti i byte del contenuto. Il contenuto deve ancora essere analizzato a questo punto. Alla fine, se ci sono problemi con la richiesta HTTP ricevuto dal client, inviare un messaggio di risposta errore appropriato.

Ho separato il metodo per l'analisi di contenuto della richiesta HTTP quindi potrei aggiungere supporto per tipi di messaggi diversi, quali testo, XML, JSON e così via:

public void ParseRequestContent()
{
  if (String.IsNullOrEmpty(request.Body))
  {
    Trace.WriteLineIf(_traceSwitch.TraceVerbose,
      "No content in the body of the request!");
    return;
  }
  try
  {

In primo luogo il contenuto vengono scritti un MemoryStream così, se necessario, può essere deserializzati in tipi di oggetto a seconda del tipo di contenuto della richiesta, come certi deserializers funzionano solo con i flussi:

MemoryStream mem = new MemoryStream();
mem.Write(System.Text.Encoding.ASCII.GetBytes(request.Body), 0,
  request.Body.Length);
mem.Seek(0, 0);
if (!request.Headers.ContainsKey("Content-Type"))
{
  _lastUpdate = DateTime.Now;
  _messageFormat = MessageFormat.json;
}
else
{

Come mostrato Figura 5, ho mantenuto l'azione predefinita di gestione dei messaggi XML formattato Poiché XML è ancora un formato popolare.

Figura 5 il gestore di messaggi XML predefinito

if (request.Headers["Content-Type"].Contains("xml"))
{
  Trace.WriteLineIf(_traceSwitch.TraceVerbose, 
    "Received XML content from client.");
  _messageFormat = MessageFormat.xml;
  #region Process HTTP message as XML
  try
  {
    // Picks up message from HTTP
    XmlSerializer s = new XmlSerializer(typeof(Derrick.Web.SIServer.SIRequest));
    // Loads message into object for processing
    Derrick.Web.SIServer.SIRequest data =
      (Derrick.Web.SIServer.SIRequest)s.Deserialize(mem);
  }
  catch (Exception ex)
  {
    Trace.WriteLineIf(_traceSwitch.TraceVerbose,
      "During parse of client XML request got this exception: " + 
        ex.ToString());
  }
  #endregion Process HTTP message as XML
}

Per le applicazioni Web, tuttavia, mi raccomando formattazione messaggi JSON come, a differenza di XML, che non hanno l'overhead di inizio e cancellare il tag ed è nativamente supportato in JavaScript. Basta utilizzare l'intestazione Content-Type della richiesta HTTP per indicare se il messaggio è stato inviato in JSON e deserializzare il contenuto utilizzando lo spazio dei nomi System.Web.Script.Serialization classe JavaScriptSerializer. Questa classe rende molto facile deserializzare un messaggio JSON in un oggetto c#, come illustrato nel Figura 6.

Figura 6 la deserializzazione di un messaggio JSON

else if (request.Headers["Content-Type"].Contains("json"))
{
  Trace.WriteLineIf(_traceSwitch.TraceVerbose,
    "Received json content from client.");
  _messageFormat = MessageFormat.json;
  #region Process HTTP message as JSON
  try
  {
    JavaScriptSerializer jsonSerializer = new JavaScriptSerializer();
    ClientMessage3 clientMessage =
      jsonSerializer.Deserialize<ClientMessage3>(request.Body);
    _lastUpdate = clientMessage.SendTimestamp;
    Trace.WriteLineIf(_traceSwitch.TraceVerbose,
      "Received the following message: ");
    Trace.WriteLineIf(_traceSwitch.TraceVerbose, "SendTimestamp: " +
      clientMessage.SendTimestamp.ToString());
    Trace.WriteLineIf(_traceSwitch.TraceVerbose, "Browser: " +
      clientMessage.Browser);
    Trace.WriteLineIf(_traceSwitch.TraceVerbose, "Message: " +
      clientMessage.Message);
  }
  catch (Exception ex)
  {
    Trace.WriteLineIf(_traceSwitch.TraceVerbose,
      "Error deserializing JSON message: " + ex.ToString());
  }
  #endregion Process HTTP message as JSON
}

Infine, per scopi di test, ho aggiunto un Content-Type che risponde semplicemente con un testo di risposta HTTP contenente solo la parola PING ping. In questo modo posso facilmente testare per vedere se il mio Comet server è in esecuzione inviando un messaggio JSON con Content-Type "ping", come mostrato Figura 7.

Figura 7 Content-Type "Ping"

else if (request.Headers["Content-Type"].Contains("ping"))
{
  string msg = request.Body;
  Trace.WriteLineIf(_traceSwitch.TraceVerbose, "Ping received.");
  if (msg.Equals("PING"))
  {
    SendMessageEventArgs args = new SendMessageEventArgs();
    args.Client = this;
    args.Message = "PING";
    args.Request = request;
    args.Timestamp = DateTime.Now;
    SendResponse(args);
  }
}

Infine, ParseRequestContent è solo una stringa di metodo di analisi — niente di più, niente di meno. Come potete vedere, l'analisi di dati XML è un po' più complessa, perché il contenuto deve essere scritto in una memoria­Stream prima e quindi deserializzata, utilizza la classe XmlSerializer, in una classe creata per rappresentare il messaggio dal client.

Per meglio organizzare il codice sorgente, creare una classe richiesta, mostrata Figura 8, che contiene semplicemente membri per contenere le intestazioni e le altre informazioni inviati nella richiesta HTTP in un modo facilmente accessibile all'interno del servizio. Se lo si desidera, è possibile aggiungere metodi di supporto per determinare se la richiesta ha qualsiasi contenuto o non, e controlli di autenticazione, troppo. Tuttavia, non fare questo qui per mantenere questo servizio semplice e facile da implementare.

Figura 8 la classe richiesta

public class Request
{
  public string Method;
  public string Url;
  public string Version;
  public string Body;
  public int ContentLength;
  public Dictionary<string, string> Headers = 
    new Dictionary<string, string>();
  public bool HasContent()
  {
    if (Headers.ContainsKey("Content-Length"))
    {
      ContentLength = int.Parse(Headers["Content-Length"]);
      return true;
    }
    return false;
  }

La classe di risposta, come la classe richiesta, contiene metodi per memorizzare le informazioni di risposta HTTP in modo facilmente accessibile da un servizio di Windows c#. Nel metodo SendResponse, ho aggiunto una logica per fissare le intestazioni HTTP personalizzate come richiesto per cross-origine resource sharing (CORS), e avevano delle intestazioni caricato da un file di configurazione quindi possono essere facilmente modificati. La classe di risposta contiene anche metodi per generare i messaggi per alcuni comuni stati HTTP, ad esempio 200, 401, 404, 405 e 500.

Il membro SendResponse della classe risposta scrive semplicemente il messaggio nel flusso di risposta HTTP che deve essere ancora vivo, come il timeout impostato dal cliente è abbastanza lungo (10 minuti):

public void SendResponse(NetworkStream stream, Client client)
{

Come mostrato Figura 9, le intestazioni appropriate vengono aggiunte alla risposta HTTP per adattarsi con le specifiche W3C per CORS. Per semplicità, le intestazioni vengono lette dal file di configurazione quindi può essere facilmente modificato il contenuto dell'intestazione.

Ora aggiungere le intestazioni di risposta HTTP regolari e contenuto, come mostrato Figura 10.

Figura 9 aggiungendo intestazioni CORS

if (client.Request.Headers.ContainsKey("Origin"))
{
  AddHeader("Access-Control-Allow-Origin", client.Request.Headers["Origin"]);
  Trace.WriteLineIf(_traceSwitch.TraceVerbose,
    "Access-Control-Allow-Origin from client: " +
    client.Request.Headers["Origin"]);
}
else
{
  AddHeader("Access-Control-Allow-Origin",
    ConfigurationManager.AppSettings["RequestOriginUrl"]);
  Trace.WriteLineIf(_traceSwitch.TraceVerbose,
    "Access-Control-Allow-Origin from config: " +
    ConfigurationManager.AppSettings["RequestOriginUrl"]);
}
AddHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
AddHeader("Access-Control-Max-Age", "1000");
// AddHeader("Access-Control-Allow-Headers", "Content-Type");
string allowHeaders = ConfigurationManager.AppSettings["AllowHeaders"];
// AddHeader("Access-Control-Allow-Headers", "Content-Type, x-requested-with");
AddHeader("Access-Control-Allow-Headers", allowHeaders);
StringBuilder r = new StringBuilder();

Figura 10 aggiungendo intestazioni di risposta HTTP regolari

r.Append("HTTP/1.1 " + GetStatusString(Status) + "\r\n");
r.Append("Server: Derrick Comet\r\n");
r.Append("Date: " + DateTime.Now.ToUniversalTime().ToString(
  "ddd, dd MMM yyyy HH':'mm':'ss 'GMT'") + "\r\n");
r.Append("Accept-Ranges: none\r\n");
foreach (KeyValuePair<string, string> header in Headers)
{
  r.Append(header.Key + ": " + header.Value + "\r\n");
}
if (File != null)
{
  r.Append("Content-Type: " + Mime + "\r\n");
  r.Append("Content-Length: " + File.Length + "\r\n");
}
else if (Body.Length > 0)
{
  r.Append("Content-Type: " + Mime + "\r\n");
  r.Append("Content-Length: " + Body.Length + "\r\n");
}
r.Append("\r\n");

Qui l'intero messaggio di risposta HTTP, che fu costruito come una stringa, ora è scritto nel flusso di risposta HTTP, che è stato passato come parametro al metodo SendResponse:

byte[] htext = Encoding.ASCII.GetBytes(r.ToString());
stream.Write(htext, 0, htext.Length);

Trasmettere messaggi

Il thread per trasmettere i messaggi è essenzialmente nient'altro che un po ' di tempo ciclo che si blocca su un Microsoft message queue. Ha un evento SendMessage che viene generato quando il thread preleva un messaggio dalla coda. L'evento è gestito da un metodo dell'oggetto server che fondamentalmente chiama il metodo SendResponse di ogni cliente, trasmettendo così il messaggio per ogni browser collegato ad esso.

Il thread in attesa nella coda messaggio appropriato fino a quando c'è che un messaggio inserito su di esso, che indica che il server dispone di alcuni contenuti desideri trasmettere ai clienti:

Message msg = _intranetBannerQueue.Receive(); 
// Holds thread until message received
Trace.WriteLineIf(_traceSwitch.TraceInfo,
  "Message retrieved from the message queue.");
SendMessageEventArgs args = new SendMessageEventArgs();
args.Timestamp = DateTime.Now.ToUniversalTime();

Quando il messaggio viene ricevuto, viene convertito nel tipo di oggetto previsto:

msg.Formatter = new XmlMessageFormatter(new Type[] { typeof(string) });
string cometMsg = msg.Body.ToString();
args.Message = cometMsg;

Dopo aver determinato che cosa verrà inviato ai clienti, sollevo un evento Windows sul server che indica c'è un messaggio da trasmettere:

if (SendMessageEvent != null)
{
  SendMessageEvent(this, args);
  Trace.WriteLineIf(_traceSwitch.TraceVerbose,
    "Message loop raised SendMessage event.");
}

Successivamente, ho bisogno di un metodo che costruirà il corpo della risposta HTTP effettivo — il contenuto del messaggio il server sarà trasmesso a tutti i clienti. Il messaggio precedente prende il contenuto del messaggio oggetto di dumping sul Microsoft message queue e formatta come oggetto JSON per la trasmissione ai client tramite un messaggio di risposta HTTP, come mostrato Figura 11.

Figura 11 edificio del corpo di risposta HTTP

public void SendResponse(SendMessageEventArgs args)
{
  Trace.WriteLineIf(_traceSwitch.TraceVerbose,
    "Client.SendResponse(args) called...");
  if (args == null || args.Timestamp == null)
  {
    return;
  }
  if (_lastUpdate > args.Timestamp)
  {
    return;
  }
  bool errorInSendResponse = false;
  JavaScriptSerializer jsonSerializer = null;

Successivamente, è necessario creare un'istanza dell'oggetto JavaScriptSerializer per mettere il contenuto del messaggio in formato JSON. Aggiungere il seguente try/catch error handling perché a volte ci sono Difficoltà un'istanza di un'istanza di un oggetto JavaScriptSerializer:

try
{
  jsonSerializer = new JavaScriptSerializer();
}
catch (Exception ex)
{
  errorInSendResponse = true;
  Trace.WriteLine("Cannot instantiate JSON serializer: " + 
    ex.ToString());
}

Quindi creare una variabile di stringa per contenere il messaggio in formato JSON e un'istanza della classe risposta per inviare il messaggio JSON.

Faccio subito qualche errore base di controllo per assicurarsi che sto lavorando con una richiesta HTTP valida. Perché questo servizio cometa depone le uova in un thread per ogni client TCP, così come per gli oggetti server, mi sentivo più sicuro includere questi controlli di sicurezza ogni tanto, per rendere più facile di debug.

Una volta che verifico che è una richiesta valida, ho messo insieme un messaggio JSON per inviare il flusso di risposta HTTP. Si noti che I basta creare il messaggio JSON, serializzarlo e utilizzarlo per creare un messaggio di risposta HTML:

if (request.HasContent())
{
  if (_messageFormat == MessageFormat.json)
  {
    ClientMessage3 jsonObjectToSend = new ClientMessage3();
    jsonObjectToSend.SendTimestamp = args.Timestamp;
    jsonObjectToSend.Message = args.Message;
    jsonMessageToSend = jsonSerializer.Serialize(jsonObjectToSend);
    response = Response.GetHtmlResponse(jsonMessageToSend,
      args.Timestamp, _messageFormat);
    response.SendResponse(stream, this);
  }

Per collegare tutto insieme, in primo luogo creare istanze dell'oggetto messaggio loop e il ciclo oggetto server durante l'evento di avvio del servizio. Si noti che questi oggetti devono essere membri protetti della classe del servizio affinché i metodi su di essi possono essere chiamati durante altri eventi di servizio. Ora il ciclo di messaggi invia messaggio evento deve essere gestito con il metodo MessaggioDaTrasmettere dell'oggetto server:

public override void BroadcastMessage(Object sender, 
  SendMessageEventArgs args)
{
  // Throw new NotImplementedException();
  Trace.WriteLineIf(_traceSwitch.TraceVerbose,
    "Broadcasting message [" + args.Message + "] to all clients.");
  int numOfClients = clients.Count;
  for (int i = 0; i < numOfClients; i++)
  {
    clients[i].SendResponse(args);
  }
}

La MessaggioDaTrasmettere solo invia lo stesso messaggio a tutti i clienti. Se lo si desidera, è possibile modificarlo per inviare il messaggio solo per i client che si desidera; in questo modo è possibile utilizzare questo servizio per gestire, ad esempio, più stanze di chat online.

Quando il servizio viene arrestato, viene chiamato il metodo OnStop. Successivamente chiama il metodo Shutdown dell'oggetto server, che passa attraverso l'elenco di oggetti client che sono ancora validi e li arresta.

A questo punto, ho un lavoro abbastanza decente servizio di cometa, che posso installare nell'applet Servizi dal prompt dei comandi utilizzando il comando installutil (per ulteriori informazioni, vedere bit.ly/OtQCB7). È inoltre possibile creare il proprio Windows installer per distribuire, come hai già aggiunto i componenti del programma di installazione servizio al progetto del servizio.

Perché non funziona? Il problema con CORS

Ora, provare a impostare l'URL nella chiamata Ajax $ del client browser per indicare l'URL del servizio di cometa. Avviare il servizio di cometa e aprire il client del browser Firefox. Assicurarsi di che avere l'estensione Firebug installato nel browser Firefox. Iniziare a Firebug e aggiornare la pagina; si noterà si ottiene un errore nell'area output console affermando "Accesso negato". Ciò è dovuto al CORS, dove per motivi di sicurezza, JavaScript non può accedere risorse di fuori della stessa applicazione Web e la directory virtuale pagina relativa custodia risiede in. Ad esempio, se la pagina del browser client è in http://www.somedomain.com/somedir1/somedir2/client.aspx, qualsiasi chiamata AJAX fatto su quella pagina può andare solo a risorse nella stessa directory virtuale o una sottodirectory. Questo è grande se si sta chiamando un'altra pagina o gestore HTTP all'interno dell'applicazione Web, ma voi non volete pagine e gestori di bloccare su una coda dei messaggi quando si trasmette lo stesso messaggio a tutti i clienti, quindi è necessario utilizzare il servizio Windows cometa e avete bisogno di un modo di ottenere intorno alla restrizione CORS.

Per fare questo, vi consiglio di costruzione una pagina proxy nella stessa directory virtuale, la cui unica funzione è quella di intercettare il messaggio HTTP da client browser, estrarre tutte le intestazioni pertinenti e contenuti e costruire un altro oggetto di richiesta HTTP che si connette al servizio di cometa. Perché questo collegamento è fatto sul server, esso non è influenzata da CORS. Così, attraverso un proxy, è possibile mantenere una connessione lunga vita tra il tuo browser client e il servizio di cometa. Inoltre, è ora possibile trasmettere un singolo messaggio quando arriva su una coda di messaggi a tutti i client browser connessi simultaneamente.

In primo luogo, prendo la richiesta HTTP e streaming in una matrice di byte così posso passare un nuovo oggetto di richiesta HTTP che avrete un'istanza poco:

byte[] bytes;
using (Stream reader = Request.GetBufferlessInputStream())
{
  bytes = new byte[reader.Length];
  reader.Read(bytes, 0, (int)reader.Length);
}

Successivamente, creare un nuovo oggetto HttpWebRequest e puntare al server cometa, cui URL ho messo nel file Web. config, quindi può essere facilmente modificato in seguito:

string newUrl = ConfigurationManager.AppSettings["CometServer"];
HttpWebRequest cometRequest = (HttpWebRequest)HttpWebRequest.Create(newUrl);

Questo crea una connessione al server di cometa per ogni utente, ma poiché lo stesso messaggio viene trasmesso a ciascun utente, è possibile solo incapsulare l'oggetto cometRequest in un doppio bloccaggio singleton per ridurre il carico di connessione sul server cometa e lasciate che IIS bilanciamento la connessione del carico per voi di fare.

Poi che popolano le intestazioni HttpWebRequest con gli stessi valori che ho ricevuto dal client jQuery, soprattutto l'impostazione della proprietà KeepAlive su true in modo che mantenere una connessione HTTP longeva, che è la tecnica fondamentale dietro comunicazione di cometa-stile.

Controllare qui per un'intestazione di origine, che è richiesto dalla specifica W3C quando si tratta di problemi relativi a CORS:

for (int i = 0; i < Request.Headers.Count; i++)
{
  if (Request.Headers.GetKey(i).Equals("Origin"))
  {
    containsOriginHeader = true;
    break;
  }
}

Passo poi l'intestazione di origine verso il HttpWebRequest così il server cometa si riceverà:

if (containsOriginHeader)
{
  // cometRequest.Headers["Origin"] = Request.Headers["Origin"];
  cometRequest.Headers.Set("Origin", Request.Headers["Origin"]);
}
else
{
  cometRequest.Headers.Add("Origin", Request.Url.AbsoluteUri);
}
System.Diagnostics.Trace.WriteLineIf(_proxySwitch.TraceVerbose,
  "Adding Origin header.");

Successivamente, prendere i byte dal contenuto della richiesta HTTP da client jQuery e scriverle per il flusso di richiesta di HttpWebRequest, che verrà inviato al server di cometa, come mostrato Figura 12.

Figura 12 scrittura nel flusso HttpWebRequest

Stream stream = null;
if (cometRequest.ContentLength > 0 && 
  !cometRequest.Method.Equals("OPTIONS"))
{
  stream = cometRequest.GetRequestStream();
  stream.Write(bytes, 0, bytes.Length);
}
if (stream != null)
{
  stream.Close();
}
// Console.WriteLine(System.Text.Encoding.ASCII.GetString(bytes));
System.Diagnostics.Trace.WriteLineIf(_proxySwitch.TraceVerbose, 
  "Forwarding message: " 
  + System.Text.Encoding.ASCII.GetString(bytes));

Dopo l'inoltro del messaggio al server di cometa, chiamo il metodo GetResponse dell'oggetto HttpWebRequest, che fornisce un oggetto HttpWebResponse che mi permette di elaborare la risposta del server. Aggiungo anche le intestazioni HTTP necessarie che invieremo con il messaggio al client:

try
{
  Response.ClearHeaders();
  HttpWebResponse res = (HttpWebResponse)cometRequest.GetResponse();
  for (int i = 0; i < res.Headers.Count; i++)
  {
    string headerName = res.Headers.GetKey(i);
    // Response.Headers.Set(headerName, res.Headers[headerName]);
    Response.AddHeader(headerName, res.Headers[headerName]);
  }
  System.Diagnostics.Trace.WriteLineIf(_proxySwitch.TraceVerbose,
    "Added headers.");

Quindi attendere la risposta del server:

Stream s = res.GetResponseStream();

Quando ricevo il messaggio del server cometa, io lo scrivo al flusso di risposta della richiesta HTTP originale così il cliente può ricevere, come mostrato Figura 13.

Figura 13 scrittura del messaggio del Server per il flusso di risposta HTTP

string msgSizeStr = ConfigurationManager.AppSettings["MessageSize"];
int messageSize = Convert.ToInt32(msgSizeStr);
byte[] read = new byte[messageSize];
// Reads 256 characters at a time
int count = s.Read(read, 0, messageSize);
while (count > 0)
{
  // Dumps the 256 characters on a string and displays the string to the console
  byte[] actualBytes = new byte[count];
  Array.Copy(read, actualBytes, count);
  string cometResponseStream = Encoding.ASCII.GetString(actualBytes);
  Response.Write(cometResponseStream);
  count = s.Read(read, 0, messageSize);
}
Response.End();
System.Diagnostics.Trace.WriteLineIf(_proxySwitch.TraceVerbose, 
  "Sent Message.");
s.Close();
}

Eseguire il test dell'applicazione

Per testare l'applicazione, creare un sito Web per tenere le pagine di applicazione di esempio. Assicurarsi che l'URL al tuo servizio Windows è corretta e la coda di messaggi è configurato correttamente e utilizzabile. Avviare il servizio e aprire la pagina client cometa in un browser e la pagina per inviare i messaggi in un altro. Digitare un messaggio e premere invio; Dopo circa 10 ms si dovrebbe vedere il messaggio visualizzato nella finestra del browser. Prova questo con i vari browser — soprattutto alcune di quelle più vecchie. Fintanto che supportano l'oggetto xmlHttpRequest, dovrebbe funzionare. Questo fornisce un comportamento Web quasi in tempo reale (en.wikipedia.org/wiki/Real-time_web), il cui contenuto è spinto al browser quasi istantaneamente senza la necessità di azione da parte dell'utente.

Prima di ogni nuova applicazione è distribuita, devi fare prestazioni e test di carico. Per fare questo, si devono innanzitutto identificare le metriche che si desidera raccogliere. Vi suggerisco di utilizzo carico di misura contro i tempi di risposta e la dimensione di trasferimento dei dati. Inoltre, è necessario testare gli scenari di utilizzo che sono rilevanti per la cometa, in particolare trasmettendo un singolo messaggio a più client senza postback.

Per fare il test, costruito una utility che consente di aprire più thread, ognuno con una connessione al server di cometa e attende che il server genera una risposta. Questa utilità di test mi permette di impostare alcuni parametri, quali il numero totale di utenti che si connetteranno al mio server Comet e il numero di volte riaprire la connessione (attualmente la connessione viene chiusa dopo la risposta del server viene inviata).

Poi ho creato un programma di utilità che il dump di un messaggio di numero di byte per la coda di messaggi, con il numero di byte impostato da un campo di testo sullo schermo principale e un campo di testo per impostare il numero di millisecondi da attendere tra i messaggi inviati dal server x. Questo utilizzerà per inviare il messaggio al client. Poi ho iniziato il test client, specificate il numero di utenti più il numero di volte che il client si riapre la connessione cometa, e le discussioni aperte le connessioni contro il mio server. I aspettato pochi secondi per tutti i collegamenti devono essere aperti, poi è andato all'utilità l'invio di messaggi e presentato un certo numero di byte. Ho ripetuto questo per varie combinazioni di utenti totali, ripetizioni totali e le dimensioni del messaggio.

Il primo campionamento di dati che ho preso era per un singolo utente con l'aumento di ripetizioni ma con il messaggio di risposta coerente (piccole) dimensioni durante il test. Come si può vedere Figura 14, il numero di ripetizioni non sembra avere un impatto sulle prestazioni del sistema o l'affidabilità.

Figura 14 variando il numero di utenti

Utenti Ripetizioni Dimensione di messaggio (in byte) Tempo di risposta (in millisecondi)
1,000 10 512 2.56
5,000 10 512 4.404
10,000 10 512 18.406
15,000 10 512 26.368
20,000 10 512 36.612
25,000 10 512 48.674
30,000 10 512 64.016
35,000 10 512 79.972
40,000 10 512 99.49
45,000 10 512 122.777
50,000 10 512 137.434

I tempi sono gradualmente aumentando in maniera lineare/costante, che significa che il codice sul server cometa è generalmente robusto. Figura 15 grafici del numero di utenti contro il tempo di risposta per un messaggio di 512 byte. Figura 16 mostra alcune statistiche per una dimensione del messaggio di 1.024 byte. Infine, Figura 17 illustrato il grafico da Figura 16 in formato grafico.Tutti questi test sono stati fatti su un singolo computer portatile con 8 GB di RAM e un 2,4 GHz CPU Intel Core i3.

Response Times for Varying Numbers of Users for a 512-Byte Message
Figura 15 tempi di risposta per numero di utenti per un messaggio di 512 Byte

Figura 16 test con un messaggio di 1.024 byte

Utenti Ripetizioni Tempo di risposta (in millisecondi)
1,000 10 144.227
5,000 10 169.648
10,000 10 233.031
15,000 10 272.919
20,000 10 279.701
25,000 10 220.209
30,000 10 271.799
35,000 10 230.114
40,000 10 381.29
45,000 10 344.129
50,000 10 342.452

User Load vs Response Time for a 1KB Message
Figura 17 carico utente vs tempo di risposta per un 1 messaggio KB

I numeri non mostrano alcuna tendenza particolare, tranne per il fatto che i tempi di risposta sono ragionevoli, rimanendo in seguito un secondo messaggio di dimensioni fino a 1 KB. Non mi preoccupai di rilevamento della larghezza di banda utilizzare perché che è influenzato dal formato del messaggio. Inoltre, poiché tutti i test è stato fatto su un singolo computer, latenza di rete è stata eliminata come un fattore. Potrei aver provato contro la mia rete domestica, ma non pensavo che sarebbe utile perché il pubblico di Internet è molto più complesso rispetto la mia configurazione di modem wireless router e cavo. Tuttavia, poiché le tecniche di comunicazione punto chiave della cometa è quello di ridurre il round trip server spingendo contenuto dal server come aggiornato, teoricamente metà l'utilizzo di larghezza di banda di rete dovrebbe essere ridotto attraverso tecniche di cometa.

Conclusioni

Spero che si può ora correttamente implementare applicazioni cometa-stile e utilizzarli in modo efficace per ridurre la banda di larghezza e aumentare le prestazioni delle applicazioni del sito Web. Naturalmente, ti consigliamo di controllare le nuove tecnologie incluse con HTML5, che può sostituire la cometa, come WebSockets (bit.ly/UVMcBg) e Server-Sent eventi (SSE) (bit.ly/UVMhoD). Queste tecnologie promettono di fornire un modo più semplice di spingendo contenuto nel browser, ma richiedono all'utente di avere un browser che supporta HTML5. Se avete ancora supportare gli utenti su browser meno recenti, comunicazione di cometa-stile rimane la scelta migliore.

Derrick Lau è un software con esperienza sviluppo team leader con circa 15 anni di esperienza. Ha lavorato nei negozi IT delle imprese finanziarie e il governo, così come le sezioni di sviluppo software della società incentrata sulla tecnologia. Egli ha vinto il Gran Premio in un concorso di sviluppo di EMC nel 2010 ed è entrato come finalista nel 2011. Egli è inoltre certificato come un MCSD e come uno sviluppatore di EMC content management.

Grazie all'esperto tecnica seguente per la revisione di questo articolo: Francis Cheung