Sie sind hier: Weblog

Streaming in ASP.NET Web API mit Self-Hosting und IIS

Foto ,
10.09.2012 05:34:00

Damit riesige Datenmengen, wie Datendateien, Filme oder Bilder, nicht komplett in den Hauptspeicher des Clients bzw. des Servers geladen werden müssen, bietet ASP.NET WebAPI die Möglichkeit, diese bereits im Zuge des Empfanges zu verarbeiten. Dieser Mechanismus, der allgemein als Streaming bezeichnet wird, ermöglicht auch das Senden von Datenmengen, ohne diese vollständig in den Speicher laden zu müssen.
 
Action-Methoden für Streaming vorbereiten

Um eine Action-Methode zu veranlassen, Daten an den Aufrufer zu streamen, verwendet der Entwickler eine Instanz von StreamContent und weist diese zur Eigenschaft Content der HttpResponseMessage zu. An den Konstruktor von StreamContent übergibt er den gewünschten Stream.
public HttpResponseMessage Get()
{
    var response = new HttpResponseMessage();
    var stream = new FileStream(@"c:\temp\bilder\info.dat", FileMode.Open);
    response.Content = new StreamContent(stream);

    return response;
}
Eine Action-Methode, welche einen Stream entgegennehmen möchte, erhält diesen über die Methode Content.ReadAsStreamAsync des aktuellen Request-Objektes. Das Beispiel im nachfolgenden liest aus diesem Stream nach und nach jeweils 100 KB und gibt danach eine Statusmeldung im Debug-Fenster aus. Durch diese Ausgabe, welche im Zuge des Uploads immer wieder erfolgt, kann nachvollzogen werden, dass aus dem Stream gelesen werden kann, noch bevor der Client ihn komplett übertragen hat.
public async void Post() 
    {
        Stream s = await Request.Content.ReadAsStreamAsync();
        long count = 0;

        byte[] buffer = new byte[100*1024];
        while (s.Read(buffer,0, buffer.Length) > 0)
        {
            count++;
            Debug.WriteLine(count); // Just for displaying progress
        }
    }
Streaming in Self-Hosting-Szenarien konfigurieren
Das Bereitstellen von Action-Methoden, welche in der Lage sind, mit Streams umzugehen ist nur die halbe Miete. Zusätzlich ist der Entwickler angehalten, den jeweils verwendeten Service-Host, sprich IIS oder einen selbst-entwickelten Host, dahingehend zu konfigurieren.
In Self-Hosting-Szenarien ist dazu die Konfigurations-Eigenschaft Transfermode zu setzen (siehe folgendes Listing). Die Standardeinstellung ist Buffered, was bedeutet, dass eine Nachricht erst nachdem sie vollkommen empfangen wurde, Verwendung findet – in diesem Fall wird somit nicht gestreamt. Alternativ dazu legt StreamedRequest fest, dass die Anfrage gestreamt wird. StreamedResponse veranlasst ein Streamen der Antwort und Streamed ist die Kombination aus diesen beiden Optionen.
Darüber hinaus ist der Entwickler bei Streaming-Szenarien, bei denen in der Regel größere Datemengen im Spiel sind, gut beraten, sich Gedanken über die Eigenschaften  MaxReceivedMessageSize  und MaxBufferSize zu machen: MaxReceivedMessageSize legt die maximale empfangbare Nachrichtengröße fest; MaxBufferSize hingegen die maximale Größe jenes Nachrichtenteils, der gepuffert werden kann. Im Falle von Streaming kann dieser Wert kleiner als jener von MaxReceivedMessageSize sein, da hier immer nur ein Teil der gesamten Datenmenge im Hauptspeicher zur Verarbeitung vorliegt. Soll Streaming nicht zum Einsatz kommen, müssen beide Parameter dieselbe Größe aufweisen.
var config = new HttpSelfHostConfiguration("http://localhost:8080");

config.Routes.MapHttpRoute(
    "API Default", "api/{controller}/{id}",
    new { id = RouteParameter.Optional });

config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;

config.TransferMode = System.ServiceModel.TransferMode.Streamed; 
config.MaxReceivedMessageSize = 1000000000;
config.MaxBufferSize = 1024*100;

config.SendTimeout = TimeSpan.FromMinutes(5);
config.MaxConcurrentRequests = 20;
            
using (HttpSelfHostServer server = new HttpSelfHostServer(config))
{
    server.OpenAsync().Wait();
    Console.WriteLine("Press Enter to quit.");
    Console.ReadLine();
}

Streaming für IIS konfigurieren
Beim Hosting innerhalb von IIS kann der Entwickler das Streaming-Verhalten feingranularer beeinflussen. Dazu legt er eine Subklasse von WebHostBufferPolicySelector an und überschreibt die beiden Methoden UseBufferedInputStream und UseBufferedOutputStream. Erstere wird von IIS aufgerufen, um zu ermitteln, ob empfangene Daten gestreamt werden dürfen; letztere, um herauszufinden, ob dies für zu sendende Daten der Fall ist.
UseBufferedInputStream nimmt einen Parameter hostContext, welcher bei Verwendung innerhalb von IIS nach HttpContextBase gecastet werden kann, entgegen. Über diese HttpContextBase-Instanz kann die Methode Informationen über die aktuelle HTTP-basierte Anfrage in Erfahrung bringen und aufgrund dieser entscheiden, ob Streaming aktiviert werden soll. Im betrachteten Fall liefert diese Methode bei einer Anfrage, die an den BilderController gerichtet ist, den Wert false. Dies Bewirkt, dass WebAPI die Daten im Rahmen dieser Anfrage nicht Puffert sondern streamt.
Innerhalb von UseBufferedOutputStream würde man analog dazu vorgehen. Im betrachteten Fall delegiert diese Methode jedoch lediglich an ihre Basis-Implementierung weiter.
public class CustomWebHostBufferPolicySelector : WebHostBufferPolicySelector
{
    public override bool UseBufferedInputStream(object hostContext)
    {
        HttpContextBase contextBase = hostContext as HttpContextBase;

        if (contextBase != null)
        {
            RouteData routeData = contextBase.Request.RequestContext.RouteData;

            if (routeData.Values["controller"].ToString().ToLower() == "bilder")
            {
                return false;
            }
        }

        return true;
    }

    public override bool UseBufferedOutputStream(HttpResponseMessage response)
    {
        return base.UseBufferedOutputStream(response);
    }
}
Damit eine benutzerdefinierte WebHostBufferPolicySelector auch Verwendung findet, ist der Entwickler angehalten, sie in der Konfiguration zu registrieren. Dies kann zum Beispiel innerhalb der Methode WebApiConfig.Register erfolgen. Das nächste Listing zeigt den dazu notwendigen Aufruf.
GlobalConfiguration.Configuration.Services.Replace(
                           typeof(IHostBufferPolicySelector), 
                           new CustomWebHostBufferPolicySelector());
Streams über HttpClient verwenden
Um über einen HttpClient Daten hochzuladen, verwendet der Entwickler ebenfalls eine Instanz von StreamContent, welche auf den jeweiligen Stream verweist und der Eigenschaft Content der zu sendenden HttpRequestMessage zugewiesen wird.
 
static async void UploadWithStreamingDemo()
{
    var url = "http://localhost:1307/api/Bilder";
   
    var fileName = @"c:\temp\4upload.dat";
    var fileStream = new FileStream(fileName, FileMode.Open);

    HttpClient client = new HttpClient();
    var request = new HttpRequestMessage();
    request.Method = HttpMethod.Post;
    request.RequestUri = new Uri(url);
    request.Content = new StreamContent(fileStream);

    var response = await client.SendAsync(request);
    var text = await response.Content.ReadAsStringAsync();

    Console.WriteLine(text);
}
Um über einen HttpClient einen Stream zu beziehen, ist dessen Methode GetStreamAsync zu verwenden. Das Beispiel im folgenden Listing, welches auf diese Art einen Stream erhält, liest aus diesem Blöcke von jeweils 2 KB während die Daten heruntergeladen werden.
HttpClient client = new HttpClient();

var response = await client.GetAsync("http://localhost:1307/api/Bilder");
var stream = await response.Content.ReadAsStreamAsync();

Console.WriteLine("Habe Stream erhalten!");

using (FileStream fs = new FileStream(@"c:\temp\download.dat", FileMode.Create))
{
    int counter = 0;
    byte[] buffer = new byte[200*1024];
                
    while (stream.Read(buffer, 0, buffer.Length) > 0)
    {
        counter++;
        Console.WriteLine(counter * 200 * 1024 + " Bytes gelesen");
    }
}
Console.WriteLine("Fertig gelesen");