Introduzione
Tutto è iniziato quando un collega mi ha proposto di creare un piccolo servizio web. Doveva essere qualcosa di simile a Tinder, ma per il mondo IT. La funzionalità è estremamente semplice: ti registri, compili il profilo e passi al nocciolo della questione, ovvero la ricerca di interlocutori, l'espansione delle tue connessioni e l'opportunità di fare nuove conoscenze.
Qui devo fare una digressione e raccontare un po' di me, affinché sia più chiaro in seguito perché ho intrapreso tali passi nello sviluppo.
Attualmente ricopro il ruolo di Technical Artist in uno studio di gioco; la mia esperienza di programmazione in C# si è sviluppata esclusivamente nella scrittura di script e utility per Unity, oltre alla creazione di plugin per lavori a basso livello con dispositivi Android. Non avevo ancora avuto l'opportunità di uscire da questo ambiente e ora si è presentata questa possibilità.
Parte 1. Prototipazione della struttura
Decidendo cosa dovesse essere questo servizio, ho cominciato a cercare opzioni per la sua realizzazione. La cosa più semplice sarebbe stata trovare qualche soluzione già pronta, su cui potessimo applicare le nostre meccaniche e lanzarci in pubblico con il nostro progetto.
Ma non è interessante, non vedevo alcuna sfida o significato in questo, quindi ho iniziato a studiare le tecnologie web e i metodi di interazione con esse.
Ho iniziato a studiare guardando articoli e documentazione su C# .Net. Qui ho trovato vari modi per completare il compito. Ci sono molti meccanismi di interazione con la rete, da soluzioni complete come ASP.Net o servizi Azure, a interazioni dirette con connessioni TcpHttp.
Dopo il mio primo tentativo con ASP, l'ho subito scartato; a mio parere era una soluzione troppo pesante per il nostro servizio. Non utilizzeremo nemmeno un terzo delle possibilità di questa piattaforma, quindi ho continuato la mia ricerca. La scelta si è ridotta tra TCP e un client-server Http. Qui, su Habr, ho trovato un articolo su , che ho assemblato e testato, e ho deciso di concentrarmi sull'interazione con le connessioni TCP, perché pensavo che HTTP non mi avrebbe permesso di creare una soluzione multipiattaforma.
La prima versione del server includeva la gestione delle connessioni, restituiva contenuti statici delle pagine web e includeva un database utenti. Per iniziare, ho deciso di costruire le funzionalità per lavorare con il sito, in modo da poter successivamente integrare anche la gestione dell'applicazione su Android e iOS.
Ecco un po' di codice
Il thread principale, in un ciclo infinito per accettare i client:
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);
}
}
}Il gestore dei client:
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 =
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();
}
}
}
E il primo database costruito su SQL locale: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}"
}
}
}
}
}
Come si può notare, questa versione differisce poco da quella presente nell'articolo. In sostanza, è stata aggiunta solo la possibilità di caricare le pagine dalla cartella del computer e un database (che tra l'altro, in questa versione non ha funzionato, a causa di un'architettura di connessione errata).Capitolo 2. Attacco delle ruote
Testando il funzionamento del server, sono giunto alla conclusione che sarebbe una soluzione eccellente (spoiler: no), per il nostro servizio, quindi il progetto ha iniziato a dotarsi di logica.
Passo dopo passo sono iniziati ad apparire nuovi moduli e le funzionalità del server si sono ampliate. Il server ha acquisito un dominio di test e ssl la crittografia della connessione.Ancora un po' di codice che descrive la logica del funzionamento del server e la gestione dei clienti.
Versione aggiornata del server, che include l'uso di un certificato.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("Avviando il 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("Passando a un altro dominio"); 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(); } } }E anche un nuovo gestore client con autorizzazione 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);
}}
}
Tuttavia, poiché il server funziona esclusivamente con connessione TCP, è necessario creare un modulo in grado di riconoscere il contesto della richiesta. Ho deciso che un parser sarebbe adatto, in quanto suddividerà la richiesta del client in parti separate, con cui posso interagire per fornire le risposte necessarie al cliente.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("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;
{
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, @"([^&].*?)=([^&]*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;
}
}
}
La sua essenza consiste nell'utilizzare espressioni regolari per suddividere la richiesta in parti. Riceviamo un messaggio dal client, estraiamo la prima riga, che contiene il metodo e l'URL della richiesta. Poi leggiamo le intestazioni e le convertiamo in un array del tipo NomeIntestazione=Contenuto, e troviamo, se presente, il contenuto accompagnatorio (ad esempio, query string) che anche noi mettiamo in un array analogo. Inoltre, il parser determina se il cliente attuale è autorizzato e memorizza i suoi dati. Tutte le richieste provenienti da clienti autorizzati contengono un hash di autorizzazione, che è memorizzato nei cookie; grazie a questo, possiamo separare la logica operativa futura per i due tipi di clienti e fornire loro le risposte corrette.E una piccola e piacevole funzione che meriterebbe di essere separata in un modulo a parte, la conversione delle richieste del tipo «site.com/@UserName» in pagine utente generate dinamicamente. Dopo l'elaborazione della richiesta, entrano in gioco i seguenti moduli.
Capitolo 3. Installazione del volante, lubrificazione della catena
Una volta che il parser ha lavorato, interviene il gestore che fornisce ulteriori istruzioni al server e separa il controllo in due parti.
Gestore semplice
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); }; } } }In sostanza, qui c'è solo un controllo per l'autorizzazione dell'utente, dopo il quale inizia l'elaborazione della richiesta.
Controller dei clienti
Se l'utente non è autorizzato, le funzionalità sono limitate alla visualizzazione dei profili utente e alla finestra di registrazione/autenticazione. Il codice per l'utente autenticato appare più o meno allo stesso modo, quindi non vedo il motivo di duplicarlo.Utente non autorizzato
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 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;}
}
}
}
Naturalmente, l'utente deve ricevere qualche contenuto delle pagine, quindi esiste il seguente modulo che si occupa di rispondere alle richieste delle risorse.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();
}
}
}
Per mostrare all'utente il proprio profilo e i profili di altri utenti, ho deciso di utilizzare RazorEngine, o meglio, una sua parte. Esso include anche la gestione delle richieste non valide e la restituzione del relativo codice di errore.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(); } } } }E naturalmente, per far sì che la verifica degli utenti autorizzati funzioni, è necessaria l'autenticazione. Il modulo di autenticazione interagisce con il database. I dati ricevuti dai moduli sul sito vengono analizzati dal contesto, l'utente viene salvato e riceve, in cambio, un cookie e accesso al servizio.
Modulo di autenticazione
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 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);
}
}
}
}
Ecco come appare l'elaborazione del database:Database
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 = @"percorsoalldb"; 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"); } catch (Exception e) { Console.WriteLine(e); } } public bool LoginValidate(string login) { if (users.Any(x => x.login.ToLower() == login.ToLower())) { Console.WriteLine("Login già esistente"); 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"); } public User CookieValidate(string CookieInput) { User user = null; user = users.SingleOrDefault(x => x.cookie == CookieInput); if (user != null) return user; else return null; } public User FindUser(string login) { user = users.Single(x => x.login.ToLower() == login.ToLower()); } } }Utente {user.name} con ID {user.uid} aggiunto
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("Il login esiste già");
return false;
}
return true;
}
public void UserUpdate(User user)
{
var UserToUpdate = users.FirstOrDefault(x => x.uid == user.uid);
UserToUpdate = user;
DataBase.SubmitChanges();
Console.WriteLine(Utente {UserToUpdate.name} con ID {UserToUpdate.uid} aggiornato
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());
se (utente != null)
{
ritorna utente;
}
else
{
return null;
}
}
catch (Exception)
{
return null;
}
}
}
}
E tutto funziona a meraviglia, l'autenticazione e la registrazione sono operative, il minimo funzionamento di accesso al servizio è già presente e ora è il momento di scrivere l'applicazione e di legare tutto questo alle funzioni principali per cui è stato fatto tutto.Capitolo 4. Evitare di reinventare la ruota
Per ridurre il lavoro necessario per scrivere due applicazioni su due piattaforme, ho deciso di sviluppare un'app cross-platform con Xamarin.Forms, ancora una volta, grazie al fatto che utilizza C#. Dopo aver creato un'app di prova che invia semplicemente dati al server, mi sono imbattuto in un aspetto interessante. Per il numero di richiesta dal dispositivo, ho implementato HttpClient e ho inviato una HttpRequestMessage al server contenente i dati del modulo di autorizzazione in formato json. Non aspettandomi molto, ho aperto il log del server e ho visto una richiesta dal dispositivo con tutti i dati. Un momento di stupore, realizzando tutto ciò che era stato fatto nell'ultima settimana di serate impegnative. Per verificare l'accuratezza dei dati inviati, ho creato un server di test con HttpListener. Ricevendo una nuova richiesta da esso, sono riuscito a decomporla in poche righe di codice e ho ottenuto un KeyValuePair dei dati dal modulo. L'analisi della richiesta è stata ridotta a due righe.
Ho iniziato a testare ulteriormente, non ne avevamo parlato prima, ma sul server precedente avevo anche implementato una chat basata su WebSocket. Funzionava abbastanza bene, ma il principio di interazione tramite TCP era deludente, c'era bisogno di generare troppi elementi per costruire correttamente l'interazione tra due utenti con la registrazione della chat. Questo includeva il parsing della richiesta per il cambio di connessione e la raccolta delle risposte secondo il protocollo RFC 6455. Perciò, su questo server di prova, ho deciso di creare una semplice connessione WebSocket. Solo per curiosità.
Collegamento alla 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(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(); } } } } }E ha funzionato. Il server ha impostato autonomamente la connessione, generando una chiave di risposta. Non ho nemmeno dovuto configurare separatamente la registrazione del server per SSL, era sufficiente che il certificato fosse già installato sul porto richiesto.
Sul lato dispositivo e sul lato sito, due client scambiavano messaggi, tutto ciò veniva registrato. Non sono stati necessari enormi parser che rallentassero il server. Il tempo di risposta è diminuito da 200 ms a 40-30 ms. E sono giunto alla sola soluzione corretta.
Eliminare l'attuale implementazione del server su Tcp e riscrivere tutto su Http. Ora il progetto è in fase di riprogettazione, ma già secondo principi di interazione completamente diversi. Il funzionamento dei dispositivi e del sito è sincronizzato e ottimizzato e segue un concetto comune, con la sola differenza che per i dispositivi non è necessario generare pagine HTML.
Risultato
«Non sapere dove mettere i piedi, non tuffarti in acqua» Penso che, prima di iniziare il lavoro, avrei dovuto definire più chiaramente gli obiettivi e i compiti, oltre a approfondire lo studio delle tecnologie e dei metodi necessari per la loro attuazione su vari clienti. Il progetto è già vicino al termine, ma forse tornerò per raccontare come ho di nuovo commesso errori in alcune aree. Ho imparato molto nel corso dello sviluppo, ma c'è ancora tanto da imparare in futuro. Se sei arrivato fin qui, grazie per questo.
Fonte: habr.com

