Zaawansowana aplikacja cykliczna lub klient-serwer oparta na frameworku C# .Net

Wejście

Wszystko zaczęło się, gdy kolega zasugerował, żebym zrobił mały serwis internetowy. Miało to być coś w rodzaju krzesiwa, ale na spotkanie IT. Funkcjonalność jest banalnie prosta, rejestrujesz się, uzupełniasz profil i przechodzisz do sedna, czyli znalezienia rozmówcy i poszerzenia znajomości oraz nawiązania nowych znajomości.

Tutaj muszę zrobić dygresję i opowiedzieć trochę o sobie, aby w przyszłości było jaśniej, dlaczego podjąłem takie kroki w rozwoju.

W chwili obecnej pełnię funkcję Artysty Technicznego w studiu gier, moje doświadczenie w programowaniu w C# opierało się wyłącznie na pisaniu skryptów i narzędzi dla Unity oraz tworzeniu wtyczek do niskopoziomowej pracy z urządzeniami z systemem Android. Poza tym światem jeszcze nie wybrałem, a potem trafiłem na taką okazję.

Część 1. Prototypowanie ramek

Po podjęciu decyzji, jaka będzie ta usługa, zacząłem szukać opcji wdrożenia. Najprościej byłoby znaleźć jakieś gotowe rozwiązanie, na które niczym sowa na globusie można wciągnąć naszą mechanikę i wystawić całość do publicznego potępienia.
Ale to nie jest ciekawe, nie widziałem w tym żadnego wyzwania i sensu, dlatego zacząłem studiować technologie internetowe i metody interakcji z nimi.

Badanie rozpoczęto od przejrzenia artykułów i dokumentacji w języku C# .Net. Tutaj znalazłem różne sposoby wykonania zadania. Istnieje wiele mechanizmów interakcji z siecią, od pełnoprawnych rozwiązań, takich jak ASP.Net czy usługi Azure, po bezpośrednią interakcję z połączeniami TcpHttp.

Po pierwszej próbie z ASP od razu ją odwołałem, moim zdaniem była to zbyt trudna decyzja dla naszego serwisu. Nie wykorzystamy nawet jednej trzeciej możliwości tej platformy, więc kontynuowałem poszukiwania. Wybór powstał między TCP a Http klient-serwer. Tutaj, na Habré, natknąłem się na artykuł nt serwer wielowątkowy, po ich zebraniu i przetestowaniu postanowiłem skupić się na interakcji z połączeniami TCP, z jakiegoś powodu pomyślałem, że http nie pozwoli mi stworzyć rozwiązania międzyplatformowego.

Pierwsza wersja serwera obejmowała obsługę połączeń, udostępnianie statycznej zawartości strony internetowej oraz zawierało bazę danych użytkowników. A na początek postanowiłem zbudować funkcjonał do pracy z serwisem, aby później móc tu powiązać przetwarzanie aplikacji na android i ios.

Oto trochę kodu
Główny wątek przyjmowania klientów w nieskończonej pętli:

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

Sam program obsługi klienta:

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

I pierwsza baza danych zbudowana na lokalnym 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}");
}
}
}
}
}

Jak widać, ta wersja niewiele różni się od tej w artykule. Właściwie dodano tutaj tylko ładowanie stron z folderu na komputerze i bazy danych (co notabene nie działało w tej wersji, ze względu na niepoprawną architekturę połączenia).

Rozdział 2

Po przetestowaniu serwera doszedłem do wniosku, że byłoby to świetne rozwiązanie (spoiler: nie), za naszą usługę, więc projekt zaczął nabierać logiki.
Krok po kroku zaczęły pojawiać się nowe moduły i rosła funkcjonalność serwera. Serwer posiada domenę testową oraz szyfrowanie połączenia ssl.

Trochę więcej kodu opisującego logikę serwera i przetwarzanie klientów
Zaktualizowana wersja serwera, w tym korzystanie z certyfikatu.

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

Jak również nowy moduł obsługi klienta z autoryzacją przez 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);
}

}
}

Ponieważ jednak serwer działa wyłącznie na połączeniu TCP, konieczne jest stworzenie modułu rozpoznającego kontekst żądania. Uznałem, że odpowiedni jest tutaj parser, który podzieli żądanie od klienta na osobne części, z którymi mogę wchodzić w interakcję, aby udzielić klientowi niezbędnych odpowiedzi.

parser

using ClearServer.Core.UserController;
using ReServer.Core.Classes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;

namespace ClearServer.Core.Requester
{
    public class RequestContext
    {
        public string Message = "";
        private readonly byte[] buffer = new byte[1024];
        public string RequestMethod;
        public string RequestUrl;
        public User RequestProfile;
        public User CurrentUser = null;
        public List<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;
}
}
}

Jego istota polega na tym, że za pomocą wyrażeń regularnych można podzielić żądanie na części. Otrzymujemy wiadomość od klienta, wybieramy pierwszą linię, która zawiera metodę i url żądania. Następnie odczytujemy nagłówki, które umieszczamy w tablicy w postaci HeaderName = Content, a także znajdujemy, jeśli istnieje, towarzyszącą treść (na przykład ciąg zapytania), którą również umieszczamy w podobnej tablicy. Ponadto parser sprawdza, czy aktualny klient jest autoryzowany i zapisuje jego dane. Wszystkie żądania od autoryzowanych klientów zawierają hash autoryzacyjny, który jest przechowywany w plikach cookie, dzięki czemu możliwe jest rozdzielenie dalszej logiki pracy dla dwóch typów klientów i udzielenie im poprawnych odpowiedzi.

Cóż, mała, fajna funkcja, którą warto umieścić w osobnym module, konwertująca zapytania typu „site.com/@UserName” na dynamicznie generowane strony użytkownika. Po przetworzeniu żądania do gry wchodzą następujące moduły.

Rozdział 3. Montaż kierownicy, smarowanie łańcucha

Gdy tylko parser zakończy pracę, do gry wchodzi program obsługi, przekazując serwerowi dalsze instrukcje i dzieląc kontrolę na dwie części.

prosta obsługa

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

W rzeczywistości istnieje tylko jedno sprawdzenie autoryzacji użytkownika, po którym rozpoczyna się przetwarzanie żądania.

Kontrolery klienta
Jeżeli użytkownik nie jest autoryzowany, to dla niego funkcjonalność opiera się jedynie na wyświetlaniu profili użytkowników oraz oknie rejestracji uprawnień. Kod dla autoryzowanego użytkownika wygląda mniej więcej tak samo, więc nie widzę powodu, aby go powielać.

Nieautoryzowany użytkownik

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;

}

}

}
}

I oczywiście użytkownik musi otrzymać jakąś zawartość stron, więc za odpowiedzi jest następujący moduł, który odpowiada za odpowiedź na prośbę o zasoby.

Kontroler pisarza

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

Aby jednak pokazać użytkownikowi jego profil oraz profile innych użytkowników, zdecydowałem się użyć RazorEngine, a raczej jego części. Obejmuje to również obsługę złych żądań i wydawanie odpowiedniego kodu błędu.

Kontroler Razor

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

I oczywiście, aby weryfikacja uprawnionych użytkowników działała, potrzebna jest autoryzacja. Moduł autoryzacji współpracuje z bazą danych. Dane otrzymane z formularzy na stronie są analizowane z kontekstu, użytkownik jest zapisywany iw zamian otrzymuje pliki cookies oraz dostęp do usługi.

Moduł autoryzacji

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

A tak wygląda baza danych:

Baza danych

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


I wszystko działa jak w zegarku, autoryzacja i rejestracja działają, minimalna funkcjonalność dostępu do usługi jest już dostępna i czas napisać aplikację i powiązać całość z głównymi funkcjami, dla których wszystko jest zrobione.

Rozdział 4

Aby obniżyć koszty pracy związane z napisaniem dwóch aplikacji na dwie platformy, zdecydowałem się na cross-platform na Xamarin.Forms. Ponownie, dzięki temu, że jest w C#. Po wykonaniu aplikacji testowej, która po prostu wysyła dane na serwer, natknąłem się na jeden ciekawy moment. Na żądanie z urządzenia, dla zabawy, zaimplementowałem to na HttpClient i wrzuciłem na serwer HttpRequestMessage, który zawiera dane z formularza autoryzacyjnego w formacie json. Nie spodziewając się niczego szczególnego, otworzyłem dziennik serwera i zobaczyłem żądanie z urządzenia ze wszystkimi danymi. Lekkie otępienie, świadomość wszystkiego, co zostało zrobione w ciągu ostatnich 3 tygodni ospałego wieczoru. Aby sprawdzić poprawność przesłanych danych, zmontowałem serwer testowy na HttpListner. Otrzymawszy już kolejne żądanie, rozłożyłem je w kilku liniach kodu, otrzymałem dane KeyValuePair z formularza. Analiza zapytania zredukowana do dwóch linii.

Zacząłem dalej testować, wcześniej o tym nie było mowy, ale na poprzednim serwerze nadal zaimplementowałem chat zbudowany na websocketach. Działało to całkiem dobrze, ale sama zasada interakcji przez Tcp była przygnębiająca, zbyt wiele trzeba było wyprodukować, aby poprawnie zbudować interakcję dwóch użytkowników z logowaniem korespondencji. Obejmuje to parsowanie żądania przełączenia połączenia i zebranie odpowiedzi przy użyciu protokołu RFC 6455. Dlatego na serwerze testowym postanowiłem stworzyć proste połączenie websocket. Czysto w interesie.

Połączenie czatu

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

I zadziałało. Serwer sam zestawił połączenie, wygenerował klucz odpowiedzi. Nie musiałem nawet osobno konfigurować rejestracji serwera przez ssl, wystarczy, że system ma już zainstalowany certyfikat na wymaganym porcie.

Po stronie urządzenia i po stronie serwisu dwóch klientów wymieniało wiadomości, wszystko to było rejestrowane. Żadnych wielkich parserów spowalniających serwer, nic z tego nie było wymagane. Czas odpowiedzi został skrócony z 200 ms do 40-30 ms. I doszedłem do jedynej słusznej decyzji.

Zaawansowana aplikacja cykliczna lub klient-serwer oparta na frameworku C# .Net

Wyrzuć obecną implementację serwera na Tcp i przepisz wszystko pod Http. Teraz projekt jest na etapie przeprojektowywania, ale według zupełnie innych zasad interakcji. Działanie urządzeń i serwisu jest zsynchronizowane i debugowane oraz ma wspólną koncepcję, z tą tylko różnicą, że urządzenia nie muszą generować stron html.

Wniosek

"Nie znając brodu nie wkładaj głowy do wody" Myślę, że przed rozpoczęciem pracy powinienem był dokładniej określić cele i zadania, a także zagłębić się w badanie niezbędnych technologii i metod ich wdrażania na różnych klientach. Projekt jest już bliski ukończenia, ale może wrócę, aby porozmawiać o tym, jak znowu schrzaniłem pewne rzeczy. Wiele się nauczyłem podczas procesu tworzenia, ale wciąż jest wiele do nauczenia się w przyszłości. Jeśli doczytałeś tak daleko, to dzięki za przeczytanie.

Źródło: www.habr.com