ulazak
Sve je počelo kada mi je kolega predložio da napravim mali web servis. Trebalo je da bude nešto poput Tindera, ali za IT publiku. Funkcionalnost je krajnje jednostavna, registrujete se, popunjavate profil i prelazite na glavnu stvar, odnosno pronalaženje osobe za razgovor i proširenje vaših veza i sklapanje novih poznanstava.
Ovdje se moram povući i ispričati nešto o sebi, kako bi u budućnosti bilo jasnije zašto sam poduzeo takve korake u razvoju.
Trenutno sam na poziciji tehničkog umjetnika u jednom studiju za igre, moje iskustvo programiranja u C#-u izgrađeno je samo na pisanju skripti i uslužnih programa za Unity i, pored ovoga, kreiranju dodataka za rad na niskom nivou sa Android uređajima. Nisam se još usudio izaći izvan ovog malog svijeta, a onda mi se ukazala takva prilika.
Dio 1. Izrada prototipa okvira
Odlučivši kakva će biti ova usluga, počeo sam tražiti opcije za implementaciju. Najlakše bi bilo pronaći nekakvo gotovo rješenje, na koje se, kao sova na globusu, može navući naša mehanika i cijela stvar izložiti javnoj osudi.
Ali ovo nije zanimljivo, nisam vidio nikakav izazov ili smisao u tome, pa sam počeo proučavati web tehnologije i metode interakcije s njima.
Počeo sam proučavati gledajući članke i dokumentaciju o C# .Net. Ovdje sam pronašao razne načine da izvršim zadatak. Postoji mnogo mehanizama za interakciju s mrežom, od punopravnih rješenja poput ASP.Net ili Azure usluga, do direktne interakcije s TcpHttp konekcijama.
Nakon što sam napravio prvi pokušaj sa ASP-om, odmah sam ga odbio, po mom mišljenju, ovo je bila preteška odluka za našu uslugu. Ne bismo koristili ni trećinu mogućnosti ove platforme, pa sam nastavio potragu. Izbor je bio između TCP i Http klijent-server. Ovdje, na Habréu, naišao sam na članak o tome , nakon što sam ga prikupio i testirao, odlučio sam da se posebno fokusiram na interakciju sa TCP konekcijama, iz nekog razloga sam mislio da mi http neće dozvoliti da kreiram višeplatformsko rešenje.
Prva verzija servera je uključivala obradu veze, služila je statički sadržaj web stranice i uključivala je korisničku bazu podataka. I za početak, odlučio sam da napravim funkcionalnost za rad sa sajtom, kako bih kasnije mogao da dodam obradu aplikacije na Android i iOS.
Evo malo koda
Glavna nit koja prima klijente u beskrajnoj petlji:
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);
}
}
}Sam rukovalac klijentom:
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 =quot;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 =quot;image/{Extension.Substring(1)}";
break;
default:
if (Extension.Length > 1)
{
ContentType =quot;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 =
quot;HTTP/1.1 200 OKnContent-Type: {ContentType}nContent-Length: {FS.Length}nn";
byte[] HeadersBuffer = Encoding.ASCII.GetBytes(Headers);
Client.GetStream().Write(HeadersBuffer, 0, HeadersBuffer.Length);while (FS.Position < FS.Length)
{
Count = FS.Read(Buffer, 0, Buffer.Length);
Client.GetStream().Write(Buffer, 0, Count);
}FS.Close();
Client.Close();
}
}
}
I prva baza podataka izgrađena na lokalnom SQL-u: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(quot;{item.login} {item.password}");
}
}
}
}
}
Kao što vidite, ova verzija se malo razlikuje od one u članku. Zapravo, ovdje smo samo dodali učitavanje stranica iz foldera na računaru i baze podataka (što, inače, u ovoj verziji nije radilo zbog neispravne arhitekture povezivanja).Poglavlje 2. Zavrtnje točkova
Nakon testiranja servera, došao sam do zaključka da bi ovo bilo odlično rješenje (spojler: ne), za našu uslugu, pa je projekat počeo da dobija logiku.
Korak po korak, počeli su se pojavljivati novi moduli i funkcionalnost servera se proširila. Server je stekao testnu domenu i ssl šifriranje veze.Još malo koda koji opisuje logiku obrade servera i klijenta
Ažurirana verzija servera koja uključuje korištenje certifikata.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(); } } }I također novi rukovatelj klijentima sa SSL autorizacijom:
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" +quot;|{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);
}}
}
Ali pošto server radi isključivo na TCP konekciji, potrebno je kreirati modul koji bi mogao prepoznati kontekst zahtjeva. Odlučio sam da bi ovdje bio prikladan parser koji bi razbio zahtjev klijenta na zasebne dijelove s kojima bih mogao komunicirati kako bih klijentu dao potrebne odgovore.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<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(quot;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;
}
}
}
Njegova suština je da se zahtjev razbije na dijelove koristeći regularne izraze. Dobijamo poruku od klijenta, biramo prvi red, koji sadrži metodu i url zahtjeva. Zatim čitamo naslove koje stavljamo u niz oblika HeaderName=Content, a takođe nalazimo, ako postoji, prateći sadržaj (na primjer, string upita) koji smo također stavili u sličan niz. Osim toga, parser otkriva da li je trenutni klijent ovlašten i pohranjuje njegove podatke. Svi zahtjevi ovlaštenih klijenata sadrže heš autorizacije, koji se pohranjuje u kolačiće, zahvaljujući čemu je moguće odvojiti daljnju operativnu logiku za dvije vrste klijenata i dati im tačne odgovore.Pa, mala, zgodna karakteristika koju bi vrijedilo staviti u poseban modul, pretvaranje upita poput “site.com/@UserName” u dinamički generirane korisničke stranice. Nakon obrade zahtjeva, u igru dolaze sljedeći moduli.
Poglavlje 3. Ugradnja volana, podmazivanje lanca
Čim parser završi svoj posao, u igru ulazi rukovalac, koji daje dalje upute serveru i dijeli kontrolu na dva dijela.
Jednostavan rukovalac
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); }; } } }Zapravo, postoji samo jedna provjera autorizacije korisnika, nakon čega počinje obrada zahtjeva.
Klijentski kontrolori
Ako korisnik nije ovlašten, tada se za njega funkcionalnost zasniva samo na prikazu korisničkih profila i prozoru za registraciju autorizacije. Kôd za ovlaštenog korisnika izgleda otprilike isto, tako da ne vidim razlog da ga dupliram.Neovlašteni korisnik
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;}
}
}
}
I naravno, korisnik mora dobiti neku vrstu sadržaja stranice, tako da za odgovore postoji sljedeći modul, koji je odgovoran za odgovaranje na zahtjeve za resursima.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();
}
}
}
Ali kako bih korisniku pokazao njegov profil i profile drugih korisnika, odlučio sam koristiti RazorEngine, odnosno dio njega. To također uključuje obradu nevažećih zahtjeva i izdavanje odgovarajućeg koda greške.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(); } } } }I naravno, da bi provjera ovlaštenih korisnika funkcionirala potrebna je autorizacija. Modul za autorizaciju je u interakciji s bazom podataka. Primljeni podaci iz obrazaca na stranici se raščlanjuju iz konteksta, korisnik se sprema i zauzvrat dobiva kolačiće i pristup servisu.
Modul za autorizaciju
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(quot;HTTP/1.1 301 Moved PermanentlynLocation: /@{user.login}nSet-Cookie: {cookies.AuthCookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnlynn");
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(quot;{user.password} {user.password.Trim().Length}");
DatabaseWorker.UserRegister(user);
var response = Encoding.UTF8.GetBytes(quot;HTTP/1.1 301 Moved PermanentlynLocation: /@{user.login}nSet-Cookie: {user.cookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnlynn");
ClientStream.BeginWrite(response, 0, response.Length, WriteController.OnClientSend, null);
}
else
{
RazorController.ErrorLoader(401);
}
}
}
}
A ovako izgleda obrada baze podataka:Baza podataka
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(quot;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(quot;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;
}
}
}
}
I sve radi kao sat, autorizacija i registracija rade, minimalna funkcionalnost za pristup servisu je već tu i došlo je vrijeme da se napiše aplikacija i cijela stvar poveže sa glavnim funkcijama za koje se sve radi.Poglavlje 4. Bacanje bicikla
Kako bih smanjio troškove rada pisanja dvije aplikacije za dvije platforme, odlučio sam napraviti cross-platformu na Xamarin.Forms. Opet, zahvaljujući činjenici da je u C#. Nakon što sam napravio probnu aplikaciju koja jednostavno šalje podatke na server, naišao sam na zanimljivu stvar. Za zahtjev sa uređaja, iz zabave, implementirao sam ga na HttpClient i poslao na HttpRequestMessage server koji sadrži podatke iz formulara za autorizaciju u json formatu. Bez posebno očekivanja, otvorio sam serverski log i tamo vidio zahtjev uređaja sa svim podacima. Lagana omamljenost, svest o svemu što je urađeno u protekle 3 nedelje u tmurnoj večeri. Da provjerim tačnost poslanih podataka, sastavio sam test server na HttpListner. Pošto sam već primio još jedan zahtjev na njemu, rastavio sam ga u nekoliko redova koda i dobio KeyValuePair podataka iz obrasca. Parsiranje upita svedeno je na dva reda.
Počeo sam dalje da testiram, nije ranije spomenuto, ali sam na prethodnom serveru implementirao i chat izgrađen na websockets. Radilo je prilično dobro, ali sam princip interakcije preko Tcp-a je bio depresivan, previše nepotrebnog posla je trebalo obaviti kako bi se kompetentno izgradila interakcija dva korisnika s dnevnikom korespondencije. Ovo uključuje raščlanjivanje zahtjeva za prebacivanje veze i prikupljanje odgovora pomoću RFC 6455 protokola. Samo za zabavu.
Povežite se na chat
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(); } } } } }I uspjelo je. Server je sam konfigurisao vezu i generisao ključ odgovora. Nisam čak morao posebno da konfigurišem registraciju servera putem SSL-a, bilo je dovoljno da sistem već ima instaliran sertifikat na traženom portu.
Na strani uređaja i na strani stranice, dva klijenta su razmijenila poruke, sve je to evidentirano. Nema velikih parsera koji usporavaju server, ništa od ovoga nije bilo potrebno. Vrijeme odziva je smanjeno sa 200ms na 40-30ms. I došao sam do jedine ispravne odluke.
Izbacite trenutnu implementaciju servera na Tcp i prepišite sve pod Http. Sada je projekat u fazi redizajna, ali po potpuno drugačijim principima interakcije. Rad uređaja i sajta je sinhronizovan i debagovan i ima zajednički koncept, sa jedinom razlikom što nema potrebe za generisanjem HTML stranica za uređaje.
zaključak
"Ako ne poznaješ ford, ne idi u vodu" Mislim da bih prije početka rada trebao imati jasnije definirane ciljeve i zadatke, kao i da se udubim u proučavanje potrebnih tehnologija i metoda za njihovu implementaciju na različitim klijentima. Projekat je već pri kraju, ali možda se vratim da ponovo pričam o tome kako sam neke stvari sačuvao. Mnogo sam naučio tokom procesa razvoja, ali ima još više za naučiti u budućnosti. Ako ste čitali do sada, hvala vam što ste to učinili.
izvor: www.habr.com

