Продвинутое велосипедостроение или клиент-серверное приложение на базе 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 страницы.

Вывод

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

ссылка на оригинал статьи https://habr.com/ru/post/517670/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *