C# .Net framework дээр суурилсан дэвшилтэт дугуй эсвэл клиент-сервер програм

нэвтрэх

Хамт ажилладаг хүн надад жижиг вэб үйлчилгээ хийхийг санал болгосноор бүх зүйл эхэлсэн. Энэ нь tinder шиг зүйл байх ёстой байсан ч IT hangout-д зориулагдсан. Үйл ажиллагаа нь маш энгийн бөгөөд та бүртгүүлж, профайлаа бөглөж, ярилцагчийг хайж олох, харилцаа холбоогоо өргөжүүлэх, шинэ танилууд хийх гол зүйл рүү шилжинэ.

Би эндээс ухарч, өөрийнхөө тухай бага зэрэг ярих хэрэгтэй, тэгвэл ирээдүйд яагаад хөгжлийн төлөө ийм алхам хийсэн бэ гэдгийг илүү тодорхой болгох болно.

Одоогийн байдлаар би тоглоомын студид Техникийн зураачаар ажиллаж байгаа бөгөөд миний C# програмчлалын туршлага нь зөвхөн Unity-д зориулсан скрипт, хэрэгслүүд бичих, үүнээс гадна андройд төхөөрөмжтэй доод түвшний ажилд зориулсан залгаасуудыг бий болгоход тулгуурласан. Энэ ертөнцөөс гадуур би ийм боломжийг хараахан сонгоогүй байна.

1-р хэсэг. Хүрээний загварчлал

Энэ үйлчилгээ ямар байхыг шийдсэний дараа би хэрэгжүүлэх хувилбаруудыг хайж эхлэв. Хамгийн хялбар арга бол бөмбөрцөг дээрх шар шувуу шиг манай механикуудыг татаж, бүх зүйлийг олон нийтэд буруушааж болох бэлэн шийдлийг олох явдал юм.
Гэхдээ энэ нь тийм ч сонирхолтой биш, би үүнд ямар ч бэрхшээл, мэдрэмжийг олж хараагүй тул вэб технологи, тэдэнтэй харилцах аргуудыг судалж эхэлсэн.

Судалгааг C # .Net дээрх нийтлэл, баримт бичгийг үзэж эхэлсэн. Энд би даалгавраа биелүүлэх янз бүрийн арга замыг олсон. ASP.Net эсвэл Azure үйлчилгээ зэрэг бүрэн хэмжээний шийдлүүдээс эхлээд TcpHttp холболттой шууд харьцах хүртэл сүлжээтэй харилцах олон механизм байдаг.

ASP-тэй анхны оролдлого хийсний дараа би үүнийг шууд цуцалсан, миний бодлоор энэ нь манай үйлчилгээний хувьд хэтэрхий хэцүү шийдвэр байсан. Бид энэ платформын боломжуудын гуравны нэгийг ч ашиглахгүй тул би хайлтаа үргэлжлүүлэв. Сонголт нь TCP болон Http клиент серверийн хооронд үүссэн. Хабре дээр би энэ тухай нийтлэл олж уншлаа олон урсгалтай сервер, үүнийг цуглуулж, туршиж үзээд би TCP холболтуудтай ажиллахад анхаарлаа төвлөрүүлэхээр шийдсэн тул яагаад ч юм http намайг хөндлөн платформ шийдэл үүсгэхийг зөвшөөрөхгүй гэж бодсон.

Серверийн эхний хувилбарт холболтуудыг зохицуулах, статик вэб хуудасны контентыг хангах, хэрэглэгчийн мэдээллийн бааз зэрэг багтсан. Эхлэхийн тулд би энэ сайттай ажиллах функцийг бүтээхээр шийдсэн бөгөөд ингэснээр би дараа нь Android болон iOS дээр програмын боловсруулалтыг холбох боломжтой болсон.

Энд зарим код байна
Үйлчлүүлэгчдийг эцэс төгсгөлгүй давталтаар хүлээн авдаг гол урсгал:

using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;

namespace ClearServer
{

    class Server
    {
        TcpListener Listener;
        public Server(int Port)
        {
            Listener = new TcpListener(IPAddress.Any, Port);
            Listener.Start();

            while (true)
            {
                TcpClient Client = Listener.AcceptTcpClient();
                Thread Thread = new Thread(new ParameterizedThreadStart(ClientThread));
                Thread.Start(Client);
            }
        }

        static void ClientThread(Object StateInfo)
        {
            new Client((TcpClient)StateInfo);
        }

        ~Server()
        {
            if (Listener != null)
            {
                Listener.Stop();
            }
        }

        static void Main(string[] args)
        {
            DatabaseWorker sqlBase = DatabaseWorker.GetInstance;

            new Server(80);
        }
    }
}

Үйлчлүүлэгч өөрөө:

using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;

namespace ClearServer
{
    class Client
    {


        public Client(TcpClient Client)
        {

            string Message = "";
            byte[] Buffer = new byte[1024];
            int Count;
            while ((Count = Client.GetStream().Read(Buffer, 0, Buffer.Length)) > 0)
            {
                Message += Encoding.UTF8.GetString(Buffer, 0, Count);

                if (Message.IndexOf("rnrn") >= 0 || Message.Length > 4096)
                {
                    Console.WriteLine(Message);
                    break;
                }
            }

            Match ReqMatch = Regex.Match(Message, @"^w+s+([^s?]+)[^s]*s+HTTP/.*|");
            if (ReqMatch == Match.Empty)
            {
                ErrorWorker.SendError(Client, 400);
                return;
            }
            string RequestUri = ReqMatch.Groups[1].Value;
            RequestUri = Uri.UnescapeDataString(RequestUri);
            if (RequestUri.IndexOf("..") >= 0)
            {
                ErrorWorker.SendError(Client, 400);
                return;
            }
            if (RequestUri.EndsWith("/"))
            {
                RequestUri += "index.html";
            }

            string FilePath =

quot;D:/Web/TestSite{RequestUri}";

if (!File.Exists(FilePath))
{
ErrorWorker.SendError(Client, 404);
return;
}

string Extension = RequestUri.Substring(RequestUri.LastIndexOf('.'));

string ContentType = "";

switch (Extension)
{
case ".htm":
case ".html":
ContentType = "text/html";
break;
case ".css":
ContentType = "text/css";
break;
case ".js":
ContentType = "text/javascript";
break;
case ".jpg":
ContentType = "image/jpeg";
break;
case ".jpeg":
case ".png":
case ".gif":
ContentType =


quot;image/{Extension.Substring(1)}";
break;
default:
if (Extension.Length > 1)
{
ContentType =


quot;application/{Extension.Substring(1)}";
}
else
{
ContentType = "application/unknown";
}
break;
}

FileStream FS;
try
{
FS = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
}
catch (Exception)
{
ErrorWorker.SendError(Client, 500);
return;
}

string Headers =


quot;HTTP/1.1 200 OKnContent-Type: {ContentType}nContent-Length: {FS.Length}nn";
byte[] HeadersBuffer = Encoding.ASCII.GetBytes(Headers);
Client.GetStream().Write(HeadersBuffer, 0, HeadersBuffer.Length);

while (FS.Position < FS.Length)
{
Count = FS.Read(Buffer, 0, Buffer.Length);
Client.GetStream().Write(Buffer, 0, Count);
}

FS.Close();
Client.Close();
}
}
}

Мөн локал SQL дээр бүтээгдсэн анхны мэдээллийн сан:

using System;
using System.Data.Linq;
namespace ClearServer
{
    class DatabaseWorker
    {

        private static DatabaseWorker instance;

        public static DatabaseWorker GetInstance
        {
            get
            {
                if (instance == null)
                    instance = new DatabaseWorker();
                return instance;
            }
        }


        private DatabaseWorker()
        {
            string connectionStr = databasePath;
            using (DataContext db = new DataContext(connectionStr))
            {
                Table<User> users = db.GetTable<User>();
                foreach (var item in users)
                {
                    Console.WriteLine(

quot;{item.login} {item.password}");
}
}
}
}
}

Таны харж байгаагаар энэ хувилбар нь нийтлэл дэх хувилбараас бага зэрэг ялгаатай байна. Үнэн хэрэгтээ энд зөвхөн компьютер дээрх фолдероос хуудас ачаалах, мэдээллийн баазыг нэмсэн (дашрамд хэлэхэд буруу холболтын архитектурын улмаас энэ хувилбарт ажиллахгүй байсан).

2-р бүлэг

Серверийг туршиж үзээд энэ нь маш сайн шийдэл байх болно гэсэн дүгнэлтэд хүрсэн (спойлер: үгүй), манай үйлчилгээний хувьд төсөл нь логикийг олж авч эхэлсэн.
Алхам алхмаар шинэ модулиуд гарч ирж, серверийн ажиллагаа нэмэгдэв. Сервер нь туршилтын домэйн болон ssl холболтын шифрлэлттэй болсон.

Серверийн логик, үйлчлүүлэгчдийн боловсруулалтыг тайлбарласан арай илүү код
Серверийн шинэчилсэн хувилбар, үүнд гэрчилгээ ашиглах.

using System;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Security;
using System.Security.Cryptography.X509Certificates;
using System.Security.Permissions;
using System.Security.Policy;
using System.Threading;


namespace ClearServer
{

    sealed class Server
    {
        readonly bool ServerRunning = true;
        readonly TcpListener sslListner;
        public static X509Certificate serverCertificate = null;
        Server()
        {
            serverCertificate = X509Certificate.CreateFromSignedFile(@"C:sslitinder.online.crt");
            sslListner = new TcpListener(IPAddress.Any, 443);
            sslListner.Start();
            Console.WriteLine("Starting server.." + serverCertificate.Subject + "n" + Assembly.GetExecutingAssembly().Location);
            while (ServerRunning)
            {
                TcpClient SslClient = sslListner.AcceptTcpClient();
                Thread SslThread = new Thread(new ParameterizedThreadStart(ClientThread));
                SslThread.Start(SslClient);
            }
            
        }
        static void ClientThread(Object StateInfo)
        {
            new Client((TcpClient)StateInfo);
        }

        ~Server()
        {
            if (sslListner != null)
            {
                sslListner.Stop();
            }
        }

        public static void Main(string[] args)
        {
            if (AppDomain.CurrentDomain.IsDefaultAppDomain())
            {
                Console.WriteLine("Switching another domain");
                new AppDomainSetup
                {
                    ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase
                };
                var current = AppDomain.CurrentDomain;
                var strongNames = new StrongName[0];
                var domain = AppDomain.CreateDomain(
                    "ClearServer", null,
                    current.SetupInformation, new PermissionSet(PermissionState.Unrestricted),
                    strongNames);
                domain.ExecuteAssembly(Assembly.GetExecutingAssembly().Location);
            }
            new Server();
        }
    }
}

Мөн ssl-ээр дамжуулан зөвшөөрөл авсан шинэ үйлчлүүлэгч зохицуулагч:

using ClearServer.Core.Requester;
using System;
using System.Net.Security;
using System.Net.Sockets;

namespace ClearServer
{
    public class Client
    {
        public Client(TcpClient Client)
        {
            SslStream SSlClientStream = new SslStream(Client.GetStream(), false);
            try
            {
                SSlClientStream.AuthenticateAsServer(Server.serverCertificate, clientCertificateRequired: false, checkCertificateRevocation: true);
            }
            catch (Exception e)
            {
                Console.WriteLine(
                    "---------------------------------------------------------------------n" +


quot;|{DateTime.Now:g}n|------------n|{Client.Client.RemoteEndPoint}n|------------n|Exception: {e.Message}n|------------n|Authentication failed - closing the connection.n" +
"---------------------------------------------------------------------n");
SSlClientStream.Close();
Client.Close();
}
new RequestContext(SSlClientStream, Client);
}

}
}

Гэхдээ сервер нь зөвхөн TCP холболт дээр ажилладаг тул хүсэлтийн контекстийг таних модулийг бий болгох шаардлагатай. Үйлчлүүлэгчээс ирсэн хүсэлтийг салангид хэсгүүдэд хувааж, үйлчлүүлэгчид шаардлагатай хариултуудыг өгөхийн тулд би харилцаж болох задлан шинжлэгч энд тохиромжтой гэж би шийдсэн.

задлан шинжлэгч

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

namespace ClearServer.Core.Requester
{
    public class RequestContext
    {
        public string Message = "";
        private readonly byte[] buffer = new byte[1024];
        public string RequestMethod;
        public string RequestUrl;
        public User RequestProfile;
        public User CurrentUser = null;
        public List<RequestValues> HeadersValues;
        public List<RequestValues> FormValues;
        private TcpClient TcpClient;

        private event Action<SslStream, RequestContext> OnRead = RequestHandler.OnHandle;

        DatabaseWorker databaseWorker = new DatabaseWorker();

        public RequestContext(SslStream ClientStream, TcpClient Client)
        {

            this.TcpClient = Client;
            try
            {
                ClientStream.BeginRead(buffer, 0, buffer.Length, ClientRead, ClientStream);
            }
            catch { return; }
        }
        private void ClientRead(IAsyncResult ar)
        {
            SslStream ClientStream = (SslStream)ar.AsyncState;

            if (ar.IsCompleted)
            {
                Message = Encoding.UTF8.GetString(buffer);
                Message = Uri.UnescapeDataString(Message);
                Console.WriteLine(

quot;n{DateTime.Now:g} Client IP:{TcpClient.Client.RemoteEndPoint}n{Message}");
RequestParse();
HeadersValues = HeaderValues();
FormValues = ContentValues();
UserParse();
ProfileParse();
OnRead?.Invoke(ClientStream, this);
}
}

private void RequestParse()
{
Match methodParse = Regex.Match(Message, @"(^w+)s+([^s?]+)[^s]*s+HTTP/.*|");
RequestMethod = methodParse.Groups[1].Value.Trim();
RequestUrl = methodParse.Groups[2].Value.Trim();
}
private void UserParse()
{
string cookie;
try
{
if (HeadersValues.Any(x => x.Name.Contains("Cookie")))
{
cookie = HeadersValues.FirstOrDefault(x => x.Name.Contains("Cookie")).Value;
try
{
CurrentUser = databaseWorker.CookieValidate(cookie);
}
catch { }
}
}
catch { }

}
private List<RequestValues> HeaderValues()
{
var values = new List<RequestValues>();
var parse = Regex.Matches(Message, @"(.*?): (.*?)n");
foreach (Match match in parse)
{
values.Add(new RequestValues()
{
Name = match.Groups[1].Value.Trim(),
Value = match.Groups[2].Value.Trim()
});
}
return values;
}

private void ProfileParse()
{
if (RequestUrl.Contains("@"))
{
RequestProfile = databaseWorker.FindUser(RequestUrl.Substring(2));
RequestUrl = "/profile";
}
}
private List<RequestValues> ContentValues()
{
var values = new List<RequestValues>();
var output = Message.Trim('n').Split().Last();
var parse = Regex.Matches(output, @"([^&].*?)=([^&]*b)");
foreach (Match match in parse)
{
values.Add(new RequestValues()
{
Name = match.Groups[1].Value.Trim(),
Value = match.Groups[2].Value.Trim().Replace('+', ' ')
});
}
return values;
}
}
}

Үүний мөн чанар нь байнгын хэллэгийн тусламжтайгаар хүсэлтийг хэсэг болгон хуваах явдал юм. Бид үйлчлүүлэгчээс мессеж хүлээн авч, арга, хүсэлтийг агуулсан эхний мөрийг сонго. Дараа нь бид HeaderName = Content хэлбэрийн массив руу хөтлөх толгойнуудыг уншиж, хэрэв байгаа бол дагалдах контентыг (жишээ нь querystring) олж, ижил массив руу хөтлөх болно. Нэмж дурдахад, задлан шинжлэгч нь одоогийн үйлчлүүлэгчийн зөвшөөрөлтэй эсэхийг олж мэдээд түүний өгөгдлийг хадгалдаг. Эрх бүхий үйлчлүүлэгчдийн бүх хүсэлт нь күүкид хадгалагдсан зөвшөөрлийн хэш агуулсан бөгөөд үүний ачаар хоёр төрлийн үйлчлүүлэгчийн цаашдын ажлын логикийг салгаж, тэдэнд зөв хариулт өгөх боломжтой.

"site.com/@UserName" гэх мэт хүсэлтийг динамикаар үүсгэгдсэн хэрэглэгчийн хуудас болгон хувиргах жижиг, сайхан функцийг тусдаа модуль болгон шилжүүлэх ёстой. Хүсэлтийг боловсруулсны дараа дараах модулиуд ажиллаж эхэлнэ.

Бүлэг 3. Бариулыг суурилуулах, гинжийг тослох

Шинжилгээ хийж дуусмагц зохицуулагч ажиллаж, серверт нэмэлт заавар өгч, хяналтыг хоёр хэсэгт хуваадаг.

энгийн зохицуулагч

using ClearServer.Core.UserController;
using System.Net.Security;
namespace ClearServer.Core.Requester
{
    public class RequestHandler
    {
        public static void OnHandle(SslStream ClientStream, RequestContext context)
        {

            if (context.CurrentUser != null)
            {
                new AuthUserController(ClientStream, context);
            }
            else 
            {
                new NonAuthUserController(ClientStream, context);
            };
        }
    }
}

Үнэн хэрэгтээ хэрэглэгчийн зөвшөөрлийг шалгах ганцхан зүйл байдаг бөгөөд үүний дараа хүсэлтийг боловсруулж эхэлдэг.

Үйлчлүүлэгчийн хянагч
Хэрэв хэрэглэгч зөвшөөрөлгүй бол түүний хувьд функц нь зөвхөн хэрэглэгчийн профайлыг харуулах, зөвшөөрлийн бүртгэлийн цонхонд суурилдаг. Зөвшөөрөгдсөн хэрэглэгчийн код нь ойролцоогоор ижил харагддаг тул би үүнийг хуулбарлах ямар ч шалтгаан олж харахгүй байна.

Зөвшөөрөлгүй хэрэглэгч

using ClearServer.Core.Requester;
using System.IO;
using System.Net.Security;

namespace ClearServer.Core.UserController
{
    internal class NonAuthUserController
    {
        private readonly SslStream ClientStream;
        private readonly RequestContext Context;
        private readonly WriteController WriteController;
        private readonly AuthorizationController AuthorizationController;

        private readonly string ViewPath = "C:/Users/drdre/source/repos/ClearServer/View";

        public NonAuthUserController(SslStream clientStream, RequestContext context)
        {
            this.ClientStream = clientStream;
            this.Context = context;
            this.WriteController = new WriteController(clientStream);
            this.AuthorizationController = new AuthorizationController(clientStream, context);
            ResourceLoad();
        }

        void ResourceLoad()
        {
            string[] blockextension = new string[] {"cshtml", "html", "htm"};
            bool block = false;
            foreach (var item in blockextension)
            {
                if (Context.RequestUrl.Contains(item))
                {
                    block = true;
                    break;
                }
            }
            string FilePath = "";
            string Header = "";
            var RazorController = new RazorController(Context, ClientStream);
            
            switch (Context.RequestMethod)
            {
                case "GET":
                    switch (Context.RequestUrl)
                    {
                        case "/":
                            FilePath = ViewPath + "/loginForm.html";
                            Header =

quot;HTTP/1.1 200 OKnContent-Type: text/html";
WriteController.DefaultWriter(Header, FilePath);
break;
case "/profile":
RazorController.ProfileLoader(ViewPath);
break;
default:
//в данном блоке кода происходит отсечение запросов к серверу по прямому адресу страницы вида site.com/page.html
if (!File.Exists(ViewPath + Context.RequestUrl) | block)
{
RazorController.ErrorLoader(404);

}
else if (Path.HasExtension(Context.RequestUrl) && File.Exists(ViewPath + Context.RequestUrl))
{
Header = WriteController.ContentType(Context.RequestUrl);
FilePath = ViewPath + Context.RequestUrl;
WriteController.DefaultWriter(Header, FilePath);
}
break;
}
break;

case "POST":
AuthorizationController.MethodRecognizer();
break;

}

}

}
}

Мэдээжийн хэрэг, хэрэглэгч хуудасны зарим контентыг хүлээн авах ёстой тул хариултын хувьд эх сурвалжийн хүсэлтэд хариу өгөх үүрэгтэй дараах модуль байдаг.

WriterController

using System;
using System.IO;
using System.Net.Security;
using System.Text;

namespace ClearServer.Core.UserController
{
    public class WriteController
    {
        SslStream ClientStream;
        public WriteController(SslStream ClientStream)
        {
            this.ClientStream = ClientStream;
        }

        public void DefaultWriter(string Header, string FilePath)
        {
            FileStream fileStream;
            try
            {
                fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
                Header =

quot;{Header}nContent-Length: {fileStream.Length}nn";
ClientStream.Write(Encoding.UTF8.GetBytes(Header));
byte[] response = new byte[fileStream.Length];
fileStream.BeginRead(response, 0, response.Length, OnFileRead, response);
}
catch { }
}

public string ContentType(string Uri)
{
string extension = Path.GetExtension(Uri);
string Header = "HTTP/1.1 200 OKnContent-Type:";
switch (extension)
{
case ".html":
case ".htm":
return


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


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


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


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


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


quot;{Header} application/unknown";
}
}
}

public void OnFileRead(IAsyncResult ar)
{
if (ar.IsCompleted)
{
var file = (byte[])ar.AsyncState;
ClientStream.BeginWrite(file, 0, file.Length, OnClientSend, null);
}
}

public void OnClientSend(IAsyncResult ar)
{
if (ar.IsCompleted)
{
ClientStream.Close();
}
}
}

Гэхдээ хэрэглэгчдэд өөрийн профайл болон бусад хэрэглэгчдийн профайлыг харуулахын тулд би RazorEngine, эсвэл түүний нэг хэсгийг ашиглахаар шийдсэн. Үүнд муу хүсэлтийг шийдвэрлэх, зохих алдааны кодыг гаргах зэрэг орно.

RazorController

using ClearServer.Core.Requester;
using RazorEngine;
using RazorEngine.Templating;
using System;
using System.IO;
using System.Net;
using System.Net.Security;

namespace ClearServer.Core.UserController
{
    internal class RazorController
    {
        private RequestContext Context;
        private SslStream ClientStream;
        dynamic PageContent;


        public RazorController(RequestContext context, SslStream clientStream)
        {
            this.Context = context;
            this.ClientStream = clientStream;

        }

        public void ProfileLoader(string ViewPath)
        {
            string Filepath = ViewPath + "/profile.cshtml";
            if (Context.RequestProfile != null)
            {
                if (Context.CurrentUser != null && Context.RequestProfile.login == Context.CurrentUser.login)
                {
                    try
                    {
                        PageContent = new { isAuth = true, Name = Context.CurrentUser.name, Login = Context.CurrentUser.login, Skills = Context.CurrentUser.skills };
                        ClientSend(Filepath, Context.CurrentUser.login);
                    }
                    catch (Exception e) { Console.WriteLine(e); }

                }
                else
                {
                    try
                    {
                        PageContent = new { isAuth = false, Name = Context.RequestProfile.name, Login = Context.RequestProfile.login, Skills = Context.RequestProfile.skills };
                        ClientSend(Filepath, "PublicProfile:"+ Context.RequestProfile.login);
                    }
                    catch (Exception e) { Console.WriteLine(e); }
                }
            }
            else
            {
                ErrorLoader(404);
            }


        }

        public void ErrorLoader(int Code)
        {
            try
            {
                PageContent = new { ErrorCode = Code, Message = ((HttpStatusCode)Code).ToString() };
                string ErrorPage = "C:/Users/drdre/source/repos/ClearServer/View/Errors/ErrorPage.cshtml";
                ClientSend(ErrorPage, Code.ToString());
            }
            catch { }

        }

        private void ClientSend(string FilePath, string Key)
        {
            var template = File.ReadAllText(FilePath);
            var result = Engine.Razor.RunCompile(template, Key, null, (object)PageContent);
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(result);
            ClientStream.BeginWrite(buffer, 0, buffer.Length, OnClientSend, ClientStream);
        }

        private void OnClientSend(IAsyncResult ar)
        {
            if (ar.IsCompleted)
            {
                ClientStream.Close();
            }
        }
    }
}

Мэдээжийн хэрэг эрх бүхий хэрэглэгчдийг шалгахын тулд зөвшөөрөл авах шаардлагатай. Зөвшөөрлийн модуль нь мэдээллийн сантай харилцдаг. Сайт дээрх маягтуудаас хүлээн авсан өгөгдлийг контекстээс задлан шинжилж, хэрэглэгч хадгалж, хариуд нь күүки хүлээн авч, үйлчилгээнд хандах боломжтой.

Зөвшөөрлийн модуль

using ClearServer.Core.Cookies;
using ClearServer.Core.Requester;
using ClearServer.Core.Security;
using System;
using System.Linq;
using System.Net.Security;
using System.Text;

namespace ClearServer.Core.UserController
{
    internal class AuthorizationController
    {
        private SslStream ClientStream;
        private RequestContext Context;
        private UserCookies cookies;
        private WriteController WriteController;
        DatabaseWorker DatabaseWorker;
        RazorController RazorController;
        PasswordHasher PasswordHasher;
        public AuthorizationController(SslStream clientStream, RequestContext context)
        {
            ClientStream = clientStream;
            Context = context;
            DatabaseWorker = new DatabaseWorker();
            WriteController = new WriteController(ClientStream);
            RazorController = new RazorController(context, clientStream);
            PasswordHasher = new PasswordHasher();
        }

        internal void MethodRecognizer()
        {
            if (Context.FormValues.Count == 2 && Context.FormValues.Any(x => x.Name == "password")) Authorize();
            else if (Context.FormValues.Count == 3 && Context.FormValues.Any(x => x.Name == "regPass")) Registration();
            else
            {
                RazorController.ErrorLoader(401);
            }
        }

        private void Authorize()
        {
            var values = Context.FormValues;
            var user = new User()
            {
                login = values[0].Value,
                password = PasswordHasher.PasswordHash(values[1].Value)
            };
            user = DatabaseWorker.UserAuth(user);
            if (user != null)
            {
                cookies = new UserCookies(user.login, user.password);
                user.cookie = cookies.AuthCookie;
                DatabaseWorker.UserUpdate(user);
                var response = Encoding.UTF8.GetBytes(

quot;HTTP/1.1 301 Moved PermanentlynLocation: /@{user.login}nSet-Cookie: {cookies.AuthCookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnlynn");
ClientStream.BeginWrite(response, 0, response.Length, WriteController.OnClientSend, null);

}
else
{
RazorController.ErrorLoader(401);

}
}

private void Registration()
{
var values = Context.FormValues;
var user = new User()
{
name = values[0].Value,
login = values[1].Value,
password = PasswordHasher.PasswordHash(values[2].Value),
};
cookies = new UserCookies(user.login, user.password);
user.cookie = cookies.AuthCookie;
if (DatabaseWorker.LoginValidate(user.login))
{
Console.WriteLine("User ready");
Console.WriteLine(


quot;{user.password} {user.password.Trim().Length}");
DatabaseWorker.UserRegister(user);
var response = Encoding.UTF8.GetBytes(


quot;HTTP/1.1 301 Moved PermanentlynLocation: /@{user.login}nSet-Cookie: {user.cookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnlynn");
ClientStream.BeginWrite(response, 0, response.Length, WriteController.OnClientSend, null);
}
else
{
RazorController.ErrorLoader(401);
}
}
}
}

Мэдээллийн сан нь дараах байдалтай байна.

Өгөгдлийн сан

using ClearServer.Core.UserController;
using System;
using System.Data.Linq;
using System.Linq;

namespace ClearServer
{
    class DatabaseWorker
    {

        private readonly Table<User> users = null;
        private readonly DataContext DataBase = null;
        private const string connectionStr = @"путькбазе";

        public DatabaseWorker()
        {
            DataBase = new DataContext(connectionStr);
            users = DataBase.GetTable<User>();
        }

        public User UserAuth(User User)
        {
            try
            {
                var user = users.SingleOrDefault(t => t.login.ToLower() == User.login.ToLower() && t.password == User.password);
                if (user != null)
                    return user;
                else
                    return null;
            }
            catch (Exception)
            {
                return null;
            }

        }

        public void UserRegister(User user)
        {
            try
            {
                users.InsertOnSubmit(user);
                DataBase.SubmitChanges();
                Console.WriteLine(

quot;User{user.name} with id {user.uid} added");
foreach (var item in users)
{
Console.WriteLine(item.login + "n");
}
}
catch (Exception e)
{
Console.WriteLine(e);
}

}

public bool LoginValidate(string login)
{
if (users.Any(x => x.login.ToLower() == login.ToLower()))
{
Console.WriteLine("Login already exists");
return false;
}
return true;
}
public void UserUpdate(User user)
{
var UserToUpdate = users.FirstOrDefault(x => x.uid == user.uid);
UserToUpdate = user;
DataBase.SubmitChanges();
Console.WriteLine(


quot;User {UserToUpdate.name} with id {UserToUpdate.uid} updated");
foreach (var item in users)
{
Console.WriteLine(item.login + "n");
}
}
public User CookieValidate(string CookieInput)
{
User user = null;
try
{
user = users.SingleOrDefault(x => x.cookie == CookieInput);
}
catch
{
return null;
}
if (user != null) return user;
else return null;
}
public User FindUser(string login)
{
User user = null;
try
{
user = users.Single(x => x.login.ToLower() == login.ToLower());
if (user != null)
{
return user;
}
else
{
return null;
}
}
catch (Exception)
{
return null;
}
}
}
}


Цагийн ажиллагаа, зөвшөөрөл, бүртгэлийн ажил гэх мэт бүх зүйл ажилладаг, үйлчилгээнд нэвтрэх хамгийн бага функц аль хэдийн бэлэн болсон бөгөөд өргөдөл бичиж, бүх зүйлийг хийдэг үндсэн функцуудтай холбох цаг болжээ.

4-р бүлэг

Хоёр платформд зориулж хоёр програм бичих хөдөлмөрийн зардлыг бууруулахын тулд би Xamarin.Forms дээр хөндлөн платформ хийхээр шийдсэн. Дахин хэлэхэд C# хэл дээр байгаадаа баярлалаа. Зүгээр л сервер рүү өгөгдөл илгээдэг туршилтын програмыг хийсний дараа би нэг сонирхолтой мөчтэй тулгарсан. Төхөөрөмжөөс хүсэлт гаргахын тулд би үүнийг HttpClient дээр хэрэгжүүлж, json формат дахь зөвшөөрлийн маягтын өгөгдлийг агуулсан HttpRequestMessage сервер дээр шидсэн. Би ямар нэг зүйл хүлээхгүйгээр серверийн бүртгэлийг нээж, тэнд байгаа бүх өгөгдөл бүхий төхөөрөмжөөс хүсэлт ирсэнийг харав. Хөнгөн түгшүүр, өнгөрсөн 3 долоо хоногийн турш хийсэн бүх зүйлээ ухамсарлахуйц орой. Илгээсэн өгөгдлийн зөв эсэхийг шалгахын тулд би HttpListner дээр туршилтын сервер угсарсан. Дараагийн хүсэлтийг аль хэдийн хүлээн авсны дараа би үүнийг хэд хэдэн кодын мөрөнд хувааж, KeyValuePair өгөгдлийг маягтаас авсан. Асуулгын дүн шинжилгээг хоёр мөр болгон багасгасан.

Би цаашид тест хийж эхэлсэн, энэ талаар өмнө нь дурдаагүй, гэхдээ өмнөх сервер дээр би вэбсокет дээр баригдсан чатыг хэрэгжүүлсэн. Энэ нь маш сайн ажилласан боловч Tcp-ээр дамжуулан харилцах зарчим нь сэтгэл дундуур байсан тул хоёр хэрэглэгчийн захидал харилцааны бүртгэлтэй харилцах харилцааг зөв бий болгохын тулд хэт их зүйл хийх шаардлагатай болсон. Үүнд RFC 6455 протоколыг ашиглан холболт солих хүсэлтийг задлан шинжилж, хариу цуглуулах зэрэг орно.Тиймээс туршилтын сервер дээр би энгийн вэбсокет холболт үүсгэхээр шийдсэн. Зөвхөн ашиг сонирхлын үүднээс.

Чат холболт

 private static async void HandleWebsocket(HttpListenerContext context)
        {
            var socketContext = await context.AcceptWebSocketAsync(null);
            var socket = socketContext.WebSocket;
            Locker.EnterWriteLock();
            try
            {
                Clients.Add(socket);
            }
            finally
            {
                Locker.ExitWriteLock();
            }

            while (true)
            {
                var buffer = new ArraySegment<byte>(new byte[1024]);
                var result = await socket.ReceiveAsync(buffer, CancellationToken.None);
                var str = Encoding.Default.GetString(buffer);
                Console.WriteLine(str);

                for (int i = 0; i < Clients.Count; i++)
                {
                    WebSocket client = Clients[i];

                    try
                    {
                        if (client.State == WebSocketState.Open)
                        {
                            
                            await client.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None);
                        }
                    }
                    catch (ObjectDisposedException)
                    {
                        Locker.EnterWriteLock();
                        try
                        {
                            Clients.Remove(client);
                            i--;
                        }
                        finally
                        {
                            Locker.ExitWriteLock();
                        }
                    }
                }
            }
        }

Тэгээд ч ажилласан. Сервер өөрөө холболтыг тохируулж, хариултын түлхүүр үүсгэсэн. Би ssl-ээр серверийн бүртгэлийг тусад нь тохируулах шаардлагагүй байсан, систем нь шаардлагатай порт дээр суулгасан гэрчилгээтэй байхад л хангалттай.

Төхөөрөмжийн тал болон сайтын тал дээр хоёр үйлчлүүлэгч мессеж солилцож, энэ бүгдийг бүртгэсэн. Серверийг удаашруулдаг асар том задлан шинжлэгч байхгүй, эдгээрийн аль нь ч шаардлагагүй. Хариу өгөх хугацааг 200 мс-ээс 40-30 мс болгон бууруулсан. Тэгээд би цорын ганц зөв шийдвэрт хүрсэн.

C# .Net framework дээр суурилсан дэвшилтэт дугуй эсвэл клиент-сервер програм

Tcp дээрх одоогийн серверийн хэрэгжилтийг хаяж, Http доор байгаа бүх зүйлийг дахин бичнэ үү. Одоо төсөл нь дахин төлөвлөлтийн шатанд байгаа боловч харилцан үйлчлэлийн тэс өөр зарчмуудын дагуу. Төхөөрөмж болон сайтын ажиллагаа нь синхрончлогдсон, дибаг хийгдсэн бөгөөд нийтлэг ойлголттой бөгөөд цорын ганц ялгаа нь төхөөрөмжүүд html хуудас үүсгэх шаардлагагүй юм.

дүгнэлт

"Гарц мэдэхгүй тул толгойгоо ус руу бүү хий" Ажил эхлэхийн өмнө би зорилго, зорилтоо илүү тодорхой тодорхойлж, янз бүрийн үйлчлүүлэгчдэд хэрэгжүүлэх шаардлагатай технологи, аргыг судлах хэрэгтэй гэж бодож байна. Төсөл аль хэдийн дуусах дөхөж байгаа, гэхдээ би дахин зарим зүйлийг хэрхэн сүйтгэсэн тухайгаа ярихаар буцаж ирэх байх. Би хөгжлийн явцад маш их зүйлийг сурсан, гэхдээ цаашид сурах зүйл их байна. Хэрэв та энэ хүртэл уншсан бол уншсанд баярлалаа.

Эх сурвалж: www.habr.com