Hyrje
E gjitha filloi kur një koleg më sugjeroi të krijoja një shërbim të vogël në internet. Duhej të ishte diçka si Tinder, por për ambientin IT. Funksionaliteti është jashtëzakonisht i thjeshtë: regjistrohesh, plotëson një profil dhe kalon në thelb, që është kërkimi i një biseduesi dhe zgjerimi i lidhjeve të tua për të bërë njohje të reja.
Këtu duhet të bëj një ndërprerje dhe të flas pak për veten, në mënyrë që më vonë të jetë më e qartë pse kam ndjekur këto hapa në zhvillim.
Aktualisht, unë mbaj pozita si Artisti Teknik në një studio lojërash, përvoja ime në programimin e C# është ndërtuar vetëm mbi shkrimin e skripteve dhe utilitarëve për Unity dhe, përveç kësaj, krijimin e plugina për punë të ulët me pajisjet Android. Nuk kam dalë jashtë këtij universi dhe aty më doli një mundësi e tillë.
Pjesa 1. Prototipimi i kornizës
Pas vendosjes se çfarë do të përfaqësonte ky shërbim, fillova të kërkoj mundësi për realizimin e tij. Më e lehta do të ishte të gjeja një zgjidhje të gatshme, mbi të cilën, si të një sulli mbi glob, mund të vendosja mekanizmat tanë dhe ta nxirrja këtë për dënim publik.
Por kjo nuk është interesante, nuk pashë asnjë sfidë dhe kuptim në të, prandaj fillova të studioja teknologjitë e webit dhe metodat e ndërveprimit me to.
Studimin e fillova me leximin e artikujve dhe dokumentacionit mbi C# .Net. Aty gjeta mënyra të ndryshme për të realizuar detyrën. Ka shumë mekanizma ndërveprimi me rrjetin, nga zgjidhjet e plota si ASP.Net ose shërbimet Azure, deri në ndërveprimin e drejtpërdrejtë me lidhjet TcpHttp.
Pas përpjekjes së parë me ASP, përveç se e hodha poshtë menjëherë, në mendimin tim ishte një zgjidhje tepër e rëndë për shërbimin tonë. Nuk do të përdorim as një të tretën e mundësive të kësaj platforme, prandaj vazhdova kërkimi. Zgjedhja ishte midis TCP dhe klient-serverit Http. Aty, në Habr, u ndesha me një artikull për , duke e mbledhur dhe testuar atë, vendosa të ndalem pikërisht në ndërveprimin me lidhjet TCP, sepse për një arsye e konsiderova se http nuk do të më lejonte të krijoja një zgjidhje shumëplatformëshe.
Versioni e parë e serverit përfshinte përpunimin e lidhjeve, ofronte përmbajtje statike të faqeve të internetit dhe përfshinte një bazë të dhënash për përdoruesit. Në fillim, unë vendosa të ndërtoj funksionalitetin për të punuar me faqen, për të lidhur më vonë edhe përpunimin e aplikacionit në Android dhe iOS.
Ja pak kod
Pjesa kryesore, në një cikël të pafund duke pranuar klientët:
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);
}
}
}Vetë përpunuesi i klientëve:
përdorim System; përdorim System.IO; përdorim System.Net.Sockets; përdorim System.Text; përdorim System.Text.RegularExpressions; namespace ClearServer { class Client { public Client(TcpClient Client) { string Mesazhi = ""; byte[] Buffer = new byte[1024]; int Numri; while ((Numri = Client.GetStream().Read(Buffer, 0, Buffer.Length)) > 0) { Mesazhi += Encoding.UTF8.GetString(Buffer, 0, Numri); if (Mesazhi.IndexOf("rnrn") >= 0 || Mesazhi.Length > 4096) { Console.WriteLine(Mesazhi); break; } } Match ReqMatch = Regex.Match(Mesazhi, @"^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 Zgjatja = RequestUri.Substring(RequestUri.LastIndexOf('.'));
string LlojiITëmbjes = "";
switch (Zgjatja)
{
case ".htm":
case ".html":
LlojiITëmbjes = "text/html";
break;
case ".css":
LlojiITëmbjes = "text/css";
break;
case ".js":
LlojiITëmbjes = "text/javascript";
break;
case ".jpg":
LlojiITëmbjes = "image/jpeg";
break;
case ".jpeg":
case ".png":
case ".gif":
LlojiITëmbjes ="image/{Zgjatja.Substring(1)}";
break;
default:
if (Zgjatja.Length > 1)
{
LlojiITëmbjes ="application/{Zgjatja.Substring(1)}";
}
else
{
LlojiITëmbjes = "application/unknown";
}
break;
}FileStream FS;
përpiqem
{
FS = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
}
përjashtim (Exception)
{
ErrorWorker.SendError(Client, 500);
return;
}string Kryetarë =
"HTTP/1.1 200 OKnContent-Type: {LlojiITëmbjes}nContent-Length: {FS.Length}nn";
byte[] HeadersBuffer = Encoding.ASCII.GetBytes(Kryetarë);
Client.GetStream().Write(HeadersBuffer, 0, HeadersBuffer.Length);while (FS.Position < FS.Length)
{
Numri = FS.Read(Buffer, 0, Buffer.Length);
Client.GetStream().Write(Buffer, 0, Numri);
}FS.Close();
Client.Close();
}
}
}
Dhe baza e parë e dhënash e ndërtuar në SQL lokal:përdorim System; përdorim 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}";
}
}
}
}
}
Siç mund të vëreni, kjo version nuk ndryshon shumë nga ajo që ishte në artikull. Në thelb, këtu është shtuar vetëm ngarkimi i faqeve nga dosja në kompjuter dhe baza e të dhënave (e cila, përndryshe, në këtë version nuk punoi, për shkak të arkitekturës së gabuar të lidhjes).Kreu 2. Shtimi i rrotave
Pas testimit të funksionimit të serverit, arrita në përfundimin se do të ishte një zgjidhje e shkëlqyer (spoiler: jo), për shërbimin tonë, prandaj projekti filloi të mbushej me logjikë.
Hap pas hapi, filluan të shfaqeshin module të reja dhe funksionaliteti i serverit po zgjerohej. Serveri fitoi një domain testimi dhe ssl kriptimi i lidhjes.Pak më shumë kod, që përshkruan logjikën e funksionimit të serverit dhe përpunimin e klientëve
Versioni i përditësuar i serverit, përfshirë përdorimin e certifikatës.përdorim System; përdorim System.Net; përdorim System.Net.Sockets; përdorim System.Reflection; përdorim System.Security; përdorim System.Security.Cryptography.X509Certificates; përdorim System.Security.Permissions; përdorim System.Security.Policy; përdorim System.Threading; namespace ClearServer { classi i mbyllur 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("Duke nisur serverin.." + 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("Duke kaluar në një domene tjetër"); 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(); } } }Gjithashtu, një trajtues i ri të klientit me autorizim përmes ssl:
duke 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|Përjashtimi: {e.Message}n|------------n|Autentikimi dështoi - duke mbyllur lidhjen.n" +
"---------------------------------------------------------------------n");
SSlClientStream.Close();
Client.Close();
}
new RequestContext(SSlClientStream, Client);
}}
}
Por për shkak se serveri punon ekskluzivisht mbi lidhje TCP, është e nevojshme të krijohet një modul që mund të njohë kontekstin e kërkesës. Vendosa që këtu do të përshtatet një parser që do të ndante kërkesën nga klienti në pjesë të veçanta, me të cilat do të mund të interaktoja, për të dhënë klientit përgjigjet e nevojshme.Parser
duke ClearServer.Core.UserController; duke ReServer.Core.Classes; duke System; duke System.Collections.Generic; duke System.Linq; duke System.Net.Security; duke System.Net.Sockets; duke System.Text; duke 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": "n{DateTime.Now:g} Client IP:{TcpClient.Client.RemoteEndPoint}n{Message}"); } }n{DateTime.Now:g} IP e Klientit:{TcpClient.Client.RemoteEndPoint}n{Message}");
KërkesaParse();
HeadersValues = VleratHeader();
FormValues = VleratPërmbajtjes();
PërdoruesParse();
ProfileParse();
OnRead?.Invoke(ClientStream, this);
}
}private void KërkesaParse()
{
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 PërdoruesParse()
{
string cookie;
përpiqem
{
if (HeadersValues.Any(x => x.Name.Contains("Cookie")))
{
cookie = HeadersValues.FirstOrDefault(x => x.Name.Contains("Cookie")).Value;
përpiqem
{
CurrentUser = databaseWorker.CookieValidate(cookie);
}
catch { }
}
}
catch { }}
private List VleratHeader()
{
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 VleratPërmbajtjes()
{
var values = new List();
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;
}
}
}
Ideja e tij është që, duke përdorur shprehje të rregullta, ta ndajë kërkesën në pjesë. Marrim mesazhin nga klienti, nxjerrim rreshtin e parë, ku përmban metodën dhe URL-në e kërkesës. Më pas lexojmë titujt, të cilët i fusim në një array formati EmriTitullit=Kërkesa, si dhe gjejmë, nëse ka, përmbajtjen përcjellëse (për shembull, querystring) që gjithashtu e fusim në një array të ngjashëm. Po ashtu, parseri zbardh nëse klienti aktual është i autorizuar dhe ruan të dhënat e tij. Të gjitha kërkesat nga klientët e autorizuar përmbajnë një hash autorizimi, që ruhet në cookies, duke e bërë të mundur ndarjen e logjikës së mëtejshme të punës për dy tipe klientësh dhe të jepen përgjigjet e duhura.Dhe një karakteristikë e vogël, e këndshme që do ishte mirë ta nxirrnim në një modul të veçantë, është transformimi i kërkesave të llojit "site.com/@UserName" në faqe të gjeneruara dinamikisht për përdoruesit. Pas përpunimit të kërkesës, modulojnë e tjera hyjnë në punë.
Kapitulli 3. Vendosja e timonit, lubrifikimi i zinxhirit.
Sa herë që parseri ka punuar, hyn në lojë procesori, i cili jep udhëzime të mëtejshme serverit dhe ndan menaxhimin në dy pjesë.
Procesori i thjeshtë.
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); }; } } }Në thelb, këtu ka vetëm një verifikim mbi autorizimin e përdoruesit, pas së cilës fillon përpunimi i kërkesës.
Kontrollerët e klientëve.
Nëse përdoruesi nuk është i autorizuar, funksionaliteti për të është i bazuar vetëm në shfaqjen e profileve të përdoruesve dhe dritares së regjistrimit/ autorizimit. Kodi për një përdorues të autorizuar duket njësoj, prandaj nuk e shoh arsyen për ta kopjuar.Përdoruesi i paautorizuar.
duke ClearServer.Core.Requester; duke System.IO; duke 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 Content-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;}
}
}
}
Dhe natyrisht, përdoruesi duhet të marrë ndonjë përmbajtje të faqeve, prandaj për përgjigjet ekziston moduli i mëposhtëm, i cili përgjigjet ndaj kërkesave për burime.WriterController.
përdorimi System; përdorimi System.IO; përdorimi System.Net.Security; përdorimi System.Text; namespace ClearServer.Core.UserController { publike klasa WriteController { SslStream ClientStream; publike WriteController(SslStream ClientStream) { ky.ClientStream = ClientStream; } publike void DefaultWriter(string Header, string FilePath) { FileStream fileStream; provo { 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 { }
}publike 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:
nëse (extension.Length > 1)
{
returnquot;{Header} application/" + extension.Substring(1);
}
else
{
returnquot;{Header} application/unknown";
}
}
}publike void OnFileRead(IAsyncResult ar)
{
nëse (ar.IsCompleted)
{
var file = (byte[])ar.AsyncState;
ClientStream.BeginWrite(file, 0, file.Length, OnClientSend, null);
}
}publike void OnClientSend(IAsyncResult ar)
{
nëse (ar.IsCompleted)
{
ClientStream.Close();
}
}
}
Por për të treguar përdoruesit profilin e tij dhe profilin e përdoruesve të tjerë, unë vendosa të përdor RazorEngine, më saktësisht një pjesë të tij. Ai gjithashtu përfshin përpunimin e kërkesave të pavlefshme dhe dhënien e kodit përkatës të gabimit.RazorController.
duke ClearServer.Core.Requester; duke RazorEngine; duke RazorEngine.Templating; duke System; duke System.IO; duke System.Net; duke 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(); } } } }Sigurisht, për të siguruar kontrollin e përdoruesve të autorizuar, kërkohet autorizimi. Moduli i autorizimit bashkëpunon me bazën e të dhënave. Të dhënat e marra nga format në faqen e internetit nxirren nga konteksti, përdoruesi ruhet dhe merr në shkëmbim cookie dhe qasje në shërbim.
Moduli i autorizimit
duke ClearServer.Core.Cookies; duke ClearServer.Core.Requester; duke ClearServer.Core.Security; duke Sistemi; duke Sistemi.Linq; duke Sistemi.Net.Security; duke Sistemi.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 Location: /@{user.login} Set-Cookie: {cookies.AuthCookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnly
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 Location: /@{user.login} Set-Cookie: {user.cookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnly
ClientStream.BeginWrite(response, 0, response.Length, WriteController.OnClientSend, null);
}
else
{
RazorController.ErrorLoader(401);
}
}
}
}
Këtu është si duket përpunimi i bazës së të dhënave:Baza e të dhënave
duke ClearServer.Core.UserController; duke System; duke System.Data.Linq; duke System.Linq; emri ClearServer { klasa DatabaseWorker { private readonly Table users = null; private readonly DataContext DataBase = null; private const string connectionStr = @"rrugaedatabases"; 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("Përdoruesi {user.name} me id {user.uid} u shtua"); }"Përdoruesi {user.name} me id {user.uid} u shtua");
për çdo item në users
{
Console.WriteLine(item.login + "n");
}
}
përjashto (Përjashtim e)
{
Console.WriteLine(e);
}}
public bool LoginValidate(string login)
{
nëse (users.Any(x => x.login.ToLower() == login.ToLower()))
{
Console.WriteLine("Login tashmë ekziston");
kthe false;
}
kthe true;
}
public void UserUpdate(User user)
{
var UserToUpdate = users.FirstOrDefault(x => x.uid == user.uid);
UserToUpdate = user;
DataBase.SubmitChanges();
Console.WriteLine("Përdoruesi {UserToUpdate.name} me id {UserToUpdate.uid} u përditësua");
për çdo item në users
{
Console.WriteLine(item.login + "n");
}
}
public User CookieValidate(string CookieInput)
{
User user = null;
përpiqem
{
user = users.SingleOrDefault(x => x.cookie == CookieInput);
}
përjashto
{
kthe null;
}
nëse (user != null) kthe user;
ndryshe kthe null;
}
public User FindUser(string login)
{
User user = null;
përpiqem
{
user = users.Single(x => x.login.ToLower() == login.ToLower());
nëse (user != null)
{
kthe user;
}
else
{
kthe null;
}
}
përjashtim (Exception)
{
kthe null;
}
}
}
}
Dhe gjithçka funksionon si orë, autorizimi dhe regjistrimi funksionojnë, funksionaliteti minimal i aksesit në shërbim tashmë ekziston dhe ka ardhur koha për të shkruar aplikacionin dhe për të lidhur të gjitha këto me funksionalitetet kryesore për të cilat gjithçka bëhet.Kreu 4. Zbrazja e biçikletës
Për të reduktuar ngarkesën e punës për shkruajtjen e dy aplikacioneve për dy platforma, vendosa të krijoja një zgjidhje multi-platformë me Xamarin.Forms. Po ashtu, falë faktit që është në C#. Pas krijimit të një aplikacioni testues, i cili thjesht dërgon të dhëna në server, u ndesha me një moment interesant. Për kërkesën nga pajisja, për kuriozitet, e realizova me HttpClient dhe e dërgova në server një HttpRequestMessage që përmbante të dhënat nga forma e autentifikimit në formatin json. Pa pritur asgjë të veçantë, hapa log-un e serverit dhe pashë atje një kërkesë nga pajisja me të gjitha të dhënat. Një pak surprizë, ndërgjegjësimi i gjithçkaje që ishte realizuar gjatë tri javëve të fundit në mbrëmje të zgjatura. Për të verifikuar saktësinë e të dhënave të dërguara, ndërtova një server testues me HttpListener. Pasi mora kërkesën e radhës aty, përmes disa linjave të kodit e ndava atë në pjesë, duke marrë një KeyValuePair të të dhënave nga forma. Analiza e kërkesës u reduktua në dy linja.
Fillova të testoj më tej, nuk u përmend më parë, por në serverin e mëparshëm kishte implementuar gjithashtu një bisedë të ndërtuar mbi websockets. Ai punonte mjaft mirë, por vetë principi i ndërveprimit përmes Tcp ishte frustues, shumë gjëra të panevojshme duhej të krijoheshin për të ndërtuar siç duhet ndërveprimin mes dy përdoruesve me zhvillimin e regjistrit të bisedës. Kjo përfshinte parsing-un e kërkesës për të kaluar lidhjen dhe mbledhjen e përgjigjes sipas protokollit RFC 6455. Prandaj, në serverin testues vendosa të krijoja një lidhje të thjeshtë websocket. Thjesht për kuriozitet.
Lidhja me bisedën
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(); } } } } }Dhe ai filloi punën. Serveri e konfiguroi lidhjen vetë dhe gjeneroi çelësin e përgjigjes. As që më nevojitej ta konfiguroja veçmas regjistrimin e serverit në ssl, mjaftonte që në sistem tashmë ishte instaluar një certifikatë në portin e nevojshëm.
Në anën e pajisjes dhe në anën e ueb-it, dy klientë shkëmbenin mesazhe, ndërsa gjithçka regjistrohej. Nuk nevojiteshin parserë të mëdhenj që ngadalësonin punën e serverit, këto nuk ishin të nevojshme. Koha e përgjigjes u ul nga 200ms në 40-30ms. Dhe kam arritur në zgjidhjen e vetme të duhur.
Të heqim implementimin aktual të serverit mbi Tcp dhe ta riprogramojmë gjithçka nën Http. Tani projekti është në fazën e riprojektimit, por tashmë sipas parimeve të tjera të interaksionit. Puna e pajisjeve dhe e sitit është sinkronizuar dhe e rregulluar dhe ka një koncept të përbashkët, me dallimin se për pajisjet nuk nevojitet të gjenerohen faqe html.
Përfundimi
«Nuk e di ujërin, mos e fut veten në të» Mendoj se para fillimit të punës, duhej të përcaktoja më qartë qëllimet dhe detyrat, si dhe të thellohem në studimin e teknologjive të nevojshme dhe metodave të zbatimit të tyre në cliente të ndryshëm. Projekti është afër përfundimit, por ndoshta do të kthehem përsëri për të treguar se si përsëri e kam kaluar keq këtë ose atë gjë. Kam mësuar shumë gjatë procesit të zhvillimit, por më shumë duhet të mësoj në vazhdim. Nëse keni arritur deri në këtë moment, faleminderit për këtë.
Burimi: habr.com

