Inleiding
Alles begon met het voorstel van een collega om een kleine webservice te creëren. Het moest iets worden als Tinder, maar voor de IT-community. De functionaliteit is extreem eenvoudig: je registreert je, vult je profiel in en gaat naar de kern, namelijk het zoeken naar gesprekspartners, het uitbreiden van je netwerk en het leggen van nieuwe contacten.
Hier moet ik een zijpad inslaan en iets over mezelf vertellen, zodat duidelijker wordt waarom ik specifieke stappen in de ontwikkeling heb ondernomen.
Op dit moment bekleed ik de functie van Technisch Artiest bij een game studio. Mijn ervaring met programmeren in C# is opgebouwd uit het schrijven van scripts en hulpprogramma's voor Unity, en daarnaast het creëren van plugins voor laag-niveau interactie met Android-apparaten. Buiten deze wereld ben ik nog niet gekomen en nu kwam er een kans voorbij.
Deel 1. Prototyping van het kader
Nadat ik besloten had wat deze service zou moeten zijn, begon ik naar opties te zoeken voor de implementatie. Het zou het eenvoudigst zijn geweest om een kant-en-klare oplossing te vinden waar we onze mechanismen op konden toepassen en het geheel aan de publieke kritiek konden onderwerpen.
Maar dat was niet interessant; ik zag er geen uitdaging of zin in, en daarom begon ik webtechnologieën en methoden om ermee om te gaan te bestuderen.
Ik begon met het bekijken van artikelen en documentatie over C# .Net. Hier vond ik verschillende manieren om de taak uit te voeren. Er zijn tal van mechanismen voor netwerkintegratie, van volledige oplossingen zoals ASP.Net of Azure-diensten tot directe interactie met TcpHttp-verbindingen.
Na mijn eerste poging met ASP verwierp ik het direct; naar mijn mening was het te zwaar voor onze service. We zouden niet eens een derde van de mogelijkheden van dit platform gebruiken, dus ging ik verder met zoeken. De keuze viel tussen TCP en een HTTP-client-server. Hier ontdekte ik op Habr een artikel over , dat ik verzameld en getest heb, en besloot toen me te concentreren op de interactie met TCP-verbindingen, omdat ik dacht dat http me niet in staat zou stellen een multiplatformoplossing te creëren.
De eerste versie van de server omvatte het verwerken van verbindingen, het aanbieden van statische inhoud van webpagina's en het bevatte een gebruikersdatabase. Voorlopig besloot ik functionaliteit voor de website te bouwen, zodat ik later ook de verwerking voor de Android- en iOS-apps kon integreren.
Hier is een beetje code
De hoofdthread, die in een oneindige lus klanten aanneemt:
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);
}
}
}De handler voor klanten:
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 ="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 ="image\/ {Extension.Substring(1)}";
break;
default:
if (Extension.Length > 1)
{
ContentType ="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 =
"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();
}
}
}
En de eerste database gebouwd op local 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 users = db.GetTable(); foreach (var item in users) { Console.WriteLine("{item.login} {item.password}"); } } } } }"{item.login} {item.password}"
}
}
}
}
}
Zoals je kunt zien, verschilt deze versie weinig van die in het artikel. In wezen is er alleen de optie toegevoegd om pagina's vanuit een map op de computer te laden en een database (die in deze versie trouwens niet werkte, vanwege de foutieve verbindingsarchitectuur).Hoofdstuk 2. De wielen bevestigen
Na het testen van de werking van de server kwam ik tot de conclusie dat dit een uitstekende oplossing zou zijn (spoiler: nee), voor onze service, zodat het project begon uit te groeien met logica.
Stap voor stap begonnen er nieuwe modules en functionaliteit van de server uit te breiden. De server kreeg een testdomein en ssl versleuteling van de verbinding.Nog wat code die de logica van de server en de verwerking van klanten beschrijft
De bijgewerkte versie van de server, inclusief het gebruik van een certificaat.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(); } } }En ook een nieuwe client-handler met SSL-autorisatie:
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" +"|{DateTime.Now:g}n|------------n|{Client.Client.RemoteEndPoint}n|------------n|Exception: {e.Message}n|------------n|Authenticatie is mislukt - verbinding wordt gesloten.n" +
"---------------------------------------------------------------------n");
SSlClientStream.Close();
Client.Close();
}
new RequestContext(SSlClientStream, Client);
}}
}
Aangezien de server uitsluitend op TCP-verbinding werkt, is het noodzakelijk om een module te creëren die de context van het verzoek kan herkennen. Ik besloot dat een parser die het verzoek van de client in afzonderlijke delen kan splitsen, geschikt zou zijn, zodat ik ermee kan interageren om de juiste antwoorden aan de client te geven.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 HeadersValues; public List FormValues; private TcpClient TcpClient; private event Action 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("{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;
{
if (HeadersValues.Any(x => x.Name.Contains("Cookie")))
try
{
cookie = HeadersValues.FirstOrDefault(x => x.Name.Contains("Cookie")).Value;
{
CurrentUser = databaseWorker.CookieValidate(cookie);
try
{
catch { }
}
private List HeaderValues()
}
}
private List HeaderValues()}
var values = new List();
{
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 ContentValues()
}
}
var output = Message.Trim('\n').Split().Last();
{
var parse = Regex.Matches(Message, @"(.*?): (.*?)\n");
var parse = Regex.Matches(output, @"([^&].*?)=([^&]*)\b");
Value = match.Groups[2].Value.Trim().Replace('+', ' ')
values.Add(new RequestValues()
{
Name = match.Groups[1].Value.Trim(),
{
Value = match.Groups[2].Value.Trim()
Value = match.Groups[2].Value.Trim().Replace('+', ' ');
});
}
private void ProfileParse()
}
}
}
De essentie ervan is om met behulp van reguliere expressies de aanvraag in stukken te splitsen. We ontvangen een bericht van de klant, halen de eerste regel eruit waarin de methode en de URL van de aanvraag staan. Vervolgens lezen we de headers, die we in een array van het type NaamHeader=Inhoud plaatsen, en zoeken we eventuele bijbehorende inhoud (zoals querystring), die we ook in een soortgelijke array plaatsen. Bovendien bepaalt de parser of de huidige klant geautoriseerd is en slaat diens gegevens op. Alle aanvragen van geautoriseerde klanten bevatten een autorisatiehash, die in cookies wordt opgeslagen. Hierdoor kunnen we de verdere logica voor twee soorten klanten scheiden en hen de juiste antwoorden geven.En natuurlijk is er een kleine, leuke functie die het waard is om in een apart module te worden opgenomen: het omzetten van aanvragen zoals "site.com/@UserName" naar dynamisch gegenereerde gebruikerspagina's. Na het verwerken van de aanvraag treden de volgende modules in werking.
Hoofdstuk 3. Het installeren van de stuurinrichting, het smeren van de ketting
Zodra de parser heeft gewerkt, komt de handler aan bod die verdere instructies aan de server geeft en het beheer in twee delen splitst.
Eenvoudige handler
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); }; } } }In wezen is er hier slechts één controle op de autorisatie van de gebruiker, waarna de verwerking van de aanvraag begint.
Klantencontrollers
Als de gebruiker niet geautoriseerd is, is de functionaliteit beperkt tot het weergeven van gebruikersprofielen en het registratie-/inlogvenster. De code voor een geautoriseerde gebruiker ziet er ongeveer hetzelfde uit, zodat ik het niet nodig vind om die te dupliceren.Niet-geautoriseerde gebruiker
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 OK Content-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;}
}
}
}
En natuurlijk moet de gebruiker enige inhoud van de pagina's ontvangen, dus voor de antwoorden bestaat er de volgende module die verantwoordelijk is voor het antwoord op aanvragen om middelen.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);
}
private List HeaderValues()
}public string ContentType(string Uri)
{
string extension = Path.GetExtension(Uri);
string Header = "HTTP/1.1 200 OKnContent-Type:";
switch (extension)
{
case ".html":
case ".htm":
terugquot;{Header} text/html";
case ".css":
terugquot;{Header} text/css";
case ".js":
terugquot;{Header} text/javascript";
case ".jpg":
case ".jpeg":
case ".png":
case ".gif":
terugquot;{Header} image/{extension}";
default:
if (extension.Length > 1)
{
terugquot;{Header} application/" + extension.Substring(1);
}
else
{
terugquot;{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();
}
}
}
Maar om de gebruiker zijn profiel en de profielen van andere gebruikers te tonen, besloot ik RazorEngine te gebruiken, of beter gezegd, een deel daarvan. Het omvat ook de afhandeling van ongeldig verzoeken en de afgifte van de bijbehorende foutcode.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(); } } } }En uiteraard is autorisatie nodig voor de controle van geauthenticeerde gebruikers. De autorisatiemodule communiceert met de database. De gegevens die uit de formulieren op de site worden verkregen, worden uit de context geparsed, de gebruiker wordt opgeslagen en ontvangt cookies en toegang tot de service.
Autorisatiemodule
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("HTTP/1.1 301 Moved Permanently Location: /@{user.login} Set-Cookie: {cookies.AuthCookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnly
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("{user.password} {user.password.Trim().Length}");
DatabaseWorker.UserRegister(user);
var response = Encoding.UTF8.GetBytes("HTTP/1.1 301 Moved Permanently Location: /@{user.login} Set-Cookie: {user.cookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnly");
ClientStream.BeginWrite(response, 0, response.Length, WriteController.OnClientSend, null);
}
else
{
RazorController.ErrorLoader(401);
}
}
}
}
Zo ziet de databaseverwerking eruit:Database
using ClearServer.Core.UserController; using System; using System.Data.Linq; using System.Linq; namespace ClearServer { class DatabaseWorker { private readonly Table users = null; private readonly DataContext DataBase = null; private const string connectionStr = @"databasepad"; public DatabaseWorker() { DataBase = new DataContext(connectionStr); users = DataBase.GetTable(); } 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("User {user.name} met id {user.uid} toegevoegd"); }quot;User {user.name} met id {user.uid} toegevoegd";
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 bestaat al");
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} met id {UserToUpdate.uid} bijgewerkt";
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;
}
}
}
}
En alles werkt als een trein, de autorisatie en registratie werken, de minimale toegang tot de service is al aanwezig en het is tijd om de applicatie te schrijven en alles om te bouwen met de hoofdfunctionaliteiten waarvoor alles is opgezet.Hoofdstuk 4. Het heruitvinden van de fiets
Om de werklast van het schrijven van twee applicaties voor twee platforms te verminderen, besloot ik om een cross-platform oplossing te maken met Xamarin.Forms. Ook weer, dankzij het feit dat het op C# is. Nadat ik een testapplicatie had gemaakt die simpelweg gegevens naar de server verzendt, stuitte ik op een interessant moment. Voor het verzoek van het apparaat implementeerde ik, uit nieuwsgierigheid, het met HttpClient en stuurde een HttpRequestMessage naar de server waarin de gegevens van het aanmeldingsformulier in JSON-formaat waren opgenomen. Zonder veel te verwachten, opende ik de serverlog en zag daar het verzoek van het apparaat met alle gegevens. Een lichte verbijstering, het besef van alles wat ik de afgelopen 3 weken slaaploze avonden had gedaan. Om de juistheid van de verzonden gegevens te controleren, stelde ik een testserver samen met HttpListener. Toen ik een nieuw verzoek op die server ontving, kon ik het met een paar regels code in delen opsplitsen en kreeg ik een KeyValuePair van de gegevens uit het formulier. De verwerking van het verzoek was beperkt tot twee regels.
Ik begon verder te testen; eerder werd vermeld dat ik op de vorige server ook een chat had geïmplementeerd die was opgebouwd op websockets. Het werkte behoorlijk goed, maar het principe van interactie via Tcp was frustrerend, er moest te veel overbodig worden gecreëerd om correct de interactie tussen twee gebruikers en het bijhouden van het logboek van de correspondentie op te bouwen. Dit omvatte het parseren van het verzoek om de verbinding te wisselen en het verzamelen van antwoorden volgens het RFC 6455-protocol. Daarom besloot ik om op de testserver een eenvoudige websocketverbinding te creëren. Gewoon uit nieuwsgierigheid.
Verbinden met de chat
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(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(); } } } } }En het werkte. De server configureerde zelf de verbinding en genereerde de respons-sleutel. Ik hoefde de serverregistratie via SSL niet apart in te stellen, het volstond dat het certificaat al op de juiste poort in het systeem was geïnstalleerd.
Aan de kant van het apparaat en aan de kant van de website wisselden twee klanten berichten uit, en alles werd gelogd. Er waren geen enorme parsers nodig die de server vertraagden. De responstijd is verkort van 200 ms naar 40-30 ms. En ik kwam tot de enige juiste oplossing.
De huidige serverimplementatie op Tcp verwijderen en alles herschrijven naar Http. Het project bevindt zich nu in de herontwerpfase, maar het heeft nu geheel andere interactieprincipes. Het werk van de apparaten en de website is gesynchroniseerd en afgestemd, met een gemeenschappelijk concept, met dit enige verschil dat voor de apparaten geen HTML-pagina's gegenereerd hoeven te worden.
Uitslag
"Als je de diepte niet kent, ga dan niet het water in" Ik denk dat ik bij de start van het project duidelijker mijn doelen en taken had moeten definiëren, en me verder had moeten verdiepen in de benodigde technologieën en de methoden voor hun implementatie op verschillende klanten. Het project nadert al zijn voltooiing, maar misschien kom ik nog terug om te vertellen hoe ik bepaalde dingen opnieuw heb verprutst. Ik heb veel geleerd tijdens het ontwikkelingsproces, maar er is nog veel meer te leren in de toekomst. Als je tot dit punt hebt gelezen, bedankt daarvoor.
Bron: habr.com

