Fortgeschrittene Fahrradkonstruktion oder client-server Anwendung basierend auf dem C# .Net Framework

Einleitung

Alles begann damit, dass ein Kollege mir vorschlug, einen kleinen Webdienst zu erstellen. Dies sollte eine Art Tinder für die IT-Community werden. Die Funktionalität ist denkbar einfach: Man registriert sich, füllt ein Profil aus und kommt dann zur Hauptsache, nämlich der Suche nach Gesprächstpartnern, um sein Netzwerk zu erweitern und neue Bekanntschaften zu schließen.

An dieser Stelle sollte ich kurz innehalten und ein wenig über mich erzählen, damit später klarer wird, warum ich bestimmte Schritte in der Entwicklung unternommen habe.

Momentan arbeite ich als Technischer Künstler in einem Spieleentwicklungsstudio. Meine Programmiererfahrung in C# beschränkt sich auf das Schreiben von Skripten und Tools für Unity sowie die Entwicklung von Plugins für die direkte Arbeit mit Android-Geräten. Jenseits dieser Welt bin ich bislang noch nicht vorgestoßen, aber nun bot sich mir diese Möglichkeit.

Teil 1. Prototyping des Rahmens

Nachdem ich mir überlegt hatte, wie dieser Dienst aussehen sollte, begann ich nach Möglichkeiten zur Umsetzung zu suchen. Am einfachsten wäre es gewesen, eine fertige Lösung zu finden, die wir, wie eine Eule auf einem Globus, mit unseren Mechaniken überziehen und der Öffentlichkeit zur Bewertung präsentieren könnten.
Doch das war nicht interessant, ich sah darin weder Herausforderung noch Sinn; deshalb begann ich, mich mit Webtechnologien und deren Interaktionsmethoden zu beschäftigen.

Ich begann mein Studium mit dem Durchsehen von Artikeln und Dokumentationen zu C# .Net. Dort fand ich verschiedene Möglichkeiten zur Lösung der Aufgabe. Es gibt viele Mechanismen zur Netzwerkinteraktion, von umfassenden Lösungen wie ASP.Net oder Azure-Diensten bis hin zur direkten Interaktion mit TcpHttp-Verbindungen.

Nachdem ich meinen ersten Versuch mit ASP unternommen hatte, lehnte ich es sofort ab – ich hielt es für eine zu schwere Lösung für unseren Dienst. Wir würden nicht einmal ein Drittel der Möglichkeiten dieser Plattform nutzen, also setzte ich meine Suche fort. Die Wahl fiel zwischen TCP und Http-Client-Server. Auch hier stieß ich auf Habr auf einen Artikel über einen Multithread-Server, nachdem ich es zusammengesetzt und getestet hatte, entschied ich mich, auf die Interaktion mit TCP-Verbindungen zu setzen, da ich der Meinung war, dass HTTP mir keine plattformübergreifende Lösung ermöglichen würde.

Die erste Version des Servers umfasste die Verarbeitung von Verbindungen, lieferte statische Inhalte von Webseiten und beinhaltete eine Benutzerdatenbank. Zu Beginn wollte ich die Funktionalitäten für die Arbeit mit der Website entwickeln, um später auch die Verarbeitung von Anwendungen für Android und iOS integrieren zu können.

Hier ist ein wenig Code
Der Hauptthread, der im Endloszyklus Clients annimmt:

using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;

namespace ClearServer
{

    class Server
    {
        TcpListener Listener;
        public Server(int Port)
        {
            Listener = new TcpListener(IPAddress.Any, Port);
            Listener.Start();

            while (true)
            {
                TcpClient Client = Listener.AcceptTcpClient();
                Thread Thread = new Thread(new ParameterizedThreadStart(ClientThread));
                Thread.Start(Client);
            }
        }

        static void ClientThread(Object StateInfo)
        {
            new Client((TcpClient)StateInfo);
        }

        ~Server()
        {
            if (Listener != null)
            {
                Listener.Stop();
            }
        }

        static void Main(string[] args)
        {
            DatabaseWorker sqlBase = DatabaseWorker.GetInstance;

            new Server(80);
        }
    }
}

Der Kundenbearbeiter:

using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;

namespace ClearServer
{
    class Client
    {


        public Client(TcpClient Client)
        {

            string Message = "";
            byte[] Buffer = new byte[1024];
            int Count;
            while ((Count = Client.GetStream().Read(Buffer, 0, Buffer.Length)) > 0)
            {
                Message += Encoding.UTF8.GetString(Buffer, 0, Count);

                if (Message.IndexOf("rnrn") >= 0 || Message.Length > 4096)
                {
                    Console.WriteLine(Message);
                    break;
                }
            }

            Match ReqMatch = Regex.Match(Message, @"^w+s+([^s?]+)[^s]*s+HTTP/.*|");
            if (ReqMatch == Match.Empty)
            {
                ErrorWorker.SendError(Client, 400);
                return;
            }
            string RequestUri = ReqMatch.Groups[1].Value;
            RequestUri = Uri.UnescapeDataString(RequestUri);
            if (RequestUri.IndexOf("..") >= 0)
            {
                ErrorWorker.SendError(Client, 400);
                return;
            }
            if (RequestUri.EndsWith("/"))
            {
                RequestUri += "index.html";
            }

            string FilePath ="D:/Web/TestSite{RequestUri}";

"D:/Web/TestSite{RequestUri}";

if (!File.Exists(FilePath))
{
ErrorWorker.SendError(Client, 404);
return;
}

string Extension = RequestUri.Substring(RequestUri.LastIndexOf('.'));

string ContentType = "";

switch (Extension)
{
case ".htm":
case ".html":
ContentType = "text/html";
break;
case ".css":
ContentType = "text/css";
break;
case ".js":
ContentType = "text/javascript";
break;
case ".jpg":
ContentType = "image/jpeg";
break;
case ".jpeg":
case ".png":
case ".gif":
ContentType =

"image/{Extension.Substring(1)}";
break;
default:
if (Extension.Length > 1)
{
ContentType =

"application/{Extension.Substring(1)}";
}
else
{
ContentType = "application/unknown";
}
break;
}

FileStream FS;
try
{
FS = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
}
catch (Exception)
{
ErrorWorker.SendError(Client, 500);
return;
}

string Headers =

quot;HTTP/1.1 200 OKnContent-Type: {ContentType}nContent-Length: {FS.Length}nn";
byte[] HeadersBuffer = Encoding.ASCII.GetBytes(Headers);
Client.GetStream().Write(HeadersBuffer, 0, HeadersBuffer.Length);

while (FS.Position < FS.Length)
{
Count = FS.Read(Buffer, 0, Buffer.Length);
Client.GetStream().Write(Buffer, 0, Count);
}

FS.Close();
Client.Close();
}
}
}


Und die erste Datenbank, die auf local SQL basiert:
using System;
using System.Data.Linq;
namespace ClearServer
{
    class DatabaseWorker
    {

        private static DatabaseWorker instance;

        public static DatabaseWorker GetInstance
        {
            get
            {
                if (instance == null)
                    instance = new DatabaseWorker();
                return instance;
            }
        }


        private DatabaseWorker()
        {
            string connectionStr = databasePath;
            using (DataContext db = new DataContext(connectionStr))
            {
                Table users = db.GetTable();
                foreach (var item in users)
                {
                    Console.WriteLine("{item.login} {item.password}");
                }
            }
        }
    }
}

Console.WriteLine("{item.login} {item.password}");
}
}
}
}
}


Wie man sehen kann, unterscheidet sich diese Version kaum von der, die im Artikel beschrieben wurde. Tatsächlich wurde hier lediglich die Möglichkeit hinzugefügt, Seiten aus einem Ordner auf dem Computer zu laden, und eine Datenbank (die in dieser Version übrigens aufgrund einer falschen Verbindungsarchitektur nicht funktioniert).

Kapitel 2. Anbringen der Räder

Nachdem ich die Serverleistung getestet hatte, kam ich zu dem Schluss, dass dies eine hervorragende Lösung sein wird (Spoiler: nein), für unseren Dienst, weshalb das Projekt begann, Logik zu entwickeln.
Schritt für Schritt tauchten neue Module auf und die Funktionalität des Servers wuchs. Der Server erhielt eine Testdomain und ssl Verschlüsselung der Verbindung.

Noch ein wenig Code, der die Logik des Serverbetriebs und die Kundenbearbeitung beschreibt.
Die aktualisierte Version des Servers, die den Einsatz eines Zertifikats beinhaltet.

using System;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Security;
using System.Security.Cryptography.X509Certificates;
using System.Security.Permissions;
using System.Security.Policy;
using System.Threading;


namespace ClearServer
{

    sealed class Server
    {
        readonly bool ServerRunning = true;
        readonly TcpListener sslListner;
        public static X509Certificate serverCertificate = null;
        Server()
        {
            serverCertificate = X509Certificate.CreateFromSignedFile(@"C:sslitinder.online.crt");
            sslListner = new TcpListener(IPAddress.Any, 443);
            sslListner.Start();
            Console.WriteLine("Server wird gestartet.." + serverCertificate.Subject + "n" + Assembly.GetExecutingAssembly().Location);
            while (ServerRunning)
            {
                TcpClient SslClient = sslListner.AcceptTcpClient();
                Thread SslThread = new Thread(new ParameterizedThreadStart(ClientThread));
                SslThread.Start(SslClient);
            }
            
        }
        static void ClientThread(Object StateInfo)
        {
            new Client((TcpClient)StateInfo);
        }

        ~Server()
        {
            if (sslListner != null)
            {
                sslListner.Stop();
            }
        }

        public static void Main(string[] args)
        {
            if (AppDomain.CurrentDomain.IsDefaultAppDomain())
            {
                Console.WriteLine("Wechsle zu einem anderen Domain");
                new AppDomainSetup
                {
                    ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase
                };
                var current = AppDomain.CurrentDomain;
                var strongNames = new StrongName[0];
                var domain = AppDomain.CreateDomain(
                    "ClearServer", null,
                    current.SetupInformation, new PermissionSet(PermissionState.Unrestricted),
                    strongNames);
                domain.ExecuteAssembly(Assembly.GetExecutingAssembly().Location);
            }
            new Server();
        }
    }
}

Außerdem gibt es einen neuen Client-Handler mit SSL-Authentifizierung:

using ClearServer.Core.Requester;
using System;
using System.Net.Security;
using System.Net.Sockets;

namespace ClearServer
{
    public class Client
    {
        public Client(TcpClient Client)
        {
            SslStream SSlClientStream = new SslStream(Client.GetStream(), false);
            try
            {
                SSlClientStream.AuthenticateAsServer(Server.serverCertificate, clientCertificateRequired: false, checkCertificateRevocation: true);
            }
            catch (Exception e)
            {
                Console.WriteLine(
                    "---------------------------------------------------------------------n" +

"|{DateTime.Now:g}n|------------n|{Client.Client.RemoteEndPoint}n|------------n|Exception: {e.Message}n|------------n|Authentication failed - closing the connection.n" +
"---------------------------------------------------------------------n");
SSlClientStream.Close();
Client.Close();
}
new RequestContext(SSlClientStream, Client);
}

}
}


Da der Server jedoch ausschließlich über eine TCP-Verbindung arbeitet, ist es notwendig, ein Modul zu erstellen, das den Kontext der Anfrage erkennen kann. Ich habe entschieden, dass hier ein Parser geeignet ist, der die Anfrage des Clients in einzelne Teile zerlegt, mit denen ich interagieren kann, um dem Client die benötigten Antworten zu geben.

Parser

using ClearServer.Core.UserController;
using ReServer.Core.Classes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;

namespace ClearServer.Core.Requester
{
    public class RequestContext
    {
        public string Message = "";
        private readonly byte[] buffer = new byte[1024];
        public string RequestMethod;
        public string RequestUrl;
        public User RequestProfile;
        public User CurrentUser = null;
        public List HeadersValues;
        public List FormValues;
        private TcpClient TcpClient;

        private event Action OnRead = RequestHandler.OnHandle;

        DatabaseWorker databaseWorker = new DatabaseWorker();

        public RequestContext(SslStream ClientStream, TcpClient Client)
        {

            this.TcpClient = Client;
            try
            {
                ClientStream.BeginRead(buffer, 0, buffer.Length, ClientRead, ClientStream);
            }
            catch { return; }
        }
        private void ClientRead(IAsyncResult ar)
        {
            SslStream ClientStream = (SslStream)ar.AsyncState;

            if (ar.IsCompleted)
            {
                Message = Encoding.UTF8.GetString(buffer);
                Message = Uri.UnescapeDataString(Message);
                Console.WriteLine("n{DateTime.Now:g} Client IP:{TcpClient.Client.RemoteEndPoint}n{Message}");
            }

n{DateTime.Now:g} Client IP:{TcpClient.Client.RemoteEndPoint}n{Message}");
RequestParse();
HeadersValues = HeaderValues();
FormValues = ContentValues();
UserParse();
ProfileParse();
OnRead?.Invoke(ClientStream, this);
}
}

private void RequestParse()
{
Match methodParse = Regex.Match(Message, @"(^w+)s+([^s?]+)[^s]*s+HTTP/.*|");
RequestMethod = methodParse.Groups[1].Value.Trim();
RequestUrl = methodParse.Groups[2].Value.Trim();
}
private void UserParse()
{
string cookie;
try
{
if (HeadersValues.Any(x => x.Name.Contains("Cookie")))
{
cookie = HeadersValues.FirstOrDefault(x => x.Name.Contains("Cookie")).Value;
try
{
CurrentUser = databaseWorker.CookieValidate(cookie);
}
catch { }
}
}
catch { }

}
private List HeaderValues()
{
var values = new List();
var parse = Regex.Matches(Message, @"(.*?): (.*?)n");
foreach (Match match in parse)
{
values.Add(new RequestValues()
{
Name = match.Groups[1].Value.Trim(),
Value = match.Groups[2].Value.Trim()
});
}
return values;
}

private void ProfileParse()
{
if (RequestUrl.Contains("@"))
{
RequestProfile = databaseWorker.FindUser(RequestUrl.Substring(2));
RequestUrl = "/profile";
}
}
private List ContentValues()
{
var values = new List();
var output = Message.Trim('n').Split().Last();
var parse = Regex.Matches(output, @"([^&].*?)=([^&]*b)");
foreach (Match match in parse)
{
values.Add(new RequestValues()
{
Name = match.Groups[1].Value.Trim(),
Value = match.Groups[2].Value.Trim().Replace('+', ' ')
});
}
return values;
}
}
}


Der Kern besteht darin, mit Hilfe von regulären Ausdrücken die Anfrage in Teile zu zerlegen. Wir erhalten eine Nachricht vom Client, extrahieren die erste Zeile, in der die Methode und die URL der Anfrage enthalten sind. Dann lesen wir die Header, die wir in einem Array im Format Name=Inhalt speichern, und erfassen auch den eventuellen Begleitinhalt (zum Beispiel Querystring), den wir ebenfalls in ein ähnliches Array einfügen. Darüber hinaus stellt der Parser fest, ob der aktuelle Client authentifiziert ist und speichert dessen Daten. Alle Anfragen von authentifizierten Clients enthalten einen Authentifizierungs-Hash, der in Cookies gespeichert wird. Dadurch kann die weitere Logik der Bearbeitung für zwei Typen von Clients getrennt werden, um ihnen die richtigen Antworten zu geben.

Eine kleine, nützliche Funktion, die es wert wäre, in ein separates Modul ausgelagert zu werden, ist die Umwandlung von Anfragen im Format „site.com/@UserName“ in dynamisch generierte Benutzerseiten. Nach der Verarbeitung der Anfrage kommen die folgenden Module zum Einsatz.

Kapitel 3. Installation des Lenkrads, Schmierung der Kette

Sobald der Parser seine Arbeit erledigt hat, übernimmt ein Handler, der dem Server weitere Anweisungen gibt und die Steuerung in zwei Teile aufteilt.

Ein einfacher Handler

using ClearServer.Core.UserController;
using System.Net.Security;
namespace ClearServer.Core.Requester
{
    public class RequestHandler
    {
        public static void OnHandle(SslStream ClientStream, RequestContext context)
        {

            if (context.CurrentUser != null)
            {
                new AuthUserController(ClientStream, context);
            }
            else 
            {
                new NonAuthUserController(ClientStream, context);
            };
        }
    }
}

Hier gibt es im Grunde nur eine Überprüfung auf die Autorisierung des Benutzers, danach beginnt die Verarbeitung der Anfrage.

Benutzercontroller
Wenn der Benutzer nicht autorisiert ist, beschränkt sich die Funktionalität auf die Anzeige von Benutzerprofilen und das Anmeldefenster. Der Code für autorisierte Benutzer sieht ungefähr gleich aus, deshalb sehe ich keinen Sinn darin, ihn zu duplizieren.

Nicht autorisierter Benutzer

using ClearServer.Core.Requester;
using System.IO;
using System.Net.Security;

namespace ClearServer.Core.UserController
{
    internal class NonAuthUserController
    {
        private readonly SslStream ClientStream;
        private readonly RequestContext Context;
        private readonly WriteController WriteController;
        private readonly AuthorizationController AuthorizationController;

        private readonly string ViewPath = "C:/Users/drdre/source/repos/ClearServer/View";

        public NonAuthUserController(SslStream clientStream, RequestContext context)
        {
            this.ClientStream = clientStream;
            this.Context = context;
            this.WriteController = new WriteController(clientStream);
            this.AuthorizationController = new AuthorizationController(clientStream, context);
            ResourceLoad();
        }

        void ResourceLoad()
        {
            string[] blockextension = new string[] {"cshtml", "html", "htm"};
            bool block = false;
            foreach (var item in blockextension)
            {
                if (Context.RequestUrl.Contains(item))
                {
                    block = true;
                    break;
                }
            }
            string FilePath = "";
            string Header = "";
            var RazorController = new RazorController(Context, ClientStream);
            
            switch (Context.RequestMethod)
            {
                case "GET":
                    switch (Context.RequestUrl)
                    {
                        case "/":
                            FilePath = ViewPath + "/loginForm.html";
                            Header =

quot;HTTP/1.1 200 OKnContent-Type: text/html";
WriteController.DefaultWriter(Header, FilePath);
break;
case "/profile":
RazorController.ProfileLoader(ViewPath);
break;
default:
//в данном блоке кода происходит отсечение запросов к серверу по прямому адресу страницы вида site.com/page.html
if (!File.Exists(ViewPath + Context.RequestUrl) | block)
{
RazorController.ErrorLoader(404);

}
else if (Path.HasExtension(Context.RequestUrl) && File.Exists(ViewPath + Context.RequestUrl))
{
Header = WriteController.ContentType(Context.RequestUrl);
FilePath = ViewPath + Context.RequestUrl;
WriteController.DefaultWriter(Header, FilePath);
}
break;
}
break;

case "POST":
AuthorizationController.MethodRecognizer();
break;

}

}

}
}


Natürlich muss der Benutzer auch Inhalte auf den Seiten erhalten. Daher gibt es das folgende Modul, das für die Beantwortung von Ressourcenanfragen zuständig ist.

WriterController

using System;
using System.IO;
using System.Net.Security;
using System.Text;

namespace ClearServer.Core.UserController
{
    public class WriteController
    {
        SslStream ClientStream;
        public WriteController(SslStream ClientStream)
        {
            this.ClientStream = ClientStream;
        }

        public void DefaultWriter(string Header, string FilePath)
        {
            FileStream fileStream;
            try
            {
                fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
                Header =

"{Header}nContent-Length: {fileStream.Length}nn";
ClientStream.Write(Encoding.UTF8.GetBytes(Header));
byte[] response = new byte[fileStream.Length];
fileStream.BeginRead(response, 0, response.Length, OnFileRead, response);
}
catch { }
}

public string ContentType(string Uri)
{
string extension = Path.GetExtension(Uri);
string Header = "HTTP/1.1 200 OKnContent-Type:";
switch (extension)
{
case ".html":
case ".htm":
return

"{Header} text/html";
case ".css":
return

"{Header} text/css";
case ".js":
return

"{Header} text/javascript";
case ".jpg":
case ".jpeg":
case ".png":
case ".gif":
return

"{Header} image/{extension}";
default:
if (extension.Length > 1)
{
return

"{Header} application/" + extension.Substring(1);
}
else
{
return

"{Header} application/unknown";
}
}
}

public void OnFileRead(IAsyncResult ar)
{
if (ar.IsCompleted)
{
var file = (byte[])ar.AsyncState;
ClientStream.BeginWrite(file, 0, file.Length, OnClientSend, null);
}
}

public void OnClientSend(IAsyncResult ar)
{
if (ar.IsCompleted)
{
ClientStream.Close();
}
}
}


Um dem Benutzer sein Profil sowie die Profile anderer Benutzer anzuzeigen, habe ich mich entschieden, RazorEngine zu verwenden, genauer gesagt, einen Teil davon. Dieser umfasst ebenfalls die Verarbeitung von ungültigen Anfragen und die Ausgabe des entsprechenden Fehlercodes.

RazorController

using ClearServer.Core.Requester;
using RazorEngine;
using RazorEngine.Templating;
using System;
using System.IO;
using System.Net;
using System.Net.Security;

namespace ClearServer.Core.UserController
{
    internal class RazorController
    {
        private RequestContext Context;
        private SslStream ClientStream;
        dynamic PageContent;


        public RazorController(RequestContext context, SslStream clientStream)
        {
            this.Context = context;
            this.ClientStream = clientStream;

        }

        public void ProfileLoader(string ViewPath)
        {
            string Filepath = ViewPath + "\/profile.cshtml";
            if (Context.RequestProfile != null)
            {
                if (Context.CurrentUser != null && Context.RequestProfile.login == Context.CurrentUser.login)
                {
                    try
                    {
                        PageContent = new { isAuth = true, Name = Context.CurrentUser.name, Login = Context.CurrentUser.login, Skills = Context.CurrentUser.skills };
                        ClientSend(Filepath, Context.CurrentUser.login);
                    }
                    catch (Exception e) { Console.WriteLine(e); }

                }
                else
                {
                    try
                    {
                        PageContent = new { isAuth = false, Name = Context.RequestProfile.name, Login = Context.RequestProfile.login, Skills = Context.RequestProfile.skills };
                        ClientSend(Filepath, "PublicProfile:" + Context.RequestProfile.login);
                    }
                    catch (Exception e) { Console.WriteLine(e); }
                }
            }
            else
            {
                ErrorLoader(404);
            }


        }

        public void ErrorLoader(int Code)
        {
            try
            {
                PageContent = new { ErrorCode = Code, Message = ((HttpStatusCode)Code).ToString() };
                string ErrorPage = "C:\Users\drdre\source\repos\ClearServer\View\Errors\ErrorPage.cshtml";
                ClientSend(ErrorPage, Code.ToString());
            }
            catch { }

        }

        private void ClientSend(string FilePath, string Key)
        {
            var template = File.ReadAllText(FilePath);
            var result = Engine.Razor.RunCompile(template, Key, null, (object)PageContent);
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(result);
            ClientStream.BeginWrite(buffer, 0, buffer.Length, OnClientSend, ClientStream);
        }

        private void OnClientSend(IAsyncResult ar)
        {
            if (ar.IsCompleted)
            {
                ClientStream.Close();
            }
        }
    }
}

Natürlich benötigt die Überprüfung der autorisierten Nutzer eine Authentifizierung. Das Authentifizierungsmodul interagiert mit der Datenbank. Die erhaltenen Daten aus den Formularen auf der Website werden im Kontext geparsed, der Nutzer wird gespeichert und erhält Cookies sowie Zugriff auf den Dienst.

Authmodul

using ClearServer.Core.Cookies;
using ClearServer.Core.Requester;
using ClearServer.Core.Security;
using System;
using System.Linq;
using System.Net.Security;
using System.Text;

namespace ClearServer.Core.UserController
{
    internal class AuthorizationController
    {
        private SslStream ClientStream;
        private RequestContext Context;
        private UserCookies cookies;
        private WriteController WriteController;
        DatabaseWorker DatabaseWorker;
        RazorController RazorController;
        PasswordHasher PasswordHasher;
        public AuthorizationController(SslStream clientStream, RequestContext context)
        {
            ClientStream = clientStream;
            Context = context;
            DatabaseWorker = new DatabaseWorker();
            WriteController = new WriteController(ClientStream);
            RazorController = new RazorController(context, clientStream);
            PasswordHasher = new PasswordHasher();
        }

        internal void MethodRecognizer()
        {
            if (Context.FormValues.Count == 2 && Context.FormValues.Any(x => x.Name == "password")) Authorize();
            else if (Context.FormValues.Count == 3 && Context.FormValues.Any(x => x.Name == "regPass")) Registration();
            else
            {
                RazorController.ErrorLoader(401);
            }
        }

        private void Authorize()
        {
            var values = Context.FormValues;
            var user = new User()
            {
                login = values[0].Value,
                password = PasswordHasher.PasswordHash(values[1].Value)
            };
            user = DatabaseWorker.UserAuth(user);
            if (user != null)
            {
                cookies = new UserCookies(user.login, user.password);
                user.cookie = cookies.AuthCookie;
                DatabaseWorker.UserUpdate(user);
                var response = Encoding.UTF8.GetBytes("

HTTP/1.1 301 Moved Permanently Location: /@{user.login} Set-Cookie: {cookies.AuthCookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnly
ClientStream.BeginWrite(response, 0, response.Length, WriteController.OnClientSend, null);

}
else
{
RazorController.ErrorLoader(401);

}
}

private void Registrierung()
{
var werte = Context.FormValues;
var benutzer = new User()
{
name = werte[0].Value,
login = werte[1].Value,
password = PasswordHasher.PasswordHash(werte[2].Value),
};
cookies = new UserCookies(benutzer.login, benutzer.password);
benutzer.cookie = cookies.AuthCookie;
if (DatabaseWorker.LoginValidate(benutzer.login))
{
Console.WriteLine("Benutzer bereit");
Console.WriteLine(

"{benutzer.password} {benutzer.password.Trim().Length}");
DatabaseWorker.UserRegister(benutzer);
var response = Encoding.UTF8.GetBytes(

HTTP/1.1 301 Moved Permanently Location: /@{benutzer.login} Set-Cookie: {benutzer.cookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnly
ClientStream.BeginWrite(response, 0, response.Length, WriteController.OnClientSend, null);
}
else
{
RazorController.ErrorLoader(401);
}
}
}
}


So sieht die Verarbeitung der Datenbank aus:

Datenbank

using ClearServer.Core.UserController;
using System;
using System.Data.Linq;
using System.Linq;

namespace ClearServer
{
    class DatabaseWorker
    {

        private readonly Table users = null;
        private readonly DataContext DataBase = null;
        private const string connectionStr = @"pfadzurDatenbank";

        public DatabaseWorker()
        {
            DataBase = new DataContext(connectionStr);
            users = DataBase.GetTable();
        }

        public User UserAuth(User User)
        {
            try
            {
                var user = users.SingleOrDefault(t => t.login.ToLower() == User.login.ToLower() && t.password == User.password);
                if (user != null)
                    return user;
                else
                    return null;
            }
            catch (Exception)
            {
                return null;
            }

        }

        public void UserRegister(User user)
        {
            try
            {
                users.InsertOnSubmit(user);
                DataBase.SubmitChanges();
                Console.WriteLine("Benutzer {user.name} mit ID {user.uid} hinzugefügt");
            }
            catch (Exception)
            {
                Console.WriteLine(e);
            }
        }

        public bool LoginValidate(string login)
        {
            if (users.Any(x => x.login.ToLower() == login.ToLower())
            {
                Console.WriteLine("Benutzername bereits vorhanden");
                return false;
            }
            return true;
        }

        public void UserUpdate(User user)
        {
            var UserToUpdate = users.FirstOrDefault(x => x.uid == user.uid);
            UserToUpdate = user;
            DataBase.SubmitChanges();
            Console.WriteLine("Benutzer {UserToUpdate.name} mit ID {UserToUpdate.uid} aktualisiert");
        }

        public User CookieValidate(string CookieInput)
        {
            User user = null;
            user = users.SingleOrDefault(x => x.cookie == CookieInput);
            if (user != null) return user;
            else return null;
        }

        public User FindUser(string login)
        {
            // Implementierung der Benutzerfindung hier
        }
    }
}

Benutzer {user.name} mit ID {user.uid} hinzugefügt
foreach (var item in users)
{
Console.WriteLine(item.login + "\n");
}
}
catch (Exception e)
{
Console.WriteLine(e);
}

}

public bool LoginValidate(string login)
{
if (users.Any(x => x.login.ToLower() == login.ToLower()))
{
Console.WriteLine("Benutzername bereits vorhanden");
return false;
}
return true;
}
public void UserUpdate(User user)
{
var UserToUpdate = users.FirstOrDefault(x => x.uid == user.uid);
UserToUpdate = user;
DataBase.SubmitChanges();
Console.WriteLine(

Console.WriteLine("Benutzer {UserToUpdate.name} mit ID {UserToUpdate.uid} aktualisiert");
foreach (var item in users)
{
Console.WriteLine(item.login + "\n");
}
}
public User CookieValidate(string CookieInput)
{
User user = null;
try
{
user = users.SingleOrDefault(x => x.cookie == CookieInput);
}
catch
{
return null;
}
if (user != null) return user;
else return null;
}
public User FindUser(string login)
{
User user = null;
try
{
user = users.Single(x => x.login.ToLower() == login.ToLower());
if (user != null)
{
return user;
}
else
{
return null;
}
}
catch (Exception)
{
return null;
}
}
}
}


Und alles funktioniert einwandfrei, die Authentifizierung und Registrierung läuft, ein minimaler Zugang zum Dienst ist bereits vorhanden, und es ist an der Zeit, die Anwendung zu schreiben und alles mit den Hauptfunktionen zu verbinden, für die das Ganze entwickelt wurde.

Kapitel 4. Das Rad neu erfinden

Um den Aufwand für die Entwicklung zweier Anwendungen für zwei Plattformen zu verringern, habe ich mich entschieden, eine plattformübergreifende Lösung mit Xamarin.Forms zu entwickeln. Glücklicherweise basiert es auf C#. Nachdem ich eine Testanwendung erstellt hatte, die einfach Daten an den Server sendet, stieß ich auf einen interessanten Aspekt. Für die Anfragen von den Geräten habe ich aus Neugier den HttpClient verwendet und eine HttpRequestMessage an den Server gesendet, die die Anmeldedaten im JSON-Format enthält. Ohne große Erwartungen öffnete ich das Serverprotokoll und sah dort eine Anfrage vom Gerät mit all den Daten. Ein kurzer Moment des Staunens über alles, was ich in den letzten drei Wochen intensiver Abende getan hatte. Um die Richtigkeit der gesendeten Daten zu überprüfen, habe ich einen Testserver auf HttpListener eingerichtet. Nachdem ich eine weitere Anfrage darauf erhalten hatte, konnte ich sie mit wenigen Codezeilen in ihre Bestandteile zerlegen und erhielt ein KeyValuePair der Daten aus dem Formular. Die Analyse der Anfrage reduzierte sich auf zwei Zeilen.

Ich habe weiter getestet. Zuvor wurde das nicht erwähnt, aber auf dem vorherigen Server habe ich einen Chat auf Basis von WebSockets implementiert. Er funktionierte recht gut, jedoch war das Prinzip der Interaktion über TCP frustrierend; es mussten zu viele unnötige Informationen generiert werden, um die Interaktion zwischen zwei Nutzern mit einem Protokoll über den Chatverlauf ordentlich zu gestalten. Dazu gehört das Parsen der Anfrage zur Verbindungsswitch und das Sammeln der Antworten gemäß RFC 6455. Daher habe ich beschlossen, auf dem Testserver eine einfache WebSocket-Verbindung zu erstellen, nur aus reiner Neugierde.

Chat beitreten

 private static async void HandleWebsocket(HttpListenerContext context)
        {
            var socketContext = await context.AcceptWebSocketAsync(null);
            var socket = socketContext.WebSocket;
            Locker.EnterWriteLock();
            try
            {
                Clients.Add(socket);
            }
            finally
            {
                Locker.ExitWriteLock();
            }

            while (true)
            {
                var buffer = new ArraySegment(new byte[1024]);
                var result = await socket.ReceiveAsync(buffer, CancellationToken.None);
                var str = Encoding.Default.GetString(buffer);
                Console.WriteLine(str);

                for (int i = 0; i < Clients.Count; i++)
                {
                    WebSocket client = Clients[i];

                    try
                    {
                        if (client.State == WebSocketState.Open)
                        {
                            
                            await client.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None);
                        }
                    }
                    catch (ObjectDisposedException)
                    {
                        Locker.EnterWriteLock();
                        try
                        {
                            Clients.Remove(client);
                            i--;
                        }
                        finally
                        {
                            Locker.ExitWriteLock();
                        }
                    }
                }
            }
        }

Und es hat funktioniert. Der Server hat die Verbindung selbst konfiguriert und den Antwortschlüssel generiert. Ich musste nicht einmal die Registrierung des Servers über SSL separat einrichten, es reichte aus, dass das Zertifikat bereits auf dem benötigten Port im System installiert war.

Auf der Seite des Geräts und der Webseite haben zwei Clients Nachrichten ausgetauscht, und alles wurde protokolliert. Keine riesigen Parser, die die Serverleistung bremsen – all das war nicht nötig. Die Antwortzeit wurde von 200 ms auf 40-30 ms verkürzt. Und ich kam zu der einzig richtigen Entscheidung.

Fortgeschrittene Fahrradkonstruktion oder client-server Anwendung basierend auf dem C# .Net Framework

Die aktuelle Implementierung des Servers auf TCP zu entfernen und alles auf HTTP umzuschreiben. Jetzt befindet sich das Projekt in der Phase der Neugestaltung, jedoch nach ganz anderen Interaktionsprinzipien. Die Arbeiten der Geräte und der Webseite sind synchronisiert und optimiert und folgen einem gemeinsamen Konzept, mit dem einzigen Unterschied, dass für die Geräte keine HTML-Seiten generiert werden müssen.

Fazit

„Wer den Fluss nicht kennt, sollte nicht ins Wasser gehen.“ Ich denke, ich hätte zu Beginn besser die Ziele und Aufgaben definieren sowie mich eingehender mit den erforderlichen Technologien und deren Umsetzung bei verschiedenen Kunden beschäftigen müssen. Das Projekt neigt sich dem Ende zu, aber möglicherweise werde ich zurückkehren, um zu berichten, wie ich wieder einige Dinge verkackt habe. Ich habe im Verlauf der Entwicklung viel gelernt, aber es gibt noch viel mehr zu lernen. Wenn Sie bis hierher gelesen haben, danke dafür.

Quelle: habr.com

Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen 🔥 Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen | ProHoster