Развито велосипедостроене или клиент-серверно приложение на базата на C# .Net framework

Въведение

Всичко започна, когато колегата ми предложи да направя малък уебсервис. Това трябваше да бъде нещо като Tinder, но за IT общността. Функционалността е изключително проста – регистрираш се, попълваш профил и преминаваш към основната цел, а именно търсене на партньор и разширяване на контактите и получаване на нови запознанства.

Тук трябва да направя отстъпление и да разкажа малко за себе си, за да е по-разбираемo защо предприемах точно такива стъпки в разработката.

В момента заемам позицията Технически Артист в една игрова студия, а опитът ми с програмиране на C# е само в създаването на скриптове и утилити за Unity, като допълнително съм създавал и плъгини за нискоуровнева работа с устройства с Android. Все още не бях излязъл извън този малък свят, но сега се появи такава възможност.

Част 1. Прототипиране на рамка

Решавайки какво ще представлява този сервис, започнах да търся варианти за реализация. Най-лесно щеше да е да намеря някакво готово решение, на което, както совата на глобус, да натегнем нашите механики и да изложим всичко на обществено порицание.
Но това не е интересно, не виждах никакъв предизвикателство и смисъл в това, затова започнах да изучавам уеб технологии и методи за взаимодействие с тях.

Започнах изучаването с четене на статии и документация по C# .Net. Тук намерих разнообразни начини за изпълнение на задачата. Има множество механизми за взаимодействие с мрежата - от пълноценни решения като ASP.Net или услуги на Azure, до директно взаимодействие с TcpHttp връзки.

След като направих първа опит с ASP, го отхвърлих веднага, защото за мен това беше твърде сложно решение за нашия сервис. Няма да използваме и една трета от възможностите на тази платформа, затова продължих търсенето. Изборът беше между TCP и HTTP клиент-сървър. тук, на Хабра, попаднах на статия замногопоточен сървър, и след като го събрах и тествувах, реших да се спра именно на взаимодействието с TCP връзки, защото по някаква причина смятах, че HTTP няма да ми позволи да създам кросплатформено решение.

Първата версия на сървъра включваше обработка на връзки, предоставяше статично съдържание на уеб страници и съдържаше база данни с потребители. И за начало реших да изградя функционалност за работа с сайта, за да мога по-късно да добавя и обработка на приложение за Android и iOS.

Ето малко код
Основният поток, в неограничен цикъл приемащ клиенти:

using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;

namespace ClearServer
{

    class Server
    {
        TcpListener Listener;
        public Server(int Port)
        {
            Listener = new TcpListener(IPAddress.Any, Port);
            Listener.Start();

            while (true)
            {
                TcpClient Client = Listener.AcceptTcpClient();
                Thread Thread = new Thread(new ParameterizedThreadStart(ClientThread));
                Thread.Start(Client);
            }
        }

        static void ClientThread(Object StateInfo)
        {
            new Client((TcpClient)StateInfo);
        }

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

        static void Main(string[] args)
        {
            DatabaseWorker sqlBase = DatabaseWorker.GetInstance;

            new Server(80);
        }
    }
}

Самият обработчик на клиенти:

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

namespace ClearServer
{
    class Client
    {


        public Client(TcpClient Client)
        {

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

                if (Message.IndexOf("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)}";
}
иначе
{
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();
}
}
}


И първата база данни, построена на локален SQL:
използвайки System;
използвайки 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}" );
}
}
}
}
}


Както може да се забележи, тази версия не се различава много от онази, която беше в статията. По същество тук само беше добавено зареждане на страници от папка на компютъра и база данни (която всъщност в тази версия не заработи, поради грешна архитектура на свързване).

Глава 2. Прикрепяне на колела

След като тествах работата на сървъра, стигнах до извода, че това ще бъде отличен вариант (спойлер: не), за нашия сервис, затова проектът започна да се обогатява с логика.
Стъпка по стъпка започнаха да се появяват нови модули и функционалността на сървъра се разширяваше. Сървърът получи тестов домейн и ssl шифриране на връзката.

Още малко код, описващ логиката на работа на сървъра и обработката на клиенти.
Актуализиран вариант на сървъра, включващ използването на сертификат.

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

И нов обработчик на клиента с 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);
}

}
}


Понеже сървърът работи изключително на TCP връзки, е необходимо да се създаде модул, който може да разпознава контекста на заявката. Реших, че тук ще подхожда парсер, който ще разделя заявката от клиента на отделни части, с които ще мога да взаимодействам, за да предоставям на клиента нужните отговори.

Парсер

използване на ClearServer.Core.UserController;
използване на ReServer.Core.Classes;
използване на System;
използване на System.Collections.Generic;
използване на System.Linq;
използване на System.Net.Security;
използване на System.Net.Sockets;
използване на System.Text;
използване на System.Text.RegularExpressions;

пространство от имена ClearServer.Core.Requester
{
    публичен клас RequestContext
    {
        публичен низ Сообщение = "";
        частен readonly байт[] буфер = нов байт[1024];
        публичен низ Метод на заявка;
        публичен низ URL на заявка;
        публичен Профил на потребителя;
        публичен Потребител Текущ потребител = null;
        публичен списък<Стойности на заявка> Заглавия на стойности;
        публичен списък<Стойности на заявка> Стойности на формуляри;
        частен TcpClient TcpClient;

        частно събитие Действие<SslStream, RequestContext> OnRead = RequestHandler.OnHandle;

        DatabaseWorker databaseWorker = нов DatabaseWorker();

        публичен RequestContext(SslStream ClientStream, TcpClient Client)
        {

            this.TcpClient = Client;
            опитай
            {
                ClientStream.BeginRead(буфер, 0, буфер.Length, ClientRead, ClientStream);
            }
            хващай { връщай; }
        }
        частен void ClientRead(IAsyncResult ar)
        {
            SslStream ClientStream = (SslStream)ar.AsyncState;

            ако (ar.IsCompleted)
            {
                Сообщение = Encoding.UTF8.GetString(буфер);
                Сообщение = Uri.UnescapeDataString(Сообщение);
                Console.WriteLine(","

n{DateTime.Now:g} IP на клиента:{TcpClient.Client.RemoteEndPoint}n{Сообщение}");
RequestParse();
Заглавия на стойности = HeaderValues();
Стойности на формуляри = ContentValues();
UserParse();
ProfileParse();
OnRead?.Invoke(ClientStream, this);
}
}

частен void RequestParse()
{
Match methodParse = Regex.Match(Сообщение, @"(^w+)s+([^s?]+)[^s]*s+HTTP/.*|");
Метод на заявка = methodParse.Groups[1].Value.Trim();
URL на заявка = methodParse.Groups[2].Value.Trim();
}
частен void UserParse()
{
низ бисквитка;
try
{
ако (Заглавия на стойности.Any(x => x.Name.Contains("Cookie")))
{
бисквитка = Заглавия на стойности.FirstOrDefault(x => x.Name.Contains("Cookie")).Value;
try
{
Текущ потребител = databaseWorker.CookieValidate(бисквитка);
}
хващай { }
}
}
хващай { }

}
частен списък<Стойности на заявка> HeaderValues()
{
вар стойности = нов списък<Стойности на заявка>();
вар parse = Regex.Matches(Сообщение, @"(.*?): (.*?)n");
всяко (Match match в parse)
{
стойности.Add(нови Стойности на заявка()
{
Име = match.Groups[1].Value.Trim(),
Стойност = match.Groups[2].Value.Trim()
});
}
връщай стойности;
}

частен void ProfileParse()
{
ако (URL на заявка.Contains("@"))
{
Профил на заявка = databaseWorker.FindUser(URL на заявка.Substring(2));
URL на заявка = "\/профил";
}
}
частен списък<Стойности на заявка> ContentValues()
{
вар стойности = нов списък<Стойности на заявка>();
вар output = Сообщение.Trim('n').Split().Last();
вар parse = Regex.Matches(output, @"([^&].*?)=([^&]*b)");
всяко (Match match в parse)
{
стойности.Add(нови Стойности на заявка()
{
Име = match.Groups[1].Value.Trim(),
Стойност = match.Groups[2].Value.Trim().Replace('+', ' ')
});
}
връщай стойности;
}
}
}


Същността му е да разбие заявката на части с помощта на регулярни изрази. Получаваме съобщение от клиента, извличаме първия ред, в който е съдържан методът и URL на заявката. След това четем заглавията, които поставяме в масив от вида ИмеЗаглавия=Съдържание, както и намираме, ако има такъв, придружаващ контент (например querystring), който също поставяме в аналогичен масив. Освен това парсерът установява дали текущият клиент е авторизиран и запазва данните му. Всички заявки от авторизирани клиенти съдържат хеш на авторизация, който се съхранява в бисквитките, благодарение на което можем да разделяме по-нататъшната логика на работа за двата типа клиенти и да им даваме правилните отговори.

И малка, приятна функция, която би било хубаво да се изведе в отделен модул, е преобразуването на заявки от типа „site.com/@UserName“ в динамично генерирани страници на потребители. След обработка на заявката в действие влизат следващите модули.

Глава 3. Инсталиране на волана, смазване на веригата

След като парсерът е обработил заявката, влиза в действие обработчик, който дава по-нататъшни указания на сървера и разделя управлението на две части.

Прост обработчик

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

По същество тук има само една проверка за авторизация на потребителя, след което започва обработката на заявката.

Контролери на клиентите
Ако потребителят не е авторизиран, функционалността за него е базирана само на показването на профили на потребители и прозорец за регистрация/авторизация. Кодът за авторизирания потребител изглежда почти същия, затова не виждам смисъл да го дублирам.

Неавторизиран потребител

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

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

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

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

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

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

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

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

}

}

}
}


И разбира се, потребителят трябва да получава някакво съдържание на страниците, затова за отговорите съществува следния модул, отговарящ за отговор на заявки за ресурси.

WriterController

използвайки System;
използвайки System.IO;
използвайки System.Net.Security;
използвайки System.Text;

пространство ClearServer.Core.UserController
{
    публичен клас WriteController
    {
        SslStream ClientStream;
        публичен WriteController(SslStream ClientStream)
        {
            този.ClientStream = ClientStream;
        }

        публичен void DefaultWriter(string Header, string FilePath)
        {
            FileStream fileStream;
            опитайте
            {
                fileStream = нов FileStream(FilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
                Header =

quot;{Header}nContent-Length: {fileStream.Length}nn";
ClientStream.Write(Encoding.UTF8.GetBytes(Header));
byte[] response = нов byte[fileStream.Length];
fileStream.BeginRead(response, 0, response.Length, OnFileRead, response);
}
хващай { }
}

публичен string ContentType(string Uri)
{
string extension = Path.GetExtension(Uri);
string Header = "HTTP\/1.1 200 OKnContent-Type:";
превключване (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:
ако (extension.Length > 1)
{
return

quot;{Header} application\/" + extension.Substring(1);
}
иначе
{
return

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

публичен void OnFileRead(IAsyncResult ar)
{
ако (ar.IsCompleted)
{
var file = (byte[])ar.AsyncState;
ClientStream.BeginWrite(file, 0, file.Length, OnClientSend, null);
}
}

публичен void OnClientSend(IAsyncResult ar)
{
ако (ar.IsCompleted)
{
ClientStream.Close();
}
}
}


Но за да покажа на потребителя неговия профил и профилите на други потребители, реших да използвам RazorEngine, по-точно неговата част. Той също така включва обработка на невалидни заявки и издаване на съответния код за грешка.

RazorController

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

namespace ClearServer.Core.UserController
{
    internal class RazorController
    {
        private RequestContext Context;
        private SslStream ClientStream;
        dynamic PageContent;


        public RazorController(RequestContext context, SslStream clientStream)
        {
            this.Context = context;
            this.ClientStream = clientStream;

        }

        public void ProfileLoader(string ViewPath)
        {
            string Filepath = ViewPath + "\/profile.cshtml";
            if (Context.RequestProfile != null)
            {
                if (Context.CurrentUser != null && Context.RequestProfile.login == Context.CurrentUser.login)
                {
                    try
                    {
                        PageContent = new { isAuth = true, Name = Context.CurrentUser.name, Login = Context.CurrentUser.login, Skills = Context.CurrentUser.skills };
                        ClientSend(Filepath, Context.CurrentUser.login);
                    }
                    catch (Exception e) { Console.WriteLine(e); }

                }
                else
                {
                    try
                    {
                        PageContent = new { isAuth = false, Name = Context.RequestProfile.name, Login = Context.RequestProfile.login, Skills = Context.RequestProfile.skills };
                        ClientSend(Filepath, "PublicProfile:"+ Context.RequestProfile.login);
                    }
                    catch (Exception e) { Console.WriteLine(e); }
                }
            }
            else
            {
                ErrorLoader(404);
            }


        }

        public void ErrorLoader(int Code)
        {
            try
            {
                PageContent = new { ErrorCode = Code, Message = ((HttpStatusCode)Code).ToString() };
                string ErrorPage = "C:\Users\drdre\source\repos\ClearServer\View\Errors\ErrorPage.cshtml";
                ClientSend(ErrorPage, Code.ToString());
            }
            catch { }

        }

        private void ClientSend(string FilePath, string Key)
        {
            var template = File.ReadAllText(FilePath);
            var result = Engine.Razor.RunCompile(template, Key, null, (object)PageContent);
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(result);
            ClientStream.BeginWrite(buffer, 0, buffer.Length, OnClientSend, ClientStream);
        }

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

И, разбира се, за да функционира проверката на авторизовани потребители, е необходима авторизация. Модулът за авторизация взаимодейства с базата данни. Получените данни от формите на сайта се извличат от контекста, потребителят се запазва и получава обратно бисквитки и достъп до услугата.

Модул за авторизация

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

}
иначе
{
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);
}
иначе
{
RazorController.ErrorLoader(401);
}
}
}
}


И така изглежда обработката на базата данни:

База данни.

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 = @"пъткбазе";

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

Console.WriteLine("User {user.name} with id {user.uid} добавен");
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("Логинът вече съществува");
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("User {UserToUpdate.name} with id {UserToUpdate.uid} обновен");
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;
}
иначе
{
return null;
}
}
catch (Exception)
{
return null;
}
}
}
}


И всичко работи без проблеми, авторизацията и регистрацията работят, минималният функционал за достъп до услугата вече съществува и е време да напишем приложението и да свържем всичко това с основните функции, ради които всичко е направено.

Глава 4. Изхвърляне на велосипеда

За да намаля разходите за разработка на две приложения за две платформи, реших да създам кросплатформено приложение на Xamarin.Forms. Отново, благодарение на факта, че е на C#. След като направих тестово приложение, което просто изпраща данни на сървъра, се натъкнах на един интересен момент. За заявката от устройството, за интерес, я реализирах с HttpClient и изпратих на сървъра HttpRequestMessage, в който са включени данни от формата за автентикация в формат json. Не очаквайки нищо особено, отворих логовете на сървъра и видях там заявка от устройството с всички данни. Малко съм в шок, осъзнавайки всичко, което беше направено през последните 3 седмици мъчителни вечери. За да проверя верността на изпратените данни, създадох тестови сървър на HttpListener. Получавайки следващата заявка на него, с няколко реда код я разделих на части, получих KeyValuePair данни от формата. Разборът на заявката се сви до два реда.

Започнах да тествам по-нататък, преди не споменах, но на предишния сървър реализирах чат, който беше построен на уебсокети. Работеше доста добре, но самият принцип на взаимодействие през Tcp беше досаден, твърде много излишно трябваше да се създава, за да се организира правилно взаимодействието между двама потребители с водене на запис на кореспонденцията. Това включваше парсинг на заявката за смяна на връзката и събиране на отговора съгласно протокола RFC 6455. Поради това в тестовия сървър реших да създам прост уебсокет връзка. Изцяло от интерес.

Свързване с чата

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

И то заработи. Сървърът сам настрои връзката, генерираше отговорен ключ. Дори не се наложи отделно да настройвам регистрацията на сървъра по ssl, достатъчно беше, че в системата вече беше инсталиран сертификат на нужния порт.

От страната на устройството и от страната на сайта двата клиента обменяха съобщения, всичко това се логваше. Никакви огромни парсъри, забавящи работата на сървъра, не бяха необходими. Времето за отговор намаля от 200мс на 40-30мс. И достигнах до единственото правилно решение.

Развито велосипедостроене или клиент-серверно приложение на базата на C# .Net framework

Да изхвърля текущата реализация на сървъра на Tcp и да пренапиша всичко под Http. Сега проектът е в етап на преработка, но вече по съвсем различни принципи на взаимодействие. Работата на устройствата и сайта е синхронизирана и настроена и има обща концепция, с единственото отличие, че за устройствата не е необходимо да се генерират html страници.

Извод

«Не зная брод, не се хвърлям в вода» Мисля, че преди да започна работата, трябваше по-ясно да определя целите и задачите, както и да се задълбоча в изучаването на необходимите технологии и методи за тяхното реализиране при различни клиенти. Проектът вече близо е до завършване, но възможно е да се върна, за да разкажа как отново обърках нещата. Научих много в процеса на разработката, но има още много, което трябва да науча в бъдеще. Ако сте прочели до този момент, благодаря за това.

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster