Construcción avanzada de bicicletas o aplicación cliente-servidor basada en C# .Net framework

Introducción

Todo comenzó cuando un colega me propuso crear un pequeño servicio web. Debería ser algo así como Tinder, pero para la comunidad de TI. La funcionalidad es extremadamente simple: te registras, completas tu perfil y pasas a la esencia principal, que es buscar un compañero de conversación, ampliar tus conexiones y hacer nuevos amigos.

Aquí debo hacer un paréntesis y contar un poco sobre mí para que más adelante se entienda por qué tomé estos pasos en el desarrollo.

En este momento, ocupo el puesto de Artista Técnico en un estudio de juegos. Mi experiencia en programación en C# se ha basado únicamente en la escritura de scripts y utilidades para Unity, además de la creación de plugins para trabajar a un nivel inferior con dispositivos Android. No he salido de este pequeño mundo hasta que se me presentó esta oportunidad.

Parte 1. Prototipado del marco

Decidido sobre lo que sería este servicio, comencé a buscar opciones para su implementación. La forma más sencilla habría sido encontrar una solución lista para usar sobre la cual, como un búho en un globo, pudiéramos aplicar nuestras mecánicas y presentar todo esto a la crítica pública.
Pero eso no es interesante; no vi ningún desafío ni sentido en ello, por lo que comencé a estudiar tecnologías web y métodos de interacción con ellas.

Empecé mi estudio viendo artículos y documentación sobre C# .Net. Aquí encontré diversas formas de abordar la tarea. Hay muchos mecanismos para interactuar con la red, desde soluciones completas como ASP.Net o servicios de Azure, hasta interacciones directas con conexiones TcpHttp.

Después de mi primer intento con ASP, lo descarté de inmediato, ya que, en mi opinión, era una solución demasiado pesada para nuestro servicio. No íbamos a utilizar ni la mitad de las capacidades de esta plataforma, así que continué buscando. La elección se redujo entre TCP y el cliente-servidor Http. Fue aquí, en Habré, donde encontré un artículo sobre servidor multihilo, y tras haberlo ensamblado y probado, decidí centrarme en la interacción con conexiones TCP, ya que consideré que HTTP no me permitiría crear una solución multiplataforma.

La primera versión del servidor incluía el manejo de conexiones, entregaba contenido estático de páginas web e incluía una base de datos de usuarios. Y para comenzar decidí construir la funcionalidad para trabajar con el sitio, para posteriormente integrar el manejo de la aplicación en Android y iOS.

Aquí hay un poco de código
El hilo principal, en un ciclo infinito aceptando clientes:

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

El propio manejador de clientes:

usando System;
usando System.IO;
usando System.Net.Sockets;
usando System.Text;
usando 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, @"^\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;
intentar
{
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();
}
}
}


Y la primera base de datos construida en SQL local:
usando System;
usando System.Data.Linq;
nombre del espacio ClearServer
{
    clase DatabaseWorker
    {

        private static DatabaseWorker instance;

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


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

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


Como se puede notar, esta versión difiere poco de la que se presentó en el artículo. En esencia, solo se ha agregado la carga de páginas desde una carpeta en el ordenador y la base de datos (que, por cierto, en esta versión no funcionó debido a la arquitectura incorrecta de conexión).

Capítulo 2. Instalación de ruedas

Después de probar el funcionamiento del servidor, llegué a la conclusión de que sería una gran solución (spoiler: no), para nuestro servicio, por lo que el proyecto comenzó a adquirir lógica.
Paso a paso comenzaron a aparecer nuevos módulos y la funcionalidad del servidor se expandía. El servidor adquirió un dominio de prueba y ssl cifrado de la conexión.

Un poco más de código, describiendo la lógica de funcionamiento del servidor y el manejo de los clientes
Versión actualizada del servidor, que incluye el uso de un certificado.

usando System;
usando System.Net;
usando System.Net.Sockets;
usando System.Reflection;
usando System.Security;
usando System.Security.Cryptography.X509Certificates;
usando System.Security.Permissions;
usando System.Security.Policy;
usando 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("Iniciando el servidor.." + 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("Cambiando a otro dominio");
                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();
        }
    }
}

Y también un nuevo manejador de cliente con autorización por ssl:

usando 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|Excepción: {e.Message}n|------------n|Autenticación fallida - cerrando la conexión.n" +
"---------------------------------------------------------------------n");
SSlClientStream.Close();
Client.Close();
}
new RequestContext(SSlClientStream, Client);
}

}
}


Pero dado que el servidor funciona exclusivamente en la conexión TCP, es necesario crear un módulo que pueda reconocer el contexto de la solicitud. Decidí que aquí sería adecuado un analizador que separara la solicitud del cliente en partes individuales, con las que puedo interactuar, para proporcionar las respuestas adecuadas al cliente.

Analizador

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

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

        private event Action OnRead = RequestHandler.OnHandle;

        DatabaseWorker databaseWorker = new DatabaseWorker();

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

            if (ar.IsCompleted)
            {
                Message = Encoding.UTF8.GetString(buffer);
                Message = Uri.UnescapeDataString(Message);
                Console.WriteLine("{DateTime.Now:g} Cliente 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;
{
if (HeadersValues.Any(x => x.Name.Contains("Cookie")))
intentar
{
cookie = HeadersValues.FirstOrDefault(x => x.Name.Contains("Cookie")).Value;
{
CurrentUser = databaseWorker.CookieValidate(cookie);
intentar
{
catch { }
}
private List HeaderValues()
}
}
private List HeaderValues()

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

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


La esencia de esto es utilizar expresiones regulares para dividir la solicitud en partes. Recibimos un mensaje del cliente, extraemos la primera línea que contiene el método y la URL de la solicitud. Luego leemos las cabeceras, que almacenamos en un arreglo del tipo NombreCabecera=Contenido, y también encontramos, si está presente, el contenido adicional (como el querystring) que también se guarda en un arreglo similar. Además, el parser determina si el cliente actual está autorizado y guarda sus datos. Todas las solicitudes de los clientes autorizados contienen un hash de autorización que se almacena en las cookies, lo que permite separar la lógica de trabajo adicional para los dos tipos de clientes y brindarles respuestas correctas.

Y una pequeña, agradable característica que valdría la pena separar en un módulo propio, es la transformación de solicitudes del tipo «site.com/@UserName» en páginas de usuario generadas dinámicamente. Después de procesar la solicitud, entran en juego los siguientes módulos.

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

Una vez que el parser ha trabajado, entra en acción el controlador, que da instrucciones adicionales al servidor y divide el control en dos partes.

Controlador 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 esencia, solo hay una verificación de autorización del usuario, después de lo cual comienza el procesamiento de la solicitud.

Controladores de clientes
Si el usuario no está autorizado, su funcionalidad se basa únicamente en mostrar perfiles de usuario y la ventana de registro/autorización. El código para el usuario autorizado se ve aproximadamente igual, por lo que no veo sentido en duplicarlo.

Usuario no autorizado

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

namespace ClearServer.Core.UserController
{
    clase interna NonAuthUserController
    {
        privado readonly SslStream ClientStream;
        privado readonly RequestContext Context;
        privado readonly WriteController WriteController;
        privado readonly AuthorizationController AuthorizationController;

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

}

}

}
}


Y, por supuesto, el usuario debe recibir algún contenido de las páginas, por lo que existe el siguiente módulo, responsable de responder a las solicitudes de recursos.

WriterController

usando System;
usando System.IO;
usando System.Net.Security;
usando 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);
}
private List HeaderValues()
}

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

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


Pero para mostrar al usuario su perfil y los perfiles de otros usuarios, decidí usar RazorEngine, más precisamente, una de sus partes. También incluye el manejo de solicitudes incorrectas y la emisión de un código de error correspondiente.

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, se necesita autorización. El módulo de autorización interactúa con la base de datos. Los datos obtenidos de los formularios en el sitio son analizados desde el contexto, el usuario se guarda y recibe a cambio 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);
}
}
}
}


Así es como se procesa la base de datos:

Base de datos

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

namespace ClearServer
{
    class DatabaseWorker
    {

        private readonly Table users = null;
        private readonly DataContext DataBase = null;
        private const string connectionStr = @"ruta a la base";

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

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

        }

        public void UserRegister(User user)
        {
            try
            {
                users.InsertOnSubmit(user);
                DataBase.SubmitChanges();
                Console.WriteLine("Usuario {user.name} con id {user.uid} añadido");
            }

Usuario {user.name} con id {user.uid} añadido
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("El login ya existe");
return false;
}
return true;
}
public void UserUpdate(User user)
{
var UserToUpdate = users.FirstOrDefault(x => x.uid == user.uid);
UserToUpdate = user;
DataBase.SubmitChanges();
Console.WriteLine(

Console.WriteLine("Usuario {UserToUpdate.name} con id {UserToUpdate.uid} actualizado");
foreach (var item in users)
{
Console.WriteLine(item.login + "n");
}
}
public User CookieValidate(string CookieInput)
{
User user = null;
intentar
{
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;
intentar
{
user = users.Single(x => x.login.ToLower() == login.ToLower());
if (user != null)
{
return user;
}
else
{
return null;
}
}
catch (Exception)
{
return null;
}
}
}
}


Y todo funciona a la perfección, la autorización y el registro están operativos, ya existe una funcionalidad mínima de acceso al servicio y ha llegado el momento de escribir la aplicación y vincular todo esto con las funciones principales para las cuales se realiza todo.

Capítulo 4. Desechando la bicicleta

Para reducir el esfuerzo en la escritura de dos aplicaciones para dos plataformas, decidí hacer una aplicación multiplataforma con Xamarin.Forms. Una vez más, gracias a que está basado en C#. Hice una aplicación de prueba que simplemente envía datos al servidor y me encontré con un momento interesante. Para la solicitud del dispositivo, implementé HttpClient por curiosidad y envié al servidor un HttpRequestMessage que contiene los datos del formulario de autorización en formato JSON. Sin esperar mucho, abrí el registro del servidor y vi allí la solicitud del dispositivo con todos los datos. Un ligero desconcierto, la realización de todo lo que había hecho en las últimas tres semanas durante las largas noches. Para verificar la validez de los datos enviados, monté un servidor de prueba en HttpListener. Al recibir otra solicitud, ya en él, descompuse el mensaje en partes con unas pocas líneas de código, obtuve un KeyValuePair de los datos del formulario. El análisis de la solicitud se redujo a dos líneas.

Comencé a probar más, no lo había mencionado antes, pero en el servidor anterior también implementé un chat basado en WebSockets. Funcionaba bastante bien, pero el principio de interacción a través de Tcp era desalentador, había que generar muchas cosas innecesarias para construir correctamente la interacción entre dos usuarios manteniendo un registro de la conversación. Esto incluía analizar la solicitud en busca de un cambio de conexión y recopilar la respuesta según el protocolo RFC 6455. Por lo tanto, en el servidor de prueba decidí crear una simple conexión WebSocket. Solo por curiosidad.

Conexión 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(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ó automáticamente la conexión, generó la clave de respuesta. Ni siquiera tuve que configurar por separado el registro del servidor por ssl, con tener el certificado instalado en el puerto correcto fue suficiente.

En el lado del dispositivo y en el lado del sitio, dos clientes intercambiaban mensajes, todo esto se registraba. No se necesitaban enormes analizadores que ralentizaran el servidor, nada de eso era necesario. El tiempo de respuesta se redujo de 200 ms a 40-30 ms. Y llegué a la única solución correcta.

Construcción avanzada de bicicletas o aplicación cliente-servidor basada en C# .Net framework

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

Salida

«No te metas en el agua sin conocer el fondo» Creo que antes de comenzar, debí haber definido más claramente los objetivos y tareas, así como profundizar en el estudio de las tecnologías necesarias y los métodos para implementarlas en diversos clientes. El proyecto ya está cerca de finalizar, pero es posible que regrese para contar cómo volví a cometer errores en ciertos aspectos. He aprendido mucho en el proceso de desarrollo, pero aún me queda mucho por aprender en el futuro. Si has llegado hasta aquí, gracias por eso.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster