[Разработка веб-сайтов, Разработка мобильных приложений, C#, Облачные сервисы] Продвинутое велосипедостроение или клиент-серверное приложение на базе C# .Net framework
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Вступление
Всё началось с того, что коллега предложил мне сделать небольшой веб сервис. Это должно было стать чем то вроде тиндера, но для IT тусовки. Функционал донельзя прост, регистрируешься, заполняешь профиль и переходишь к основной сути, а именно поиску собеседника и расширению своих связей и получению новых знакомств.
Здесь я должен сделать отступление и немного рассказать о себе, что бы в дальнейшем было понятнее, почему именно такие шаги в разработке я предпринимал.
На данный момент я занимаю должность Технического Артиста в одной игровой студии, мой опыт программирования на C# строился только на написании скриптов и утилит для Unity и в довесок к этому создание плагинов для низкоуровневой работы с андроид девайсами. За пределы этого мирка я ещё не выбирался и тут подвернулась такая возможность.
Часть 1. Прототипирование рамы
Решив, что из себя будет представлять данный сервис, я принялся искать варианты для реализации. Проще всего было бы найти какой то готовое решение, на которое, как сову на глобус, можно натянуть наши механики и выложить всё это дело на общественное порицание.
Но это же не интересно, никакого челенджа и смысла в этом я не видел, а посему начал изучать веб технологии и методы взаимодействия с ними.
Изучение начал с просмотра статей и документации по C# .Net. Тут я нашёл разнообразные способы для выполнения задачи. Здесь есть множество механизмов взаимодействия с сетью, от полноценных решений вроде ASP.Net или служб Azure, до прямого взаимодействия с Tcp\Http подключениями.
Сделав первую попытку с ASP я его сразу же отмёл, на мой взгляд это было слишком тяжёлым решением для нашего сервиса. Мы не будем использовать и трети возможностей этой платформы, поэтому я продолжил поиски. Выбор встал между TCP и Http клиент-сервером. Здесь же, на Хабре, я наткнулся на статью про многопоточный сервер, собрав и протестировав который, я решил остановиться именно на взаимодействии с TCP подключениями, почему то я посчитал, что http не позволит создать мне кроссплатформенное решение.
Первая версия сервера включала в себя обработку подключений, отдавала статическое содержимое веб-страниц и включала в себя базу данных пользователей. И для начала я решил строить функционал для работы с сайтом, что бы в последствии прикрутить сюда и обработку приложения на андроиде и ios.
Здесь немного кода
SPL
Основной поток, в бесконечном цикле принимающий клиентов:
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 шифрованием соединения.
Ещё немного кода, описывающий логику работы сервера и обработку клиентов
SPL
Обновлённый вариант сервера, включающий в себя использование сертификата.
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 подключении, то необходимо создать модуль, который мог распознавать контекст запроса. Я решил что здесь подойдёт парсер который будет разбивать запрос от клиента на отдельные части, с которыми я смогу взаимодействовать, что бы отдавать клиенту нужные ответы.
Парсер
SPL
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. Установка руля, смазывание цепи
Как только парсер отработал, в дело вступает обработчик, отдающий дальнейшие указания серверу и разделяющий управление на две части.
Простой обработчик
SPL
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);
};
}
}
}
По сути здесь всего одна проверка на авторизацию юзера, после чего начинается обработка запроса.
Контроллеры клиентов
SPL
Если юзер не авторизован, то для него функционал базируется только на отображении профилей пользователя и окне регистрации\авторизации. Код для авторизованного пользователя выглядит примерно так же, поэтому не вижу смысла его дублировать.
Неавторизованный пользователь
SPL
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
SPL
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
SPL
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();
}
}
}
}
Ну и конечно же, для того, что бы работала проверка авторизованных пользователей, нужна авторизация. Модуль авторизации взаимодействует с базой данных. Полученные данные из форм на сайте парсятся из контекста, юзер сохраняется и получает взамен куки и доступ к сервису.
Модуль авторизации
SPL
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);
}
}
}
}
А так выглядит обработка базы данных:
База данных
SPL
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. Поэтому в тестовом сервере я решил создать простое вебсокет соединение. Чисто ради интереса.
Подключение к чату
SPL
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
===========
Похожие новости:
- [Карьера в IT-индустрии, Разработка веб-сайтов, Учебный процесс в IT] Как вырастить веб-разработчика от стажера до архитектора. Матрица компетенций
- [Open source, Java, Облачные сервисы, Openshift] Разработка Java-приложений для Kubernetes с использованием Eclipse JKube
- [Разработка под iOS, Разработка мобильных приложений, Разработка под Android] «Ну, покати!» или CI/CD мобильных приложений на основе контракта
- [Программирование микроконтроллеров] Еще немного RISC-V
- [Информационная безопасность, Системное администрирование, Сетевые технологии, Облачные сервисы] 6. NGFW для малого бизнеса. Smart-1 Cloud
- [Настройка Linux, Разработка веб-сайтов, PHP, JavaScript, Серверное администрирование] Перенос почты между серверами через интерфейс пользователя посредством IMAPSync
- [Python, Функциональное программирование] Функциональное программирование в питоне. Генераторы, как питонячий декларативный стиль
- [.NET, ASP, C#, Программирование, Тестирование IT-систем] Я десять лет страдал от ужасных архитектур в C# приложениях — и вот нашел, как их исправить
- [Kotlin, Голосовые интерфейсы, Разработка мобильных приложений, Разработка под Android] Как встроить голосового помощника в любое мобильное приложение. Разбираем на примере Habitica
- [JavaScript, Программирование, Разработка веб-сайтов] JavaScript: область видимости простыми словами (перевод)
Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_c#, #_oblachnye_servisy (Облачные сервисы), #_veb_razrabotki (веб разработки), #_programmirovanie (программирование), #_velosipedostroenie (велосипедостроение), #_razrabotka_vebsajtov (
Разработка веб-сайтов
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_c#, #_oblachnye_servisy (
Облачные сервисы
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 23:15
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Вступление Всё началось с того, что коллега предложил мне сделать небольшой веб сервис. Это должно было стать чем то вроде тиндера, но для IT тусовки. Функционал донельзя прост, регистрируешься, заполняешь профиль и переходишь к основной сути, а именно поиску собеседника и расширению своих связей и получению новых знакомств. Здесь я должен сделать отступление и немного рассказать о себе, что бы в дальнейшем было понятнее, почему именно такие шаги в разработке я предпринимал.
На данный момент я занимаю должность Технического Артиста в одной игровой студии, мой опыт программирования на C# строился только на написании скриптов и утилит для Unity и в довесок к этому создание плагинов для низкоуровневой работы с андроид девайсами. За пределы этого мирка я ещё не выбирался и тут подвернулась такая возможность. Решив, что из себя будет представлять данный сервис, я принялся искать варианты для реализации. Проще всего было бы найти какой то готовое решение, на которое, как сову на глобус, можно натянуть наши механики и выложить всё это дело на общественное порицание. Но это же не интересно, никакого челенджа и смысла в этом я не видел, а посему начал изучать веб технологии и методы взаимодействия с ними. Изучение начал с просмотра статей и документации по C# .Net. Тут я нашёл разнообразные способы для выполнения задачи. Здесь есть множество механизмов взаимодействия с сетью, от полноценных решений вроде ASP.Net или служб Azure, до прямого взаимодействия с Tcp\Http подключениями. Сделав первую попытку с ASP я его сразу же отмёл, на мой взгляд это было слишком тяжёлым решением для нашего сервиса. Мы не будем использовать и трети возможностей этой платформы, поэтому я продолжил поиски. Выбор встал между TCP и Http клиент-сервером. Здесь же, на Хабре, я наткнулся на статью про многопоточный сервер, собрав и протестировав который, я решил остановиться именно на взаимодействии с TCP подключениями, почему то я посчитал, что http не позволит создать мне кроссплатформенное решение. Первая версия сервера включала в себя обработку подключений, отдавала статическое содержимое веб-страниц и включала в себя базу данных пользователей. И для начала я решил строить функционал для работы с сайтом, что бы в последствии прикрутить сюда и обработку приложения на андроиде и ios. Здесь немного кодаSPLОсновной поток, в бесконечном цикле принимающий клиентов:
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 шифрованием соединения. Ещё немного кода, описывающий логику работы сервера и обработку клиентовSPLОбновлённый вариант сервера, включающий в себя использование сертификата.
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 подключении, то необходимо создать модуль, который мог распознавать контекст запроса. Я решил что здесь подойдёт парсер который будет разбивать запрос от клиента на отдельные части, с которыми я смогу взаимодействовать, что бы отдавать клиенту нужные ответы. ПарсерSPLusing 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. Установка руля, смазывание цепи Как только парсер отработал, в дело вступает обработчик, отдающий дальнейшие указания серверу и разделяющий управление на две части. Простой обработчикSPLusing 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); }; } } } По сути здесь всего одна проверка на авторизацию юзера, после чего начинается обработка запроса. Контроллеры клиентовSPLЕсли юзер не авторизован, то для него функционал базируется только на отображении профилей пользователя и окне регистрации\авторизации. Код для авторизованного пользователя выглядит примерно так же, поэтому не вижу смысла его дублировать.
Неавторизованный пользовательSPLusing 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; } } } } Ну и конечно же, пользователь должен получать какое то содержимое страниц, поэтому для ответов существует следующий модуль, отвечающий за ответ на запрос ресурсов. WriterControllerSPLusing 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, вернее его часть. Он так же включает в себя обработку неверных запросов и выдачу соответствующего кода ошибки. RazorControllerSPLusing 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(); } } } } Ну и конечно же, для того, что бы работала проверка авторизованных пользователей, нужна авторизация. Модуль авторизации взаимодействует с базой данных. Полученные данные из форм на сайте парсятся из контекста, юзер сохраняется и получает взамен куки и доступ к сервису. Модуль авторизацииSPLusing 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); } } } } А так выглядит обработка базы данных: База данныхSPLusing 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. Поэтому в тестовом сервере я решил создать простое вебсокет соединение. Чисто ради интереса. Подключение к чатуSPLprivate 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 =========== Похожие новости:
Разработка веб-сайтов ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ), #_c#, #_oblachnye_servisy ( Облачные сервисы ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 23:15
Часовой пояс: UTC + 5