2016-08-01 9 views
1

Ich versuche, einen Befehlszeilen-Chatraum zu bauen, in dem der Server die Verbindungen verarbeitet und Eingaben von einem Client zu allen anderen Clients wiederholt. Momentan ist der Server in der Lage, Eingaben von mehreren Clients zu übernehmen, kann jedoch Informationen nur einzeln an diese Clients zurücksenden. Ich denke mein Problem ist, dass jede Verbindung in einem einzelnen Thread behandelt wird. Wie würde ich zulassen, dass die Threads miteinander kommunizieren oder Daten an jeden Thread senden können?Senden von Daten über NetworkStream mit mehreren Threads

Server Code:

namespace ConsoleApplication 
{ 


    class TcpHelper 
    { 


     private static object _lock = new object(); 
     private static List<Task> _connections = new List<Task>(); 


     private static TcpListener listener { get; set; } 
     private static bool accept { get; set; } = false; 

     private static Task StartListener() 
     { 
      return Task.Run(async() => 
      { 
       IPAddress address = IPAddress.Parse("127.0.0.1"); 
       int port = 5678; 
       listener = new TcpListener(address, port); 

       listener.Start(); 

       Console.WriteLine($"Server started. Listening to TCP clients at 127.0.0.1:{port}"); 

       while (true) 
       { 
        var tcpClient = await listener.AcceptTcpClientAsync(); 
        Console.WriteLine("Client has connected"); 
        var task = StartHandleConnectionAsync(tcpClient); 
        if (task.IsFaulted) 
         task.Wait(); 
       } 
      }); 
     } 

     // Register and handle the connection 
     private static async Task StartHandleConnectionAsync(TcpClient tcpClient) 
     { 
      // start the new connection task 
      var connectionTask = HandleConnectionAsync(tcpClient); 



      // add it to the list of pending task 
      lock (_lock) 
       _connections.Add(connectionTask); 

      // catch all errors of HandleConnectionAsync 
      try 
      { 
       await connectionTask; 

      } 
      catch (Exception ex) 
      { 
       // log the error 
       Console.WriteLine(ex.ToString()); 
      } 
      finally 
      { 
       // remove pending task 
       lock (_lock) 
        _connections.Remove(connectionTask); 
      } 
     } 






     private static async Task HandleConnectionAsync(TcpClient client) 
     { 

      await Task.Yield(); 


      { 
       using (var networkStream = client.GetStream()) 
       { 

        if (client != null) 
        { 
         Console.WriteLine("Client connected. Waiting for data."); 



         StreamReader streamreader = new StreamReader(networkStream); 
         StreamWriter streamwriter = new StreamWriter(networkStream); 

         string clientmessage = ""; 
         string servermessage = ""; 


         while (clientmessage != null && clientmessage != "quit") 
         { 
          clientmessage = await streamreader.ReadLineAsync(); 
          Console.WriteLine(clientmessage); 
          servermessage = clientmessage; 
          streamwriter.WriteLine(servermessage); 
          streamwriter.Flush(); 


         } 
         Console.WriteLine("Closing connection."); 
         networkStream.Dispose(); 
        } 
       } 

      } 

     } 
     public static void Main(string[] args) 
     { 
      // Start the server 

      Console.WriteLine("Hit Ctrl-C to close the chat server"); 
      TcpHelper.StartListener().Wait(); 

     } 

    } 

} 

Client-Code:

namespace Client2 
{ 
    public class Program 
    { 

     private static void clientConnect() 
     { 
      TcpClient socketForServer = new TcpClient(); 
      bool status = true; 
      string userName; 
      Console.Write("Input Username: "); 
      userName = Console.ReadLine(); 

      try 
      { 
       IPAddress address = IPAddress.Parse("127.0.0.1"); 
       socketForServer.ConnectAsync(address, 5678); 
       Console.WriteLine("Connected to Server"); 
      } 
      catch 
      { 
       Console.WriteLine("Failed to Connect to server{0}:999", "localhost"); 
       return; 
      } 
      NetworkStream networkStream = socketForServer.GetStream(); 
      StreamReader streamreader = new StreamReader(networkStream); 
      StreamWriter streamwriter = new StreamWriter(networkStream); 
      try 
      { 
       string clientmessage = ""; 
       string servermessage = ""; 
       while (status) 
       { 
        Console.Write(userName + ": "); 
        clientmessage = Console.ReadLine(); 
        if ((clientmessage == "quit") || (clientmessage == "QUIT")) 
        { 
         status = false; 
         streamwriter.WriteLine("quit"); 
         streamwriter.WriteLine(userName + " has left the conversation"); 
         streamwriter.Flush(); 

        } 
        if ((clientmessage != "quit") && (clientmessage != "quit")) 
        { 
         streamwriter.WriteLine(userName + ": " + clientmessage); 
         streamwriter.Flush(); 
         servermessage = streamreader.ReadLine(); 
         Console.WriteLine("Server:" + servermessage); 
        } 
       } 
      } 
      catch 
      { 
       Console.WriteLine("Exception reading from the server"); 
      } 
      streamreader.Dispose(); 
      networkStream.Dispose(); 
      streamwriter.Dispose(); 
     } 
     public static void Main(string[] args) 
     { 
      clientConnect(); 
     } 
    } 
} 

Antwort

2

Die Hauptsache ist falsch in Ihrem Code ist, dass Sie nicht versuchen, Daten von einem Client zu den anderen angeschlossenen Clients empfangen zu senden. Sie haben die -Liste in Ihrem Server, aber das einzige, was in der Liste gespeichert ist, sind die Task Objekte für die Verbindungen, und Sie tun nicht einmal etwas mit denen.

Stattdessen sollten Sie eine Liste der Verbindungen selbst verwalten, sodass Sie diese Nachricht an die anderen Clients übertragen können, wenn Sie eine Nachricht von einem Client erhalten haben.

Zumindest sollte dies eine List<TcpClient> sein, aber weil Sie StreamReader und StreamWriter verwenden, möchten Sie diese Objekte auch in der Liste initialisieren und speichern. Außerdem sollten Sie eine Client-ID angeben. Eine offensichtliche Wahl hierfür wäre der Name des Clients (dh was der Benutzer als seinen Namen eingibt), aber Ihr Beispiel sieht keinen Mechanismus im Chat-Protokoll vor, um diese Identifikation als Teil der Verbindungsinitialisierung zu übertragen, so in meinem Beispiel (unten) Ich benutze nur einen einfachen Integer-Wert.

Es gibt einige andere Unregelmäßigkeiten im Code Sie auf dem Laufenden, wie zum Beispiel:

  • eine Aufgabe in einem ganz neuen Thread starten, um nur einige Anweisungen auszuführen, die Sie zu dem Punkt der Einleitung einer asynchronen Operation erhalten . In meinem Beispiel lasse ich einfach den Task.Run() Teil des Codes weg, da er nicht benötigt wird.
  • Überprüfen der verbindungsspezifischen Aufgabe, wenn sie für IsFaulted zurückgegeben wird. Da es unwahrscheinlich ist, dass zu dem Zeitpunkt, zu dem dieses Task Objekt zurückgegeben wird, tatsächlich eine E/A aufgetreten ist, hat diese Logik sehr wenig Verwendung. Der Aufruf an Wait() wird eine Ausnahme auslösen, die an den Wait()-Aufruf des Hauptthreads weitergegeben wird, der den Server beendet. Aber Sie beenden den Server nicht im Falle eines anderen Fehlers, daher ist nicht klar, warum Sie das hier tun möchten.
  • Es gibt einen falschen Ruf an Task.Yield(). Ich habe keine Ahnung, was Sie dort erreichen wollen, aber was auch immer es ist, diese Aussage ist nicht nützlich. Ich habe es einfach entfernt.
  • In Ihrem Client-Code versuchen Sie nur, Daten vom Server zu empfangen, wenn Sie Daten gesendet haben. Das ist sehr falsch; Sie möchten, dass Clients reagieren und Daten empfangen, sobald sie an sie gesendet werden. In meiner Version habe ich eine einfache kleine anonyme Methode eingefügt, die sofort aufgerufen wird, um eine separate Nachrichtenempfangsschleife zu starten, die asynchron und gleichzeitig mit der Hauptbenutzereingangsschleife ausgeführt wird.
  • Auch in der Client-Code, Sie senden die "& hellip; hat verlassen & hellip;" Nachricht nach die "quit" Nachricht, die dazu führen würde, dass der Server die Verbindung schließt. Dies bedeutet, dass der Server niemals die "& hellip; hat verlassen & hellip;" Botschaft.Ich habe die Reihenfolge der Nachrichten umgekehrt, so dass "Beenden" immer das letzte ist, was der Client jemals sendet.

Meine Version sieht wie folgt aus:

Server:

class TcpHelper 
{ 
    class ClientData : IDisposable 
    { 
     private static int _nextId; 

     public int ID { get; private set; } 
     public TcpClient Client { get; private set; } 
     public TextReader Reader { get; private set; } 
     public TextWriter Writer { get; private set; } 

     public ClientData(TcpClient client) 
     { 
      ID = _nextId++; 
      Client = client; 

      NetworkStream stream = client.GetStream(); 

      Reader = new StreamReader(stream); 
      Writer = new StreamWriter(stream); 
     } 

     public void Dispose() 
     { 
      Writer.Close(); 
      Reader.Close(); 
      Client.Close(); 
     } 
    } 

    private static readonly object _lock = new object(); 
    private static readonly List<ClientData> _connections = new List<ClientData>(); 

    private static TcpListener listener { get; set; } 
    private static bool accept { get; set; } 

    public static async Task StartListener() 
    { 
     IPAddress address = IPAddress.Any; 
     int port = 5678; 
     listener = new TcpListener(address, port); 

     listener.Start(); 

     Console.WriteLine("Server started. Listening to TCP clients on port {0}", port); 

     while (true) 
     { 
      var tcpClient = await listener.AcceptTcpClientAsync(); 
      Console.WriteLine("Client has connected"); 
      var task = StartHandleConnectionAsync(tcpClient); 
      if (task.IsFaulted) 
       task.Wait(); 
     } 
    } 

    // Register and handle the connection 
    private static async Task StartHandleConnectionAsync(TcpClient tcpClient) 
    { 
     ClientData clientData = new ClientData(tcpClient); 

     lock (_lock) _connections.Add(clientData); 

     // catch all errors of HandleConnectionAsync 
     try 
     { 
      await HandleConnectionAsync(clientData); 
     } 
     catch (Exception ex) 
     { 
      // log the error 
      Console.WriteLine(ex.ToString()); 
     } 
     finally 
     { 
      lock (_lock) _connections.Remove(clientData); 
      clientData.Dispose(); 
     } 
    } 

    private static async Task HandleConnectionAsync(ClientData clientData) 
    { 
     Console.WriteLine("Client connected. Waiting for data."); 

     string clientmessage; 

     while ((clientmessage = await clientData.Reader.ReadLineAsync()) != null && clientmessage != "quit") 
     { 
      string message = "From " + clientData.ID + ": " + clientmessage; 

      Console.WriteLine(message); 

      lock (_lock) 
      { 
       // Locking the entire operation ensures that a) none of the client objects 
       // are disposed before we can write to them, and b) all of the chat messages 
       // are received in the same order by all clients. 
       foreach (ClientData recipient in _connections.Where(r => r.ID != clientData.ID)) 
       { 
        recipient.Writer.WriteLine(message); 
        recipient.Writer.Flush(); 
       } 
      } 
     } 
     Console.WriteLine("Closing connection."); 
    } 
} 

Auftraggeber:

class Program 
{ 
    private const int _kport = 5678; 

    private static async Task clientConnect() 
    { 
     IPAddress address = IPAddress.Loopback; 
     TcpClient socketForServer = new TcpClient(); 
     string userName; 
     Console.Write("Input Username: "); 
     userName = Console.ReadLine(); 

     try 
     { 
      await socketForServer.ConnectAsync(address, _kport); 
      Console.WriteLine("Connected to Server"); 
     } 
     catch (Exception e) 
     { 
      Console.WriteLine("Failed to Connect to server {0}:{1}", address, _kport); 
      return; 
     } 


     using (NetworkStream networkStream = socketForServer.GetStream()) 
     { 
      var readTask = ((Func<Task>)(async() => 
      { 
       using (StreamReader reader = new StreamReader(networkStream)) 
       { 
        string receivedText; 

        while ((receivedText = await reader.ReadLineAsync()) != null) 
        { 
         Console.WriteLine("Server:" + receivedText); 
        } 
       } 
      }))(); 

      using (StreamWriter streamwriter = new StreamWriter(networkStream)) 
      { 
       try 
       { 
        while (true) 
        { 
         Console.Write(userName + ": "); 
         string clientmessage = Console.ReadLine(); 
         if ((clientmessage == "quit") || (clientmessage == "QUIT")) 
         { 
          streamwriter.WriteLine(userName + " has left the conversation"); 
          streamwriter.WriteLine("quit"); 
          streamwriter.Flush(); 
          break; 
         } 
         else 
         { 
          streamwriter.WriteLine(userName + ": " + clientmessage); 
          streamwriter.Flush(); 
         } 
        } 

        await readTask; 
       } 
       catch (Exception e) 
       { 
        Console.WriteLine("Exception writing to server: " + e); 
        throw; 
       } 
      } 
     } 
    } 

    public static void Main(string[] args) 
    { 
     clientConnect().Wait(); 
    } 
} 

Es gibt noch eine Menge zu arbeiten brauchen. Wahrscheinlich möchten Sie die ordnungsgemäße Initialisierung von Chat-Benutzernamen auf der Serverseite implementieren. Zumindest sollten Sie bei realem Code mehr Fehlerprüfungen durchführen und sicherstellen, dass die Client-ID zuverlässig generiert wird (wenn Sie nur positive ID-Werte haben möchten, können Sie nicht mehr als 2^31-1 haben Verbindungen, bevor es auf 0 zurückrollt).

Ich machte auch einige andere kleinere Änderungen, die nicht unbedingt notwendig waren, wie die Verwendung der 10 und IPAddress.Loopback Werte anstelle der Analyse von Strings, und einfach nur vereinfachen und bereinigen den Code hier und da. Außerdem verwende ich im Moment keinen C# 6-Compiler, daher habe ich den Code, in dem Sie C# 6-Features verwendeten, so geändert, dass er stattdessen mit C# 5 kompiliert wurde.

Um einen ausgewachsenen Chat-Server zu erstellen, haben Sie immer noch Ihre Arbeit für Sie erledigt. Aber ich hoffe, dass das obige dich wieder auf den richtigen Weg bringt.

+0

WOW. Sehr hilfreich. Ja, ich habe meistens nur gegoogelt und zusammengezählt, was ich gefunden hatte, und Fehler ausgebügelt. Du bist wirklich über alles hinaus gegangen für mich und ich schätze es sehr. Ich hatte irgendwie gedacht, dass ich die Kunden speichern musste, aber ich war mir nicht sicher, ob das überflüssig war oder nicht. Vielen Dank für das Aufräumen! – hereswilson

+0

@hereswilson: glücklich zu helfen. Beachten Sie, dass das obige nur ein Beispiel dafür ist, wie Sie etwas tun könnten. Es ist nicht das letzte Wort darüber, wie du es tun sollst. Sie können beispielsweise festlegen, dass die Nachricht auch an den Client gesendet wird, der sie gesendet hat (die Client-Enumeration wird dadurch ein wenig vereinfacht). Beachten Sie auch, dass ich die Synchronisierung für den Relaying-Part geändert habe, indem ich den gesamten Vorgang synchronisiert habe und nicht nur den Teil, an den die Clients gesendet werden. Ich entschied, dass mir das besser gefällt (aus Gründen, die im Kommentar im Code beschrieben sind). –