Pokročilé výroba bicyklů nebo klient-serverová aplikace na bázi C# .Net framework

Úvod

Vše začalo tím, že mi kolega navrhl vytvořit malou webovou službu. Mělo to být něco jako Tinder, ale pro IT komunitu. Funkcionalita je nesmírně jednoduchá, registroješ se, vyplníš profil a dostaneš se k hlavnímu cíli, a to hledání partnera na konverzaci a rozšiřování svých kontaktů a získávání nových známostí.

Zde bych měl udělat odbočku a trochu o sobě povědět, aby bylo jasnější, proč jsem se v procesu vývoje ubíral tímto směrem.

V současné době zastávám pozici technického umělce v jednom herním studiu. Můj programátorský zkušenosti v C# se zaměřily pouze na psaní skriptů a utilit pro Unity a k tomu jsem vytvářel pluginy pro nízkoúrovňovou práci s android zařízeními. Mimo tento malý svět jsem ještě nevystoupil a teď se naskytla taková příležitost.

Část 1. Prototypování rámu

Poté, co jsem se rozhodl, jakou podobu bude tato služba mít, začal jsem hledat možnosti realizace. Nejdůležitější by bylo najít nějaké hotové řešení, na které bychom mohli natáhnout naše mechaniky a zveřejnit to k veřejné kritice.
Ale to není zajímavé, žádná výzva a smysl v tom jsem neviděl, a proto jsem začal studovat webové technologie a metody interakce s nimi.

Studium jsem začal prohlížením článků a dokumentace k C# .Net. Zde jsem našel různé způsoby, jak úkol splnit. Existuje mnoho mechanismů pro interakci se sítí, od kompletních řešení jako ASP.Net nebo služby Azure až po přímou interakci s TcpHttp připojeními.

Po první pokusu s ASP jsem ho hned zavrhl, podle mého názoru to bylo příliš těžké řešení pro naši službu. Nebudeme používat ani třetinu možností této platformy, takže jsem pokračoval v hledání. Volba stála mezi TCP a Http klient-serverem. Zde jsem na Habr natrefil na článek o vícevláknovém serveru, který jsem sestavil a otestoval, a rozhodl jsem se zastavit právě na interakci s TCP připojeními, protože jsem měl pocit, že http mi nedovolí vytvořit multiplatformní řešení.

První verze serveru zahrnovala zpracování připojení, poskytování statického obsahu webových stránek a obsahovala databázi uživatelů. A na začátek jsem se rozhodl vytvořit funkčnost pro práci se stránkami, abych mohl později připojit zpracování aplikace na Android a iOS.

Tady je trochu kódu
Hlavní smyčka, která v nekonečné smyčce přijímá klienty:

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

Sám zpracovatel klientů:

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);
{
string Extension = RequestUri.Substring(RequestUri.LastIndexOf('.'));
return;
}

string ContentType = "";

switch (Extension)

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

if (Extension.Length > 1)
case ".css":
ContentType = "application/{Extension.Substring(1)}";
ContentType = "application/unknown";
{
default:

FileStream FS;
}
else
{
try
}
case ".css":
}

FS = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
catch (Exception)
{
ErrorWorker.SendError(Client, 500);
}
string Headers = "HTTP/1.1 200 OKnContent-Type: {ContentType}nContent-Length: {FS.Length}nn";
{
byte[] HeadersBuffer = Encoding.ASCII.GetBytes(Headers);
return;
}

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();
Klient.GetStream().Write(Buffer, 0, Count);
}

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


A první databáze postavená na local SQL:
using System;
using System.Data.Linq;
namespace ClearServer
{
    class DatabaseWorker
    {

        private static DatabaseWorker instance;

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


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

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


Jak si můžete všimnout, tato verze se málo liší od té, která byla v článku. V podstatě zde byla pouze přidána možnost načítání stránek z počítače a databáze (která mimochodem v této verzi nefungovala kvůli nesprávné architektuře připojení).

Kapitola 2. Připojení kol

Po otestování práce serveru jsem dospěl k závěru, že to bude skvělé řešení (spojler: ne), pro naši službu, proto projekt začal nabývat logiky.
Krok za krokem se začaly objevovat nové moduly a funkčnost serveru se rozšiřovala. Server získal testovací doménu a ssl šifrování spojení.

Ještě trochu kódu, popisující logiku práce serveru a zpracování klientů
Aktualizovaná varianta serveru, zahrnující použití 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 také nový zpracovatel klienta s autorizací přes 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|Ověření selhalo - zavírám připojení.n" +
"---------------------------------------------------------------------n");
SSlClientStream.Close();
Client.Close();
}
new RequestContext(SSlClientStream, Client);
}

}
}


Ale protože server funguje výhradně na TCP připojení, je nutné vytvořit modul, který dokáže rozpoznat kontext požadavku. Rozhodl jsem se, že zde se hodí parser, který rozdělí požadavek od klienta na jednotlivé části, se kterými budu moci pracovat, abych mohl klientovi vracet potřebné odpovědi.

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("{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;
{
if (HeadersValues.Any(x => x.Name.Contains("Cookie")))
catch (Exception)
{
cookie = HeadersValues.FirstOrDefault(x => x.Name.Contains("Cookie")).Value;
{
CurrentUser = databaseWorker.CookieValidate(cookie);
catch (Exception)
{
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, @("([^&].*?)=([^&]*)"));
Value = match.Groups[2].Value.Trim().Replace('+', ' ');
values.Add(new RequestValues()
{
Name = match.Groups[1].Value.Trim(),
{
Value = match.Groups[2].Value.Trim()
Hodnota = match.Groups[2].Value.Trim().Replace('+', ' ')}
});
}
private void ProfileParse()
}
}
}


Podstatou je rozdělit dotaz na části pomocí regulárních výrazů. Získáme zprávu od klienta, vybereme první řádek, ve kterém se nachází metoda a url dotazu. Poté přečteme hlavičky, které uložíme do pole ve formátu NázevHlavy=Obsah, a také najdeme, pokud existuje, doprovodný obsah (například querystring), který také uložíme do podobného pole. Navíc parser zjistí, zda je aktuální klient autorizován, a uchová si jeho údaje. Všechny dotazy od autorizovaných klientů obsahují hash autorizace, který je uložen v cookies, což umožňuje dále oddělovat logiku práce pro dva typy klientů a poskytovat jim správné odpovědi.

A malá, příjemná funkce, kterou by bylo dobré přenést do samostatného modulu, přetváření dotazů typu „site.com/@UserName“ na dynamicky generované stránky uživatelů. Po zpracování dotazu vstupují do akce následující moduly.

Kapitola 3. Instalace úchytu, mazání řetězu

Jakmile parser dokončí svou práci, vstupuje do akce zpracovatel, který dává další pokyny serveru a rozděluje řízení na dvě části.

Jednoduchý zpracovatel

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 podstatě se zde provádí pouze jedna kontrola na autorizaci uživatele, po čemž začíná zpracování dotazu.

Klientští kontrolery
Pokud uživatel není autorizován, jeho funkčnost vychází pouze z zobrazení profilů uživatelů a registračního/přihlašovacího okna. Kód pro autorizovaného uživatele vypadá přibližně stejně, takže nevidím smysl v jeho duplikaci.

Neautorizovaný uživatel

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 OK Content-Type: text/html";
WriteController.DefaultWriter(Header, FilePath);
case ".css":
case "/profile":
RazorController.ProfileLoader(ViewPath);
case ".css":
ContentType = "application/{Extension.Substring(1)}";
//в данном блоке кода происходит отсечение запросов к серверу по прямому адресу страницы вида 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);
}
case ".css":
}
case ".css":

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

}

}

}
}


Samozřejmě, že uživatel by měl obdržet nějaký obsah stránek, takže pro odpovědi existuje následující modul, který odpovídá na dotaz zdrojů.

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

public string ContentType(string Uri)
{
string extension = Path.GetExtension(Uri);
string Header = "HTTP\/1.1 200 OKnContent-Type:";
switch (extension)
{
ContentType = "text/html";
case ".html":
vrátit

quot;{Header} text\/html";
ContentType = "text/css";
vrátit

quot;{Header} text\/css";
ContentType = "text/javascript";
vrátit

quot;{Header} text\/javascript";
ContentType = "image/jpeg";
case ".png":
case ".gif":
ContentType = "image/{Extension.Substring(1)}";
vrátit

quot;{Header} image\/{extension}";
ContentType = "application/{Extension.Substring(1)}";
if (extension.Length > 1)
{
vrátit

quot;{Header} application\" + extension.Substring(1);
}
else
{
vrátit

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 se uživateli mohl zobrazit jeho profil a profily dalších uživatelů, rozhodl jsem se použít RazorEngine, přesněji jeho část. Také zahrnuje zpracování neplatných dotazů a vydávání odpovídajícího kódu chyby.

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 samozřejmě, pro to, aby fungovalo ověření autorizovaných uživatelů, je potřeba autorizace. Modulo autorizace interaguje s databází. Získaná data z formulářů na webových stránkách jsou parsnuta z kontextu, uživatel je uložen a za to získává cookies a přístup k službě.

Modul autorizace

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

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


A takto vypadá zpracování databáze:

Databáze

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 = @"cesta_k_databázi";

        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("Uživatel {user.name} s id {user.uid} přidán");

Uživatel {user.name} s id {user.uid} přidán
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("Přihlašovací jméno již existuje");
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("Uživatel {UserToUpdate.name} s id {UserToUpdate.uid} aktualizován");
foreach (var item in users)
{
Console.WriteLine(item.login + "n");
}
}
public User CookieValidate(string CookieInput)
{
User user = null;
catch (Exception)
{
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;
catch (Exception)
{
user = users.Single(x => x.login.ToLower() == login.ToLower());
if (user != null)
{
return user;
}
else
{
return null;
}
}
string Headers = "HTTP/1.1 200 OKnContent-Type: {ContentType}nContent-Length: {FS.Length}nn";
{
return null;
}
}
}
}


A všechno funguje jako hodinky, autorizace a registrace fungují, základní funkčnost přístupu k službě už existuje a nastal čas napsat aplikaci a obalit to všechno hlavními funkcemi, kvůli nimž se to všechno dělá.

Kapitola 4. Vyhazování kola

Aby snížit pracovní zátěž při psaní dvou aplikací pro dvě platformy, rozhodl jsem se vytvořit multiplatformní aplikaci pomocí Xamarin.Forms. Opět díky tomu, že je to v C#. Když jsem vytvořil testovací aplikaci, která jednoduše odesílá data na server, setkal jsem se s jedním zajímavým momentem. Pro požadavek z zařízení jsem pro zajímavost implementoval HttpClient a odeslal na server HttpRequestMessage, který obsahuje data z formuláře pro autorizaci ve formátu JSON. Bez očekávání jsem otevřel log serveru a uviděl tam žádost z zařízení se všemi daty. Mírné omráčení, uvědomění si všeho, co bylo uděláno během posledních tří týdnů únavných večerů. Pro ověření správnosti odeslaných dat jsem sestavil testovací server na HttpListener. Po obdržení dalšího požadavku na něm jsem za pár řádků kódu rozložil žádost na části a obdržel KeyValuePair údajů z formuláře. Analýza požadavku byla zjednodušena na dva řádky.

Začal jsem testovat dále, dříve nebylo zmíněno, ale na předchozím serveru jsem také implementoval chat postavený na webových soketech. Fungoval docela dobře, ale samotný princip interakce přes TCP byl nepříjemný, bylo třeba vytvořit příliš mnoho zbytečného kódu, aby bylo možné správně navázat interakci mezi dvěma uživateli a vést záznam konverzace. To zahrnovalo analýzu požadavku ohledně přepnutí spojení a shromažďování odpovědi podle protokolu RFC 6455. Proto jsem se na testovacím serveru rozhodl vytvořit jednoduché websocketové spojení. Čistě ze zvědavosti.

Připojení 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(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 fungovalo to. Server automaticky nastavoval spojení, generoval klíč pro odpověď. Ani jsem nemusel zvlášť nastavovat registraci serveru přes ssl, stačilo, že je v systému již nainstalován certifikát na potřebném portu.

Na straně zařízení a na straně webu dva klienti vyměňovali zprávy, vše bylo logováno. Nepotřebovali jsme žádné obrovské parsery, které by zpomalily práci serveru. Doba odezvy se zkrátila z 200 ms na 40-30 ms. A došel jsem k jedinému správnému řešení.

Pokročilé výroba bicyklů nebo klient-serverová aplikace na bázi C# .Net framework

Zlikvidovat aktuální implementaci serveru na Tcp a přepsat vše na Http. Projekt je nyní ve fázi redesignu, ale již na zcela odlišných principech interakce. Práce zařízení a webu jsou synchronizovány a vyladěny a mají společnou koncepci, s tím rozdílem, že pro zařízení není nutné generovat html stránky.

Výstup

„Nezkušený se do vody nevrhá“ Myslím, že před začátkem prací jsem měl jasněji definovat cíle a úkoly, a také se hlouběji zabývat potřebnými technologiemi a metodami jejich realizace na různých klientech. Projekt se blíží k dokončení, ale je možné, že se ještě vrátím, abych vyprávěl o tom, jak jsem znovu udělal některé věci špatně. Hodně jsem se naučil během vývoje, ale ještě se musím mnohému naučit v budoucnu. Pokud jste dočetli až sem, děkuji vám za to.

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster