Aplikasi bersepeda lanjutan atau klien-server berdasarkan kerangka kerja C# .Net

Masuk

Semuanya berawal ketika seorang rekan menyarankan agar saya membuat layanan web kecil. Seharusnya itu seperti Tinder, tetapi untuk kelompok IT. Fungsinya sangat sederhana, Anda mendaftar, mengisi profil dan melanjutkan ke poin utama, yaitu menemukan seseorang untuk diajak bicara dan memperluas koneksi Anda serta menjalin kenalan baru.

Disini saya harus melakukan retret dan bercerita sedikit tentang diri saya, agar kedepannya lebih jelas mengapa saya mengambil langkah pengembangan tersebut.

Saat ini saya memegang posisi Artis Teknis di salah satu studio game, pengalaman pemrograman saya di C# dibangun hanya dengan menulis skrip dan utilitas untuk Unity dan, selain itu, membuat plugin untuk pekerjaan tingkat rendah dengan perangkat Android. Saya belum berkelana melampaui dunia kecil ini, dan kemudian kesempatan seperti itu muncul.

Bagian 1. Pembuatan prototipe bingkai

Setelah memutuskan seperti apa layanan ini, saya mulai mencari opsi untuk implementasi. Hal yang paling mudah adalah menemukan solusi yang siap pakai, yang mana, seperti burung hantu di bola dunia, mekanik kita bisa ditarik dan semuanya bisa dikecam publik.
Tapi ini tidak menarik, saya tidak melihat adanya tantangan atau makna di dalamnya, dan oleh karena itu saya mulai mempelajari teknologi web dan metode berinteraksi dengannya.

Saya mulai belajar dengan melihat artikel dan dokumentasi di C# .Net. Di sini saya menemukan berbagai cara untuk menyelesaikan tugas. Ada banyak mekanisme untuk berinteraksi dengan jaringan, mulai dari solusi lengkap seperti ASP.Net atau layanan Azure, hingga interaksi langsung dengan koneksi TcpHttp.

Setelah melakukan percobaan pertama saya dengan ASP, saya langsung menolaknya; menurut pendapat saya, ini adalah keputusan yang terlalu sulit untuk layanan kami. Kami tidak akan menggunakan sepertiga dari kemampuan platform ini, jadi saya melanjutkan pencarian saya. Pilihannya adalah antara TCP dan Http client-server. Di sini, di Habré, saya menemukan artikel tentang server multithread, setelah mengumpulkan dan mengujinya, saya memutuskan untuk fokus secara khusus pada interaksi dengan koneksi TCP, untuk beberapa alasan saya berpikir bahwa http tidak akan mengizinkan saya membuat solusi lintas platform.

Versi pertama server menyertakan pemrosesan koneksi, menyajikan konten halaman web statis, dan menyertakan database pengguna. Dan untuk memulainya, saya memutuskan untuk membangun fungsionalitas untuk bekerja dengan situs ini, sehingga nantinya saya dapat menambahkan pemrosesan aplikasi di Android dan iOS.

Ini beberapa kode
Thread utama yang menerima klien dalam loop tanpa akhir:

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

Penangan klien itu sendiri:

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


Dan database pertama yang dibangun di atas SQL lokal:
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}");
}
}
}
}
}


Seperti yang Anda lihat, versi ini sedikit berbeda dari yang ada di artikel. Faktanya, di sini kami baru saja menambahkan pemuatan halaman dari folder di komputer dan database (yang, omong-omong, tidak berfungsi di versi ini karena arsitektur koneksi yang salah).

Bab 2. Mengacaukan roda

Setelah menguji server, saya sampai pada kesimpulan bahwa ini akan menjadi solusi terbaik (spoiler: tidak), untuk layanan kami, sehingga proyek mulai mendapatkan logika.
Selangkah demi selangkah, modul-modul baru mulai muncul dan fungsionalitas server pun meluas. Server tersebut memperoleh domain uji dan ssl Enkripsi koneksi.

Sedikit lagi kode yang menjelaskan logika pemrosesan server dan klien
Versi server yang diperbarui yang mencakup penggunaan sertifikat.

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

Dan juga pengendali klien baru dengan otorisasi 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);
}

}
}


Namun karena server berjalan secara eksklusif pada koneksi TCP, maka perlu dibuat modul yang dapat mengenali konteks permintaan. Saya memutuskan bahwa parser akan cocok di sini yang akan membagi permintaan dari klien menjadi beberapa bagian terpisah yang dapat saya gunakan untuk berinteraksi untuk memberikan jawaban yang diperlukan kepada klien.

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


Esensinya adalah memecah permintaan menjadi beberapa bagian menggunakan ekspresi reguler. Kami menerima pesan dari klien, pilih baris pertama, yang berisi metode dan url permintaan. Kemudian kita membaca judulnya, yang kita masukkan ke dalam array dengan bentuk HeaderName=Content, dan kita juga menemukan, jika ada, konten yang menyertainya (misalnya, string kueri) yang juga kita masukkan ke dalam array serupa. Selain itu, parser mengetahui apakah klien saat ini diotorisasi dan menyimpan datanya. Semua permintaan dari klien resmi berisi hash otorisasi, yang disimpan dalam cookie, berkat ini dimungkinkan untuk memisahkan logika operasi lebih lanjut untuk kedua jenis klien dan memberi mereka jawaban yang benar.

Ya, fitur kecil dan bagus yang layak dimasukkan ke dalam modul terpisah, konversi kueri seperti “site.com/@UserName” menjadi halaman pengguna yang dibuat secara dinamis. Setelah memproses permintaan, modul berikut mulai berlaku.

Bab 3. Memasang setir, melumasi rantai

Segera setelah parser menyelesaikan pekerjaannya, handler ikut bermain, memberikan instruksi lebih lanjut kepada server dan membagi kontrol menjadi dua bagian.

Penangan sederhana

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

Faktanya, hanya ada satu pemeriksaan untuk otorisasi pengguna, setelah itu pemrosesan permintaan dimulai.

Pengontrol klien
Jika pengguna tidak diotorisasi, maka fungsinya hanya didasarkan pada tampilan profil pengguna dan jendela pendaftaran otorisasi. Kode untuk pengguna yang berwenang terlihat hampir sama, jadi saya tidak melihat alasan untuk menduplikasinya.

Pengguna yang tidak sah

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;

}

}

}
}


Dan tentu saja, pengguna harus menerima semacam konten halaman, jadi untuk tanggapannya terdapat modul berikut, yang bertanggung jawab untuk menanggapi permintaan sumber daya.

Pengontrol Penulis

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


Tetapi untuk menunjukkan kepada pengguna profilnya dan profil pengguna lain, saya memutuskan untuk menggunakan RazorEngine, atau lebih tepatnya bagian darinya. Ini juga mencakup pemrosesan permintaan yang tidak valid dan mengeluarkan kode kesalahan yang sesuai.

Pengendali Pisau Cukur

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

Dan tentunya agar verifikasi pengguna yang berwenang dapat berfungsi, diperlukan otorisasi. Modul otorisasi berinteraksi dengan database. Data yang diterima dari formulir di situs diurai dari konteksnya, pengguna disimpan dan sebagai imbalannya menerima cookie dan akses ke layanan.

Modul otorisasi

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


Dan seperti inilah pemrosesan databasenya:

Basis data

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


Dan semuanya berfungsi seperti jarum jam, otorisasi dan registrasi berfungsi, fungsionalitas minimum untuk mengakses layanan sudah ada dan waktunya telah tiba untuk menulis aplikasi dan menghubungkan semuanya dengan fungsi utama yang semuanya sedang dilakukan.

Bab 4. Membuang sepedanya

Untuk mengurangi biaya tenaga kerja dalam menulis dua aplikasi untuk dua platform, saya memutuskan untuk membuat lintas platform di Xamarin.Forms. Sekali lagi, berkat fakta bahwa itu ada di C#. Setelah membuat aplikasi pengujian yang hanya mengirimkan data ke server, saya menemukan hal yang menarik. Untuk permintaan dari perangkat, untuk bersenang-senang, saya mengimplementasikannya di HttpClient dan mengirimkannya ke server HttpRequestMessage, yang berisi data dari formulir otorisasi dalam format json. Tanpa mengharapkan apa pun, saya membuka log server dan melihat ada permintaan dari perangkat dengan semua datanya. Sedikit pingsan, kesadaran akan segala sesuatu yang telah dilakukan selama 3 minggu terakhir di malam yang lesu. Untuk memeriksa keakuratan data yang dikirim, saya membuat server uji di HttpListner. Setelah menerima permintaan lain, saya memisahkannya dalam beberapa baris kode dan menerima data KeyValuePair dari formulir. Penguraian kueri dikurangi menjadi dua baris.

Saya mulai menguji lebih lanjut, tidak disebutkan sebelumnya, tetapi di server sebelumnya saya juga mengimplementasikan chat yang dibangun di websockets. Ini bekerja cukup baik, tetapi prinsip interaksi melalui Tcp menyedihkan, terlalu banyak pekerjaan yang tidak perlu harus dilakukan untuk membangun interaksi dua pengguna dengan log korespondensi secara kompeten. Ini termasuk mengurai permintaan untuk mengalihkan koneksi dan mengumpulkan respons menggunakan protokol RFC 6455. Oleh karena itu, di server pengujian, saya memutuskan untuk membuat koneksi websocket sederhana. Hanya untuk bersenang-senang.

Hubungkan ke obrolan

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

Dan itu berhasil. Server itu sendiri yang mengonfigurasi koneksi dan menghasilkan kunci respons. Saya bahkan tidak perlu mengonfigurasi pendaftaran server melalui SSL secara terpisah; sistem sudah cukup memiliki sertifikat yang diinstal pada port yang diperlukan.

Di sisi perangkat dan di sisi situs, dua klien bertukar pesan, semua ini dicatat. Tidak ada parser besar yang memperlambat server, semua ini tidak diperlukan. Waktu respons berkurang dari 200 md menjadi 40-30 md. Dan saya mengambil satu-satunya keputusan yang tepat.

Aplikasi bersepeda lanjutan atau klien-server berdasarkan kerangka kerja C# .Net

Buang implementasi server saat ini di Tcp dan tulis ulang semuanya di bawah Http. Sekarang proyek tersebut sedang dalam tahap desain ulang, tetapi berdasarkan prinsip interaksi yang sama sekali berbeda. Pengoperasian perangkat dan situs disinkronkan dan di-debug serta memiliki konsep yang sama, dengan satu-satunya perbedaan adalah tidak perlu membuat halaman HTML untuk perangkat.

Keluaran

“Jika Anda tidak tahu cara mengarunginya, jangan masuk ke dalam air” Saya pikir sebelum mulai bekerja, saya harus memiliki tujuan dan sasaran yang lebih jelas, serta mempelajari studi tentang teknologi dan metode yang diperlukan untuk penerapannya pada berbagai klien. Proyek ini sudah hampir selesai, tapi mungkin saya akan kembali lagi untuk membicarakan bagaimana saya menyelamatkan hal-hal tertentu. Saya belajar banyak selama proses pengembangan, namun masih banyak lagi yang harus dipelajari di masa depan. Jika Anda sudah membaca sejauh ini, terima kasih telah melakukannya.

Sumber: www.habr.com