Pokročilé budovanie bicyklov alebo aplikácia klient-server založená na C# .Net frameworku

Vstup

Všetko to začalo, keď mi kolega navrhol, aby som vytvoril malú webovú službu. Malo to byť niečo ako Tinder, ale pre IT dav. Funkcionalita je mimoriadne jednoduchá, zaregistrujete sa, vyplníte profil a prejdete k hlavnému bodu, a to nájsť osobu, s ktorou sa porozprávať, rozšíriť svoje kontakty a nadviazať nové známosti.

Tu musím ustúpiť a povedať niečo o sebe, aby bolo v budúcnosti jasnejšie, prečo som urobil takéto kroky vo vývoji.

Momentálne zastávam pozíciu Technical Artist v jednom hernom štúdiu, moje skúsenosti s programovaním v C# boli postavené len na písaní skriptov a utilít pre Unity a popri tom na tvorbe pluginov pre nízkoúrovňovú prácu s Android zariadeniami. Za tento malý svet som sa ešte neodvážil a vtedy sa naskytla takáto príležitosť.

Časť 1. Prototypovanie rámu

Keď som sa rozhodol, aká bude táto služba, začal som hľadať možnosti implementácie. Najjednoduchšie by bolo nájsť nejaké hotové riešenie, na ktoré ako sovu na zemeguli natiahneme našich mechanikov a celé to vystavíme verejnej nedôvere.
Ale to nie je zaujímavé, nevidel som v tom žiadnu výzvu ani zmysel, a preto som začal študovať webové technológie a spôsoby interakcie s nimi.

Začal som študovať prezeraním článkov a dokumentácie na C# .Net. Tu som našiel rôzne spôsoby dokončenia úlohy. Existuje mnoho mechanizmov na interakciu so sieťou, od plnohodnotných riešení, ako sú služby ASP.Net alebo Azure, až po priamu interakciu s pripojeniami TcpHttp.

Po prvom pokuse s ASP som to okamžite zamietol; podľa môjho názoru to bolo príliš ťažké rozhodnutie pre našu službu. Nevyužili by sme ani tretinu možností tejto platformy, tak som pokračoval v hľadaní. Voľba bola medzi TCP a Http klient-server. Tu na Habré som natrafil na článok o viacvláknový server, po zhromaždení a otestovaní som sa rozhodol zamerať sa konkrétne na interakciu s TCP pripojeniami, z nejakého dôvodu som si myslel, že http mi nedovolí vytvoriť multiplatformové riešenie.

Prvá verzia servera zahŕňala spracovanie pripojenia, obsluhovala obsah statickej webovej stránky a zahŕňala databázu používateľov. A na začiatok som sa rozhodol postaviť funkcionalitu pre prácu so stránkou, aby som neskôr mohol pridať spracovanie aplikácie na Android a iOS.

Tu je nejaký kód
Hlavné vlákno prijímajúce klientov v nekonečnej slučke:

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

Samotný spracovateľ klienta:

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

namespace ClearServer
{
    class Client
    {


        public Client(TcpClient Client)
        {

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

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

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

            string FilePath =

quot;D:/Web/TestSite{RequestUri}";

if (!File.Exists(FilePath))
{
ErrorWorker.SendError(Client, 404);
return;
}

string Extension = RequestUri.Substring(RequestUri.LastIndexOf('.'));

string ContentType = "";

switch (Extension)
{
case ".htm":
case ".html":
ContentType = "text/html";
break;
case ".css":
ContentType = "text/css";
break;
case ".js":
ContentType = "text/javascript";
break;
case ".jpg":
ContentType = "image/jpeg";
break;
case ".jpeg":
case ".png":
case ".gif":
ContentType =


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


quot;application/{Extension.Substring(1)}";
}
else
{
ContentType = "application/unknown";
}
break;
}

FileStream FS;
try
{
FS = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
}
catch (Exception)
{
ErrorWorker.SendError(Client, 500);
return;
}

string Headers =


quot;HTTP/1.1 200 OKnContent-Type: {ContentType}nContent-Length: {FS.Length}nn";
byte[] HeadersBuffer = Encoding.ASCII.GetBytes(Headers);
Client.GetStream().Write(HeadersBuffer, 0, HeadersBuffer.Length);

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

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

A prvá databáza postavená na lokálnom 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(

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

Ako vidíte, táto verzia sa len málo líši od verzie v článku. V skutočnosti sme sem pridali len načítanie stránok z priečinka v počítači a databázy (ktorá mimochodom v tejto verzii nefungovala kvôli nesprávnej architektúre pripojenia).

Kapitola 2. Skrutkovanie kolies

Po testovaní servera som dospel k záveru, že by to bolo vynikajúce riešenie(spojler: nie), pre našu službu, takže projekt začal nadobúdať logiku.
Postupne sa začali objavovať nové moduly a rozširovala sa funkčnosť servera. Server získal testovaciu doménu a šifrovanie pripojenia SSL.

Trochu viac kódu popisujúceho logiku spracovania servera a klienta
Aktualizovaná verzia servera, ktorá zahŕňa použitie certifikátu.

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

A tiež nový obslužný program klienta s autorizáciou SSL:

using ClearServer.Core.Requester;
using System;
using System.Net.Security;
using System.Net.Sockets;

namespace ClearServer
{
    public class Client
    {
        public Client(TcpClient Client)
        {
            SslStream SSlClientStream = new SslStream(Client.GetStream(), false);
            try
            {
                SSlClientStream.AuthenticateAsServer(Server.serverCertificate, clientCertificateRequired: false, checkCertificateRevocation: true);
            }
            catch (Exception e)
            {
                Console.WriteLine(
                    "---------------------------------------------------------------------n" +


quot;|{DateTime.Now:g}n|------------n|{Client.Client.RemoteEndPoint}n|------------n|Exception: {e.Message}n|------------n|Authentication failed - closing the connection.n" +
"---------------------------------------------------------------------n");
SSlClientStream.Close();
Client.Close();
}
new RequestContext(SSlClientStream, Client);
}

}
}

Ale keďže server beží výhradne na TCP spojení, je potrebné vytvoriť modul, ktorý dokáže rozpoznať kontext požiadavky. Rozhodol som sa, že tu by bol vhodný parser, ktorý by rozdelil požiadavku od klienta na samostatné časti, s ktorými by som mohol interagovať, aby som klientovi poskytol potrebné odpovede.

Analyzátor

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

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

        private event Action<SslStream, RequestContext> OnRead = RequestHandler.OnHandle;

        DatabaseWorker databaseWorker = new DatabaseWorker();

        public RequestContext(SslStream ClientStream, TcpClient Client)
        {

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

            if (ar.IsCompleted)
            {
                Message = Encoding.UTF8.GetString(buffer);
                Message = Uri.UnescapeDataString(Message);
                Console.WriteLine(

quot;n{DateTime.Now:g} Client IP:{TcpClient.Client.RemoteEndPoint}n{Message}");
RequestParse();
HeadersValues = HeaderValues();
FormValues = ContentValues();
UserParse();
ProfileParse();
OnRead?.Invoke(ClientStream, this);
}
}

private void RequestParse()
{
Match methodParse = Regex.Match(Message, @"(^w+)s+([^s?]+)[^s]*s+HTTP/.*|");
RequestMethod = methodParse.Groups[1].Value.Trim();
RequestUrl = methodParse.Groups[2].Value.Trim();
}
private void UserParse()
{
string cookie;
try
{
if (HeadersValues.Any(x => x.Name.Contains("Cookie")))
{
cookie = HeadersValues.FirstOrDefault(x => x.Name.Contains("Cookie")).Value;
try
{
CurrentUser = databaseWorker.CookieValidate(cookie);
}
catch { }
}
}
catch { }

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

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

Jeho podstatou je rozdeliť požiadavku na časti pomocou regulárnych výrazov. Dostaneme správu od klienta, vyberte prvý riadok, ktorý obsahuje metódu a url požiadavky. Potom si prečítame nadpisy, ktoré vložíme do poľa v tvare HeaderName=Content, a tiež nájdeme prípadne sprievodný obsah (napríklad reťazec dotazu), ktorý tiež vložíme do podobného poľa. Okrem toho parser zistí, či je aktuálny klient autorizovaný a uloží jeho údaje. Všetky požiadavky od autorizovaných klientov obsahujú autorizačný hash, ktorý sa ukladá do cookies, vďaka čomu je možné oddeliť ďalšiu prevádzkovú logiku pre dva typy klientov a dať im správne odpovede.

Malá, pekná funkcia, ktorú by sa oplatilo dať do samostatného modulu, konverzia dopytov ako „site.com/@UserName“ na dynamicky generované používateľské stránky. Po spracovaní požiadavky prichádzajú na rad nasledujúce moduly.

Kapitola 3. Montáž volantu, mazanie reťaze

Hneď ako syntaktický analyzátor dokončí svoju prácu, vstupuje do hry handler, ktorý dáva ďalšie pokyny serveru a rozdeľuje riadenie na dve časti.

Jednoduchý manipulátor

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

V skutočnosti existuje iba jedna kontrola autorizácie používateľa, po ktorej sa začne spracovanie požiadavky.

Klientske ovládače
Ak užívateľ nie je autorizovaný, tak je pre neho funkcionalita založená len na zobrazení užívateľských profilov a okne registrácie autorizácie. Kód pre oprávneného používateľa vyzerá približne rovnako, takže nevidím dôvod ho duplikovať.

Neoprávnený používateľ

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

namespace ClearServer.Core.UserController
{
    internal class NonAuthUserController
    {
        private readonly SslStream ClientStream;
        private readonly RequestContext Context;
        private readonly WriteController WriteController;
        private readonly AuthorizationController AuthorizationController;

        private readonly string ViewPath = "C:/Users/drdre/source/repos/ClearServer/View";

        public NonAuthUserController(SslStream clientStream, RequestContext context)
        {
            this.ClientStream = clientStream;
            this.Context = context;
            this.WriteController = new WriteController(clientStream);
            this.AuthorizationController = new AuthorizationController(clientStream, context);
            ResourceLoad();
        }

        void ResourceLoad()
        {
            string[] blockextension = new string[] {"cshtml", "html", "htm"};
            bool block = false;
            foreach (var item in blockextension)
            {
                if (Context.RequestUrl.Contains(item))
                {
                    block = true;
                    break;
                }
            }
            string FilePath = "";
            string Header = "";
            var RazorController = new RazorController(Context, ClientStream);
            
            switch (Context.RequestMethod)
            {
                case "GET":
                    switch (Context.RequestUrl)
                    {
                        case "/":
                            FilePath = ViewPath + "/loginForm.html";
                            Header =

quot;HTTP/1.1 200 OKnContent-Type: text/html";
WriteController.DefaultWriter(Header, FilePath);
break;
case "/profile":
RazorController.ProfileLoader(ViewPath);
break;
default:
//в данном блоке кода происходит отсечение запросов к серверу по прямому адресу страницы вида site.com/page.html
if (!File.Exists(ViewPath + Context.RequestUrl) | block)
{
RazorController.ErrorLoader(404);

}
else if (Path.HasExtension(Context.RequestUrl) && File.Exists(ViewPath + Context.RequestUrl))
{
Header = WriteController.ContentType(Context.RequestUrl);
FilePath = ViewPath + Context.RequestUrl;
WriteController.DefaultWriter(Header, FilePath);
}
break;
}
break;

case "POST":
AuthorizationController.MethodRecognizer();
break;

}

}

}
}

A samozrejme, používateľ musí dostať nejaký obsah stránky, takže pre odpovede je tu nasledujúci modul, ktorý je zodpovedný za odpovede na požiadavky na zdroje.

WriterController

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

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

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

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

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


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


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


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


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


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


quot;{Header} application/unknown";
}
}
}

public void OnFileRead(IAsyncResult ar)
{
if (ar.IsCompleted)
{
var file = (byte[])ar.AsyncState;
ClientStream.BeginWrite(file, 0, file.Length, OnClientSend, null);
}
}

public void OnClientSend(IAsyncResult ar)
{
if (ar.IsCompleted)
{
ClientStream.Close();
}
}
}

Ale aby som používateľovi ukázal svoj profil a profily ostatných používateľov, rozhodol som sa použiť RazorEngine, alebo skôr jeho časť. Zahŕňa aj spracovanie neplatných požiadaviek a vydanie príslušného chybového kódu.

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

A na to, aby fungovalo overovanie oprávnených používateľov, je samozrejme potrebná autorizácia. Autorizačný modul spolupracuje s databázou. Prijaté údaje z formulárov na stránke sa analyzujú z kontextu, používateľ sa uloží a na oplátku dostane súbory cookie a prístup k službe.

Autorizačný modul

using ClearServer.Core.Cookies;
using ClearServer.Core.Requester;
using ClearServer.Core.Security;
using System;
using System.Linq;
using System.Net.Security;
using System.Text;

namespace ClearServer.Core.UserController
{
    internal class AuthorizationController
    {
        private SslStream ClientStream;
        private RequestContext Context;
        private UserCookies cookies;
        private WriteController WriteController;
        DatabaseWorker DatabaseWorker;
        RazorController RazorController;
        PasswordHasher PasswordHasher;
        public AuthorizationController(SslStream clientStream, RequestContext context)
        {
            ClientStream = clientStream;
            Context = context;
            DatabaseWorker = new DatabaseWorker();
            WriteController = new WriteController(ClientStream);
            RazorController = new RazorController(context, clientStream);
            PasswordHasher = new PasswordHasher();
        }

        internal void MethodRecognizer()
        {
            if (Context.FormValues.Count == 2 && Context.FormValues.Any(x => x.Name == "password")) Authorize();
            else if (Context.FormValues.Count == 3 && Context.FormValues.Any(x => x.Name == "regPass")) Registration();
            else
            {
                RazorController.ErrorLoader(401);
            }
        }

        private void Authorize()
        {
            var values = Context.FormValues;
            var user = new User()
            {
                login = values[0].Value,
                password = PasswordHasher.PasswordHash(values[1].Value)
            };
            user = DatabaseWorker.UserAuth(user);
            if (user != null)
            {
                cookies = new UserCookies(user.login, user.password);
                user.cookie = cookies.AuthCookie;
                DatabaseWorker.UserUpdate(user);
                var response = Encoding.UTF8.GetBytes(

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

}
else
{
RazorController.ErrorLoader(401);

}
}

private void Registration()
{
var values = Context.FormValues;
var user = new User()
{
name = values[0].Value,
login = values[1].Value,
password = PasswordHasher.PasswordHash(values[2].Value),
};
cookies = new UserCookies(user.login, user.password);
user.cookie = cookies.AuthCookie;
if (DatabaseWorker.LoginValidate(user.login))
{
Console.WriteLine("User ready");
Console.WriteLine(


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


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

A takto vyzerá spracovanie databázy:

databázy

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

namespace ClearServer
{
    class DatabaseWorker
    {

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

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

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

        }

        public void UserRegister(User user)
        {
            try
            {
                users.InsertOnSubmit(user);
                DataBase.SubmitChanges();
                Console.WriteLine(

quot;User{user.name} with id {user.uid} added");
foreach (var item in users)
{
Console.WriteLine(item.login + "n");
}
}
catch (Exception e)
{
Console.WriteLine(e);
}

}

public bool LoginValidate(string login)
{
if (users.Any(x => x.login.ToLower() == login.ToLower()))
{
Console.WriteLine("Login already exists");
return false;
}
return true;
}
public void UserUpdate(User user)
{
var UserToUpdate = users.FirstOrDefault(x => x.uid == user.uid);
UserToUpdate = user;
DataBase.SubmitChanges();
Console.WriteLine(


quot;User {UserToUpdate.name} with id {UserToUpdate.uid} updated");
foreach (var item in users)
{
Console.WriteLine(item.login + "n");
}
}
public User CookieValidate(string CookieInput)
{
User user = null;
try
{
user = users.SingleOrDefault(x => x.cookie == CookieInput);
}
catch
{
return null;
}
if (user != null) return user;
else return null;
}
public User FindUser(string login)
{
User user = null;
try
{
user = users.Single(x => x.login.ToLower() == login.ToLower());
if (user != null)
{
return user;
}
else
{
return null;
}
}
catch (Exception)
{
return null;
}
}
}
}


A všetko funguje ako hodinky, autorizácia a registrácia fungujú, minimálna funkcionalita pre prístup k službe už existuje a nastal čas napísať aplikáciu a spojiť to celé s hlavnými funkciami, pre ktoré sa všetko robí.

Kapitola 4. Vyhodenie bicykla

Aby som znížil náklady na prácu pri písaní dvoch aplikácií pre dve platformy, rozhodol som sa vytvoriť multiplatformovú platformu na Xamarin.Forms. Opäť vďaka tomu, že je v C#. Po vytvorení testovacej aplikácie, ktorá jednoducho odosiela dáta na server, som narazil na zaujímavý bod. Pre požiadavku zo zariadenia som to zo srandy implementoval na HttpClient a poslal na server HttpRequestMessage, ktorý obsahuje údaje z autorizačného formulára vo formáte json. Bez toho, aby som niečo zvlášť očakával, som otvoril denník servera a videl som tam požiadavku zo zariadenia so všetkými údajmi. Mierna strnulosť, uvedomenie si všetkého, čo sa urobilo za posledné 3 týždne v mdlý večer. Pre kontrolu správnosti odoslaných údajov som zostavil testovací server na HttpListner. Po prijatí ďalšej požiadavky som ju rozobral na pár riadkov kódu a z formulára som dostal údaje KeyValuePair. Analýza dotazu bola zredukovaná na dva riadky.

Začal som testovať ďalej, nebolo to spomenuté skôr, ale na predošlom serveri som implementoval aj chat postavený na websocketoch. Fungovalo to celkom dobre, ale samotný princíp interakcie cez Tcp bol deprimujúci, bolo potrebné urobiť príliš veľa zbytočnej práce, aby sa kompetentne vybudovala interakcia dvoch používateľov s korešpondenčným denníkom. To zahŕňa analýzu požiadavky na prepnutie pripojenia a zhromaždenie odpovede pomocou protokolu RFC 6455. Preto som sa v testovacom serveri rozhodol vytvoriť jednoduché websocket pripojenie. Len pre zábavu.

Pripojte sa k chatu

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

A podarilo sa. Samotný server nakonfiguroval pripojenie a vygeneroval kľúč odpovede. Nemusel som ani samostatne konfigurovať registráciu servera cez SSL, stačilo, že systém už mal nainštalovaný certifikát na požadovanom porte.

Na strane zariadenia a na strane lokality si dvaja klienti vymieňali správy, toto všetko bolo protokolované. Žiadne obrovské analyzátory spomaľujúce server, nič z toho nebolo potrebné. Čas odozvy sa znížil z 200 ms na 40-30 ms. A dospel som k jedinému správnemu rozhodnutiu.

Pokročilé budovanie bicyklov alebo aplikácia klient-server založená na C# .Net frameworku

Vyhoďte aktuálnu implementáciu servera na Tcp a prepíšte všetko pod Http. Teraz je projekt v štádiu redizajnu, ale podľa úplne iných princípov interakcie. Prevádzka zariadení a stránky je synchronizovaná a odladená a má spoločnú koncepciu, len s tým rozdielom, že nie je potrebné generovať HTML stránky pre zariadenia.

Výkon

"Ak nepoznáš brod, nechoď do vody" Myslím si, že pred začatím práce by som mal mať jasnejšie definované ciele a zámery, ako aj ponorený do štúdia potrebných technológií a metód na ich implementáciu u rôznych klientov. Projekt sa už blíži ku koncu, ale možno sa vrátim a porozprávam o tom, ako som opäť zachránil určité veci. Počas procesu vývoja som sa veľa naučil, no v budúcnosti sa toho ešte veľa naučím. Ak ste sa dočítali až sem, ďakujem, že ste tak urobili.

Zdroj: hab.com