Русский
Русский
English
Статистика
Реклама

Из песочницы Продвинутое велосипедостроение или клиент-серверное приложение на базе C .Net framework

Вступление


Всё началось с того, что коллега предложил мне сделать небольшой веб сервис. Это должно было стать чем то вроде тиндера, но для IT тусовки. Функционал донельзя прост, регистрируешься, заполняешь профиль и переходишь к основной сути, а именно поиску собеседника и расширению своих связей и получению новых знакомств.
Здесь я должен сделать отступление и немного рассказать о себе, что бы в дальнейшем было понятнее, почему именно такие шаги в разработке я предпринимал.

На данный момент я занимаю должность Технического Артиста в одной игровой студии, мой опыт программирования на C# строился только на написании скриптов и утилит для Unity и в довесок к этому создание плагинов для низкоуровневой работы с андроид девайсами. За пределы этого мирка я ещё не выбирался и тут подвернулась такая возможность.

Часть 1. Прототипирование рамы


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

Изучение начал с просмотра статей и документации по C# .Net. Тут я нашёл разнообразные способы для выполнения задачи. Здесь есть множество механизмов взаимодействия с сетью, от полноценных решений вроде ASP.Net или служб Azure, до прямого взаимодействия с Tcp\Http подключениями.

Сделав первую попытку с ASP я его сразу же отмёл, на мой взгляд это было слишком тяжёлым решением для нашего сервиса. Мы не будем использовать и трети возможностей этой платформы, поэтому я продолжил поиски. Выбор встал между TCP и Http клиент-сервером. Здесь же, на Хабре, я наткнулся на статью про многопоточный сервер, собрав и протестировав который, я решил остановиться именно на взаимодействии с TCP подключениями, почему то я посчитал, что http не позволит создать мне кроссплатформенное решение.

Первая версия сервера включала в себя обработку подключений, отдавала статическое содержимое веб-страниц и включала в себя базу данных пользователей. И для начала я решил строить функционал для работы с сайтом, что бы в последствии прикрутить сюда и обработку приложения на андроиде и 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("\r\n\r\n") >= 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)}";                    }                    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 = $"HTTP/1.1 200 OK\nContent-Type: {ContentType}\nContent-Length: {FS.Length}\n\n";            byte[] HeadersBuffer = Encoding.ASCII.GetBytes(Headers);            Client.GetStream().Write(HeadersBuffer, 0, HeadersBuffer.Length);            while (FS.Position < FS.Length)            {                Count = FS.Read(Buffer, 0, Buffer.Length);                Client.GetStream().Write(Buffer, 0, Count);            }            FS.Close();            Client.Close();        }    }}

И первая база данных построенная на 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<User> users = db.GetTable<User>();                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:\ssl\itinder.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 подключении, то необходимо создать модуль, который мог распознавать контекст запроса. Я решил что здесь подойдёт парсер который будет разбивать запрос от клиента на отдельные части, с которыми я смогу взаимодействовать, что бы отдавать клиенту нужные ответы.

Парсер
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($"\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;        }    }}


Суть его заключается в том, что бы при помощи регулярных выражений разбить запрос на части. Получаем сообщение от клиента, выделяем первую строку, в которой содержится метод и 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
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 = $"{Header}\nContent-Length: {fileStream.Length}\n\n";                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 OK\nContent-Type:";            switch (extension)            {                case ".html":                case ".htm":                    return $"{Header} text/html";                case ".css":                    return $"{Header} text/css";                case ".js":                    return $"{Header} text/javascript";                case ".jpg":                case ".jpeg":                case ".png":                case ".gif":                    return $"{Header} image/{extension}";                default:                    if (extension.Length > 1)                    {                        return $"{Header} application/" + extension.Substring(1);                    }                    else                    {                        return $"{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();            }        }    }


Но что бы показывать пользователю его профиль и профили других пользователей я решил использовать 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\nLocation: /@{user.login}\nSet-Cookie: {cookies.AuthCookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnly\n\n");                ClientStream.BeginWrite(response, 0, response.Length, WriteController.OnClientSend, null);            }            else            {                RazorController.ErrorLoader(401);            }        }        private void Registration()        {            var values = Context.FormValues;            var user = new User()            {                name = values[0].Value,                login = values[1].Value,                password = PasswordHasher.PasswordHash(values[2].Value),            };            cookies = new UserCookies(user.login, user.password);            user.cookie = cookies.AuthCookie;            if (DatabaseWorker.LoginValidate(user.login))            {                Console.WriteLine("User ready");                Console.WriteLine($"{user.password} {user.password.Trim().Length}");                DatabaseWorker.UserRegister(user);                var response = Encoding.UTF8.GetBytes($"HTTP/1.1 301 Moved Permanently\nLocation: /@{user.login}\nSet-Cookie: {user.cookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnly\n\n");                ClientStream.BeginWrite(response, 0, response.Length, WriteController.OnClientSend, null);            }            else            {                RazorController.ErrorLoader(401);            }        }    }}


А так выглядит обработка базы данных:

База данных
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($"User{user.name} with id {user.uid} added");                foreach (var item in users)                {                    Console.WriteLine(item.login + "\n");                }            }            catch (Exception e)            {                Console.WriteLine(e);            }                    }        public bool LoginValidate(string login)        {            if (users.Any(x => x.login.ToLower() == login.ToLower()))            {                Console.WriteLine("Login already exists");                return false;            }            return true;        }        public void UserUpdate(User user)        {            var UserToUpdate = users.FirstOrDefault(x => x.uid == user.uid);            UserToUpdate = user;            DataBase.SubmitChanges();            Console.WriteLine($"User {UserToUpdate.name} with id {UserToUpdate.uid} updated");            foreach (var item in users)            {                Console.WriteLine(item.login + "\n");            }        }        public User CookieValidate(string CookieInput)        {            User user = null;            try            {                user = users.SingleOrDefault(x => x.cookie == CookieInput);            }            catch            {                return null;            }            if (user != null) return user;            else return null;        }        public User FindUser(string login)        {            User user = null;            try            {                user = users.Single(x => x.login.ToLower() == login.ToLower());                if (user != null)                {                    return user;                }                else                {                    return null;                }            }            catch (Exception)            {                return null;            }        }    }}


И всё работает как часы, авторизация и регистрация работает, минимальный функционал доступа к сервису уже имеется и пришла пора писать приложение и обвязывать всё это дело основными функциями, ради которых всё и делается.

Глава 4. Выбрасывание велосипеда


Что бы сократить трудозатраты на написание двух приложений под две платформы, я решил сделать кроссплатформу на Xamarin.Forms. Опять же, благодаря тому, что она на C#. Сделав тестовое приложение, которое просто отсылает серверу данные, я столкнулся с одним интересным моментом. Для запроса от устройства я для интереса реализовал его на HttpClient и кинул на сервер HttpRequestMessage в котором содержатся данные из формы авторизации в формате json. Особо ничего не ожидая, открыл лог сервера и увидел там реквест с девайса со всеми данными. Лёгкий ступор, осознание всего, что было проделано за последние 3 недели томных вечером. Для проверки верности отправленных данных собрал тестовый сервер на HttpListner. Получив очередной запрос уже на нём, я за пару строк кода разобрал его на части, получил 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<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();                        }                    }                }            }        }



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

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

Выкинуть текущую реализацию сервера на Tcp и переписать всё под Http. Теперь же проект находится в стадии перепроектирования, но уже по совсем другим принципам взаимодействия. Работа устройств и сайта синхронизирована и отлажена и имеет общую концепцию, с тем лишь отличием, что для девайсов не нужно генерировать html страницы.

Вывод


Не зная броду, не суйся в воду думаю, перед началом работ, мне следовало более четко определить цели и задачи, а так же углубиться в изучение необходимых технологий и методах их реализации на различных клиентах. Проект уже близиться к завершению, но возможно я ещё вернусь, что бы рассказать о том, как я снова зафакапил те или иные вещи. Я многому научился в процессе разработки, но ещё большему предстоит научиться в дальнейшем. Если вы дочитали до этого момента, то спасибо за это.
Источник: habr.com
К списку статей
Опубликовано: 03.09.2020 14:17:53
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Разработка веб-сайтов

Разработка мобильных приложений

C

Облачные сервисы

Веб разработки

Программирование

Велосипедостроение

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru