Είσοδος
Όλα ξεκίνησαν όταν ένας συνάδελφος μου πρότεινε να δημιουργήσω μια μικρή υπηρεσία web. Υποτίθεται ότι θα ήταν κάτι σαν το Tinder, αλλά για το πλήθος της πληροφορικής. Η λειτουργικότητα είναι εξαιρετικά απλή, εγγράφεσαι, συμπληρώνεις το προφίλ σου και προχωράς στο κύριο σημείο, δηλαδή την εύρεση συνομιλητή και την επέκταση των συνδέσεων σου και τη δημιουργία νέων γνωριμιών.
Εδώ θα πρέπει να κάνω μια παρέκβαση και να πω λίγα λόγια για τον εαυτό μου, ώστε αργότερα να είναι πιο ξεκάθαρο γιατί έκανα αυτά τα συγκεκριμένα βήματα στην ανάπτυξη.
Αυτή τη στιγμή που κατέχω τη θέση του Τεχνικού Καλλιτέχνη σε ένα στούντιο παιχνιδιών, η εμπειρία μου στον προγραμματισμό σε C# βασίστηκε μόνο στη σύνταξη σεναρίων και βοηθητικών προγραμμάτων για το Unity και επιπλέον, στη δημιουργία πρόσθετων για εργασία χαμηλού επιπέδου με συσκευές Android. Ποτέ δεν είχα αποτολμήσει πέρα από αυτόν τον μικρό κόσμο πριν, και τώρα παρουσιάστηκε μια τέτοια ευκαιρία.
Μέρος 1. Πρωτοτυποποίηση πλαισίου
Έχοντας αποφασίσει ποια θα είναι αυτή η υπηρεσία, άρχισα να αναζητώ επιλογές για εφαρμογή. Το πιο εύκολο πράγμα θα ήταν να βρούμε κάποιο είδος έτοιμη λύση, πάνω στην οποία, όπως μια κουκουβάγια σε μια υδρόγειο, θα μπορούσαμε να απλώσουμε τους μηχανικούς μας και να βάλουμε το όλο θέμα για δημόσια μομφή.
Αλλά αυτό δεν είναι ενδιαφέρον, δεν είδα καμία πρόκληση ή νόημα σε αυτό, και ως εκ τούτου άρχισα να μελετώ τις τεχνολογίες Ιστού και τις μεθόδους αλληλεπίδρασης μαζί τους.
Άρχισα να μελετώ κοιτάζοντας άρθρα και τεκμηρίωση στο C# .Net. Εδώ βρήκα διάφορους τρόπους για να ολοκληρώσω την εργασία. Υπάρχουν πολλοί μηχανισμοί αλληλεπίδρασης με το δίκτυο, από ολοκληρωμένες λύσεις όπως υπηρεσίες ASP.Net ή Azure, μέχρι άμεση αλληλεπίδραση με συνδέσεις TcpHttp.
Έχοντας κάνει την πρώτη μου προσπάθεια με την ASP, την απέρριψα αμέσως. Κατά τη γνώμη μου, ήταν πολύ δύσκολη απόφαση για την υπηρεσία μας. Δεν θα χρησιμοποιήσουμε ούτε το ένα τρίτο των δυνατοτήτων αυτής της πλατφόρμας, γι' αυτό συνέχισα την αναζήτηση. Η επιλογή ήταν μεταξύ TCP και Http πελάτη-διακομιστή. Εδώ, στο Habr, έπεσα πάνω σε ένα άρθρο για , έχοντας συναρμολογήσει και δοκιμάσει τα οποία, αποφάσισα να επικεντρωθώ στην αλληλεπίδραση με συνδέσεις TCP, για κάποιο λόγο σκέφτηκα ότι το http δεν θα μου επέτρεπε να δημιουργήσω μια λύση cross-platform.
Η πρώτη έκδοση του διακομιστή περιλάμβανε επεξεργασία σύνδεσης, εξυπηρετούσε στατικό περιεχόμενο ιστοσελίδας και περιλάμβανε μια βάση δεδομένων χρηστών. Και για αρχή, αποφάσισα να δημιουργήσω λειτουργικότητα για την εργασία με τον ιστότοπο, ώστε αργότερα να μπορέσω να επισυνάψω την επεξεργασία της εφαρμογής σε Android και iOS.
Εδώ είναι κάποιος κώδικας
Το κύριο νήμα, σε έναν άπειρο βρόχο, που δέχεται πελάτες:
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);
}
}
}Ο ίδιος ο χειριστής πελάτη:
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();
}
}
}
Και η πρώτη βάση δεδομένων που βασίζεται σε τοπική SQL:using System; using System.Data.Linq; namespace ClearServer { class DatabaseWorker { private static DatabaseWorker instance; public static DatabaseWorker GetInstance { get { if (instance == null) instance = new DatabaseWorker(); return instance; } } private DatabaseWorker() { string connectionStr = databasePath; using (DataContext db = new DataContext(connectionStr)) { Table<User> users = db.GetTable<User>(); foreach (var item in users) { Console.WriteLine(quot;{item.login} {item.password}");
}
}
}
}
}
Όπως μπορείτε να δείτε, αυτή η έκδοση διαφέρει ελάχιστα από αυτή του άρθρου. Στην πραγματικότητα, μόνο η φόρτωση σελίδων από έναν φάκελο στον υπολογιστή και μια βάση δεδομένων (η οποία, παρεμπιπτόντως, δεν λειτούργησε σε αυτήν την έκδοση, λόγω λανθασμένης αρχιτεκτονικής σύνδεσης) προστέθηκαν εδώ.Κεφάλαιο 2. Βίδωμα στους τροχούς
Αφού δοκίμασα τη λειτουργία του διακομιστή, κατέληξα στο συμπέρασμα ότι αυτή θα ήταν μια εξαιρετική λύση(σπόιλερ: όχι), για την υπηρεσία μας, οπότε το έργο άρχισε να αποκτά λογική.
Βήμα προς βήμα, άρχισαν να εμφανίζονται νέες ενότητες και η λειτουργικότητα του διακομιστή επεκτάθηκε. Ο διακομιστής απέκτησε έναν δοκιμαστικό τομέα και ssl κρυπτογράφηση σύνδεσης.Λίγος περισσότερος κώδικας που περιγράφει τη λογική της επεξεργασίας διακομιστή και πελάτη
Μια ενημερωμένη έκδοση του διακομιστή που περιλαμβάνει τη χρήση πιστοποιητικού.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(); } } }Και επίσης ένα νέο πρόγραμμα χειρισμού πελατών με εξουσιοδότηση 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" +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);
}}
}
Επειδή όμως ο διακομιστής λειτουργεί αποκλειστικά σε σύνδεση TCP, είναι απαραίτητο να δημιουργηθεί μια λειτουργική μονάδα που θα μπορούσε να αναγνωρίσει το πλαίσιο αιτήματος. Αποφάσισα ότι εδώ θα ήταν κατάλληλος ένας αναλυτής, ο οποίος θα αναλύει το αίτημα του πελάτη σε ξεχωριστά μέρη, με τα οποία θα μπορούσα να αλληλεπιδράσω για να δώσω στον πελάτη τις απαραίτητες απαντήσεις.αναλυτής
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;
}
}
}
Η ουσία του είναι να χρησιμοποιεί κανονικές εκφράσεις για να χωρίσει ένα ερώτημα σε μέρη. Λαμβάνουμε ένα μήνυμα από τον πελάτη, επιλέγουμε την πρώτη γραμμή, η οποία περιέχει τη μέθοδο και τη διεύθυνση url του αιτήματος. Στη συνέχεια διαβάζουμε τις κεφαλίδες, τις οποίες οδηγούμε σε έναν πίνακα του τύπου HeaderName=Content, και επίσης βρίσκουμε, εάν υπάρχει, συνοδευτικό περιεχόμενο (για παράδειγμα, querystring), το οποίο επίσης οδηγούμε σε έναν παρόμοιο πίνακα. Επιπλέον, ο αναλυτής ανακαλύπτει εάν ο τρέχων πελάτης είναι εξουσιοδοτημένος και αποθηκεύει τα δεδομένα του. Όλα τα αιτήματα από εξουσιοδοτημένους πελάτες περιέχουν ένα κατακερματισμό εξουσιοδότησης, το οποίο αποθηκεύεται σε cookies, χάρη στο οποίο είναι δυνατό να διαχωριστεί περαιτέρω η λογική εργασίας για δύο τύπους πελατών και να τους δοθούν σωστές απαντήσεις.Λοιπόν, και ένα μικρό, ωραίο χαρακτηριστικό που θα άξιζε να τοποθετηθεί σε μια ξεχωριστή ενότητα, μετατρέποντας αιτήματα όπως το "site.com/@UserName" σε σελίδες χρηστών που δημιουργούνται δυναμικά. Μόλις διεκπεραιωθεί το αίτημα, μπαίνουν στο παιχνίδι οι ακόλουθες ενότητες.
Κεφάλαιο 3. Τοποθέτηση του τιμονιού, λίπανση της αλυσίδας
Μόλις ο αναλυτής ολοκληρώσει την εργασία του, ο χειριστής μπαίνει στο παιχνίδι, δίνοντας περαιτέρω οδηγίες στον διακομιστή και χωρίζοντας τον έλεγχο σε δύο μέρη.
Απλός χειριστής
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); }; } } }Ουσιαστικά, υπάρχει μόνο ένας έλεγχος για εξουσιοδότηση χρήστη, μετά τον οποίο ξεκινά η επεξεργασία του αιτήματος.
Ελεγκτές πελατών
Εάν ο χρήστης δεν είναι εξουσιοδοτημένος, τότε η λειτουργικότητα για αυτόν βασίζεται μόνο στην εμφάνιση των προφίλ χρήστη και στο παράθυρο εγγραφής/εξουσιοδότησης. Ο κωδικός για έναν εξουσιοδοτημένο χρήστη μοιάζει περίπου με τον ίδιο, επομένως δεν βλέπω κανένα νόημα να τον αντιγράψω.Μη εξουσιοδοτημένος χρήστης
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;}
}
}
}
Και φυσικά, ο χρήστης πρέπει να λάβει κάποιο περιεχόμενο από τις σελίδες, επομένως για τις απαντήσεις υπάρχει η παρακάτω ενότητα, υπεύθυνη για την απάντηση σε ένα αίτημα πόρων.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();
}
}
}
Αλλά για να δείξω στον χρήστη το προφίλ του και τα προφίλ άλλων χρηστών, αποφάσισα να χρησιμοποιήσω το RazorEngine, ή μάλλον ένα μέρος του. Περιλαμβάνει επίσης το χειρισμό μη έγκυρων αιτημάτων και την έκδοση κατάλληλου κωδικού σφάλματος.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(); } } } }Και φυσικά, για να λειτουργήσει η επαλήθευση των εξουσιοδοτημένων χρηστών, απαιτείται εξουσιοδότηση. Η μονάδα εξουσιοδότησης αλληλεπιδρά με τη βάση δεδομένων. Τα δεδομένα που λαμβάνονται από τις φόρμες στον ιστότοπο αναλύονται από το περιβάλλον, ο χρήστης αποθηκεύεται και λαμβάνει cookies και πρόσβαση στην υπηρεσία ως αντάλλαγμα.
Μονάδα εξουσιοδότησης
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);
}
}
}
}
Και έτσι μοιάζει η επεξεργασία της βάσης δεδομένων:Βάση δεδομένων
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;
}
}
}
}
Και όλα λειτουργούν όπως ρολόι, εργασίες εξουσιοδότησης και εγγραφής, η ελάχιστη λειτουργικότητα πρόσβασης στην υπηρεσία είναι ήδη διαθέσιμη και ήρθε η ώρα να γράψετε μια εφαρμογή και να συνδέσετε όλα αυτά τα πράγματα με τις βασικές λειτουργίες για τις οποίες γίνονται όλα.Κεφάλαιο 4. Πέταγμα του ποδηλάτου
Για να μειώσω το εργατικό κόστος της σύνταξης δύο εφαρμογών για δύο πλατφόρμες, αποφάσισα να φτιάξω ένα cross-platform στο Xamarin.Forms. Και πάλι, χάρη στο γεγονός ότι είναι σε C#. Έχοντας φτιάξει μια δοκιμαστική εφαρμογή που απλώς στέλνει δεδομένα στον διακομιστή, συνάντησα ένα ενδιαφέρον σημείο. Για το αίτημα από τη συσκευή, για πλάκα, το υλοποίησα στο HttpClient και το έστειλα στον διακομιστή ως HttpRequestMessage, το οποίο περιέχει δεδομένα από τη φόρμα εξουσιοδότησης σε μορφή json. Χωρίς να περιμένω κάτι συγκεκριμένο, άνοιξα το αρχείο καταγραφής διακομιστή και είδα ένα αίτημα από τη συσκευή με όλα τα δεδομένα. Μια ελαφριά κούραση, η συνειδητοποίηση όλων όσων είχαν γίνει τις τελευταίες 3 άτονες εβδομάδες το βράδυ. Για να ελέγξω την ορθότητα των απεσταλμένων δεδομένων, συναρμολόγησα έναν δοκιμαστικό διακομιστή στο HttpListner. Έχοντας λάβει άλλο ένα αίτημα ήδη σε αυτό, το χώρισα σε μερικές γραμμές κώδικα και έλαβα το KeyValuePair δεδομένων από τη φόρμα. Η ανάλυση του ερωτήματος έχει μειωθεί σε δύο γραμμές.
Άρχισα να δοκιμάζω περαιτέρω, δεν αναφέρθηκε νωρίτερα, αλλά στον προηγούμενο διακομιστή υλοποίησα επίσης μια συνομιλία χτισμένη σε υποδοχές ιστού. Λειτουργούσε αρκετά καλά, αλλά η αρχή της αλληλεπίδρασης μέσω Tcp ήταν καταθλιπτική, έπρεπε να δημιουργηθούν πάρα πολλά περιττά πράγματα για να δημιουργηθεί σωστά η αλληλεπίδραση μεταξύ δύο χρηστών με τη διατήρηση ενός αρχείου καταγραφής αλληλογραφίας. Αυτό περιλαμβάνει την ανάλυση ενός αιτήματος για εναλλαγή σύνδεσης και τη συλλογή μιας απάντησης χρησιμοποιώντας το πρωτόκολλο RFC 6455. Ως εκ τούτου, αποφάσισα να δημιουργήσω μια απλή σύνδεση websocket στον δοκιμαστικό διακομιστή. Μόνο για πλάκα.
Σύνδεση σε συνομιλία
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(); } } } } }Και λειτούργησε. Ο ίδιος ο διακομιστής διαμόρφωσε τη σύνδεση και δημιούργησε το κλειδί απόκρισης. Δεν χρειάστηκε καν να διαμορφώσω ξεχωριστά την εγγραφή διακομιστή μέσω SSL, αρκούσε το ότι το σύστημα είχε ήδη εγκατεστημένο πιστοποιητικό στην απαιτούμενη θύρα.
Από την πλευρά της συσκευής και από την πλευρά του ιστότοπου, δύο πελάτες αντάλλαξαν μηνύματα, τα οποία καταγράφηκαν όλα. Δεν υπάρχουν τεράστιοι αναλυτές που επιβραδύνουν τον διακομιστή, δεν απαιτείται τίποτα από αυτά. Ο χρόνος απόκρισης έχει μειωθεί από 200ms σε 40-30ms. Και κατέληξα στη μόνη σωστή απόφαση.
Πετάξτε την τρέχουσα υλοποίηση του διακομιστή στο Tcp και ξαναγράψτε τα πάντα για Http. Τώρα το έργο βρίσκεται σε διαδικασία επανασχεδιασμού, αλλά χρησιμοποιώντας εντελώς διαφορετικές αρχές αλληλεπίδρασης. Η λειτουργία των συσκευών και του ιστότοπου είναι συγχρονισμένη και αποσφαλμάτωση και έχει μια κοινή ιδέα, με τη μόνη διαφορά ότι δεν χρειάζεται να δημιουργηθούν σελίδες HTML για τις συσκευές.
Παραγωγή
«Μην μπαίνεις στο νερό χωρίς να ξέρεις το βάθος». Νομίζω ότι πριν ξεκινήσω την εργασία, θα έπρεπε να είχα ορίσει πιο ξεκάθαρα τους στόχους και τους στόχους, καθώς και να εμβαθύνω στη μελέτη των απαραίτητων τεχνολογιών και μεθόδων εφαρμογής τους σε διάφορους πελάτες. Το έργο πλησιάζει ήδη στην ολοκλήρωσή του, αλλά ίσως θα επιστρέψω ξανά για να μιλήσω για το πώς τσάκωσα ξανά αυτό ή εκείνο το πράγμα. Έμαθα πολλά κατά τη διάρκεια της διαδικασίας ανάπτυξης, αλλά υπάρχουν ακόμα περισσότερα να μάθω προχωρώντας. Αν έχετε διαβάσει μέχρι εδώ, σας ευχαριστώ για αυτό.
Πηγή: www.habr.com

