Úvod
Všetko sa začalo, keď mi kolega navrhol vytvoriť malý webový servis. Malo to byť niečo ako Tinder, ale pre IT komunitu. Funkcionalita je nesmierne jednoduchá, zaregistruješ sa, vyplníš profil a prejdeš k hlavnej podstate, teda k vyhľadávaniu spoločníka, rozširovaniu svojich kontaktov a nadobúdaniu nových známostí.
Tu by som mal urobiť odbočku a trochu sa o sebe porozprávať, aby bolo neskôr jasnejšie, prečo som prijal takéto kroky vo vývoji.
V súčasnosti zastávam pozíciu Technického Umeleckého v jednej hernej štúdiu, moja programátorská skúsenosť v C# sa budovala iba na písaní skriptov a utilít pre Unity a k tomu tvorbe pluginov na nízkoúrovňovú prácu s android zariadeniami. Mimo tohto malého sveta som sa zatiaľ nedostal a teraz sa naskytla takáto príležitosť.
Časť 1. Prototypovanie rámu
Keď som sa rozhodol, aký bude tento servis, začal som hľadať možnosti realizácie. Najjednoduchšie by bolo nájsť nejaké hotové riešenie, na ktoré by sme mohli natiahnuť naše mechaniky a zverejniť to na verejné posúdenie.
Ale to nie je zábavné, žiadna výzva a zmysel v tom som nevidel, a preto som začal študovať webové technológie a metódy interakcie s nimi.
Štúdium som začal prezeraním článkov a dokumentácie o C# .Net. Tu som našiel rôzne spôsoby, ako splniť úlohu. Existuje množstvo mechanizmov interakcie so sieťou, od komplexných riešení ako ASP.Net alebo Azure služby, po priamu interakciu s TcpHttp pripojeniami.
Po prvej pokusnej aplikácii s ASP som ho rýchlo zavrhol, podľa môjho názoru to bolo príliš zložité riešenie pre náš servis. Nevyužijeme ani tretinu možností tejto platformy, a preto som pokračoval v hľadaní. Rozhodnutie sa medzi TCP a Http klient-serverom. Na Habr som narazil na článok o , ktorý som poskladal a otestoval, a rozhodol som sa zastaviť práve na interakcii s TCP pripojeniami, lebo som považoval, že http mi nedovolí vytvoriť multiplatformové riešenie.
Prvá verzia servera zahŕňala spracovanie pripojení, poskytovala statický obsah webových stránok a obsahovala databázu používateľov. A na začiatok som sa rozhodol postaviť funkcie na prácu so stránkami, aby som neskôr pripojil aj spracovanie aplikácií pre Android a iOS.
Tu je trochu kódu
Hlavný tok, ktorý v nekonečnej slučke prijíma klientov:
using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;
namespace ClearServer
{
class Server
{
TcpListener Listener;
public Server(int Port)
{
Listener = new TcpListener(IPAddress.Any, Port);
Listener.Start();
while (true)
{
TcpClient Client = Listener.AcceptTcpClient();
Thread Thread = new Thread(new ParameterizedThreadStart(ClientThread));
Thread.Start(Client);
}
}
static void ClientThread(Object StateInfo)
{
new Client((TcpClient)StateInfo);
}
~Server()
{
if (Listener != null)
{
Listener.Stop();
}
}
static void Main(string[] args)
{
DatabaseWorker sqlBase = DatabaseWorker.GetInstance;
new Server(80);
}
}
}Sám spracovateľ klientov:
using System; using System.IO; using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; namespace ClearServer { class Client { public Client(TcpClient Client) { string Message = ""; byte[] Buffer = new byte[1024]; int Count; while ((Count = Client.GetStream().Read(Buffer, 0, Buffer.Length)) > 0) { Message += Encoding.UTF8.GetString(Buffer, 0, Count); if (Message.IndexOf("rnrn") >= 0 || Message.Length > 4096) { Console.WriteLine(Message); break; } } Match ReqMatch = Regex.Match(Message, @"^w+s+([^s?]+)[^s]*s+HTTP\/.*|"); if (ReqMatch == Match.Empty) { ErrorWorker.SendError(Client, 400); return; } string RequestUri = ReqMatch.Groups[1].Value; RequestUri = Uri.UnescapeDataString(RequestUri); if (RequestUri.IndexOf("..") >= 0) { ErrorWorker.SendError(Client, 400); return; } if (RequestUri.EndsWith("\/")) { RequestUri += "index.html"; } string FilePath ="D:\Web\TestSite{RequestUri}";
if (!File.Exists(FilePath))
{
ErrorWorker.SendError(Client, 404);
return;
}string Extension = RequestUri.Substring(RequestUri.LastIndexOf('.'));
string ContentType = "";
switch (Extension)
{
case ".htm":
case ".html":
ContentType = "text/html";
break;
case ".css":
ContentType = "text/css";
break;
case ".js":
ContentType = "text/javascript";
break;
case ".jpg":
ContentType = "image/jpeg";
break;
case ".jpeg":
case ".png":
case ".gif":
ContentType ="image/{Extension.Substring(1)}";
break;
default:
if (Extension.Length > 1)
{
ContentType ="application/{Extension.Substring(1)}";
}
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();
}
}
}
A prvá databáza postavená na lokalnom SQL:using System; using System.Data.Linq; namespace ClearServer { class DatabaseWorker { private static DatabaseWorker instance; public static DatabaseWorker GetInstance { get { if (instance == null) instance = new DatabaseWorker(); return instance; } } private DatabaseWorker() { string connectionStr = databasePath; using (DataContext db = new DataContext(connectionStr)) { Table users = db.GetTable(); foreach (var item in users) { Console.WriteLine("{item.login} {item.password}");"{item.login} {item.password}"
}
}
}
}
}
Ako môžete vidieť, táto verzia sa málo líši od tej, ktorá bola v článku. V podstate sa tu len pridalo načítanie stránok z priečinka na počítači a databáza (ktorá mimochodom v tejto verzii nefungovala kvôli nesprávnej architektúre pripojenia).Kapitola 2. Pripojenie kolies
Po vyskúšaní práce servera som dospel k záveru, že to bude skvelé riešenie (spoiler: nie), pre našu službu, takže projekt začal naberať logiku.
Krok za krokom sa začali objavovať nové moduly a funkcie servera sa rozširovali. Server dostal testovaciu doménu a ssl šifrovanie pripojenia.Ešte trochu kódu, ktorý opisuje logiku práce servera a spracovanie klientov
Aktualizovaná verzia servera, ktorá zahŕňa použitie certifikátu.using System; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Security; using System.Security.Cryptography.X509Certificates; using System.Security.Permissions; using System.Security.Policy; using System.Threading; namespace ClearServer { sealed class Server { readonly bool ServerRunning = true; readonly TcpListener sslListner; public static X509Certificate serverCertificate = null; Server() { serverCertificate = X509Certificate.CreateFromSignedFile(@"C:sslitinder.online.crt"); sslListner = new TcpListener(IPAddress.Any, 443); sslListner.Start(); Console.WriteLine("Starting server.." + serverCertificate.Subject + "n" + Assembly.GetExecutingAssembly().Location); while (ServerRunning) { TcpClient SslClient = sslListner.AcceptTcpClient(); Thread SslThread = new Thread(new ParameterizedThreadStart(ClientThread)); SslThread.Start(SslClient); } } static void ClientThread(Object StateInfo) { new Client((TcpClient)StateInfo); } ~Server() { if (sslListner != null) { sslListner.Stop(); } } public static void Main(string[] args) { if (AppDomain.CurrentDomain.IsDefaultAppDomain()) { Console.WriteLine("Switching another domain"); new AppDomainSetup { ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase }; var current = AppDomain.CurrentDomain; var strongNames = new StrongName[0]; var domain = AppDomain.CreateDomain( "ClearServer", null, current.SetupInformation, new PermissionSet(PermissionState.Unrestricted), strongNames); domain.ExecuteAssembly(Assembly.GetExecutingAssembly().Location); } new Server(); } } }A tiež nový spracovateľ klienta s autentifikáciou cez 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( "--------------------------------------------------------------------- " +"|{DateTime.Now:g} |------------ |{Client.Client.RemoteEndPoint} |------------ |Exception: {e.Message} |------------ |Autentifikácia zlyhala - zatváram spojenie. " +
"--------------------------------------------------------------------- ");
SSlClientStream.Close();
Client.Close();
}
new RequestContext(SSlClientStream, Client);
}}
}
Keďže server funguje výhradne na pripojení TCP, je potrebné vytvoriť modul, ktorý dokáže rozpoznať kontext požiadavky. Rozhodol som sa, že tu bude vhodný parser, ktorý rozdelí požiadavku od klienta na jednotlivé časti, s ktorými môžem interagovať, aby som vracal klientovi požadované odpovede.Parser
using ClearServer.Core.UserController; using ReServer.Core.Classes; using System; using System.Collections.Generic; using System.Linq; using System.Net.Security; using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; namespace ClearServer.Core.Requester { public class RequestContext { public string Message = ""; private readonly byte[] buffer = new byte[1024]; public string RequestMethod; public string RequestUrl; public User RequestProfile; public User CurrentUser = null; public List HeadersValues; public List FormValues; private TcpClient TcpClient; private event Action OnRead = RequestHandler.OnHandle; DatabaseWorker databaseWorker = new DatabaseWorker(); public RequestContext(SslStream ClientStream, TcpClient Client) { this.TcpClient = Client; try { ClientStream.BeginRead(buffer, 0, buffer.Length, ClientRead, ClientStream); } catch { return; } } private void ClientRead(IAsyncResult ar) { SslStream ClientStream = (SslStream)ar.AsyncState; if (ar.IsCompleted) { Message = Encoding.UTF8.GetString(buffer); Message = Uri.UnescapeDataString(Message); Console.WriteLine(","540652":"{DateTime.Now:g} Client IP:{TcpClient.Client.RemoteEndPoint}n{Message}");","540653":"RequestParse();","540654":"HeadersValues = HeaderValues();","540655":"FormValues = ContentValues();","540656":"UserParse();","540657":"ProfileParse();","540658":"OnRead?.Invoke(ClientStream, this);","540659":"private void RequestParse()","540660":"Match methodParse = Regex.Match(Message, @"(^w+)s+([^s?]+)[^s]*s+HTTP\/.*|\");","540661":"RequestMethod = methodParse.Groups[1].Value.Trim();","540662":"RequestUrl = methodParse.Groups[2].Value.Trim();","540663":"private void UserParse()","540664":"string cookie;","540665":"if (HeadersValues.Any(x => x.Name.Contains("Cookie")))","540666":"cookie = HeadersValues.FirstOrDefault(x => x.Name.Contains("Cookie")).Value;","540667":"CurrentUser = databaseWorker.CookieValidate(cookie);","540668":"catch { }","540669":"private List HeaderValues()","540670":"var values = new List();","540671":"var parse = Regex.Matches(Message, @"(.*?): (.*?)n\");","540672":"foreach (Match match in parse)","540673":"values.Add(new RequestValues()","540674":"Name = match.Groups[1].Value.Trim(),","540675":"Value = match.Groups[2].Value.Trim()","540676":"return values;","540677":"private void ProfileParse()","540678":"if (RequestUrl.Contains("@"))","540679":"RequestProfile = databaseWorker.FindUser(RequestUrl.Substring(2));","540680":"RequestUrl = "\/profile";","540681":"private List ContentValues()","540682":"var output = Message.Trim('n').Split().Last();","540683":"var parse = Regex.Matches(output, @"([^&].*?)=([^&]*)");","540684":"Value = match.Groups[2].Value.Trim().Replace('+', ' '){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 HeaderValues()
{
var values = new List();
var parse = Regex.Matches(Message, @"(.*?): (.*?)n");
foreach (Match match in parse)
{
values.Add(new RequestValues()
{
Name = match.Groups[1].Value.Trim(),
Value = match.Groups[2].Value.Trim()
});
}
return values;
}private void ProfileParse()
{
if (RequestUrl.Contains("@"))
{
RequestProfile = databaseWorker.FindUser(RequestUrl.Substring(2));
RequestUrl = "\/profile";
}
}
private List ContentValues()
{
var values = new List();
var output = Message.Trim('n').Split().Last();
var parse = Regex.Matches(output, @"([^&].*?)=([^&]*)");
foreach (Match match in parse)
{
values.Add(new RequestValues()
{
Name = match.Groups[1].Value.Trim(),
Value = match.Groups[2].Value.Trim().Replace('+', ' ')
});
}
return values;
}
}
}
Podstata spočíva v tom, aby sme pomocou regulárnych výrazov rozdelili požiadavku na časti. Získame správu od klienta, vyberieme prvý riadok, v ktorom je uvedená metóda a URL požiadavky. Potom prečítame hlavičky, ktoré vkladáme do poľa vo forme NázovHlava=Obsah, a takisto nájdeme, ak je, sprievodný obsah (napríklad querystring), ktorý tiež vkladáme do podobného poľa. Navyše, parser zisťuje, či je aktuálny klient autorizovaný a uchováva jeho údaje. Všetky požiadavky od autorizovaných klientov obsahujú hash autorizácie, ktorý sa uchováva v cookies, vďaka čomu môžeme oddeľovať ďalšiu logiku fungovania pre dva typy klientov a poskytovať im správne odpovede.A malá, príjemná funkcia, ktorú by bolo dobré presunúť do samostatného modulu, je transformácia požiadaviek vo forme „site.com/@UserName“ na dynamicky generované stránky používateľov. Po spracovaní požiadavky sa zapoja nasledujúce moduly.
Kapitola 3. Montáž volantu, mazanie reťaze
Hneď ako parser dokončí prácu, zapája sa spracovateľ, ktorý poskytuje ďalšie pokyny serveru a delí riadenie na dve časti.
Jednoduchý spracovateľ
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); }; } } }Vo svojej podstate ide len o jedno overenie autorizácie používateľa, po ktorom začína spracovanie požiadavky.
Kontroléry klientov
Ak používateľ nie je autorizovaný, jeho funkčnosť je založená len na zobrazovaní profilov používateľov a okne registrácie/autorizačného procesu. Kód pre autorizovaného používateľa vyzerá približne rovnako, takže nemám zmysel ho duplikovať.Neautorizovaný používateľ
using ClearServer.Core.Requester; using System.IO; using System.Net.Security; namespace ClearServer.Core.UserController { internal class NonAuthUserController { private readonly SslStream ClientStream; private readonly RequestContext Context; private readonly WriteController WriteController; private readonly AuthorizationController AuthorizationController; private readonly string ViewPath = "C:\/Users\/drdre\/source\/repos\/ClearServer\/View"; public NonAuthUserController(SslStream clientStream, RequestContext context) { this.ClientStream = clientStream; this.Context = context; this.WriteController = new WriteController(clientStream); this.AuthorizationController = new AuthorizationController(clientStream, context); ResourceLoad(); } void ResourceLoad() { string[] blockextension = new string[] {"cshtml", "html", "htm"}; bool block = false; foreach (var item in blockextension) { if (Context.RequestUrl.Contains(item)) { block = true; break; } } string FilePath = ""; string Header = ""; var RazorController = new RazorController(Context, ClientStream); switch (Context.RequestMethod) { case "GET": switch (Context.RequestUrl) { case "\/": FilePath = ViewPath + "\/loginForm.html"; Header =quot;HTTP\/1.1 200 OKnContent-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;}
}
}
}
Samozrejme, že používateľ musí dostávať nejaký obsah stránok, preto pre odpovede existuje nasledujúci modul, ktorý je zodpovedný za odpovedanie na požiadavky zdrojov.WriterController
using System; using System.IO; using System.Net.Security; using System.Text; namespace ClearServer.Core.UserController { public class WriteController { SslStream ClientStream; public WriteController(SslStream ClientStream) { this.ClientStream = ClientStream; } public void DefaultWriter(string Header, string FilePath) { FileStream fileStream; try { fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); Header =quot;{Header}nContent-Length: {fileStream.Length}nn";
ClientStream.Write(Encoding.UTF8.GetBytes(Header));
byte[] response = new byte[fileStream.Length];
fileStream.BeginRead(response, 0, response.Length, OnFileRead, response);
}
catch { }
}public string ContentType(string Uri)
{
string extension = Path.GetExtension(Uri);
string Header = "HTTP\/1.1 200 OKnContent-Type:";
switch (extension)
{
case ".html":
case ".htm":
returnquot;{Header} text\/html";
case ".css":
returnquot;{Header} text\/css";
case ".js":
returnquot;{Header} text\/javascript";
case ".jpg":
case ".jpeg":
case ".png":
case ".gif":
returnquot;{Header} image\/ {extension}";
default:
if (extension.Length > 1)
{
returnquot;{Header} application\/" + extension.Substring(1);
}
else
{
returnquot;{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();
}
}
}
Aby som mohol používateľovi zobraziť jeho profil a profily ostatných používateľov, rozhodol som sa použiť RazorEngine, presnejšie jeho časť. Zohľadňuje aj spracovanie nesprávnych požiadaviek a vydávanie príslušného chybového kódu.RazorController
using ClearServer.Core.Requester; using RazorEngine; using RazorEngine.Templating; using System; using System.IO; using System.Net; using System.Net.Security; namespace ClearServer.Core.UserController { internal class RazorController { private RequestContext Context; private SslStream ClientStream; dynamic PageContent; public RazorController(RequestContext context, SslStream clientStream) { this.Context = context; this.ClientStream = clientStream; } public void ProfileLoader(string ViewPath) { string Filepath = ViewPath + "\/profile.cshtml"; if (Context.RequestProfile != null) { if (Context.CurrentUser != null && Context.RequestProfile.login == Context.CurrentUser.login) { try { PageContent = new { isAuth = true, Name = Context.CurrentUser.name, Login = Context.CurrentUser.login, Skills = Context.CurrentUser.skills }; ClientSend(Filepath, Context.CurrentUser.login); } catch (Exception e) { Console.WriteLine(e); } } else { try { PageContent = new { isAuth = false, Name = Context.RequestProfile.name, Login = Context.RequestProfile.login, Skills = Context.RequestProfile.skills }; ClientSend(Filepath, "PublicProfile:" + Context.RequestProfile.login); } catch (Exception e) { Console.WriteLine(e); } } } else { ErrorLoader(404); } } public void ErrorLoader(int Code) { try { PageContent = new { ErrorCode = Code, Message = ((HttpStatusCode)Code).ToString() }; string ErrorPage = "C:\Users\drdre\source\repos\ClearServer\View\Errors\ErrorPage.cshtml"; ClientSend(ErrorPage, Code.ToString()); } catch { } } private void ClientSend(string FilePath, string Key) { var template = File.ReadAllText(FilePath); var result = Engine.Razor.RunCompile(template, Key, null, (object)PageContent); byte[] buffer = System.Text.Encoding.UTF8.GetBytes(result); ClientStream.BeginWrite(buffer, 0, buffer.Length, OnClientSend, ClientStream); } private void OnClientSend(IAsyncResult ar) { if (ar.IsCompleted) { ClientStream.Close(); } } } }A samozrejme, aby fungovala kontrola autorizovaných používateľov, je potrebná autorizácia. Modúl autorizácie interaguje s databázou. Získané údaje z formulárov na webovej stránke sú spracované z kontextu, používateľ je zachovaný a na oplátku dostáva cookies a prístup k službe.
Modul autorizácie
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);
}
}
}
}
Takto vyzerá spracovanie databázy:Databáza
using ClearServer.Core.UserController; using System; using System.Data.Linq; using System.Linq; namespace ClearServer { class DatabaseWorker { private readonly Table users = null; private readonly DataContext DataBase = null; private const string connectionStr = @"cestakbaze"; public DatabaseWorker() { DataBase = new DataContext(connectionStr); users = DataBase.GetTable(); } public User UserAuth(User User) { try { var user = users.SingleOrDefault(t => t.login.ToLower() == User.login.ToLower() && t.password == User.password); if (user != null) return user; else return null; } catch (Exception) { return null; } } public void UserRegister(User user) { try { users.InsertOnSubmit(user); DataBase.SubmitChanges(); Console.WriteLine("User {user.name} with id {user.uid} added");Console.WriteLine("User {user.name} with id {user.uid} 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 už existuje");
return false;
}
return true;
}
public void UserUpdate(User user)
{
var UserToUpdate = users.FirstOrDefault(x => x.uid == user.uid);
UserToUpdate = user;
DataBase.SubmitChanges();
Console.WriteLine(Console.WriteLine("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;
}
}
}
}
A všetko funguje ako hodinky, autorizácia a registrácia fungujú, minimálna funkcionalita prístupu k službe už existuje a prišiel čas napísať aplikáciu a obaliť to všetko hlavnými funkciami, kvôli ktorým sa to všetko robí.Kapitola 4. Vytváranie vlastného riešenia
Aby som skrátil pracovné úsilie na písanie dvoch aplikácií pre dve platformy, rozhodol som sa urobiť multiplatformu na Xamarin.Forms. Znovu, vďaka tomu, že je na C#. Po vytvorení testovacej aplikácie, ktorá jednoducho posiela údaje na server, som sa stretol s jedným zaujímavým momentom. Pre požiadavku z zariadenia som zo záujmu implementoval HttpClient a odoslal na server HttpRequestMessage, ktorý obsahuje údaje z autorizačného formulára vo formáte json. Bez veľkých očakávaní som otvoril log servera a tam som videl požiadavku z zariadenia so všetkými údajmi. Malý šok, uvedomenie si všetkého, čo sa urobilo za posledné 3 týždne tých únavných večerov. Aby som overil správnosť odoslaných údajov, postavil som testovací server na HttpListener. Po prijatí ďalšej požiadavky na ňom som za pár riadkov kódu rozobral požiadavku na časti a získal KeyValuePair údajov z formulára. Rozbor požiadavky sa zredukoval na dva riadky.
Pokračoval som v testovaní, predtým nebolo spomenuté, ale na predchádzajúcom serveri som tiež implementoval chat postavený na websocketoch. Fungoval celkom dobre, ale samotný princíp interakcie cez Tcp bol odstrašujúci, bolo potrebné vygenerovať príliš veľa nadbytočných vecí, aby sa správne zrealizovala interakcia dvoch používateľov s vedením logu konverzácie. To zahŕňalo parsovanie požiadavky na účely prepínania spojenia a zhromažďovanie odpovedí podľa protokolu RFC 6455. Preto v testovacom serveri som sa rozhodol vytvoriť jednoduché websocketové spojenie. Čisto zo zvedavosti.
Pripojenie k chatu
private static async void HandleWebsocket(HttpListenerContext context) { var socketContext = await context.AcceptWebSocketAsync(null); var socket = socketContext.WebSocket; Locker.EnterWriteLock(); try { Clients.Add(socket); } finally { Locker.ExitWriteLock(); } while (true) { var buffer = new ArraySegment(new byte[1024]); var result = await socket.ReceiveAsync(buffer, CancellationToken.None); var str = Encoding.Default.GetString(buffer); Console.WriteLine(str); for (int i = 0; i < Clients.Count; i++) { WebSocket client = Clients[i]; try { if (client.State == WebSocketState.Open) { await client.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None); } } catch (ObjectDisposedException) { Locker.EnterWriteLock(); try { Clients.Remove(client); i--; } finally { Locker.ExitWriteLock(); } } } } }A to fungovalo. Server sám nastavoval spojenie, generoval odpoveďový kľúč. Nemusel som ani osobitne nastavovať registráciu servera cez ssl, stačilo, že je v systéme už nainštalovaný certifikát na príslušnom porte.
Na strane zariadenia a na strane webu sa dva klienti vymieňali správami, všetko sa logovalo. Neboli potrebné žiadne obrovské parséry, spomaľujúce prácu servera, nič také nebolo potrebné. Čas odozvy sa skrátil z 200 ms na 40-30 ms. A dospel som k jedinému správnemu riešeniu.
Zbaviť sa aktuálnej implementácie servera na Tcp a prepísať všetko na Http. Teraz je projekt v štádiu preprojektovania, ale už podľa úplne iných princípov interakcie. Práca zariadení a webu je synchronizovaná a ladená a má spoločnú koncepciu, s tým rozdielom, že pre zariadenia nie je potrebné generovať html stránky.
Záver
„Nechoď k vode, o ktorej nič nevieš“ Myslím, že pred začiatkom prác som mal jasnejšie definovať ciele a úlohy, a zároveň sa hlbšie ponoriť do štúdia potrebných technológií a metód ich implementácie na rôznych klientoch. Projekt sa už blíži k dokončeniu, ale možno sa ešte vrátim, aby som porozprával o tom, ako som znovu spravil nejaké chyby. Počas vývoja som sa veľa naučil, ale ešte viac ma čaká v budúcnosti. Ak ste došli až sem, ďakujem za to.
Zdroj: habr.com

