Ciclism avansat sau aplicație client-server bazată pe C# .Net framework

Introducere

Totul a început când un coleg mi-a propus să fac un mic serviciu web. Ar trebui să fie ceva asemănător cu Tinder, dar pentru comunitatea IT. Funcționalitatea este extrem de simplă: te înregistrezi, completezi profilul și treci la esența principală, adică căutarea unui interlocutor și extinderea rețelei de contacte și obținerea de noi cunoștințe.

Aici trebuie să fac o digresiune și să povestesc puțin despre mine, astfel încât, mai târziu, să fie mai clar de ce am luat aceste măsuri în dezvoltare.

În prezent, ocup funcția de Artist Tehnic într-un studio de jocuri, iar experiența mea de programare în C# s-a construit exclusiv pe scrierea scripturilor și utilităților pentru Unity și, pe lângă asta, crearea de plugin-uri pentru lucrări la nivel de bază cu dispozitive Android. Nu am avut ocazia să ies din această lume și acum a apărut o astfel de oportunitate.

Partea 1. Prototiparea cadrului

După ce am decis ce va reprezenta acest serviciu, am început să caut opțiuni pentru realizare. Cel mai simplu ar fi fost să găsesc o soluție gata pregătită, pe care, ca o bufniță pe glob, să aplicăm mecanismele noastre și să publicăm totul pentru critici publice.
Dar asta nu este interesant, nu am văzut niciun provocare și sens în asta, așa că am început să studiez tehnologiile web și metodele de interacțiune cu ele.

Am început studiul prin citirea articolelor și documentației despre C# .Net. Aici am găsit diverse modalități de a îndeplini sarcina. Există multe mecanisme de interacțiune cu rețeaua, de la soluții complete precum ASP.Net sau servicii Azure, până la interacțiuni directe cu conexiunile TcpHttp.

După prima încercare cu ASP, l-am abandonat imediat, în opinia mea, era o soluție prea complicată pentru serviciul nostru. Nu vom folosi nici măcar o treime din capacitățile acestei platforme, așa că am continuat căutările. Alegerea a fost între client server TCP și Http. Aici, pe Habr, am dat peste un articol despre serverul multithreading, pe care, după ce l-am adunat și testat, am decis să mă opresc la interacțiunea cu conexiunile TCP, pentru că am avut impresia că http nu îmi va permite să creez o soluție multiplatformă.

Prima versiune a serverului includea procesarea conexiunilor, livrarea de conținut static al paginilor web și conținea o bază de date a utilizatorilor. La început, am decis să construiesc funcționalitatea pentru a funcționa cu site-ul, astfel încât mai târziu să pot integra și procesarea aplicației pe Android și iOS.

Iată puțin cod
Fluxul principal, într-un ciclu infinit care acceptă clienți:

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

Procesorul de clienți:

using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;

namespace ClearServer
{
    class Client
    {


        public Client(TcpClient Client)
        {

            string Message = "";
            byte[] Buffer = new byte[1024];
            int Count;
            while ((Count = Client.GetStream().Read(Buffer, 0, Buffer.Length)) > 0)
            {
                Message += Encoding.UTF8.GetString(Buffer, 0, Count);

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

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

            string FilePath =

"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)}";
}
altfel
{
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 OKnContent-Type: {ContentType}nContent-Length: {FS.Length}nn";
byte[] HeadersBuffer = Encoding.ASCII.GetBytes(Headers);
Client.GetStream().Write(HeadersBuffer, 0, HeadersBuffer.Length);

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

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


Și prima bază de date creată pe SQL local:
using System;
using System.Data.Linq;
namespace ClearServer
{
    class DatabaseWorker
    {

        private static DatabaseWorker instance;

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


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

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


Cum se poate observa, această versiune nu diferă mult de cea din articol. Practic, aici s-a adăugat doar încărcarea paginilor dintr-un folder de pe computer și baza de date (care, de altfel, în această versiune nu a funcționat din cauza arhitecturii greșite a conexiunii).

Capitolul 2. Adăugarea de roti

După ce am testat funcționarea serverului, am ajuns la concluzia că aceasta va fi o soluție excelentă (spoiler: nu), pentru serviciul nostru, așa că proiectul a început să se îmbogățească cu logică.
Pas cu pas au început să apară module noi, iar funcționalitatea serverului s-a extins. Serverul a primit un domeniu de testare și ssl criptarea conexiunii.

Încă puțin cod care descrie logica de funcționare a serverului și prelucrarea clienților
Varianta actualizată a serverului, care include utilizarea certificatului.

using System;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Security;
using System.Security.Cryptography.X509Certificates;
using System.Security.Permissions;
using System.Security.Policy;
using System.Threading;


namespace ClearServer
{

    sealed class Server
    {
        readonly bool ServerRunning = true;
        readonly TcpListener sslListner;
        public static X509Certificate serverCertificate = null;
        Server()
        {
            serverCertificate = X509Certificate.CreateFromSignedFile(@"C:sslitinder.online.crt");
            sslListner = new TcpListener(IPAddress.Any, 443);
            sslListner.Start();
            Console.WriteLine("Starting server.." + serverCertificate.Subject + "n" + Assembly.GetExecutingAssembly().Location);
            while (ServerRunning)
            {
                TcpClient SslClient = sslListner.AcceptTcpClient();
                Thread SslThread = new Thread(new ParameterizedThreadStart(ClientThread));
                SslThread.Start(SslClient);
            }
            
        }
        static void ClientThread(Object StateInfo)
        {
            new Client((TcpClient)StateInfo);
        }

        ~Server()
        {
            if (sslListner != null)
            {
                sslListner.Stop();
            }
        }

        public static void Main(string[] args)
        {
            if (AppDomain.CurrentDomain.IsDefaultAppDomain())
            {
                Console.WriteLine("Switching another domain");
                new AppDomainSetup
                {
                    ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase
                };
                var current = AppDomain.CurrentDomain;
                var strongNames = new StrongName[0];
                var domain = AppDomain.CreateDomain(
                    "ClearServer", null,
                    current.SetupInformation, new PermissionSet(PermissionState.Unrestricted),
                    strongNames);
                domain.ExecuteAssembly(Assembly.GetExecutingAssembly().Location);
            }
            new Server();
        }
    }
}

Și un nou handler pentru clienți cu autentificare 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);
}

}
}


Dar, deoarece serverul funcționează exclusiv pe conexiuni TCP, este necesar să creăm un modul care să poată recunoaște contextul cererii. Am decis că un parser care să descompună cererea de la client în părți separate, cu care să pot interacționa, ar fi potrivit, pentru a returna răspunsurile dorite clientului.

Parser

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

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

        private event Action OnRead = RequestHandler.OnHandle;

        DatabaseWorker databaseWorker = new DatabaseWorker();

        public RequestContext(SslStream ClientStream, TcpClient Client)
        {

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

            if (ar.IsCompleted)
            {
                Message = Encoding.UTF8.GetString(buffer);
                Message = Uri.UnescapeDataString(Message);
                Console.WriteLine(","540652": "n{DateTime.Now:g} Client IP:{TcpClient.Client.RemoteEndPoint}n{Message}");

"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 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 values = new List();
var output = Message.Trim('n').Split().Last();
var parse = Regex.Matches(output, @"([^&].*?)=([^&]*)");
foreach (Match match in parse)
{
values.Add(new RequestValues()
{
Name = match.Groups[1].Value.Trim(),
Value = match.Groups[2].Value.Trim().Replace('+', ' ')
});
}
return values;
}
}
}


Ideea este că, folosind expresii regulate, să împărțim cererea în părți. Primim un mesaj de la client, extragem prima linie, care conține metoda și URL-ul cererii. Apoi citim anteturile, pe care le stocăm într-un array în formatul NumeAntet=Conținut, precum și găsim, dacă există, conținutul însoțitor (de exemplu, querystring), pe care de asemenea îl stocăm într-un array similar. În plus, parserul determină dacă clientul curent este autentificat și își salvează datele. Toate cererile de la clienții autentificați conțin un hash de autorizare, care este stocat în cookies, ceea ce permite separarea logicii de lucru pentru cele două tipuri de clienți, oferindu-le răspunsuri corecte.

Și o mică, plăcută funcționalitate, care ar merita să fie scoasă într-un modul separat, transformarea cererilor de tip „site.com/@UserName” în pagini generate dinamic pentru utilizatori. După procesarea cererii, următoarele module intervin.

Capitolul 3. Instalarea volanului, ungerea lanțului

Odată ce parserul a funcționat, intră în scenă handler-ul, care dă instrucțiuni ulterioare serverului și împarte controlul în două părți.

Handler simplu

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

Practic, aici există doar o verificare a autorizării utilizatorului, după care începe procesarea cererii.

Controlerele clienților
Dacă utilizatorul nu este autorizat, atunci funcționalitatea pentru el se bazează doar pe afișarea profilurilor utilizatorilor și pe fereastra de înregistrare/autorizare. Codul pentru utilizatorul autorizat arată aproximativ la fel, așa că nu văd sensul de a-l duplica.

Utilizator neautorizat

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;

}

}

}
}


Și bineînțeles, utilizatorul ar trebui să primească conținutul paginilor, așa că pentru răspunsuri există următorul modul, care se ocupă cu răspunsul la cererea de resurse.

WriterController

using System;
using System.IO;
using System.Net.Security;
using System.Text;

namespace ClearServer.Core.UserController
{
    public class WriteController
    {
        SslStream ClientStream;
        public WriteController(SslStream ClientStream)
        {
            this.ClientStream = ClientStream;
        }

        public void DefaultWriter(string Header, string FilePath)
        {
            FileStream fileStream;
            try
            {
                fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
                Header =

quot;{Header}nContent-Length: {fileStream.Length}nn";
ClientStream.Write(Encoding.UTF8.GetBytes(Header));
byte[] response = new byte[fileStream.Length];
fileStream.BeginRead(response, 0, response.Length, OnFileRead, response);
}
catch { }
}

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

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

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

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

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

quot;{Header} application/" + extension.Substring(1);
}
altfel
{
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();
}
}
}


Dar pentru a-i arăta utilizatorului profilul său și profilurile altor utilizatori, am decis să folosesc RazorEngine, mai exact o parte din el. Acesta include de asemenea procesarea cererilor greșite și generarea codului de eroare corespunzător.

RazorController

folosind ClearServer.Core.Requester;
folosind RazorEngine;
folosind RazorEngine.Templating;
folosind System;
folosind System.IO;
folosind System.Net;
folosind 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();
            }
        }
    }
}

Și, desigur, pentru ca verificarea utilizatorilor autorizați să funcționeze, este necesară autentificarea. Modulul de autentificare interacționează cu baza de date. Datele obținute din formularele de pe site sunt analizate din context, utilizatorul este salvat și primește în schimb cookie-uri și acces la serviciu.

Modulul de autentificare

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 Location: /@{user.login} Set-Cookie: {cookies.AuthCookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnly
ClientStream.BeginWrite(response, 0, response.Length, WriteController.OnClientSend, null);

}
altfel
{
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 Location: /@{user.login} Set-Cookie: {user.cookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnly
ClientStream.BeginWrite(response, 0, response.Length, WriteController.OnClientSend, null);
}
altfel
{
RazorController.ErrorLoader(401);
}
}
}
}


Așa arată procesarea bazei de date:

Baza de date

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

namespace ClearServer
{
    class DatabaseWorker
    {

        private readonly Table users = null;
        private readonly DataContext DataBase = null;
        private const string connectionStr = @"caleabază";

        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("User {user.name} with id {user.uid} added");
            }

"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;
}
altfel
{
return null;
}
}
catch (Exception)
{
return null;
}
}
}
}


Și totul funcționează ca un ceas, autentificarea și înregistrarea funcționează, un minim de funcționalitate de acces la serviciu este deja disponibil și a venit momentul să scriem aplicația și să interconectăm tot acest lucru cu funcțiile principale pentru care se face totul.

Capitolul 4. Inventarea bicicletei

Pentru a reduce efortul de a dezvolta două aplicații pentru două platforme, am decis să creez o aplicație multiplatformă folosind Xamarin.Forms. Din nou, datorită faptului că este bazată pe C#. După ce am realizat o aplicație de test care trimite date către server, m-am confruntat cu un moment interesant. Pentru solicitarea de la dispozitiv, am implementat-o din curiozitate cu HttpClient și am trimis către server un HttpRequestMessage care conține datele din formularul de autentificare în format json. Fără a aștepta nimic în mod special, am deschis jurnalul serverului și am văzut acolo o solicitare de la dispozitiv cu toate datele. O ușoară confuzie, realizarea a tot ceea ce s-a realizat în ultimele 3 săptămâni de seri toropite. Pentru a verifica corectitudinea datelor trimise, am creat un server de test folosind HttpListner. Primind o nouă solicitare pe acesta, am reușit să o descompun în câteva linii de cod, obținând un KeyValuePair de date din formular. Descompunerea solicitării s-a redus la două linii.

Am început să testez mai departe, deși nu am menționat mai devreme, pe serverul anterior am realizat un chat construit pe websocket-uri. A funcționat destul de bine, dar principiul interacțiunii prin Tcp era frustrant, deoarece trebuia să produc prea multe lucruri inutile pentru a construi corect interacțiunea dintre doi utilizatori cu păstrarea jurnalului conversației. Aceasta include analiza solicitării cu privire la comutarea conexiunii și colectarea răspunsului conform protocolului RFC 6455. Așa că pe serverul de test am decis să creez o conexiune websocket simplă. Pur și simplu din curiozitate.

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

Și a început să funcționeze. Serverul a configurat singur conexiunea, generând cheia de răspuns. Nu a trebuit nici măcar să configurez separat înregistrarea serverului prin ssl, era suficient ca sistemul să aibă deja un certificat pe portul necesar.

Pe partea dispozitivului și pe partea site-ului, două clienți schimbau mesaje, totul era înregistrat. Nu au fost necesare mari parseruri care să încetinească serverul. Timpul de răspuns a fost redus de la 200 ms la 40-30 ms. Și am ajuns la singura soluție corectă.

Ciclism avansat sau aplicație client-server bazată pe C# .Net framework

Să elimin implementarea curentă a serverului pe Tcp și să rescriu totul pe Http. Acum, proiectul se află în etapa de reproiectare, dar conform unor principii complet diferite de interacțiune. Funcționarea dispozitivelor și a site-ului este sincronizată și testată, având un concept comun, cu singura diferență că pentru dispozitive nu este necesară generarea paginilor html.

Ieșire

„Nu te băga în apă fără să știi adâncimea” Cred că înainte de a începe lucrările, ar fi trebuit să îmi definesc mai clar obiectivele și sarcinile, precum și să mă aprofundez în studierea tehnologiilor necesare și a metodelor de implementare pe diferiți clienți. Proiectul se apropie de final, dar este posibil să revin pentru a povesti despre cum am gafat din nou în anumite privințe. Am învățat multe în procesul de dezvoltare, dar îmi mai rămâne să învăț și mai mult în continuare. Dacă ai citit până aici, îți mulțumesc pentru asta.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster