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: ci si registra, si compila un profilo e si passa al cuore della questione, ovvero la ricerca di un interlocutore, l'ampliamento delle proprie connessioni e la creazione di nuovi legami.
Qui devo fare una digressione e raccontare un po' di me, in modo che in seguito sia più chiaro perché ho intrapreso certi passi nello sviluppo.
Attualmente ricopro il ruolo di Technical Artist in uno studio di giochi; la mia esperienza nella programmazione in C# si è basata unicamente sulla scrittura di script e utility per Unity e, in aggiunta, sulla creazione di plugin per il lavoro a basso livello con dispositivi Android. Finora non sono uscito da questo contesto e qui mi si è presentata un'opportunità.
Parte 1. Prototipazione del framework
Decidendo cosa avrebbe dovuto rappresentare questo servizio, ho iniziato a cercare opzioni per la sua realizzazione. Sarebbe stato più semplice trovare una qualche soluzione già pronta, alla quale, come a un gufo su un globo, poter adattare le nostre meccaniche e mettere tutto sotto il giudizio pubblico.
Ma non è interessante; non vedevo alcuna sfida e significato in questo, quindi ho iniziato a studiare le tecnologie web e i metodi di interazione con esse.
Ho iniziato lo studio guardando articoli e documentazione su C# .Net. Qui ho trovato diversi modi per affrontare il problema. Ci sono molti meccanismi per interagire con la rete, da soluzioni complete come ASP.Net o i servizi Azure, fino all'interazione diretta con le connessioni TcpHttp.
Dopo aver fatto il primo tentativo con ASP, l'ho subito scartato, poiché per me era una soluzione troppo complessa per il nostro servizio. Non utilizzeremo nemmeno un terzo delle capacità di questa piattaforma, quindi ho continuato a cercare. La scelta è stata tra TCP e client-server Http. Qui su Habr, mi sono imbattuto in un articolo su , che ho assemblato e testato, e ho deciso di fermarmi proprio sull'interazione con le connessioni TCP; per qualche motivo ho ritenuto che HTTP non mi avrebbe permesso di creare una soluzione multipiattaforma.
La prima versione del server comprendeva la gestione delle connessioni, serviva contenuti statici delle pagine web e includeva un database utenti. Quindi, ho deciso di costruire la funzionalità per lavorare con il sito, per poi integrare anche la gestione delle applicazioni su Android e iOS.
Ecco un po' di codice
Il thread principale, in un ciclo infinito che accetta 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:
utilizzando System; utilizzando System.IO; utilizzando System.Net.Sockets; utilizzando System.Text; utilizzando System.Text.RegularExpressions; namespace ClearServer { class Client { public Client(TcpClient Client) { string Messaggio = ""; byte[] Buffer = new byte[1024]; int Conteggio; while ((Conteggio = Client.GetStream().Read(Buffer, 0, Buffer.Length)) > 0) { Messaggio += Encoding.UTF8.GetString(Buffer, 0, Conteggio); if (Messaggio.IndexOf("rnrn") >= 0 || Messaggio.Length > 4096) { Console.WriteLine(Messaggio); break; } } Match ReqMatch = Regex.Match(Messaggio, @"^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 Estensione = RequestUri.Substring(RequestUri.LastIndexOf('.'));
string ContentType = "";
switch (Estensione)
{
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/{Estensione.Substring(1)}";
break;
default:
if (Estensione.Length > 1)
{
ContentType ="application/{Estensione.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 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)
{
Conteggio = FS.Read(Buffer, 0, Buffer.Length);
Client.GetStream().Write(Buffer, 0, Conteggio);
}FS.Close();
Client.Close();
}
}
}
E il primo database costruito su SQL locale:utilizzando System; utilizzando 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 menzionata nell'articolo. In sostanza, qui si è solo aggiunto il caricamento delle pagine da una cartella sul computer e il database (che, tra l'altro, in questa versione non ha funzionato, a causa di un'errata architettura di connessione).Capitolo 2. Attach delle ruote
Dopo aver testato il funzionamento del server, sono giunto alla conclusione che sarebbe stata una soluzione eccellente (spoiler: no), per il nostro servizio, quindi il progetto ha iniziato a svilupparsi logicamente.
Passo dopo passo, sono iniziati a emergere nuovi moduli e la funzionalità del server si è ampliata. Il server ha acquisito un dominio di prova e ssl una crittografia della connessione.Ancora un po' di codice che descrive la logica del funzionamento del server e la gestione dei client
Versione aggiornata del server, che include l'uso di un certificato.utilizzando System; utilizzando System.Net; utilizzando System.Net.Sockets; utilizzando System.Reflection; utilizzando System.Security; utilizzando System.Security.Cryptography.X509Certificates; utilizzando System.Security.Permissions; utilizzando System.Security.Policy; utilizzando 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("Avvio del 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("Cambio di 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 del cliente con autorizzazione SSL:
utilizzando ClearServer.Core.Requester; utilizzando System; utilizzando System.Net.Security; utilizzando 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|Eccezione: {e.Message}n|------------n|Autenticazione fallita - chiusura della connessione.n" +
"---------------------------------------------------------------------n");
SSlClientStream.Close();
Client.Close();
}
new RequestContext(SSlClientStream, Client);
}}
}
Ma poiché il server funziona esclusivamente su connessione TCP, è necessario creare un modulo che possa riconoscere il contesto della richiesta. Ho deciso che qui un parser sarebbe adatto per suddividere la richiesta del cliente in parti separate, con le quali posso interagire per fornire le risposte richieste 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("{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;
{
if (HeadersValues.Any(x => x.Name.Contains("Cookie")))
try
{
cookie = HeadersValues.FirstOrDefault(x => x.Name.Contains("Cookie")).Value;
{
CurrentUser = databaseWorker.CookieValidate(cookie);
try
{
catch { }
}
private List HeaderValues()
}
}
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 output = Message.Trim('\n').Split().Last();
{
var parse = Regex.Matches(Message, @"(.*?): (.*?)\n");
var parse = Regex.Matches(output, @"([^&].*?)=([^&]*)");
Value = match.Groups[2].Value.Trim().Replace('+', ' ')
values.Add(new RequestValues()
{
Name = match.Groups[1].Value.Trim(),
{
Value = match.Groups[2].Value.Trim()
Valore = match.Groups[2].Value.Trim().Replace('+', ' ')
});
}
private void ProfileParse()
}
}
}
La sostanza di questo processo consiste nel separare la richiesta in parti utilizzando espressioni regolari. Riceviamo un messaggio dal cliente, estraiamo la prima riga che contiene il metodo e l'url della richiesta. Poi leggiamo le intestazioni, che inseriamo in un array di tipo NomeIntestazione=Contenuto, e troviamo anche, se presente, il contenuto ausiliario (ad esempio, querystring) che inseriamo in un array analogo. Inoltre, il parser determina se il cliente attuale è autorizzato e conserva i suoi dati. Tutte le richieste provenienti da clienti autorizzati contengono un hash di autorizzazione, memorizzato nei cookie, il che consente di differenziare la logica operativa per i due tipi di clienti e fornire risposte appropriate.E una piccola e piacevole funzionalità, che varrebbe la pena estrarre in un modulo separato, la trasformazione delle richieste del tipo «site.com/@UserName» in pagine utente generate dinamicamente. Dopo aver elaborato la richiesta, entrano in gioco i seguenti moduli.
Capitolo 3. Installazione del volante, lubrificazione della catena
Non appena il parser ha eseguito il suo lavoro, entra in scena il gestore, che fornisce ulteriori istruzioni al server e divide 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 sull'autorizzazione dell'utente, dopo di che inizia l'elaborazione della richiesta.
Controller dei clienti
Se l'utente non è autorizzato, le sue funzionalità si basano esclusivamente sulla visualizzazione dei profili utente e sulla finestra di registrazione/autenticazione. Il codice per l'utente autorizzato appare più o meno simile, quindi non vedo motivo di duplicarlo.Utente non autorizzato
utilizzando ClearServer.Core.Requester; utilizzando System.IO; utilizzando 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 per le risposte esiste il seguente modulo, che si occupa di fornire una risposta alla richiesta 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 ="{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);
}
private List HeaderValues()
}public string ContentType(string Uri)
{
string extension = Path.GetExtension(Uri);
string Header = "HTTP/1.1 200 OKnContent-Type:";
switch (extension)
{
case ".html":
case ".htm":
return"{Header} text/html";
case ".css":
return"{Header} text/css";
case ".js":
return"{Header} text/javascript";
case ".jpg":
case ".jpeg":
case ".png":
case ".gif":
return"{Header} image/{extension}";
default:
if (extension.Length > 1)
{
return"{Header} application/" + extension.Substring(1);
}
else
{
return"{Header} application/unknown";
}
}
}public void OnFileRead(IAsyncResult ar)
{
if (ar.IsCompleted)
{
var file = (byte[])ar.AsyncState;
ClientStream.BeginWrite(file, 0, file.Length, OnClientSend, null);
}
}public void OnClientSend(IAsyncResult ar)
{
if (ar.IsCompleted)
{
ClientStream.Close();
}
}
}
Tuttavia, per mostrare all'utente il suo profilo e i profili di altri utenti, ho deciso di utilizzare RazorEngine, o meglio una sua parte. Include anche l'elaborazione delle richieste non valide e la restituzione di un codice di errore corrispondente.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(); } } } }Naturalmente, per far funzionare il controllo degli utenti autorizzati, è 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\nLocation: /@{user.login}\nSet-Cookie: {cookies.AuthCookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnly\n\n"); } }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);
}
}
}
}
E così si presenta l'elaborazione del database:Qui la scelta è stata molto più semplice, Simple SCADA offre due prodotti da utilizzare: MS SQL Server e MySQL. Il secondo mi è sembrato più vicino, poiché avevo già lavorato con esso, quindi mi sono fermato lì.
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 = @"pathdatabase"; 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("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; }
}
}
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");
{
public User CookieValidate(string CookieInput) { User user = null; user = users.SingleOrDefault(x => x.cookie == CookieInput); catch return null; if (user != null) return user; else return null; }
}}
public User FindUser(string login) { user = users.Single(x => x.login.ToLower() == login.ToLower()); if (user != null) return user; }
{
return user;
{
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(quot;L'utente {UserToUpdate.name} con id {UserToUpdate.uid} aggiornato");
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; }
}
}
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;
}
}
}
}
E tutto funziona a meraviglia, l'autenticazione e la registrazione sono operative, le funzionalità minime per accedere al servizio sono già disponibili e ora è il momento di scrivere l'applicazione e integrare tutto con le funzioni principali per cui è stato creato.Capitolo 4. Creazione del sistema da zero
Per ridurre il lavoro necessario per scrivere due applicazioni per due piattaforme, ho deciso di creare un'applicazione multipiattaforma utilizzando Xamarin.Forms. Ancora una volta, grazie al fatto che è basata su C#. Dopo aver realizzato un'applicazione di prova che invia semplicemente dati a un server, mi sono imbattuto in un momento interessante. Per la richiesta dal dispositivo ho implementato, per curiosità, il tutto su HttpClient e ho inviato al server un HttpRequestMessage contenente i dati del modulo di autorizzazione in formato json. Non avendo particolari aspettative, ho aperto il log del server e ho visto una richiesta dal dispositivo con tutti i dati. Un leggero stupore, la consapevolezza di tutto ciò che era stato realizzato nelle ultime 3 settimane di lunghe serate. Per verificare la correttezza dei dati inviati, ho assemblato un server di prova su HttpListener. Ricevuta un'altra richiesta su di esso, con poche righe di codice, l'ho analizzata in parti, ottenendo un KeyValuePair di dati dal modulo. L'analisi della richiesta si è ridotta a due righe.
Ho iniziato a testare ulteriormente; non era stato menzionato in precedenza, ma sul server precedente avevo anche implementato una chat basata su websocket. Funzionava abbastanza bene, ma il principio stesso di interazione tramite Tcp era deprimente, era necessario generare troppe cose per gestire in modo corretto l'interazione tra due utenti con la registrazione del log della conversazione. Ciò includeva il parsing della richiesta per il passaggio della connessione e la raccolta della risposta secondo il protocollo RFC 6455. Pertanto, nel server di prova ho deciso di creare una semplice connessione websocket. Giusto per curiosità.
Connessione 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 configurato autonomamente la connessione, generando la 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.
Dalla parte del dispositivo e dalla parte del sito due clienti si scambiavano messaggi, tutto questo veniva registrato. Non erano richiesti enormi parser che rallentassero il lavoro del server. Il tempo di risposta è diminuito da 200 ms a 40-30 ms. E sono giunto all'unica decisione corretta.
Abbandonare l'attuale implementazione del server su Tcp e riscrivere tutto su Http. Ora il progetto è in fase di riprogettazione, ma secondo principi di interazione completamente diversi. Il funzionamento dei dispositivi e del sito è sincronizzato e collaudato e ha una concettualità comune, con l'unica differenza che per i dispositivi non è necessario generare pagine html.
Conclusione
«Non sapere dove mettere piede, non entrare in acqua» Penso che, prima di iniziare i lavori, avrei dovuto definire in modo più chiaro obiettivi e compiti, oltre a approfondire lo studio delle tecnologie e dei metodi necessari per la loro implementazione su diversi clienti. Il progetto è già vicino al completamento, ma forse tornerò ancora per raccontare come ho di nuovo combinato guai con alcune cose. Ho imparato molto durante il processo di sviluppo, ma mi resta ancora molto da imparare in futuro. Se sei arrivato fin qui, ti ringrazio per questo.
Fonte: habr.com

