Cycle avancé ou application client-serveur basée sur le framework C# .Net

Entrée

Tout a commencé lorsqu'un collègue m'a proposé de créer un petit service web. C'était censé être quelque chose comme Tinder, mais pour le monde informatique. La fonctionnalité est extrêmement simple, vous vous inscrivez, remplissez un profil et passez à l'essentiel, à savoir trouver une personne à qui parler, élargir vos relations et faire de nouvelles connaissances.

Ici, je dois faire une retraite et parler un peu de moi, afin qu'à l'avenir, il soit plus clair pourquoi j'ai pris de telles mesures de développement.

À l'heure actuelle, j'occupe le poste d'artiste technique dans un studio de jeux, mon expérience de programmation en C# s'est construite uniquement sur l'écriture de scripts et d'utilitaires pour Unity et, en plus, sur la création de plugins pour un travail de bas niveau avec des appareils Android. Je ne m'étais pas encore aventuré au-delà de ce petit monde, et puis une telle opportunité s'est présentée.

Partie 1. Prototypage du cadre

Après avoir décidé à quoi ressemblerait ce service, j'ai commencé à chercher des options de mise en œuvre. Le plus simple serait de trouver une sorte de solution toute faite, sur laquelle, comme un hibou sur un globe, nos mécanismes pourraient être tirés et le tout exposé à la censure publique.
Mais ce n’est pas intéressant, je n’y ai vu aucun défi ni aucun sens, et j’ai donc commencé à étudier les technologies Web et les méthodes d’interaction avec elles.

J'ai commencé à étudier en consultant des articles et de la documentation sur C# .Net. Ici, j'ai trouvé différentes façons d'accomplir la tâche. Il existe de nombreux mécanismes pour interagir avec le réseau, depuis des solutions à part entière comme les services ASP.Net ou Azure, jusqu'à l'interaction directe avec les connexions TcpHttp.

Après avoir fait ma première tentative avec ASP, je l'ai immédiatement rejeté ; à mon avis, c'était une décision trop difficile pour notre service. Nous n’utiliserions même pas un tiers des capacités de cette plateforme, j’ai donc continué mes recherches. Le choix était entre TCP et client-serveur Http. Ici, sur Habré, je suis tombé sur un article sur serveur multithread, après l'avoir collecté et testé, j'ai décidé de me concentrer spécifiquement sur l'interaction avec les connexions TCP, pour une raison quelconque, je pensais que http ne me permettrait pas de créer une solution multiplateforme.

La première version du serveur incluait le traitement des connexions, diffusait du contenu de page Web statique et incluait une base de données d'utilisateurs. Et pour commencer, j'ai décidé de créer des fonctionnalités pour travailler avec le site, afin de pouvoir ajouter ultérieurement le traitement de l'application sur Android et iOS.

Voici du code
Le thread principal recevant les clients dans une boucle sans fin :

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);
        }
    }
}

Le gestionnaire client lui-même :

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();
}
}
}

Et la première base de données construite en SQL local :

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}");
}
}
}
}
}

Comme vous pouvez le constater, cette version diffère peu de celle de l'article. En fait, nous venons d'ajouter ici le chargement des pages à partir d'un dossier sur l'ordinateur et d'une base de données (qui, d'ailleurs, ne fonctionnait pas dans cette version en raison d'une architecture de connexion incorrecte).

Chapitre 2. Vissage des roues

Après avoir testé le serveur, je suis arrivé à la conclusion que ce serait une excellente solution (spoiler : non), pour notre service, le projet a donc commencé à acquérir une logique.
Petit à petit, de nouveaux modules ont commencé à apparaître et les fonctionnalités du serveur se sont étendues. Le serveur a acquis un domaine de test et un cryptage de connexion SSL.

Un peu plus de code décrivant la logique du traitement serveur et client
Une version mise à jour du serveur qui inclut l'utilisation d'un certificat.

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();
        }
    }
}

Et aussi un nouveau gestionnaire client avec autorisation 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);
}

}
}

Mais comme le serveur fonctionne exclusivement sur une connexion TCP, il est nécessaire de créer un module capable de reconnaître le contexte de la requête. J'ai décidé qu'un analyseur conviendrait ici, qui diviserait la demande du client en parties distinctes avec lesquelles je pourrais interagir afin de donner au client les réponses nécessaires.

Analyseur

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;
}
}
}

Son essence est de diviser la demande en plusieurs parties à l'aide d'expressions régulières. Nous recevons un message du client, sélectionnons la première ligne, qui contient la méthode et l'URL de la demande. Ensuite, nous lisons les titres, que nous mettons dans un tableau de la forme HeaderName=Content, et nous trouvons également, s'il est disponible, le contenu qui les accompagne (par exemple, une chaîne de requête) que nous mettons également dans un tableau similaire. De plus, l'analyseur découvre si le client actuel est autorisé et stocke ses données. Toutes les demandes des clients autorisés contiennent un hachage d'autorisation, qui est stocké dans des cookies, grâce à quoi il est possible de séparer la logique de fonctionnement des deux types de clients et de leur donner les réponses correctes.

Eh bien, une petite fonctionnalité intéressante qui mériterait d'être intégrée dans un module séparé, la conversion de requêtes telles que « site.com/@UserName » en pages utilisateur générées dynamiquement. Après traitement de la demande, les modules suivants entrent en jeu.

Chapitre 3. Installation du volant, lubrification de la chaîne

Dès que l'analyseur a terminé son travail, le gestionnaire entre en jeu, donnant des instructions supplémentaires au serveur et divisant le contrôle en deux parties.

Gestionnaire simple

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);
            };
        }
    }
}

En effet, il n'y a qu'une seule vérification de l'autorisation de l'utilisateur, après quoi commence le traitement de la demande.

Contrôleurs clients
Si l'utilisateur n'est pas autorisé, la fonctionnalité pour lui repose uniquement sur l'affichage des profils d'utilisateurs et la fenêtre d'enregistrement des autorisations. Le code d'un utilisateur autorisé est à peu près le même, je ne vois donc aucune raison de le dupliquer.

Utilisateur non autorisé

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;

}

}

}
}

Et bien sûr, l'utilisateur doit recevoir une sorte de contenu de page, donc pour les réponses, il existe le module suivant, qui est chargé de répondre aux demandes de ressources.

ÉcrivainContrôleur

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();
}
}
}

Mais afin de montrer à l'utilisateur son profil et ceux des autres utilisateurs, j'ai décidé d'utiliser RazorEngine, ou plutôt une partie de celui-ci. Cela inclut également le traitement des demandes invalides et l’émission d’un code d’erreur approprié.

Contrôleur de rasoir

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();
            }
        }
    }
}

Et bien sûr, pour que la vérification des utilisateurs autorisés fonctionne, une autorisation est nécessaire. Le module d'autorisation interagit avec la base de données. Les données reçues des formulaires du site sont analysées à partir du contexte, l'utilisateur est enregistré et reçoit en retour des cookies et l'accès au service.

Module d'autorisation

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);
}
}
}
}

Et voici à quoi ressemble le traitement de la base de données :

База данных

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;
}
}
}
}


Et tout fonctionne comme sur des roulettes, le travail d'autorisation et d'enregistrement, la fonctionnalité minimale pour accéder au service est déjà là et le moment est venu d'écrire une application et de lier le tout avec les fonctions principales pour lesquelles tout est fait.

Chapitre 4. Jeter le vélo

Pour réduire les coûts de main-d'œuvre liés à l'écriture de deux applications pour deux plates-formes, j'ai décidé de créer une plate-forme multiplateforme sur Xamarin.Forms. Encore une fois, grâce au fait qu'il est en C#. Après avoir réalisé une application de test qui envoie simplement des données au serveur, je suis tombé sur un point intéressant. Pour une requête d'un appareil, pour m'amuser, je l'ai implémenté sur HttpClient et je l'ai envoyé au serveur HttpRequestMessage, qui contient les données du formulaire d'autorisation au format json. Sans vraiment attendre quoi que ce soit, j'ai ouvert le journal du serveur et j'y ai vu une requête de l'appareil avec toutes les données. Une légère stupeur, une prise de conscience de tout ce qui s'est fait ces 3 dernières semaines dans une soirée alanguie. Pour vérifier l'exactitude des données envoyées, j'ai assemblé un serveur de test sur HttpListner. Ayant déjà reçu une autre demande, je l'ai démontée en quelques lignes de code et j'ai reçu une KeyValuePair de données du formulaire. L'analyse de la requête a été réduite à deux lignes.

J'ai commencé à tester davantage, cela n'a pas été mentionné plus tôt, mais sur le serveur précédent, j'ai également implémenté un chat basé sur des websockets. Cela fonctionnait plutôt bien, mais le principe même de l'interaction via Tcp était déprimant : il fallait faire trop de travail inutile pour construire avec compétence l'interaction de deux utilisateurs avec un journal de correspondance. Cela inclut l'analyse d'une demande de changement de connexion et la collecte d'une réponse à l'aide du protocole RFC 6455. Par conséquent, sur le serveur de test, j'ai décidé de créer une simple connexion Websocket. Juste pour le fun.

Connectez-vous pour discuter

 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();
                        }
                    }
                }
            }
        }

Et ça a marché. Le serveur lui-même a configuré la connexion et généré une clé de réponse. Je n'ai même pas eu besoin de configurer séparément l'enregistrement du serveur via SSL, il suffisait que le système dispose déjà d'un certificat installé sur le port requis.

Côté appareil et côté site, deux clients ont échangé des messages, tout cela a été journalisé. Aucun analyseur énorme ne ralentissant le serveur, rien de tout cela n'était requis. Le temps de réponse est passé de 200 ms à 40-30 ms. Et j'ai pris la seule bonne décision.

Cycle avancé ou application client-serveur basée sur le framework C# .Net

Supprimez l'implémentation actuelle du serveur sur TCP et réécrivez tout sous Http. Le projet est désormais en phase de refonte, mais selon des principes d'interaction complètement différents. Le fonctionnement des appareils et du site est synchronisé et débogué et a un concept commun, la seule différence étant qu'il n'est pas nécessaire de générer des pages HTML pour les appareils.

conclusion

« Si tu ne connais pas le gué, n’entre pas dans l’eau » Je pense qu'avant de commencer à travailler, je devrais avoir des buts et des objectifs plus clairement définis, ainsi que me plonger dans l'étude des technologies et méthodes nécessaires à leur mise en œuvre sur différents clients. Le projet est déjà en voie d’achèvement, mais je reviendrai peut-être pour parler de la façon dont j’ai à nouveau sauvegardé certaines choses. J'ai beaucoup appris au cours du processus de développement, mais il y a encore plus à apprendre à l'avenir. Si vous avez lu jusqu'ici, merci de l'avoir fait.

Source: habr.com