Fortgeschrittene Fahrradentwicklung oder eine Client-Server-Anwendung auf Basis von C# .Net Framework

Einleitung

Alles begann damit, dass mir ein Kollege vorschlug, einen kleinen Web-Service zu erstellen. Das sollte eine Art Tinder für die IT-Community werden. Die Funktionalität ist sehr einfach: Man registriert sich, füllt sein Profil aus und kommt zum Wesentlichen, nämlich der Suche nach Gesprächspartnern, dem Ausbau seiner Kontakte und dem Knüpfen neuer Bekanntschaften.

Hier sollte ich einen Schritt zurücktreten und ein wenig über mich erzählen, damit später verständlicher wird, warum ich gerade solche Schritte in der Entwicklung unternommen habe.

Derzeit bin ich Technischer Künstler in einem Spieleentwicklungsstudio. Meine Programmiererfahrung in C# basiert hauptsächlich auf dem Schreiben von Skripten und Tools für Unity sowie der Erstellung von Plug-ins für die niedrige Ebene der Interaktion mit Android-Geräten. Darüber hinaus habe ich es noch nicht geschafft, diesen Rahmen zu verlassen, und nun bot sich mir diese Möglichkeit.

Teil 1. Prototyping des Rahmens

Nachdem ich entschieden hatte, wie dieser Service aussehen soll, begann ich nach Möglichkeiten zur Umsetzung zu suchen. Am einfachsten wäre es gewesen, eine fertige Lösung zu finden, auf die man unsere Mechaniken aufsetzen könnte, um alles der öffentlichen Kritik auszusetzen.
Aber das ist nicht interessant; ich sah keinen Reiz und keinen Sinn darin. Daher begann ich, mich mit Webtechnologien und den Methoden zu ihrer Interaktion zu beschäftigen.

Ich begann mit dem Lesen von Artikeln und Dokumentationen zu C# .Net. Hier fand ich verschiedene Ansätze zur Lösung des Problems. Es gibt zahlreiche Kommunikationsmechanismen, von vollständigen Lösungen wie ASP.Net oder Azure-Diensten bis hin zu direkten TCP- oder Http-Verbindungen.

Ich habe meine erste Versuche mit ASP sofort verworfen, denn ich hielt es für eine zu komplexe Lösung für unseren Service. 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 einem Http-Client-Server. Hier auf Habr stieß ich auf einen Artikel über einen mehrschichtigen Server, den ich zusammenstellte und testete; ich entschied mich, genau für die Interaktion mit TCP-Verbindungen, da ich der Meinung war, dass Http mir kein plattformübergreifendes Lösung ermöglichen würde.

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

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

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 Client-Handler selbst:

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 Nachricht = "";
            byte[] Puffer = new byte[1024];
            int Anzahl;
            while ((Anzahl = Client.GetStream().Read(Puffer, 0, Puffer.Length)) > 0)
            {
                Nachricht += Encoding.UTF8.GetString(Puffer, 0, Anzahl);

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

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

            string Dateipfad =

"D:\/Web\/TestSite{AnfrageUri}";

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

string Erweiterung = AnfrageUri.Substring(AnfrageUri.LastIndexOf('.'));

string Inhaltstyp = "";

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

"image\/{Erweiterung.Substring(1)}";
break;
default:
if (Erweiterung.Length > 1)
{
Inhaltstyp =

"application\/{Erweiterung.Substring(1)}";
}
else
{
Inhaltstyp = "application\/unknown";
}
break;
}

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

string Header =

"HTTP\/1.1 200 OKnContent-Type: {Inhaltstyp}nContent-Length: {FS.Length}nn";
byte[] HeaderPuffer = Encoding.ASCII.GetBytes(Header);
Client.GetStream().Write(HeaderPuffer, 0, HeaderPuffer.Length);

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

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


Und die erste auf local SQL basierende Datenbank:
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}");

"{item.login} {item.password}"
}
}
}
}
}


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

Kapitel 2. Anbau von Rädern

Nachdem ich die Arbeit des Servers getestet hatte, kam ich zu dem Schluss, dass dies eine großartige Lösung sein würde (Spoiler: nein), für unseren Dienst, daher begann das Projekt, Logik anzunehmen.
Schritt für Schritt entstanden neue Module und die Funktionalität des Servers wuchs. Der Server erhielt eine Testdomain und ssl Verschlüsselung der Verbindung.

Ein wenig mehr Code, der die Logik der Serverarbeit und die Verarbeitung von Clients beschreibt.
Die aktualisierte Server-Variante, die die Verwendung eines Zertifikats einschließt.

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("Wechsel 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();
        }
    }
}

Und auch 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|Authentifizierung fehlgeschlagen - Verbindung wird geschlossen.n" +
"---------------------------------------------------------------------n");
SSlClientStream.Close();
Client.Close();
}
new RequestContext(SSlClientStream, Client);
}

}
}


Da der Server ausschließlich über TCP-Verbindungen arbeitet, muss ein Modul erstellt werden, das den Kontext der Anfrage erkennen kann. Ich habe entschieden, dass 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("{DateTime.Now:g} Client IP:{TcpClient.Client.RemoteEndPoint}
{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;
{
if (HeadersValues.Any(x => x.Name.Contains("Cookie")))
try
{
cookie = HeadersValues.FirstOrDefault(x => x.Name.Contains("Cookie")).Value;
{
CurrentUser = databaseWorker.CookieValidate(cookie);
try
{
catch { }
}
private List HeaderValues()
}
}
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 output = Message.Trim('\n').Split().Last();
{
var parse = Regex.Matches(Message, @"(.*?): (.*?)\n");
var parse = Regex.Matches(output, @"([^&].*?)=([^&]*)");
Value = match.Groups[2].Value.Trim().Replace('+', ' ')
values.Add(new RequestValues()
{
Name = match.Groups[1].Value.Trim(),
{
Value = match.Groups[2].Value.Trim()
Wert = match.Groups[2].Value.Trim().Replace('+', ' ')
});
}
private void ProfileParse()
}
}
}


Der Kern besteht darin, dass die Anfrage mithilfe von regulären Ausdrücken in Teile zerlegt wird. Wir erhalten eine Nachricht vom Kunden, extrahieren die erste Zeile, die die Methode und die URL der Anfrage enthält. Dann lesen wir die Header, die wir in ein Array im Format HeaderName=Inhalt packen, und finden auch begleitende Inhalte (zum Beispiel den Query-String), die wir ebenfalls in ein ähnliches Array packen. Zudem stellt der Parser fest, ob der aktuelle Kunde autorisiert ist und speichert dessen Daten. Alle Anfragen von autorisierten Kunden enthalten einen Autorisierungs-Hash, der in den Cookies gespeichert wird, wodurch wir die weitere Logik der Bearbeitung für zwei Arten von Kunden trennen und korrekte Antworten geben können.

Und eine kleine, nette Funktion, die es wert wäre, in ein separates Modul ausgegliedert zu werden, ist die Umwandlung von Anfragen wie „site.com/@UserName“ in dynamisch generierte Benutzerseiten. Nach der Bearbeitung der Anfrage kommen die folgenden Module zum Einsatz.

Kapitel 3. Das Lenkrad installieren, die Kette schmieren

Sobald der Parser abgeschlossen hat, tritt der Handler in Aktion, der dem Server weitere Anweisungen erteilt und die Kontrolle 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 genommen nur eine Überprüfung der Benutzerautorisierung, nach der die Anfragebearbeitung beginnt.

Klientencontroller
Wenn der Benutzer nicht autorisiert ist, basiert die Funktionalität für ihn nur auf der Anzeige von Benutzerprofilen und dem Registrierungs-/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 =

"HTTP/1.1 200 OK\nContent-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 sollte der Benutzer auch irgendeinen Inhalt der Seiten erhalten, deshalb gibt es folgendes Modul, das für die Antwort auf 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);
}
private List HeaderValues()
}

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 und die Profile anderer Benutzer anzuzeigen, habe ich beschlossen, RazorEngine, genauer gesagt einen Teil davon, zu verwenden. Es umfasst auch die Bearbeitung ungültiger 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();
            }
        }
    }
}

Und natürlich ist für die Überprüfung der autorisierten Benutzer die Authentifizierung erforderlich. Das Authentifizierungmodul interagiert mit der Datenbank. Die aus den Formularen auf der Website erhaltenen Daten werden aus dem Kontext geparst, der Benutzer wird gespeichert und erhält im Gegenzug Cookies und Zugang zum Service.

Authentifizierungmodul

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\nLocation: /@{user.login}\nSet-Cookie: {cookies.AuthCookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnly\n\n");
            }

ClientStream.BeginWrite(response, 0, response.Length, WriteController.OnClientSend, null);
RazorController.ErrorLoader(401);

}
else
{
private void Registration()

}
}

var values = Context.FormValues;
{
var user = new User()
name = values[0].Value,
{
login = values[1].Value,
password = PasswordHasher.PasswordHash(values[2].Value),
cookies = new UserCookies(user.login, user.password);
};
user.cookie = cookies.AuthCookie;
if (DatabaseWorker.LoginValidate(user.login))
Console.WriteLine("User ready");
{
Console.WriteLine(
"{user.password} {user.password.Trim().Length}");

DatabaseWorker.UserRegister(user);
var response = Encoding.UTF8.GetBytes("HTTP/1.1 301 Moved Permanently\nLocation: /@{user.login}\nSet-Cookie: {user.cookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnly\n\n");
var response = Encoding.UTF8.GetBytes(

quot;HTTP/1.1 301 Permanently movednLocation: \/@{user.login}nSet-Cookie: {user.cookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnlynn");
RazorController.ErrorLoader(401);
}
else
{
private void Registration()
}
}
}
}


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");

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("Login existiert bereits");
{
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");
"{user.password} {user.password.Trim().Length}");

public User CookieValidate(string CookieInput)
Console.WriteLine(item.login + "n");
{
catch (Exception e)
}
}
User user = null;
{
user = users.SingleOrDefault(x => x.cookie == CookieInput);
try
{
catch
}
return null;
{
if (user != null) return user;
}
else return null;
public User FindUser(string login)
}
user = users.Single(x => x.login.ToLower() == login.ToLower());
{
user = users.SingleOrDefault(x => x.cookie == CookieInput);
try
{
if (user != null)
return user;
{
return user;
}
else
{
if (user != null) return user;
}
}
catch (Exception)
{
if (user != null) return user;
}
}
}
}


Und alles funktioniert wie am Schnürchen, die Authentifizierung und Registrierung funktionieren, die grundlegenden Zugriffsrechte auf den Service sind bereits vorhanden und es ist an der Zeit, die Anwendung zu schreiben und all das mit den Hauptfunktionen zu verbinden, wegen derer das Ganze gemacht wird.

Kapitel 4. Das Rad neu erfinden

Um den Arbeitsaufwand für die Erstellung von zwei Anwendungen für zwei Plattformen zu reduzieren, habe ich mich entschieden, eine plattformübergreifende Lösung mit Xamarin.Forms zu entwickeln. Wieder einmal dank der Tatsache, dass es in C# geschrieben ist. Nachdem ich eine Testanwendung erstellt hatte, die einfach Daten an den Server sendet, stieß ich auf einen interessanten Aspekt. Für die Anfrage vom Gerät habe ich aus Neugierde HttpClient implementiert und eine HttpRequestMessage an den Server gesendet, in der die Daten aus dem Autorisierungsformular im JSON-Format enthalten sind. Ohne große Erwartungen öffnete ich das Serverprotokoll und sah die Anfrage vom Gerät mit allen Daten. Ein kurzer Schockmoment, das Bewusstsein für alles, was in den letzten drei Wochen an langen Abenden erledigt wurde. Um die Richtigkeit der gesendeten Daten zu überprüfen, stellte ich einen Testserver auf HttpListener zusammen. Nachdem ich eine weitere Anfrage auf diesem erhalten hatte, zerlegte ich sie in ein paar Zeilen Code, um die Daten im KeyValuePair-Format aus dem Formular zu extrahieren. Die Analyse der Anfrage reduzierte sich auf zwei Zeilen.

Ich begann, weiter zu testen. Es wurde zuvor nicht erwähnt, aber auf dem vorherigen Server hatte ich auch einen Chat implementiert, der auf WebSockets basierte. Er funktionierte ziemlich gut, aber das Prinzip der Interaktion über TCP war frustrierend, da viel Überflüssiges erzeugt werden musste, um die Interaktion zwischen zwei Benutzern mit Protokollführung zu organisieren. Sowohl das Parsen der Anfrage zur Verbindungsumschaltung als auch das Zusammenstellen der Antwort gemäß dem RFC 6455-Protokoll waren problematisch. Daher beschloss ich, auf dem Testserver eine einfache WebSocket-Verbindung zu erstellen. Einfach aus Interesse.

Verbindung zum Chat

 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 eingerichtet und den Antwortschlüssel generiert. Ich musste das Serversystem für SSL nicht separat konfigurieren; es genügte, dass in der Systemumgebung bereits ein Zertifikat auf dem entsprechenden Port installiert war.

Auf der Geräte- und auf der Website-Seite haben zwei Clients Nachrichten ausgetauscht, und alles wurde protokolliert. Keine riesigen Parser, die den Server verlangsamen, waren erforderlich. Die Antwortzeit wurde von 200 ms auf 40-30 ms verkürzt. Und ich kam zu einer einzigen richtigen Lösung.

Fortgeschrittene Fahrradentwicklung oder eine Client-Server-Anwendung auf Basis von C# .Net Framework

Die aktuelle Serverimplementierung über Tcp abzulehnen und alles auf Http umzustellen. Jetzt befindet sich das Projekt in der Umplanungsphase, jedoch nach ganz anderen Interaktionsprinzipien. Die Arbeit der Geräte und der Website ist synchronisiert und getestet und hat ein gemeinsames Konzept, mit der einzigen Ausnahme, dass für die Geräte keine HTML-Seiten generiert werden müssen.

Ausgabe

„Unbekanntes Gewässer, nicht hineinwagt.“ Ich denke, bevor ich mit der Arbeit begann, hätte ich die Ziele und Aufgaben klarer definieren und mich intensiver mit den erforderlichen Technologien und deren Umsetzung bei verschiedenen Clients beschäftigen sollen. Das Projekt steht kurz vor dem Abschluss, aber möglicherweise werde ich zurückkommen, um zu berichten, wie ich wieder bestimmte Dinge vermasselt habe. Ich habe viel im Verlauf der Entwicklung gelernt, aber es gibt noch viel mehr zu lernen. Wenn Sie bis zu diesem Punkt gelesen haben, danke dafür.

Quelle: habr.com

60GB SSD 8Gb DDR4