Construcción avanzada de bicicletas o aplicación cliente / servidor basada en el marco C # .Net

Introducción



Todo comenzó cuando un colega me sugirió que creara un pequeño servicio web. Se suponía que iba a ser algo así como una yesca, pero para la gente de TI. La funcionalidad es extremadamente simple, te registras, llenas un perfil y vas al punto principal, es decir, encontrar un interlocutor y expandir tus conexiones y conocer nuevos conocidos.

Aquí debo divagar y contar un poco sobre mí, para que en el futuro quede más claro por qué tomé tales pasos en el desarrollo.



En este momento, ocupo el puesto de Artista Técnico en un estudio de juegos, mi experiencia en la programación C # se basó solo en escribir scripts y utilidades para Unity y, además, crear complementos para trabajos de bajo nivel con dispositivos Android. Todavía no he salido de este pequeño mundo, y entonces apareció esa oportunidad.

Parte 1. Prototipos del marco



Habiendo decidido cómo sería este servicio, comencé a buscar opciones de implementación. La forma más fácil sería encontrar algún tipo de solución ya preparada, que, como un búho en un globo, pueda ser jalada por nuestros mecánicos y poner todo en censura pública.

Pero esto no es interesante, no vi ningún desafío y sentido en esto, y por eso comencé a estudiar tecnologías web y métodos de interacción con ellas.



Empecé mirando los artículos y la documentación de C # .Net. Aquí encontré varias formas de realizar la tarea. Existen muchos mecanismos para interactuar con la red, desde soluciones completas como ASP.Net o servicios de Azure, hasta la interacción directa con conexiones Tcp \ Http.



Habiendo hecho el primer intento con ASP, inmediatamente lo desestimé, en mi opinión era una decisión demasiado difícil para nuestro servicio. No usaremos ni un tercio de las capacidades de esta plataforma, así que seguí buscando. La elección vino entre TCP y Http cliente-servidor. Aquí, en Habré, me encontré con un artículo sobre un servidor multiproceso , habiendo recopilado y probado cuál, decidí centrarme en la interacción con las conexiones TCP, por alguna razón pensé que http no me permitiría crear una solución multiplataforma.



La primera versión del servidor incluía el manejo de conexiones, el servicio de contenido estático en páginas web y la inclusión de una base de datos de usuarios. Y para empezar, decidí construir un funcional para trabajar con el sitio, para que luego el procesamiento de la aplicación en android e ios se adjuntara aquí.



Aquí hay un código
, :



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


, , . ( , - ).



Capítulo 2. Apriete de las ruedas



Después de probar el funcionamiento del servidor, llegué a la conclusión de que esta sería una excelente solución ( spoiler: no ) para nuestro servicio, por lo que el proyecto comenzó a adquirir lógica.

Paso a paso, empezaron a aparecer nuevos módulos y se expandió la funcionalidad del servidor. El servidor tiene un dominio de prueba y un cifrado SSL de la conexión.



Un poco más de código que describe la lógica del servidor y el manejo del cliente.
, .



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

    }
}






Pero dado que el servidor trabaja exclusivamente en una conexión TCP, es necesario crear un módulo que pueda reconocer el contexto de la solicitud. Decidí que sería adecuado un analizador aquí, que dividirá la solicitud del cliente en partes separadas, con las que puedo interactuar para darle al cliente las respuestas necesarias.



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




Su esencia radica en el hecho de que utiliza expresiones regulares para dividir la solicitud en partes. Recibimos un mensaje del cliente, seleccionamos la primera línea, que contiene el método y la url de la solicitud. Luego leemos los encabezados, que manejamos en una matriz de la forma HeaderName = Content, y también encontramos, si lo hay, contenido adjunto (por ejemplo, cadena de consulta) que también manejamos en una matriz similar. Además, el analizador averigua si el cliente actual está autorizado y guarda sus datos. Todas las solicitudes de clientes autorizados contienen un hash de autorización, que se almacena en cookies, por lo que puede separar la lógica de trabajo adicional para dos tipos de clientes y darles las respuestas correctas.



Bueno, y una característica pequeña y agradable que debería sacarse en un módulo separado, convirtiendo solicitudes como "site.com/@UserName" en páginas de usuario generadas dinámicamente. Una vez procesada la solicitud, entran en juego los siguientes módulos.



Capítulo 3. Instalación del manillar, lubricación de la cadena



Tan pronto como el analizador ha terminado de funcionar, el controlador entra en juego, dando más instrucciones al servidor y dividiendo el control en dos partes.



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




De hecho, solo hay una verificación para la autorización del usuario, después de la cual comienza el procesamiento de la solicitud.



Controladores de clientes
, \. , .



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






Y, por supuesto, para que funcione la verificación de usuarios autorizados, necesita autorización. El módulo de autorización interactúa con la base de datos. Los datos recibidos de los formularios en el sitio se analizan a partir del contexto, el usuario se guarda y, a cambio, recibe cookies y acceso al servicio.



Módulo de autorización
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);
            }
        }
    }
}




Y así es como se ve el procesamiento de la base de datos:



Base de datos
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;
            }
        }
    }
}





Y todo funciona como un reloj, la autorización y el registro están funcionando, la funcionalidad mínima de acceso al servicio ya está disponible, y es el momento de escribir una aplicación y vincular todo con las funciones principales para las que se hace todo.



Capítulo 4. Tirar la bicicleta



Para reducir los costos laborales de escribir dos aplicaciones para dos plataformas, decidí hacer una multiplataforma en Xamarin.Forms. Nuevamente, debido al hecho de que está en C #. Habiendo creado una aplicación de prueba que simplemente envía datos al servidor, encontré un punto interesante. Para una solicitud del dispositivo, por interés, lo implementé en HttpClient y lo lancé al servidor HttpRequestMessage, que contiene datos del formulario de autorización en formato json. Sin esperar nada, abrí el registro del servidor y vi una solicitud del dispositivo con todos los datos. Un leve estupor, la realización de todo lo que se ha hecho en las últimas 3 semanas de una velada lánguida. Para verificar la exactitud de los datos enviados, recopilé un servidor de prueba en HttpListner. Habiendo recibido la siguiente solicitud ya en él, en un par de líneas de código lo analicé en partes, recibí el KeyValuePair de datos del formulario.El análisis de la consulta se redujo a dos líneas.



Comencé a probar más, no se mencionó anteriormente, pero en el servidor anterior todavía estaba implementando un chat construido en websockets. Funcionó bastante bien, pero el principio mismo de la interacción a través de Tcp era deprimente, había que producir demasiado superfluo para construir de manera competente la interacción de dos usuarios con el mantenimiento de un registro de correspondencia. Se trata de analizar una solicitud de un conmutador de conexión y recopilar una respuesta mediante el protocolo RFC 6455. Por lo tanto, en el servidor de prueba, decidí crear una conexión websocket simple. Puramente por diversión.



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






Y funcionó. El servidor configuró la conexión por sí mismo, generó una clave de respuesta. Ni siquiera tuve que configurar por separado el registro del servidor a través de ssl, fue suficiente que el certificado ya estuviera instalado en el puerto requerido del sistema.



En el lado del dispositivo y en el lado del sitio, dos clientes intercambiaron mensajes, todo esto fue registrado. No hay grandes analizadores que ralenticen el servidor, nada de esto fue necesario. El tiempo de respuesta ha bajado de 200 ms a 40-30 ms. Y llegué a la única decisión correcta.



Lanza la implementación actual del servidor a Tcp y reescribe todo bajo Http. Ahora el proyecto está en la etapa de rediseño, pero ya de acuerdo con principios de interacción completamente diferentes. El funcionamiento de los dispositivos y el sitio está sincronizado y depurado y tiene un concepto común, con la única diferencia de que no es necesario generar páginas html para dispositivos.



Salida



“Sin conocer el vado, no meter la cabeza en el agua” , creo que, antes de comenzar a trabajar, debería haber definido con mayor claridad las metas y objetivos, así como profundizar en el estudio de las tecnologías y métodos necesarios para su implementación en varios clientes. El proyecto ya está casi terminado, pero tal vez vuelva para hablar sobre cómo obtuve algunas cosas nuevamente. Aprendí mucho durante el proceso de desarrollo, pero aún queda mucho por aprender en el futuro. Si ha leído hasta aquí, gracias por eso.



All Articles