Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Asynchrone TCP-Sockets als Alternative zu WCF
Zur Erstellung eines Client/Server-Systems in einer Umgebung mit Microsoft-Technologie ist Windows Communication Foundation (WCF) weit verbreitet. Es gibt natürlich zahlreiche Alternativen zu WCF, die alle ihre Vor- und Nachteile haben, darunter HTTP-Webdienste, Web-API, DCOM, AJAX-Webtechnologien, Named Pipe-Programmierung und RAW-TCP-Socketprogrammierung. Aber wenn Sie solche Faktoren wie den Entwicklungsaufwand, die Verwaltbarkeit, Skalierbarkeit, Leistung und Sicherheit berücksichtigen, ist es in vielen Situationen am effizientesten, WCF zu verwenden.
WCF kann allerdings sehr kompliziert sein und ist daher in einigen Programmierungssituationen möglicherweise zu viel des Guten. Die asynchrone Socketprogrammierung war meiner Meinung nach vor der Veröffentlichung von Microsoft .NET Framework 4.5 in den meisten Fällen zu schwierig, um ihre Nutzung zu rechtfertigen. Aber durch die einfache Verwendung der neuen C#-Sprachfeatures „await“ und „async“ verschiebt sich das Gleichgewicht, und die Socketprogrammierung für asynchrone Client/Server-Systeme ist jetzt eine attraktivere Option als zuvor. In diesem Artikel erläutere ich, wie Sie diese neuen, asynchronen .NET Framework 4.5-Features verwenden, um niedrigstufige, asynchrone Client/Server-Softwaresysteme mit hoher Leistung zu erstellen.
Das Thema des Artikels wird am deutlichsten, wenn Sie sich das Client/Server-Demosystem in Abbildung 1 ansehen. In der Befehlsshell im oberen Bildbereich wird ein asynchroner, socketbasierter TCP-Dienst ausgeführt. Dieser Dienst akzeptiert Anforderungen zur Berechnung des Durchschnitts oder Minimums aus einem Satz numerischer Werte. Im mittleren Bildbereich sehen Sie eine Windows Forms(WinForm)-Anwendung, die eine Anforderung gesendet hat, um den Durchschnitt von (3, 1, 8) zu berechnen. Der Client ist asynchron – während das Wartens auf die Antwort des Diensts, nachdem die Anforderung gesendet wurde, können die Benutzer drei Mal auf die Schaltfläche „Say Hello“ klicken, und die Anwendung ist reaktionsfähig.
Abbildung 1: TCP-basierter Demodienst mit zwei Clients
Im unteren Teil von Abbildung 1 sehen Sie einen Webanwendungsclient in Aktion. Der Client hat eine asynchrone Anforderung gesendet, um den Minimalwert von (5, 2, 7, 4) zu ermitteln. Im Screenshot ist es nicht zu erkennen, aber während die Webanwendung auf die Antwort des Diensts wartet, reagiert sie auf Benutzereingaben.
In den folgenden Abschnitten zeige ich, wie Sie den Dienst, den WinForm-Client und den Webanwendungsclient codieren. Dabei gehe ich auch auf die Vor- und Nachteile der Verwendung von Sockets ein. Dieser Artikel setzt mindestens mittlere Programmierkenntnisse in C# voraus. Es wird aber nicht davon ausgegangen, dass Sie über vertiefte Kenntnisse in asynchroner Programmierung oder umfangreiche Erfahrungen darin verfügen. Im Codedownload zu diesem Artikel finden Sie den gesamten Quellcode für die drei in Abbildung 1 gezeigten Programme. Ich habe einen Großteil der üblichen Fehlerprüfungen entfernt, damit die wichtigsten Konzepte so deutlich wie möglich sind.
Erstellen des Diensts
Abbildung 2 zeigt die Gesamtstruktur des Demodiensts, mit ein paar kleinen Änderungen, um Platz zu sparen. Visual Studio 2012 umfasst das erforderliche .NET Framework 4.5. Zum Erstellen des Diensts habe ich Visual Studio 2012 gestartet und eine neue C#-Konsolenanwendung namens DemoService erstellt. In der Praxis ist ein aussagekräftigerer Name besser, da socketbasierte Dienste in der Regel eine bestimmte, begrenzte Funktionalität haben.
Abbildung 2: Die Programmstruktur des Demodiensts
using System;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Threading.Tasks;
namespace DemoService
{
class ServiceProgram
{
static void Main(string[] args)
{
try
{
int port = 50000;
AsyncService service = new AsyncService(port);
service.Run();
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.ReadLine();
}
}
}
public class AsyncService
{
private IPAddress ipAddress;
private int port;
public AsyncService(int port) { . . }
public async void Run() { . . }
private async Task Process(TcpClient tcpClient) { . . }
private static string Response(string request)
private static double Average(double[] vals) { . . }
private static double Minimum(double[] vals) { . . }
}
}
Nach dem Laden des Vorlagencodes in den Editor habe ich den using-Anweisungen oben im Quellcode „System.Net“ und „System.Net.Sockets“ hinzugefügt. Im Projektmappen-Explorer habe ich die Datei „Program.cs“ in „ServiceProgram.cs“ umbenannt, und Visual Studio hat automatisch die Program-Klasse für mich umbenannt. Den Dienst zu starten ist einfach:
int port = 50000;
AsyncService service = new AsyncService(port);
service.Run();
Jeder benutzerdefinierte, socketbasierte Dienst auf einem Server muss einen eindeutigen Port verwenden. Für benutzerdefinierte Dienste werden im Allgemeinen die Portnummern zwischen 49152 und 65535 genutzt. Es kann schwierig sein, Portnummernkonflikte zu vermeiden. Sie können dazu Portnummern mithilfe des ReservedPorts-Eintrags in der Systemregistrierung reservieren. Der Dienst folgt einem objektorientierten Programmierungsentwurf (OOP) und wird über einen Konstruktor instanziiert, der die Portnummer übernimmt. Die Portnummern für den Dienst sind fest, und daher können Sie die Portnummer hartcodieren, anstatt sie als Parameter zu übergeben. Die Run-Methode enthält eine while-Schleife, die Clientanforderungen akzeptiert und verarbeitet, bis die Konsolenshell einen <enter>-Tastendruck empfängt.
Die AsyncService-Klasse umfasst die zwei privaten Member „ipAddress“ und „port“. Diese zwei Werte definieren im Grunde einen Socket. Der Konstruktor akzeptiert eine Portnummer und bestimmt programmgesteuert die IP-Adresse des Servers. Die öffentliche Run-Methode führt die gesamte Arbeit aus, was das Übernehmen von Anforderungen, die anschließenden Berechnungen und das Senden von Antworten angeht. Die Run-Methode ruft die Process-Hilfsmethode auf, die wiederum die Response-Hilfsmethode aufruft. Die Response-Methode ruft die Helfer „Average“ und „Minimum“ auf.
Es gibt viele Möglichkeiten, einen socketbasierten Server zu organisieren. Die im Demodienst verwendete Struktur strebt ein Gleichgewicht zwischen Modularität und Einfachheit an, und sie hat für mich in der Praxis gut funktioniert.
Die Konstruktormethode und die Run-Methode des Diensts
Abbildung 3 zeigt die zwei öffentlichen Methoden des socketbasierten Demodiensts. Nach dem Speichern des Portnamens verwendet der Konstruktor die GetHostName-Methode, um den Namen des Servers zu bestimmen und anschließend eine Struktur abzurufen, die Informationen über den Server enthält. Die AddressList-Auflistung umfasst verschiedene Computeradressen, darunter IPv4- und IPv6-Adressen. Der InterNetwort-Enumerationswert bedeutet eine IPv4-Adresse.
Abbildung 3: Die Konstruktormethode und die Run-Methode des Diensts
public AsyncService(int port)
{
this.port = port;
string hostName = Dns.GetHostName();
IPHostEntry ipHostInfo = Dns.GetHostEntry(hostName);
this.ipAddress = null;
for (int i = 0; i < ipHostInfo.AddressList.Length; ++i) {
if (ipHostInfo.AddressList[i].AddressFamily ==
AddressFamily.InterNetwork)
{
this.ipAddress = ipHostInfo.AddressList[i];
break;
}
}
if (this.ipAddress == null)
throw new Exception("No IPv4 address for server");
}
public async void Run()
{
TcpListener listener = new TcpListener(this.ipAddress, this.port);
listener.Start();
Console.Write("Array Min and Avg service is now running"
Console.WriteLine(" on port " + this.port);
Console.WriteLine("Hit <enter> to stop service\n");
while (true) {
try {
TcpClient tcpClient = await listener.AcceptTcpClientAsync();
Task t = Process(tcpClient);
await t;
}
catch (Exception ex) {
Console.WriteLine(ex.Message);
}
}
}
Der Server wird durch diesen Ansatz darauf beschränkt, auf Anforderungen zu warten, die nur die erste zugewiesene IPv4-Adresse des Servers verwenden. Eine simplere Alternative besteht darin, das Member-Feld einfach als „this.ipAddress = IPAddress.Any“ zuzuweisen. Damit lassen Sie zu, dass der Server Anforderungen akzeptiert, die an beliebige seiner Adressen gesendet werden.
Die Run-Methodensignatur verwendet den async-Modifizierer, um anzugeben, dass im Textkörper der Methode eine asynchrone Methode in Verbindung mit dem await-Schlüsselwort aufgerufen wird. Die Methode gibt „void“ anstelle vom häufigeren „Task“ zurück, da „Run“ von der Main-Methode aufgerufen wird, die als Sonderfall den async-Modifizierer nicht zulässt. Sie können alternativ definieren, dass die Run-Methode den Task-Typ zurückgibt, und dann die Methode als „service.Run().Wait“ aufrufen.
Die Run-Methode des Diensts instanziiert ein TcpListener-Objekt, das die IP-Adresse und Portnummer des Servers verwendet. Die Start-Methode des Listeners beginnt mit der Überwachung des angegebenen Ports und wartet auf eine Verbindungsanforderung.
Innerhalb der while-Hauptverarbeitungsschleife wird ein TcpClient-Objekt erstellt, das Sie sich als einen intelligenten Socket vorstellen können. Das Objekt wartet auf eine Verbindung über die AcceptTcpClientAsync-Methode. Vor .NET Framework 4.5 war es erforderlich, „BeginAcceptTcpClient“ zu verwenden und dann benutzerdefinierten, asynchronen Koordinationscode zu schreiben, und Sie können mir glauben, dass das nicht einfach ist. .NET Framework 4.5 fügt viele neue Methoden hinzu, die konventionsgemäß mit „Async“ enden. Diese neuen Methoden in Kombination mit den async- und await-Schlüsselwörtern machen die asynchrone Programmierung wesentlich einfacher.
Die Run-Methode ruft die Process-Methode mithilfe von zwei Anweisungen auf. Als Alternative können Sie eine Syntax-Kurzform verwenden und die Process-Methode in einer einzelnen Anweisung aufrufen: „await Process(tcpClient)“.
In der Zusammenfassung verwendet der Dienst TcpListener- und TcpClient-Objekte, um die Komplexität der RAW-Socketprogrammierung zu verbergen, und er verwendet die neue AcceptTcpClientAsync-Methode in Verbindung mit den neuen async- und await-Schlüsselwörtern, um die Komplexität der asynchronen Programmierung zu verbergen. Die Run-Methode richtet die Verbindungsaktivitäten ein und koordiniert sie, ruft die Process-Methode auf, um Anforderungen zu verarbeiten, und dann eine zweite Anweisung, um auf die Rückgabe von „Task“ zu warten.
Die Process- und Response-Methoden des Diensts
Abbildung 4 zeigt die Process- und Response-Methoden des Dienstobjekts. Die Process-Methodensignatur verwendet den async-Modifizierer und gibt den Task-Typ zurück.
Abbildung 4: Die Process- und Response-Methoden des Demodiensts
private async Task Process(TcpClient tcpClient)
{
string clientEndPoint =
tcpClient.Client.RemoteEndPoint.ToString();
Console.WriteLine("Received connection request from "
+ clientEndPoint);
try {
NetworkStream networkStream = tcpClient.GetStream();
StreamReader reader = new StreamReader(networkStream);
StreamWriter writer = new StreamWriter(networkStream);
writer.AutoFlush = true;
while (true) {
string request = await reader.ReadLineAsync();
if (request != null) {
Console.WriteLine("Received service request: " + request);
string response = Response(request);
Console.WriteLine("Computed response is: " + response + "\n");
await writer.WriteLineAsync(response);
}
else
break; // Client closed connection
}
tcpClient.Close();
}
catch (Exception ex) {
Console.WriteLine(ex.Message);
if (tcpClient.Connected)
tcpClient.Close();
}
}
private static string Response(string request)
{
string[] pairs = request.Split('&');
string methodName = pairs[0].Split('=')[1];
string valueString = pairs[1].Split('=')[1];
string[] values = valueString.Split(' ');
double[] vals = new double[values.Length];
for (int i = 0; i < values.Length; ++i)
vals[i] = double.Parse(values[i]);
string response = "";
if (methodName == "average") response += Average(vals);
else if (methodName == "minimum") response += Minimum(vals);
else response += "BAD methodName: " + methodName;
int delay = ((int)vals[0]) * 1000; // Dummy delay
System.Threading.Thread.Sleep(delay);
return response;
}
Einer der Vorteile bei der Verwendung von Low-Level-Sockets anstelle von Windows Communication Foundation (WCF) ist, dass Sie WriteLine-Diagnoseanweisungen auf einfache Weise an beliebigen gewünschten Positionen einfügen können. In diesem Democode habe ich aus Sicherheitsgründen „clientEndPoint“ durch den Dummy-IP-Adresswert 123.45.678.999 ersetzt.
Dies sind die drei wichtigen Zeilen in der Process-Methode:
string request = await reader.ReadLineAsync();
...
string response = Response(request);
...
await writer.WriteLineAsync(response);
Die erste Anweisung können Sie so interpretieren: „Lies eine Anforderungszeile asynchron und gestatte dabei ggf. anderen Anweisungen ihre Ausführung.“ Nachdem die Anforderungszeichenfolge abgerufen wurde, wird sie an den Response-Helfer übergeben. Anschließend wird die Antwort asynchron an den anfordernden Client zurückgesendet.
Der Server verwendet einen Zyklus mit Lesen der Anforderung und Schreiben der Antwort. Es ist einfach, aber es gibt einige Aspekte, die Sie kennen müssen. Wenn der Server liest, ohne zu schreiben, kann er keine halb offene Situation erkennen. Wenn der Server schreibt, ohne zu lesen (zum Beispiel bei einer Antwort mit einer großen Menge an Daten), kann dies einen Clientdeadlock verursachen. Ein Lese-/Schreibentwurf ist für einfache, interne Dienste akzeptabel, sollte aber nicht für Dienste verwendet werden, die kritisch oder öffentlich sind.
Die Response-Methode übernimmt die Anforderungszeichenfolge, verarbeitet die Anforderung und berechnet eine Antwortzeichenfolge. Es ist eine Schwäche und gleichzeitig eine Stärke socketbasierter Dienste, dass Sie eine Form von benutzerdefiniertem Protokoll erstellen müssen. In diesem Fall wird angenommen, dass die Anforderungen in etwa so aussehen:
method=average&data=1.1 2.2 3.3&eor
Mit anderen Worten, der Dienst erwartet das Literal „method=“, gefolgt von der Zeichenfolge „average“ oder „minimum“, anschließend ein kaufmännisches Und-Zeichen („&“), gefolgt vom Literal „data=“. Die tatsächlichen Eingabedaten müssen durch Leerzeichen getrennt sein. Die Anforderung wird mit einem „&“ beendet, gefolgt vom Literal „eor“, das für das Ende der Anforderung (End-of-Request) steht. Ein Nachteil socketbasierter Dienste im Vergleich mit WCF ist, dass die Serialisierung komplexer Parametertypen mitunter etwas schwierig sein kann.
In diesem Demobeispiel ist die Dienstantwort eine einfache Zeichenfolgendarstellung des Durchschnitts oder Minimums eines Arrays numerischer Werte. In vielen benutzerdefinierten Client/Server-Situationen muss ein Protokoll für die Dienstantwort entworfen werden. Ein Beispiel dafür ist, als Antwort „average=4.00“ anstatt einfach nur „4.00“ zu senden.
Die Process-Methode nutzt einen relativ primitiven Ansatz, um eine Verbindung im Falle einer Ausnahme zu schließen. Sie können alternativ die C#-Anweisung „using“ verwenden (die automatisch alle Verbindungen schließt) und den expliziten Aufruf der Close-Methode entfernen.
Die Hilfsmethoden „Average“ und „Minimum“ werden wie folgt definiert:
private static double Average(double[] vals)
{
double sum = 0.0;
for (int i = 0; i < vals.Length; ++i)
sum += vals[i];
return sum / vals.Length;
}
private static double Minimum(double[] vals)
{
double min = vals[0]; ;
for (int i = 0; i < vals.Length; ++i)
if (vals[i] < min) min = vals[i];
return min;
}
Wenn Sie eine ähnliche Programmstruktur wie der Demodienst verwenden, würden sich die Hilfsmethoden in den meisten Situationen an dieser Stelle mit einer Datenquelle verbinden und Daten abrufen. Ein Vorteil von Low-Level-Diensten ist, dass Sie mehr Kontrolle darüber haben, wie Sie auf Daten zugreifen. Wenn Sie zum Beispiel Daten aus SQL abrufen, können Sie herkömmliches ADO.NET, Entity Framework oder eine beliebige andere Datenzugriffsmethode nutzen.
Ein Nachteil des Low-Level-Ansatzes ist, dass Sie explizit festlegen müssen, wie Fehler in Ihrem System behandelt werden. Wenn der Demodienst in meinem Beispiel die Anforderungszeichenfolge nicht zufriedenstellend verarbeiten kann, gibt er anstelle einer gültigen Antwort (als Zeichenfolge) eine Fehlermeldung zurück. Nach meiner Erfahrung gibt es sehr wenige allgemeine Prinzipien, auf die Sie sich stützen können. Für jeden Dienst ist eine benutzerdefinierte Fehlerbehandlung erforderlich.
Beachten Sie, dass die Response-Methode eine Dummyverzögerung hat:
int delay = ((int)vals[0]) * 1000;
System.Threading.Thread.Sleep(delay);
Diese Antwortverzögerung, die willkürlich auf dem ersten numerischen Wert der Anforderung basiert, wurde eingefügt, um den Dienst zu verlangsamen, damit die WinForm- und Webanwendungsclients die UI-Reaktionsfähigkeit zeigen können, während sie auf eine Antwort warten.
Der WinForm-Anwendungsclient
Zum Erstellen des in Abbildung 1 gezeigten WinForm-Clients der Demo habe ich Visual Studio 2012 gestartet und eine neue C#-WinForm-Anwendung namens DemoFormClient erstellt. Visual Studio modularisiert standardmäßig WinForm-Anwendungen in mehrere Dateien, die den UI-Code vom logischen Code trennen. Für den Codedownload zu diesem Artikel habe ich den modularisierten Visual Studio-Code in eine einzelne Quellcodedatei umgeschrieben. Sie kompilieren die Anwendung, indem Sie eine Visual Studio-Befehlsshell (der bekannt ist, wo sich der C#-Compiler befindet) starten und folgenden Befehl ausführen: „csc.exe /target:winexe DemoFormClient.cs“.
Ich habe mithilfe der Visual Studio-Entwurfstools ein ComboBox- und ein TextBox-Steuerelement, zwei Button-Steuerelemente und ein ListBox-Steuerelement sowie zwei Label-Steuerelemente hinzugefügt. Die Items-Auflistungseigenschaft des ComboBox-Steuerelements habe ich mit den Zeichenfolgen „average“ und „minimum“ ergänzt. Ich habe die Text-Eigenschaften von „button1“ und „button2“ in „Send Async“ bzw. „Say Hello“ geändert. In der Entwurfsansicht habe ich dann doppelt auf die button1- und button2-Steuerelemente geklickt, um deren Ereignishandler zu registrieren. Die Bearbeitung der Click-Ereignishandler sehen Sie in Abbildung 5.
Abbildung 5: Die Click-Ereignishandler der Schaltflächen im WinForm-Democlient
private async void button1_Click(object sender, EventArgs e)
{
try {
string server = "mymachine.network.microsoft.com";
int port = 50000;
string method = (string)comboBox1.SelectedItem;
string data = textBox1.Text;
Task<string> tsResponse =
SendRequest(server, port, method, data);
listBox1.Items.Add("Sent request, waiting for response");
await tsResponse;
double dResponse = double.Parse(tsResponse.Result);
listBox1.Items.Add("Received response: " +
dResponse.ToString("F2"));
}
catch (Exception ex) {
listBox1.Items.Add(ex.Message);
}
}
private void button2_Click(object sender, EventArgs e)
{
listBox1.Items.Add("Hello");
}
Die Signatur vom Click-Ereignishandler des button1-Steuerelements enthält nach der Änderung den async-Modifizierer. Der Handler richtet einen hartcodierten Servercomputernamen als eine Zeichenfolge und Portnummer ein. Bei der Verwendung von socketbasierten Low-Level-Diensten ist kein automatischer Erkennungsmechanismus verfügbar, und daher müssen die Clients Zugriff auf den Servernamen oder die IP-Adresse und Portinformationen erhalten.
Die relevanten Codezeilen:
Task<string> tsResponse = SendRequest(server, port, method, data);
// Perform some actions here if necessary
await tsResponse;
double dResponse = double.Parse(tsResponse.Result);
„SendRequest“ ist eine programmdefinierte asynchrone Methode. Der Aufruf kann in etwa so interpretiert werden: „Sende eine asynchrone Anforderung, die eine Zeichenfolge zurückgibt. Wenn dies abgeschlossen ist, setze die Ausführung mit der später folgenden Anweisung ‚await tsResponse’ fort.“ Dadurch kann die Anwendung andere Aktionen ausführen, während sie auf die Antwort wartet. Da die Antwort in „Task“ gekapselt ist, muss das tatsächliche Ergebnis der Zeichenfolge mithilfe der Result-Eigenschaft extrahiert werden. Dieses Ergebnis der Zeichenfolge wird in den Typ „double“ konvertiert, um eine schöne Formatierung mit zwei Dezimalstellen zu ermöglichen.
Für den Aufruf gibt es einen alternativen Ansatz:
string sResponse = await SendRequest(server, port, method, data);
double dResponse = double.Parse(sResponse);
listBox1.Items.Add("Received response: " + dResponse.ToString("F2"));
Hier wird das await-Schlüsselwort inline in den asynchronen Aufruf von „SendRequest“ platziert. Das vereinfacht den aufrufenden Code ein wenig, und außerdem kann die zurückgegebene Zeichenfolge abgerufen werden, ohne „Task.Result“ aufzurufen. Die Entscheidung, einen await-Aufruf inline oder mithilfe einer getrennten Anweisung zu gestalten, wird abhängig von der jeweiligen Situation unterschiedlich ausfallen. Im Allgemeinen ist es jedoch besser, die explizite Verwendung der Result-Eigenschaft eines Task-Objekts zu vermeiden.
Der Großteil der asynchronen Arbeit wird in der SendRequest-Methode ausgeführt, die in Abbildung 6 aufgeführt ist. Da „SendRequest“ asynchron ist, wären „SendRequestAsync“ oder „MySendRequestAsync“ bessere Benennungen.
Abbildung 6: Die SendRequest-Methode des WinForm-Democlients
private static async Task<string> SendRequest(string server,
int port, string method, string data)
{
try {
IPAddress ipAddress = null;
IPHostEntry ipHostInfo = Dns.GetHostEntry(server);
for (int i = 0; i < ipHostInfo.AddressList.Length; ++i) {
if (ipHostInfo.AddressList[i].AddressFamily ==
AddressFamily.InterNetwork)
{
ipAddress = ipHostInfo.AddressList[i];
break;
}
}
if (ipAddress == null)
throw new Exception("No IPv4 address for server");
TcpClient client = new TcpClient();
await client.ConnectAsync(ipAddress, port); // Connect
NetworkStream networkStream = client.GetStream();
StreamWriter writer = new StreamWriter(networkStream);
StreamReader reader = new StreamReader(networkStream);
writer.AutoFlush = true;
string requestData = "method=" + method + "&" + "data=" +
data + "&eor"; // 'End-of-request'
await writer.WriteLineAsync(requestData);
string response = await reader.ReadLineAsync();
client.Close();
return response;
}
catch (Exception ex) {
return ex.Message;
}
}
„SendRequest“ akzeptiert eine Zeichenfolge, die den Servernamen darstellt, und löst diesen Namen mithilfe derselben Codelogik, die im Klassenkonstruktor des Diensts verwendet wurde, in eine IP-Adresse auf. Eine einfachere Alternative besteht darin, nur den Namen des Servers zu übergeben: „await client.ConnectAsync(server, port)“.
Nachdem die IP-Adresse des Servers bestimmt wurde, folgt die Instanziierung eines intelligenten TcpClient-Socketobjekts. Die ConnectAsync-Methode des Objekts dient dazu, eine Verbindungsanforderung an den Server zu senden. Ein StreamWriter-Netzwerkobjekt wird eingerichtet, um Daten an den Server zu senden, und ein StreamReader-Objekt, um Daten vom Server zu empfangen. Anschließend wird unter Verwendung der vom Server erwarteten Formatierung eine Anforderungszeichenfolge erstellt. Die Anforderung wird asynchron gesendet und empfangen und von der Methode als Zeichenfolge zurückgegeben.
Der Webanwendungsclient
Den in Abbildung 1 gezeigten Demo-Webanwendungsclient habe ich in zwei Schritten erstellt. Zuerst habe ich mit Visual Studio eine Website zum Hosten der Anwendung erstellt, und danach habe ich die Webanwendung in Editor codiert. Nach dem Starten von Visual Studio 2012 habe ich in C# eine neue leere Website namens DemoClient unter „http://localhost/“ erstellt. Dadurch wurde die gesamte IIS-Basis eingerichtet, die zum Hosten einer Anwendung erforderlich ist, und der physische Speicherort für die Website unter „C:\inetpub\wwwroot\DemoClient\“ erstellt. Der Prozess hat auch die Basiskonfigurationsdatei „Web.config“ erstellt. Diese Datei enthält Informationen, um den Anwendungen auf der Website den Zugriff auf die asynchrone Funktionalität in .NET Framework 4.5 zu gestatten:
<?xml version="1.0"?>
<configuration>
<system.web>
<compilation debug="false" targetFramework="4.5" />
<httpRuntime targetFramework="4.5" />
</system.web>
</configuration>
Als Nächstes habe ich Editor mit Administratorberechtigungen gestartet. Wenn ich einfache ASP.NET-Anwendungen erstelle, verwende ich manchmal lieber Editor anstelle von Visual Studio, um den gesamten Anwendungscode in einer einzelnen ASPX-Datei zu speichern, anstatt mehrere Dateien und unerwünschten Beispielcode zu generieren. Die leere Datei habe ich unter „C:\inetpub\wwwroot\DemoClient“ als „DemoWebClient.aspx“ gespeichert.
Die Gesamtstruktur der Webanwendung ist in Abbildung 7 dargestellt.
Abbildung 7: Die Struktur des Demo-Webanwendungsclients
<%@ Page Language="C#" Async="true" AutoEventWireup="true"%>
<%@ Import Namespace="System.Threading.Tasks" %>
<%@ Import Namespace="System.Net" %>
<%@ Import Namespace="System.Net.Sockets" %>
<%@ Import Namespace="System.IO" %>
<script runat="server" language="C#">
private static async Task<string> SendRequest(string server,
private async void Button1_Click(object sender, System.EventArgs e) { . . }
</script>
<head>
<title>Demo</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<p>Enter service method:
<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox></p>
<p>Enter data:
<asp:TextBox ID="TextBox2" runat="server"></asp:TextBox></p>
<p><asp:Button Text="Send Request" id="Button1"
runat="server" OnClick="Button1_Click"> </asp:Button> </p>
<p>Response:
<asp:TextBox ID="TextBox3" runat="server"></asp:TextBox></p>
<p>Dummy responsive control:
<asp:TextBox ID="TextBox4" runat="server"></asp:TextBox></p>
</div>
</form>
</body>
</html>
Ich habe oben auf der Seite Import-Anweisungen hinzugefügt, um die relevanten .NET-Namespaces in den Bereich zu bringen, und eine Page-Direktive, die das Attribut „Async=true“ umfasst.
Der C#-Skriptbereich enthält zwei Methoden, „SendRequest“ und „Button1_Click“. Der Anwendungsseiten-Textkörper verfügt über zwei TextBox- und ein Button-Steuerelement für die Eingabe, ein TextBox-Ausgabesteuerelement für die Dienstantwort und ein nicht verwendetes Dummy-TextBox-Steuerelement. Letzteres dient dazu, die Reaktionsfähigkeit der UI zu zeigen, während die Anwendung darauf wartet, dass der Dienst auf eine Anforderung antwortet.
Der Code für die SendRequest-Methode der Webanwendung ist genau derselbe wie der Code für „SendRequest“ in der WinForm-Anwendung. Der Code für den Button1_Click-Handler der Webanwendung weicht nur geringfügig von dem für den WinForm-Handler „button1_Click“ ab, um die unterschiedliche Benutzeroberfläche zu berücksichtigen:
try {
string server = "mymachine.network.microsoft.com";
int port = 50000;
string method = TextBox1.Text;
string data = TextBox2.Text;
string sResponse = await SendRequest(server, port, method, data);
double dResponse = double.Parse(sResponse);
TextBox3.Text = dResponse.ToString("F2");
}
catch (Exception ex) {
TextBox3.Text = ex.Message;
}
Der Code für die Webanwendung ist zwar im Grunde derselbe wie der Code für die WinForm-Anwendung, aber der Aufrufmechanismus unterscheidet sich deutlich. Wenn die Benutzer eine Anforderung über die WinForm-Anwendung senden, gibt diese den Aufruf direkt an den Dienst aus, und der Dienst antwortet direkt an die WinForm-Anwendung. Wenn die Benutzer eine Anforderung in der Webanwendung übermitteln, sendet die Webanwendung die Anforderungsinformationen an den Webserver, der die Anwendung hostet, der Webserver ruft den Dienst auf, der Dienst antwortet an den Webserver, der Webserver erstellt eine Antwortseite, die die Antwort enthält, und die Antwortseite wird an den Clientbrowser zurückgesendet.
Zusammenfassung
Wann sollten Sie also erwägen, asynchrone TCP-Sockets anstelle von WCF zu verwenden? Vor ungefähr 10 Jahren, vor der Entwicklung von WCF und der Vorgängertechnologie ASP.NET Web Services, waren Sockets häufig die sinnvollste Option, wenn Sie ein Client/Server-System erstellen wollten. Die Einführung von WCF stellte einen großen Fortschritt dar. Da WCF dafür konzipiert ist, eine sehr große Anzahl von Szenarios zu verarbeiten, kann es aber in manchen Situationen zu viel des Guten sein, WCF für einfache Client/Server-Systeme zu verwenden. Obwohl die aktuelle WCF-Version einfacher zu konfigurieren ist als frühere Versionen, kann die Arbeit mit WCF immer noch knifflig sein.
In Situationen mit Clients und Servern in verschiedenen Netzwerken, wodurch die Sicherheit ein wesentlicher Faktor ist, verwende ich stets WCF. Aber für viele Client/Server-Systeme, bei denen Clients und Server sich in einem einzigen, sicheren Unternehmensnetzwerk befinden, bevorzuge ich häufig TCP-Sockets.
Ein relativ neuer Ansatz zur Implementierung von Client/Server-Systemen ist, das ASP.NET-Web-API-Framework für HTTP-basierte Dienste in Kombination mit der ASP.NET-SignalR-Bibliothek für asynchrone Methoden zu verwenden. Verglichen mit der Verwendung von WCF ist diese Option in vielen Fällen einfacher zu implementieren und vermeidet viele der niedrigstufigen Details, die mit der Verwendung von Sockets verbunden sind.
Dr. James McCaffrey ist in Redmond (Washington) für Microsoft Research tätig. Er hat an verschiedenen Microsoft-Produkten mitgearbeitet, unter anderem an Internet Explorer und Bing. Sie erreichen ihn unter jammc@microsoft.com.
Unser Dank gilt den folgenden technischen Experten für ihren Rat und die Durchsicht dieses Artikels: Piali Choudhury (MS Research), Stephen Cleary (Berater), Adam Eversole (MS Research), Lynn Powers (MS Research) und Stephen Toub (Microsoft).