Erweiterte Cycling- oder Client-Server-Anwendung basierend auf dem C# .Net-Framework

Eintrag

Alles begann, als ein Kollege mir vorschlug, einen kleinen Webdienst zu erstellen. Es sollte so etwas wie ein Tinder sein, aber für einen IT-Hangout. Die Funktionalität ist denkbar einfach: Sie registrieren sich, füllen ein Profil aus und gehen zum Hauptpunkt über, nämlich einen Gesprächspartner zu finden, Ihre Kontakte zu erweitern und neue Bekanntschaften zu schließen.

Hier muss ich abschweifen und ein wenig über mich erzählen, damit in Zukunft klarer wird, warum ich solche Entwicklungsschritte gegangen bin.

Im Moment, in dem ich die Position eines technischen Künstlers in einem Spielestudio innehabe, basierte meine C#-Programmiererfahrung nur auf dem Schreiben von Skripten und Dienstprogrammen für Unity und darüber hinaus auf der Erstellung von Plugins für die Arbeit auf niedriger Ebene mit Android-Geräten. Außerhalb dieser Welt habe ich mich noch nicht für eine solche Gelegenheit entschieden und sie dann auch nicht genutzt.

Teil 1. Frame-Prototyping

Nachdem ich entschieden hatte, wie dieser Service aussehen soll, begann ich, nach Optionen für die Implementierung zu suchen. Der einfachste Weg wäre, eine Art fertige Lösung zu finden, an der man, wie eine Eule auf einem Globus, unsere Mechaniker ziehen und das Ganze zur öffentlichen Kritik hinlegen kann.
Aber das ist nicht interessant, ich sah darin weder eine Herausforderung noch einen Sinn, und deshalb begann ich, Webtechnologien und Methoden zur Interaktion mit ihnen zu studieren.

Die Studie begann mit der Betrachtung von Artikeln und Dokumentationen zu C # .Net. Hier habe ich verschiedene Möglichkeiten gefunden, die Aufgabe zu lösen. Es gibt viele Mechanismen für die Interaktion mit dem Netzwerk, von vollwertigen Lösungen wie ASP.Net oder Azure-Diensten bis hin zur direkten Interaktion mit TcpHttp-Verbindungen.

Nachdem ich den ersten Versuch mit ASP gemacht hatte, habe ich ihn sofort abgebrochen, meiner Meinung nach war es eine zu schwierige Entscheidung für unseren Service. Wir werden nicht einmal ein Drittel der Möglichkeiten dieser Plattform nutzen, also habe ich meine Suche fortgesetzt. Es gab die Wahl zwischen TCP und HTTP-Client-Server. Hier, auf Habré, bin ich auf einen Artikel darüber gestoßen Multithread-ServerNachdem ich diese gesammelt und getestet hatte, beschloss ich, mich auf die Interaktion mit TCP-Verbindungen zu konzentrieren. Aus irgendeinem Grund dachte ich, dass http es mir nicht ermöglichen würde, eine plattformübergreifende Lösung zu erstellen.

Die erste Version des Servers umfasste die Verwaltung von Verbindungen, die Bereitstellung statischer Webseiteninhalte und die Integration einer Benutzerdatenbank. Und für den Anfang habe ich beschlossen, eine Funktionalität für die Arbeit mit der Site zu erstellen, damit ich später die Anwendungsverarbeitung auf Android und iOS hier verknüpfen kann.

Hier ist ein Code
Der Hauptthread, der Clients in einer Endlosschleife 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 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 =

quot;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 =


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


quot;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 lokalem 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<User> users = db.GetTable<User>();
                foreach (var item in users)
                {
                    Console.WriteLine(

quot;{item.login} {item.password}");
}
}
}
}
}

Wie Sie sehen, unterscheidet sich diese Version kaum von der im Artikel. Tatsächlich wurde hier nur das Laden von Seiten aus einem Ordner auf dem Computer und der Datenbank hinzugefügt (was in dieser Version übrigens aufgrund der falschen Verbindungsarchitektur nicht funktionierte).

Kapitel 2

Nachdem ich den Server getestet hatte, kam ich zu dem Schluss, dass dies eine großartige Lösung wäre (Spoiler: Nein), für unseren Dienst, so begann das Projekt Logik zu erlangen.
Nach und nach kamen neue Module hinzu und die Funktionalität des Servers wuchs. Der Server verfügt über eine Testdomäne und eine SSL-Verbindungsverschlüsselung.

Etwas mehr Code, der die Logik des Servers und die Verarbeitung von Clients beschreibt
Eine aktualisierte Version des Servers, einschließlich der Verwendung eines Zertifikats.

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("Starting server.." + 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("Switching another 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();
        }
    }
}

Sowie ein neuer Client-Handler mit Autorisierung per SSL:

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" +


quot;|{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 Anforderungskontext erkennen kann. Ich habe entschieden, dass hier ein Parser geeignet ist, der die Anfrage des Clients in einzelne Teile aufteilt, mit denen ich interagieren kann, um dem Client die notwendigen 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<RequestValues> HeadersValues;
        public List<RequestValues> FormValues;
        private TcpClient TcpClient;

        private event Action<SslStream, RequestContext> 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(

quot;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<RequestValues> HeaderValues()
{
var values = new List<RequestValues>();
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<RequestValues> ContentValues()
{
var values = new List<RequestValues>();
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;
}
}
}

Sein Kern liegt darin, die Anfrage mit Hilfe regulärer Ausdrücke in Teile zu zerlegen. Wir erhalten eine Nachricht vom Client. Wählen Sie die erste Zeile aus, die die Methode und die Anforderungs-URL enthält. Dann lesen wir die Header, die wir in ein Array der Form HeaderName = Content schreiben, und suchen, falls vorhanden, auch den begleitenden Inhalt (z. B. Querystring), den wir ebenfalls in ein ähnliches Array schreiben. Darüber hinaus stellt der Parser fest, ob der aktuelle Client berechtigt ist und speichert seine Daten. Alle Anfragen von autorisierten Kunden enthalten einen Autorisierungs-Hash, der in Cookies gespeichert wird, wodurch es möglich ist, die weitere Arbeitslogik für zwei Arten von Kunden zu trennen und ihnen die richtigen Antworten zu geben.

Nun, eine kleine, nette Funktion, die in ein separates Modul verschoben werden sollte und Anfragen wie „site.com/@UserName“ in dynamisch generierte Benutzerseiten umwandelt. Nach der Bearbeitung der Anfrage kommen folgende Module ins Spiel.

Kapitel 3. Lenker montieren, Kette schmieren

Sobald der Parser fertig ist, kommt der Handler ins Spiel, der dem Server weitere Anweisungen gibt und die Kontrolle in zwei Teile aufteilt.

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

Tatsächlich erfolgt nur eine Prüfung der Benutzerberechtigung, danach beginnt die Bearbeitung der Anfrage.

Client-Controller
Wenn der Benutzer nicht autorisiert ist, basiert die Funktionalität für ihn nur auf der Anzeige von Benutzerprofilen und dem Fenster zur Autorisierungsregistrierung. Der Code für einen autorisierten Benutzer sieht ungefähr gleich aus, daher sehe ich keinen Grund, ihn zu duplizieren.

Unbefugter 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;

}

}

}
}

Und natürlich muss der Benutzer einige Inhalte der Seiten erhalten, daher gibt es für Antworten das folgende Modul, das für die Beantwortung einer Ressourcenanfrage 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 =

quot;{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


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


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


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


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


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


quot;{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 jedoch sein Profil und die Profile anderer Benutzer anzuzeigen, habe ich mich für die Verwendung von RazorEngine bzw. einem Teil davon entschieden. Dazu gehört auch die Bearbeitung fehlerhafter 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 damit die Überprüfung autorisierter Benutzer funktioniert, ist natürlich eine Autorisierung erforderlich. Das Autorisierungsmodul interagiert mit der Datenbank. Die von den Formularen auf der Website empfangenen Daten werden aus dem Kontext analysiert, der Benutzer wird gespeichert und erhält im Gegenzug Cookies und Zugriff auf den Dienst.

Autorisierungsmodul

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(

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

}
else
{
RazorController.ErrorLoader(401);

}
}

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(


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


quot;HTTP/1.1 301 Moved PermanentlynLocation: /@{user.login}nSet-Cookie: {user.cookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnlynn");
ClientStream.BeginWrite(response, 0, response.Length, WriteController.OnClientSend, null);
}
else
{
RazorController.ErrorLoader(401);
}
}
}
}

Und so sieht die Datenbank aus:

Datenbank

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

namespace ClearServer
{
    class DatabaseWorker
    {

        private readonly Table<User> users = null;
        private readonly DataContext DataBase = null;
        private const string connectionStr = @"путькбазе";

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

        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(

quot;User{user.name} with id {user.uid} added");
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 already exists");
return false;
}
return true;
}
public void UserUpdate(User user)
{
var UserToUpdate = users.FirstOrDefault(x => x.uid == user.uid);
UserToUpdate = user;
DataBase.SubmitChanges();
Console.WriteLine(


quot;User {UserToUpdate.name} with id {UserToUpdate.uid} updated");
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 wie am Schnürchen, Autorisierungs- und Registrierungsarbeit, die Mindestfunktionalität des Zugriffs auf den Dienst ist bereits vorhanden, und es ist Zeit, einen Antrag zu schreiben und das Ganze mit den Hauptfunktionen zu verknüpfen, für die alles erledigt ist.

Kapitel 4

Um die Arbeitskosten für das Schreiben von zwei Anwendungen für zwei Plattformen zu senken, habe ich beschlossen, eine plattformübergreifende Version auf Xamarin.Forms zu erstellen. Auch hier dank der Tatsache, dass es in C# ist. Nachdem ich eine Testanwendung erstellt hatte, die einfach Daten an den Server sendet, stieß ich auf einen interessanten Moment. Für eine Anfrage vom Gerät habe ich es zum Spaß auf HttpClient implementiert und auf den Server HttpRequestMessage geworfen, der Daten aus dem Autorisierungsformular im JSON-Format enthält. Ohne etwas Besonderes zu erwarten, öffnete ich das Serverprotokoll und sah dort eine Anfrage des Geräts mit allen Daten. Leichte Benommenheit, Bewusstsein für alles, was in den letzten 3 Wochen des trägen Abends getan wurde. Um die Richtigkeit der gesendeten Daten zu überprüfen, habe ich einen Testserver auf HttpListner zusammengestellt. Nachdem ich bereits die nächste Anfrage erhalten hatte, zerlegte ich sie in ein paar Codezeilen und holte die KeyValuePair-Daten aus dem Formular. Das Parsen der Abfrage wurde auf zwei Zeilen reduziert.

Ich habe mit weiteren Tests begonnen, es wurde vorher nicht erwähnt, aber auf dem vorherigen Server habe ich immer noch einen auf Websockets basierenden Chat implementiert. Es funktionierte ganz gut, aber das eigentliche Prinzip der Interaktion über Tcp war deprimierend, es musste zu viel Extra produziert werden, um die Interaktion zweier Benutzer mit der Protokollierung der Korrespondenz korrekt aufzubauen. Dazu gehört das Parsen einer Anfrage zum Verbindungswechsel und das Sammeln einer Antwort mithilfe des RFC 6455-Protokolls. Daher habe ich mich entschieden, auf dem Testserver eine einfache Websocket-Verbindung zu erstellen. Rein aus Interesse.

Chat-Verbindung

 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<byte>(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 selbst hat die Verbindung aufgebaut und einen Antwortschlüssel generiert. Ich musste nicht einmal die Serverregistrierung per SSL separat konfigurieren, es reicht aus, dass das System bereits ein Zertifikat auf dem erforderlichen Port installiert hat.

Auf der Geräteseite und auf der Site-Seite tauschten zwei Clients Nachrichten aus, dies alles wurde protokolliert. Keine riesigen Parser, die den Server verlangsamen, nichts davon war erforderlich. Die Reaktionszeit wurde von 200 ms auf 40-30 ms reduziert. Und ich habe die einzig richtige Entscheidung getroffen.

Erweiterte Cycling- oder Client-Server-Anwendung basierend auf dem C# .Net-Framework

Werfen Sie die aktuelle Serverimplementierung auf Tcp weg und schreiben Sie alles unter Http neu. Nun befindet sich das Projekt in der Phase der Neugestaltung, allerdings nach völlig anderen Interaktionsprinzipien. Der Betrieb von Geräten und der Site ist synchronisiert und debuggt und hat ein gemeinsames Konzept, mit dem einzigen Unterschied, dass Geräte keine HTML-Seiten generieren müssen.

Abschluss

„Wenn du die Furt nicht kennst, stecke deinen Kopf nicht ins Wasser“ Ich denke, bevor ich mit der Arbeit beginne, hätte ich die Ziele und Zielsetzungen klarer definieren und mich mit der Untersuchung der notwendigen Technologien und Methoden für deren Implementierung bei verschiedenen Kunden befassen sollen. Das Projekt nähert sich bereits dem Abschluss, aber vielleicht komme ich noch einmal zurück, um darüber zu sprechen, wie ich wieder einige Dinge vermasselt habe. Ich habe während des Entwicklungsprozesses viel gelernt, aber in Zukunft gibt es noch mehr zu lernen. Wenn Sie bis hierhin gelesen haben, vielen Dank fürs Lesen.

Source: habr.com