Implementieren eines guten TCP-Socket-Servers

118125
l46kok

Ich möchte eine Codeüberprüfung für meine sehr einfache Serveranwendung, die überprüft, ob die vom Client abgerufene Seriennummer gültig ist oder nicht.

  1. Gibt es eine bessere Möglichkeit, den Start / Stopp des Dienstes zu erledigen? Wenn Sie sich meine Start- und Service-Methoden anschauen, betrachte ich grundsätzlich die isServerRunningboolesche Variable und hole den Service immer aus der while-Schleife, wenn er auf false umgeschaltet wurde. Dies scheint jedoch nicht der ideale Ansatz zu sein, da der Thread im Grunde genommen der Thread ist pausierte um listner.AcceptSocket().

  2. Garantiert dies eine N: 1-Konnektivität, wobei 1 der Server ist?

  3. Ich werde ein Sende- / Empfangszeitlimit für meinen Sockel festlegen. Was ist ein generell angemessener Timeout-Wert?

Ich würde mich auch über Kommentare zum allgemeinen Codierstil und anderen Vorschlägen zur Verbesserung dieses Codes freuen.

public partial class ServerForm : Form
{
    #region Fields
    private bool isServerRunning = false;
    private const int CLIENT_LIMIT = 10;
    private TcpListener listener;

    #endregion


    #region Event Handlers

    private void btnStart_Click(object sender, EventArgs e)
    {
        try
        {
            int port;

            if (String.IsNullOrEmpty(txtPort.Text))
            {
                MessageBox.Show(Constant.ERROR_PORT_NUMBER_EMPTY);
                return;
            }

            if (isServerRunning)
                return;

            if (!int.TryParse(txtPort.Text, out port))
            {
                MessageBox.Show(Constant.ERROR_PORT_ONLY_INT_ALLOWED);
                return;
            }

            if (port < 0 || port > 65535)
            {
                MessageBox.Show(Constant.ERROR_PORT_NUMBER_OUT_OF_RANGE);
                return;
            }

            Start(port);
        }
        catch (Exception ex)
        {
            LogWriter.WriteExceptionLog(ex.ToString());
        }
    }

    private void btnStop_Click(object sender, EventArgs e)
    {
        if (isServerRunning)
        {
            isServerRunning = false;
            ServerLogWriter("Server Stopped");
        }
    }

    private void btnClear_Click(object sender, EventArgs e)
    {
        try
        {
            listBoxLog.Items.Clear();
        }
        catch (Exception ex)
        {
            LogWriter.WriteExceptionLog(ex.ToString());
        }
    }


    #endregion

    #region Constructor
    public ServerForm()
    {
        InitializeComponent();
    }
    #endregion


    #region Private Methods

    private void ServerLogWriter(string content)
    {
        string log = DateTime.Now + " : " + content;
        this.BeginInvoke(new Action(() => 
        {
           listBoxLog.Items.Add(log);
        }
        ));
        LogWriter.WriteLog(log, Constant.NETWORK_LOG_PATH);
    }

    private void Start(int port)
    {
        try
        {
            isServerRunning = true;
            listener = new TcpListener(IPAddress.Parse(LocalIPAddress()), port);
            listener.Start();

            for (int i = 0; i < CLIENT_LIMIT; i++)
            {
                Thread t = new Thread(new ThreadStart(Service));
                t.IsBackground = true;
                t.Start();
            }
            ServerLogWriter(String.Format("Server Start (Port Number: {0})", port));

        }
        catch (SocketException ex)
        {
            ServerLogWriter(String.Format("Server Start Failure (Port Number: {0}) : {1}",port,ex.Message));
            LogWriter.WriteExceptionLog(ex.ToString());
            isServerRunning = false;
        }
        catch (Exception ex)
        {
            LogWriter.WriteExceptionLog(ex.ToString());
            isServerRunning = true;
        }
    }
    private void Service()
    {
        try
        {
            while (isServerRunning)
            {
                Socket soc = listener.AcceptSocket();

                ServerLogWriter(String.Format("Client Connected : {0}", soc.RemoteEndPoint));

                try
                {
                    Stream s = new NetworkStream(soc);
                    StreamReader sr = new StreamReader(s);
                    StreamWriter sw = new StreamWriter(s);
                    sw.AutoFlush = true;

                    string validation = String.Empty;
                    string serial = sr.ReadLine();
                    if (ValidateSerial(serial))
                    {
                        validation = String.Format("Serial Number Validated (Received Serial: {1}) : {0}", soc.RemoteEndPoint, serial);
                        sw.WriteLine("VALIDATE");
                    }
                    else
                    {
                        validation = String.Format("Invalid Serial Number (Received Serial: {1}) : {0}", soc.RemoteEndPoint, serial);
                        sw.WriteLine("FAILURE");
                    }
                    ServerLogWriter(validation);
                    s.Close();
                }
                catch (Exception ex)
                {
                    ServerLogWriter(String.Format("Socket Error: {0}, {1}", soc.RemoteEndPoint, ex.Message));
                    LogWriter.WriteLog(ex.ToString(), Constant.EXCEPTION_LOG_PATH);
                }

                ServerLogWriter(String.Format("End Connection : {0}", soc.RemoteEndPoint));

                soc.Close();
            }
        }
        catch (Exception ex)
        {
            LogWriter.WriteExceptionLog(ex.ToString());
        }
    }

    private string LocalIPAddress()
    {
        string localIP = String.Empty;
        try
        {
            IPHostEntry host;
            host = Dns.GetHostEntry(Dns.GetHostName());
            foreach (IPAddress ip in host.AddressList)
            {
                if (ip.AddressFamily.ToString() == "InterNetwork")
                {
                    localIP = ip.ToString();
                }
            }
            return localIP;
        }
        catch (Exception ex)
        {
            LogWriter.WriteExceptionLog(ex.ToString());
        }

        return localIP;
    }

    #endregion
}
Antworten
22
Ist es für die Validierung der Seriennummer nicht sinnvoller, WCF zu verwenden? hast du dir eine option angesehen? Viezevingertjes vor 7 Jahren 0
Siehe folgendes Funktionsbeispiel ... [C # Socket-Beispielprogramm] (http://csharp.net-informations.com/communications/csharp-socket-programming.htm) Sehr einfach und für Anfänger gut geeignet. vor 6 Jahren 0

2 Antworten auf die Frage

23
Şafak Gür

Möglicherweise möchten Sie einige andere
Handbücher / Implementierungen dazu anzeigen : CodeProject | C # SocketAsyncEventArgs Hochleistungs-Socket-Stapelüberlauf
| So schreiben Sie einen skalierbaren TCP / IP-basierten Server

Über Ihren Code:

  • Sie sollten die Benutzeroberfläche und den Servercode voneinander trennen, damit Sie sie in einer Konsolenanwendung oder - wahrscheinlich auch in einem Windows-Dienst - verwenden können.
  • TcpListenerist eine gute Klasse, die ihre Arbeit ziemlich gut macht, aber wenn Sie etwas Skalierbareres und anpassbarer wollen, denke ich, dass es für Sie zu hoch ist . Dafür sollten Sie sich Socketdirekt mit dem Unterricht beschäftigen.

Sie sollten auch wissen:
Es gibt so viele Konzepte, die Sie über TCP- und Socket-Programmierung im Allgemeinen kennen sollten. um einen robusten, skalierbaren TCP-Server zu schreiben. Sie sollten sich mit Framing-Protokollen auskennen, einen guten Weg finden, mit Ihren Puffern umzugehen, Erfahrung mit asynchronem Code und dem Debuggen dieses Codes haben.

Edit: Diese FAQ-Seite von Stephen Cleary ist ein wirklich guter Einstieg.

Ich schlage vor, dass Sie sich die oben genannten Links ansehen. Sie bieten einige gute Implementierungen und erklären, warum sie ihre Aufgaben auf umfassende Weise durchführen. Dann können Sie selbst rollen und häufige Fallstricke vermeiden.

Zurück zu Ihrem Code:

  • Das Erstellen eines neuen Threads für jede Verbindung ist brutal . Sie sollten die asynchronen Methoden der TcpListener-Klasse verwenden. Oder Sie können es zumindest verwenden ThreadPoolund der einfachste Weg, dies zu tun, ist die Verwendung der TaskKlasse.
  • LocalIPAddress()Diese Methode gibt eine Zeichenfolge zurück, die Sie erneut für ein IPAddresszu analysierendes Objekt analysieren . Konvertieren Sie es nicht in einen String, sondern geben Sie es direkt als IPAddress.
  • AddressFamilyist ein Enum. Sie können überprüfen, ob es ist, InterNetworkohne es in einen String zu konvertieren:ip.AddressFamily == AddressFamily.InterNetwork
  • Versuchen Sie, Ihre Streams in usingBlöcken zu verwenden, oder legen Sie sie in finallyBlöcken ab. So können Sie sicher sein, dass sie geschlossen sind, auch wenn eine Ausnahme im Code ausgelöst wird.
  • Das Timeout hängt wirklich von dem Client, dem Netzwerk und der Größe der Daten ab, die Sie senden / empfangen. Unabhängig von der Zeit, in der Sie denken, dass "etwas falsch läuft", ist dies ein ausreichender Timeout-Wert.

Nichtsdestotrotz:
Mein Rat ist, diesen Vorgang nicht fortzusetzen und einen anderen zu beginnen, nachdem Sie einige andere Implementierungen wie die von mir genannten untersucht haben. Sie benötigen ein UI-unabhängiges Projekt, auf das von Ihren anderen Anwendungen verwiesen werden kann, die einen TCP-Server verwenden müssen (oder sein müssen).

Edit: Obwohl ich gesagt habe, dass Sie das können sollten, bedeutet das nicht, dass Sie einen TCP-Server in einer GUI-Anwendung haben sollten. Sie benötigen einen Windows-Dienst, der im Hintergrund ausgeführt werden kann, mit großen Verbindungsmengen umgeht und den Datenverkehr zwischen ihm und seinen Clients verwaltet. Das Ausführen dieser Arten von Operationen in einer GUI-Anwendung ist normalerweise eine schlechte Idee.

Eine Sache, auf die Sie anspielten, aber nicht ausdrücklich gesagt haben: Server mit einer grafischen Benutzeroberfläche zu haben, ist eine wirklich schlechte Idee. Der Server sollte als Windows-Dienst oder ähnliches ausgeführt werden. Eine GUI-Anwendung zum Testen zu haben ist wahrscheinlich in Ordnung. svick vor 7 Jahren 0
@svick: Ich habe meine Antwort so bearbeitet, dass das am Ende explizit heißt, danke. Åžafak Gür vor 7 Jahren 0
OK. Danke für die gute Antwort. Der Grund, warum ich GUI-Code mit Servercode mischte, besteht darin, dass eine der mir gestellten Anforderungen darin bestand, ein Protokoll des Verbindungsstatus in der Listbox anzuzeigen. Gibt es gute Ideen, wie ich damit umgehen soll? l46kok vor 7 Jahren 0
@ l46kok: Muss es eine ListBox sein? Sie können Ihre Protokolle in eine Datenbank oder eine Protokolldatei schreiben und dann eine GUI-Anwendung (z. B. LogViewer) erstellen, um die Protokolle abzufragen und anzuzeigen. Sie sollten ohnehin nicht alle Protokolle im Speicher aufbewahren, da dies dazu führen kann, dass Ihr Programm eine OutOfmemoryException auslöst, und es besteht die Tatsache, dass alle Ihre Protokolle beim Schließen der Anwendung gelöscht werden. Für LogViewer sollten Sie abhängig von Ihrer Umgebung die letzten N Protokolle abrufen. (Ich glaube nicht, dass eine ListBox mit Millionen von Artikeln eine gute Leistung erbringen würde.) Åžafak Gür vor 7 Jahren 1
@ ÅžafakGür So seltsam es auch klingt, ja, es muss eine ListBox sein, denn das ist die Anforderung an mich. Die ListBox wird automatisch gelöscht, wenn eine bestimmte Anzahl von Elementen erreicht wird. Der Speicher ist also kein großes Problem. l46kok vor 7 Jahren 0
@ l46kok: Dann kannst du es in deinem Log Viewer verwenden. Sie sollten die Protokolle des gestrigen oder letzten Monats sehen können, wenn Sie möchten. Das Speichern der Protokolle an einem persistenteren Ort (wie DB oder Dateisystem) ist also der Weg. Sie können jedes beliebige Steuerelement verwenden, um die Protokolle in Ihrer Protokollanzeige anzuzeigen. Denken Sie so: Ihr TCP-Server wird ständig ausgeführt, aber nicht jeder wird seine Protokolle jede Sekunde ständig überprüfen. Das Erstellen eines Windows-Dienstes für den TCP-Server und das Erstellen einer GUI-Anwendung, die von den Mitarbeitern ausgeführt wird, um die Protokolle zu überprüfen und zu schließen, wenn sie fertig sind, scheint hier das Richtige zu sein. Åžafak Gür vor 7 Jahren 0
9
Ron Klein

Ohne auf andere Aspekte einzugehen, sollten Sie den Code so aufteilen, dass der Server selbst in seiner Klasse (oder sogar ein eigenes Projekt) und die Benutzeroberfläche vorhanden ist.

Beispielsweise sollten Sie eine MessageBox nicht im Code eines Servers anzeigen. Es gibt so viele Gründe dafür, also lasse ich diese Aussage einfach so wie sie ist.

Mit anderen Worten, der Server sollte sich wie eine Blackbox verhalten, Fehlercodes zurückgeben oder Ausnahmen oder etwas anderes gemäß seiner eigenen API auslösen. Das Mischen des Servercodes mit der Benutzeroberfläche ist nicht empfehlenswert.