Création de vélos avancée ou application client / serveur basée sur le framework C # .Net

introduction



Tout a commencé lorsqu'un collègue m'a suggéré de créer un petit service Web. C'était censé être quelque chose comme un amadou, mais pour la foule informatique. La fonctionnalité est extrêmement simple, vous vous inscrivez, remplissez un profil et allez au point principal, à savoir, trouver un interlocuteur et élargir vos connexions et faire de nouvelles connaissances.

Ici, je dois faire une digression et parler un peu de moi-même, de sorte qu'à l'avenir, il soit plus clair pourquoi j'ai pris de telles mesures dans le développement.



Pour le moment, j'occupe le poste d'artiste technique dans un studio de jeu, mon expérience en programmation C # était basée uniquement sur l'écriture de scripts et d'utilitaires pour Unity et, en plus de cela, la création de plugins pour un travail de bas niveau avec des appareils Android. Je ne suis pas encore sorti de ce petit monde, puis une telle opportunité s'est présentée.

Partie 1. Prototypage du cadre



Après avoir décidé à quoi ressemblerait ce service, j'ai commencé à chercher des options de mise en œuvre. Le moyen le plus simple serait de trouver une sorte de solution toute faite qui, comme un hibou sur un globe, peut être tirée par nos mécaniciens et mettre le tout sous la censure du public.

Mais ce n'est pas intéressant, je n'y voyais aucun défi ni aucun sens, et j'ai donc commencé à étudier les technologies web et les méthodes d'interaction avec elles.



J'ai commencé par regarder des articles et de la documentation C # .Net. Ici, j'ai trouvé une variété de façons d'accomplir la tâche. Il existe de nombreux mécanismes d'interaction avec le réseau, des solutions complètes telles que les services ASP.Net ou Azure, à l'interaction directe avec les connexions Tcp \ Http.



Ayant fait la première tentative avec ASP, je l'ai immédiatement écartée, à mon avis c'était une décision trop difficile pour notre service. Nous n'utiliserons même pas un tiers des capacités de cette plateforme, j'ai donc continué à chercher. Le choix s'est fait entre TCP et Http client-serveur. Ici, sur Habré, je suis tombé sur un article sur un serveur multithread , après avoir collecté et testé lequel, j'ai décidé de me concentrer sur l'interaction avec les connexions TCP, pour une raison quelconque je pensais que http ne me permettrait pas de créer une solution multiplateforme.



La première version du serveur comprenait la gestion des connexions, la diffusion de contenu statique sur des pages Web et l'inclusion d'une base de données d'utilisateurs. Et pour commencer, j'ai décidé de créer des fonctionnalités pour travailler avec le site, afin que plus tard le traitement de l'application sur Android et iOS soit joint ici.



Voici du code
, :



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


:



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("\r\n\r\n") >= 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}";

            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 = $"HTTP/1.1 200 OK\nContent-Type: {ContentType}\nContent-Length: {FS.Length}\n\n";
            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();
        }
    }
}


local SQL:



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($"{item.login} {item.password}");
                }
            }
        }
    }
}


, , . ( , - ).



Chapitre 2. Serrage des roues



Après avoir testé le fonctionnement du serveur, je suis arrivé à la conclusion que ce serait une excellente solution ( spoiler: non ) pour notre service, donc le projet a commencé à acquérir de la logique.

Pas à pas, de nouveaux modules ont commencé à apparaître et les fonctionnalités du serveur se sont développées. Le serveur a un domaine de test et un cryptage SSL de la connexion.



Un peu plus de code décrivant la logique de gestion du serveur et du client
, .



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:\ssl\itinder.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();
        }
    }
}


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" +
                    $"|{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);
        }

    }
}






Mais comme le serveur fonctionne exclusivement sur une connexion TCP, il est nécessaire de créer un module capable de reconnaître le contexte de la requête. J'ai décidé qu'un analyseur serait approprié ici, qui diviserait la demande du client en parties distinctes, avec lesquelles je peux interagir afin de donner au client les réponses nécessaires.



Analyseur
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($"\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;
        }
    }
}




Son essence réside dans le fait que l'utilisation d'expressions régulières pour diviser la demande en plusieurs parties. Nous recevons un message du client, sélectionnez la première ligne, qui contient la méthode et l'url de la demande. Ensuite, nous lisons les en-têtes, que nous conduisons dans un tableau de la forme HeaderName = Content, et trouvons également, le cas échéant, le contenu d'accompagnement (par exemple, querystring), que nous conduisons également dans un tableau similaire. De plus, l'analyseur détecte si le client actuel est autorisé et sauvegarde ses données. Toutes les demandes des clients autorisés contiennent un hachage d'autorisation, qui est stocké dans des cookies, de sorte que vous pouvez séparer la logique de travail supplémentaire pour deux types de clients et leur donner les bonnes réponses.



Eh bien, et une petite fonctionnalité intéressante qui devrait être retirée dans un module séparé, la conversion de requêtes comme "site.com/@UserName" en pages utilisateur générées dynamiquement. Après avoir traité la demande, les modules suivants entrent en jeu.



Chapitre 3. Installation du guidon, lubrification de la chaîne



Dès que l'analyseur a fini de fonctionner, le gestionnaire entre en jeu, donnant des instructions supplémentaires au serveur et divisant le contrôle en deux parties.



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




En fait, il n'y a qu'un seul contrôle pour l'autorisation de l'utilisateur, après quoi le traitement de la demande commence.



Contrôleurs clients
, \. , .



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;

            }

        }

    }
}




, , , .



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}\n\n";
                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 OK\nContent-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();
            }
        }
    }




RazorEngine, . .



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






Et bien sûr, pour que la vérification des utilisateurs autorisés fonctionne, vous avez besoin d'une autorisation. Le module d'autorisation interagit avec la base de données. Les données reçues des formulaires sur le site sont analysées à partir du contexte, l'utilisateur est sauvegardé et en retour reçoit des cookies et l'accès au service.



Module d'autorisation
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);


            }
            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($"{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");
                ClientStream.BeginWrite(response, 0, response.Length, WriteController.OnClientSend, null);
            }
            else
            {
                RazorController.ErrorLoader(401);
            }
        }
    }
}




Et voici à quoi ressemble le traitement de la base de données:



Base de données
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($"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($"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;
            }
        }
    }
}





Et tout fonctionne comme sur des roulettes, l'autorisation et l'enregistrement fonctionne, la fonctionnalité minimale d'accès au service est déjà disponible, et il est temps d'écrire une application et de lier le tout aux principales fonctions pour lesquelles tout est fait.



Chapitre 4. Jeter le vélo



Afin de réduire les coûts de main-d'œuvre pour l'écriture de deux applications pour deux plates-formes, j'ai décidé de créer une plate-forme multiplateforme sur Xamarin.Forms. Encore une fois, en raison du fait qu'il est en C #. Après avoir créé une application de test qui envoie simplement des données au serveur, je suis tombé sur un point intéressant. Pour une demande de l'appareil, par intérêt, je l'ai implémenté sur HttpClient et l'ai jeté sur le serveur HttpRequestMessage, qui contient les données du formulaire d'autorisation au format json. Sans rien attendre, j'ai ouvert le journal du serveur et j'ai vu une demande de l'appareil avec toutes les données. Une légère stupeur, la réalisation de tout ce qui a été fait au cours des 3 dernières semaines d'une soirée langoureuse. Pour vérifier l'exactitude des données envoyées, j'ai collecté un serveur de test sur HttpListner. Ayant déjà reçu la demande suivante, en quelques lignes de code, je l'ai analysée en plusieurs parties, j'ai reçu la KeyValuePair de données du formulaire.L'analyse de la requête a été réduite à deux lignes.



J'ai commencé à tester davantage, cela n'a pas été mentionné plus tôt, mais sur le serveur précédent, j'implémentais toujours un chat basé sur des websockets. Cela fonctionnait plutôt bien, mais le principe même de l'interaction via Tcp était déprimant, il fallait produire trop de superflu pour construire correctement l'interaction de deux utilisateurs avec la maintenance d'un journal de correspondance. Il s'agit d'analyser une demande de commutateur de connexion et de collecter une réponse à l'aide du protocole RFC 6455. Par conséquent, dans le serveur de test, j'ai décidé de créer une simple connexion Websocket. Purement pour le plaisir.



Connectez-vous au 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<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();
                        }
                    }
                }
            }
        }






Et ça a marché. Le serveur a établi la connexion lui-même, a généré une clé de réponse. Je n'ai même pas eu à configurer séparément l'enregistrement du serveur via ssl, il suffisait que le certificat soit déjà installé dans le système sur le port requis.



Côté appareil et côté site, deux clients ont échangé des messages, tout cela a été consigné. Aucun analyseur énorme ne ralentissant le serveur, rien de tout cela n'était nécessaire. Le temps de réponse est passé de 200 ms à 40-30 ms. Et je suis arrivé à la seule décision correcte.



Lancez l'implémentation actuelle du serveur vers Tcp et réécrivez tout sous Http. Le projet est maintenant au stade de la refonte, mais déjà selon des principes d'interaction complètement différents. Le fonctionnement des appareils et du site est synchronisé et débogué et a un concept commun, à la seule différence que vous n'avez pas besoin de générer des pages html pour les appareils.



Production



«Ne connaissant pas le gué, ne vous mettez pas la tête dans l'eau» Je pense qu'avant de commencer le travail, j'aurais dû définir plus clairement les buts et objectifs, ainsi que me plonger dans l'étude des technologies nécessaires et des méthodes de leur mise en œuvre sur différents clients. Le projet est déjà en voie d'achèvement, mais je reviendrai peut-être pour parler de la façon dont j'ai récupéré des choses. J'ai beaucoup appris au cours du processus de développement, mais il y a encore plus à apprendre à l'avenir. Si vous avez lu jusqu'ici, merci pour cela.



All Articles