Προηγμένη εφαρμογή ποδηλασίας ή πελάτη-διακομιστή βασισμένη στο πλαίσιο C# .Net

Είσοδος

Όλα ξεκίνησαν όταν ένας συνάδελφος μου πρότεινε να φτιάξω μια μικρή διαδικτυακή υπηρεσία. Υποτίθεται ότι ήταν κάτι σαν 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 = Περιεχόμενο, και επίσης βρίσκουμε, εάν υπάρχει, το συνοδευτικό περιεχόμενο (για παράδειγμα, 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":
return


quot;{Header} text/html";
case ".css":
return


quot;{Header} text/css";
case ".js":
return


quot;{Header} text/javascript";
case ".jpg":
case ".jpeg":
case ".png":
case ".gif":
return


quot;{Header} image/{extension}";
default:
if (extension.Length > 1)
{
return


quot;{Header} application/" + extension.Substring(1);
}
else
{
return


quot;{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. Και κατέληξα στη μόνη σωστή απόφαση.

Προηγμένη εφαρμογή ποδηλασίας ή πελάτη-διακομιστή βασισμένη στο πλαίσιο C# .Net

Πετάξτε την τρέχουσα υλοποίηση διακομιστή στο Tcp και ξαναγράψτε τα πάντα κάτω από το Http. Τώρα το έργο βρίσκεται στο στάδιο του επανασχεδιασμού, αλλά σύμφωνα με εντελώς διαφορετικές αρχές αλληλεπίδρασης. Η λειτουργία των συσκευών και του ιστότοπου είναι συγχρονισμένη και αποσφαλμωμένη και έχει μια κοινή αντίληψη, με τη μόνη διαφορά ότι οι συσκευές δεν χρειάζεται να δημιουργούν σελίδες html.

Παραγωγή

"Μη γνωρίζοντας το Ford, μην χώνεις το κεφάλι σου στο νερό" Νομίζω ότι, πριν ξεκινήσω την εργασία, θα έπρεπε να είχα ορίσει πιο ξεκάθαρα τους στόχους και τους στόχους, καθώς και να εμβαθύνω στη μελέτη των απαραίτητων τεχνολογιών και μεθόδων για την εφαρμογή τους σε διάφορους πελάτες. Το έργο πλησιάζει ήδη στην ολοκλήρωσή του, αλλά ίσως επιστρέψω για να μιλήσω για το πώς τσάκωσα ξανά ορισμένα πράγματα. Έμαθα πολλά κατά τη διαδικασία ανάπτυξης, αλλά υπάρχουν περισσότερα να μάθω στο μέλλον. Εάν έχετε διαβάσει ως εδώ, τότε σας ευχαριστώ που διαβάσατε.

Πηγή: www.habr.com