Wprowadzenie
Wszystko zaczęło się od tego, że kolega zaproponował mi stworzenie małej usługi internetowej. Miało to być coś w rodzaju Tindera, ale dla środowiska IT. Funkcjonalność jest niezwykle prosta: rejestrujesz się, wypełniasz profil i przechodzisz do sedna, czyli poszukiwania rozmówcy, rozszerzania swoich kontaktów i nawiązywania nowych znajomości.
Tutaj muszę zrobić dygresję i trochę opowiedzieć o sobie, aby w dalszej części było jasne, dlaczego podjąłem takie kroki w rozwoju.
Obecnie zajmuję stanowisko Technical Artist w jednej z firm gamingowych. Moje doświadczenie w programowaniu w C# opierało się wyłącznie na pisaniu skryptów i narzędzi dla Unity oraz na tworzeniu wtyczek do niskopoziomowej pracy z urządzeniami z systemem Android. Jeszcze nie opuściłem tego małego światka, a tu nadarzyła się taka okazja.
Część 1. Prototypowanie ramy
Postanowiwszy, jak ma wyglądać ta usługa, zacząłem szukać opcji realizacji. Najłatwiej byłoby znaleźć jakieś gotowe rozwiązanie, na które można by nałożyć nasze mechaniki i pokazać to wszystkim na publiczną krytykę.
Ale to nie jest interesujące, nie dostrzegałem w tym challenge'u ani sensu, więc zacząłem badać technologie webowe i metody interakcji z nimi.
Nauka zaczęła się od przeglądania artykułów i dokumentacji dotyczącej C# .Net. Znalazłem tam różnorodne sposoby wykonania zadania. Istnieje wiele mechanizmów interakcji z siecią, od pełnoprawnych rozwiązań jak ASP.Net czy usługi Azure, po bezpośrednią interakcję z połączeniami TcpHttp.
Po pierwszej próbie z ASP od razu go odrzuciłem, moim zdaniem to było zbyt trudne rozwiązanie dla naszej usługi. Nie wykorzystamy nawet jednej trzeciej możliwości tej platformy, dlatego kontynuowałem poszukiwania. Wybór padł na TCP i klient-serwer Http. Tutaj, na Habrze, natknąłem się na artykuł o , którego zbudowanie i przetestowanie skłoniło mnie do zdecydowania się na interakcję z połączeniami TCP, ponieważ uznałem, że http nie pozwoli mi stworzyć rozwiązania wieloplatformowego.
Pierwsza wersja serwera obejmowała obsługę połączeń, serwowała statyczne treści stron internetowych i zawierała bazę danych użytkowników. Na początek postanowiłem zbudować funkcjonalność do pracy z witryną, aby później móc dodać obsługę aplikacji na Androida i iOS.
Oto trochę kodu
Główny wątek, w nieskończonej pętli przyjmujący klientów:
using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;
namespace ClearServer
{
class Server
{
TcpListener Listener;
public Server(int Port)
{
Listener = new TcpListener(IPAddress.Any, Port);
Listener.Start();
while (true)
{
TcpClient Client = Listener.AcceptTcpClient();
Thread Thread = new Thread(new ParameterizedThreadStart(ClientThread));
Thread.Start(Client);
}
}
static void ClientThread(Object StateInfo)
{
new Client((TcpClient)StateInfo);
}
~Server()
{
if (Listener != null)
{
Listener.Stop();
}
}
static void Main(string[] args)
{
DatabaseWorker sqlBase = DatabaseWorker.GetInstance;
new Server(80);
}
}
}Sam obsługiwacz klientów:
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)}";
}
inaczej
{
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 OK\nContent-Type: {ContentType}\nContent-Length: {FS.Length}\n\n";
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();
}
}
}
A pierwsza baza danych zbudowana na 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}"
}
}
}
}
}
Jak można zauważyć, ta wersja niewiele różni się od tej, która była w artykule. W zasadzie dodano tutaj tylko ładowanie stron z folderu na komputerze i bazę danych (która zresztą w tej wersji nie działała, z powodu błędnej architektury połączenia).Rozdział 2. Dodawanie kół
Testując działanie serwera, doszedłem do wniosku, że to będzie świetne rozwiązanie (spoiler: nie), dla naszego serwisu, dlatego projekt zaczynał nabierać logiki.
Krok po kroku zaczęły pojawiać się nowe moduły, a funkcjonalność serwera się rozrastała. Serwer zyskał testową domenę i ssl szyfrowanie połączenia.Jeszcze trochę kodu, opisującego logikę działania serwera i obsługę klientów
Zaktualizowana wersja serwera, zawierająca użycie certyfikatu.using System; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Security; using System.Security.Cryptography.X509Certificates; using System.Security.Permissions; using System.Security.Policy; using System.Threading; namespace ClearServer { sealed class Server { readonly bool ServerRunning = true; readonly TcpListener sslListner; public static X509Certificate serverCertificate = null; Server() { serverCertificate = X509Certificate.CreateFromSignedFile(@"C:sslitinder.online.crt"); sslListner = new TcpListener(IPAddress.Any, 443); sslListner.Start(); Console.WriteLine("Starting server.." + serverCertificate.Subject + "n" + Assembly.GetExecutingAssembly().Location); while (ServerRunning) { TcpClient SslClient = sslListner.AcceptTcpClient(); Thread SslThread = new Thread(new ParameterizedThreadStart(ClientThread)); SslThread.Start(SslClient); } } static void ClientThread(Object StateInfo) { new Client((TcpClient)StateInfo); } ~Server() { if (sslListner != null) { sslListner.Stop(); } } public static void Main(string[] args) { if (AppDomain.CurrentDomain.IsDefaultAppDomain()) { Console.WriteLine("Switching another domain"); new AppDomainSetup { ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase }; var current = AppDomain.CurrentDomain; var strongNames = new StrongName[0]; var domain = AppDomain.CreateDomain( "ClearServer", null, current.SetupInformation, new PermissionSet(PermissionState.Unrestricted), strongNames); domain.ExecuteAssembly(Assembly.GetExecutingAssembly().Location); } new Server(); } } }A także nowy klient z obsługą SSL:
używając ClearServer.Core.Requester; używając System; używając System.Net.Security; używając 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|Wyjątek: {e.Message}n|------------n|Uwierzytelnianie nie powiodło się - zamykam połączenie.n" +
"---------------------------------------------------------------------n");
SSlClientStream.Close();
Client.Close();
}
new RequestContext(SSlClientStream, Client);
}}
}
Ponieważ serwer działa wyłącznie na połączeniu TCP, konieczne jest stworzenie modułu, który będzie w stanie rozpoznać kontekst żądania. Postanowiłem, że przyda się tutaj parser, który będzie rozdzielał żądania od klienta na poszczególne części, z którymi będę mógł współdziałać, aby dostarczyć klientowi odpowiednie odpowiedzi.Parser
using ClearServer.Core.UserController; using ReServer.Core.Classes; using System; using System.Collections.Generic; using System.Linq; using System.Net.Security; using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; namespace ClearServer.Core.Requester { public class RequestContext { public string Message = ""; private readonly byte[] buffer = new byte[1024]; public string RequestMethod; public string RequestUrl; public User RequestProfile; public User CurrentUser = null; public List 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, @"([^&].*?)=([^&]*)");
Value = match.Groups[2].Value.Trim().Replace('+', ' ')
values.Add(new RequestValues()
{
Name = match.Groups[1].Value.Trim(),
{
Value = match.Groups[2].Value.Trim()
Wartość = match.Groups[2].Value.Trim().Replace('+', ' ')}
});
}
private void ProfileParse()
}
}
}
Istota tego polega na tym, aby za pomocą wyrażeń regularnych podzielić zapytanie na części. Otrzymujemy wiadomość od klienta, wydobywamy pierwszy wiersz, w którym znajduje się metoda i adres URL zapytania. Następnie odczytujemy nagłówki, które umieszczamy w tablicy w formie NazwaNagłówka=Zawartość, a także znajdujemy, jeśli jest dostępna, treść towarzyszącą (np. querystring), którą również umieszczamy w podobnej tablicy. Dodatkowo parser ustala, czy aktualny klient jest autoryzowany i zapisuje jego dane. Wszystkie zapytania od autoryzowanych klientów zawierają skrót autoryzacji, który jest przechowywany w ciasteczkach, dzięki czemu można rozdzielać dalszą logikę działania dla dwóch typów klientów i dostarczać im odpowiednie odpowiedzi.A oto mała, przyjemna funkcja, którą warto byłoby wyodrębnić do oddzielnego modułu: przekształcanie zapytań typu „site.com/@UserName” w dynamcznie generowane strony użytkowników. Po przetworzeniu zapytania wchodzą w grę następujące moduły.
Rozdział 3. Instalacja kierownicy, smarowanie łańcucha
Gdy parser zakończy pracę, wchodzi w użycie obsługujący, który daje dalsze instrukcje serwerowi i dzieli zarządzanie na dwie części.
Prosta obsługa
using ClearServer.Core.UserController; using System.Net.Security; namespace ClearServer.Core.Requester { public class RequestHandler { public static void OnHandle(SslStream ClientStream, RequestContext context) { if (context.CurrentUser != null) { new AuthUserController(ClientStream, context); } else { new NonAuthUserController(ClientStream, context); }; } } }W zasadzie jest tylko jedna kontrola autoryzacji użytkownika, po której rozpoczyna się przetwarzanie zapytania.
Kontrolery klientów
Jeśli użytkownik nie jest autoryzowany, to dla niego funkcjonalność opiera się tylko na wyświetlaniu profili użytkownika oraz oknie rejestracji/autoryzacji. Kod dla autoryzowanego użytkownika wygląda mniej więcej tak samo, więc nie widzę sensu w jego duplikowaniu.Nieautoryzowany użytkownik
using ClearServer.Core.Requester; using System.IO; using System.Net.Security; namespace ClearServer.Core.UserController { internal class NonAuthUserController { private readonly SslStream ClientStream; private readonly RequestContext Context; private readonly WriteController WriteController; private readonly AuthorizationController AuthorizationController; private readonly string ViewPath = "C:/Users/drdre/source/repos/ClearServer/View"; public NonAuthUserController(SslStream clientStream, RequestContext context) { this.ClientStream = clientStream; this.Context = context; this.WriteController = new WriteController(clientStream); this.AuthorizationController = new AuthorizationController(clientStream, context); ResourceLoad(); } void ResourceLoad() { string[] blockextension = new string[] {"cshtml", "html", "htm"}; bool block = false; foreach (var item in blockextension) { if (Context.RequestUrl.Contains(item)) { block = true; break; } } string FilePath = ""; string Header = ""; var RazorController = new RazorController(Context, ClientStream); switch (Context.RequestMethod) { case "GET": switch (Context.RequestUrl) { case "/":"HTTP/1.1 200 OK\nContent-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;}
}
}
}
Oczywiście użytkownik musi otrzymać jakąś treść stron, dlatego istnieje następujący moduł odpowiedzialny za odpowiedź na zapytania zasobów.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 ="{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":
return"{Header} text/html";
case ".css":
return"{Header} text/css";
case ".js":
return"{Header} text/javascript";
case ".jpg":
case ".jpeg":
case ".png":
case ".gif":
return"{Header} image/{extension}";
default:
if (extension.Length > 1)
{
return"{Header} application/" + extension.Substring(1);
}
inaczej
{
return"{Header} application/unknown";
}
}
}public void OnFileRead(IAsyncResult ar)
{
if (ar.IsCompleted)
{
var file = (byte[])ar.AsyncState;
ClientStream.BeginWrite(file, 0, file.Length, OnClientSend, null);
}
}public void OnClientSend(IAsyncResult ar)
{
if (ar.IsCompleted)
{
ClientStream.Close();
}
}
}
Aby wyświetlić użytkownikowi jego profil oraz profile innych użytkowników, zdecydowałem się użyć RazorEngine, a właściwie jego części. Zawiera on także obsługę błędnych zapytań i wydawanie odpowiedniego kodu błędu.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(); } } } }Oczywiście, aby działała walidacja zarejestrowanych użytkowników, potrzebna jest autoryzacja. Moduł autoryzacji współdziała z bazą danych. Dane uzyskane z formularzy na stronie są analizowane w kontekście, użytkownik jest zapisywany i otrzymuje w zamian ciasteczka i dostęp do usługi.
Moduł autoryzacji
using ClearServer.Core.Cookies; using ClearServer.Core.Requester; using ClearServer.Core.Security; using System; using System.Linq; using System.Net.Security; using System.Text; namespace ClearServer.Core.UserController { internal class AuthorizationController { private SslStream ClientStream; private RequestContext Context; private UserCookies cookies; private WriteController WriteController; DatabaseWorker DatabaseWorker; RazorController RazorController; PasswordHasher PasswordHasher; public AuthorizationController(SslStream clientStream, RequestContext context) { ClientStream = clientStream; Context = context; DatabaseWorker = new DatabaseWorker(); WriteController = new WriteController(ClientStream); RazorController = new RazorController(context, clientStream); PasswordHasher = new PasswordHasher(); } internal void MethodRecognizer() { if (Context.FormValues.Count == 2 && Context.FormValues.Any(x => x.Name == "password")) Authorize(); else if (Context.FormValues.Count == 3 && Context.FormValues.Any(x => x.Name == "regPass")) Registration(); else { RazorController.ErrorLoader(401); } } private void Authorize() { var values = Context.FormValues; var user = new User() { login = values[0].Value, password = PasswordHasher.PasswordHash(values[1].Value) }; user = DatabaseWorker.UserAuth(user); if (user != null) { cookies = new UserCookies(user.login, user.password); user.cookie = cookies.AuthCookie; DatabaseWorker.UserUpdate(user); var response = Encoding.UTF8.GetBytes("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);}
inaczej
{
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);
}
inaczej
{
RazorController.ErrorLoader(401);
}
}
}
}
A tak wygląda przetwarzanie bazy danych:Baza danych.
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 = @"ścieżkabazy"; 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("Użytkownik {user.name} z id {user.uid} dodany"); } catch (Exception) { Console.WriteLine(e); } } public bool LoginValidate(string login) { if (users.Any(x => x.login.ToLower() == login.ToLower())) Console.WriteLine("Login już istnieje"); return false; return true; } public void UserUpdate(User user) { var UserToUpdate = users.FirstOrDefault(x => x.uid == user.uid); UserToUpdate = user; DataBase.SubmitChanges(); Console.WriteLine("Użytkownik {UserToUpdate.name} z id {UserToUpdate.uid} zaktualizowany"); } public User CookieValidate(string CookieInput) { User user = null; user = users.SingleOrDefault(x => x.cookie == CookieInput); if (user != null) return user; else return null; } public User FindUser(string login) { user = users.Single(x => x.login.ToLower() == login.ToLower()); if (user != null) return user; return null; } } }Użytkownik {user.name} z id {user.uid} dodany
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 już istnieje");
return false;
}
return true;
}
public void UserUpdate(User user)
{
var UserToUpdate = users.FirstOrDefault(x => x.uid == user.uid);
UserToUpdate = user;
DataBase.SubmitChanges();
Console.WriteLine(Użytkownik {UserToUpdate.name} z id {UserToUpdate.uid} zaktualizowany
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;
}
inaczej
{
return null;
}
}
catch (Exception)
{
return null;
}
}
}
}
I wszystko działa jak w zegarku, autoryzacja i rejestracja działają, minimalna funkcjonalność dostępu do usługi już istnieje i nadszedł czas, aby napisać aplikację i powiązać to z głównymi funkcjami, dla których to wszystko zostało stworzone.Rozdział 4. Odrzucanie roweru
Aby zredukować nakład pracy związany z pisaniem dwóch aplikacji na dwie platformy, postanowiłem stworzyć aplikację międzyplatformową w Xamarin.Forms. Ponownie, dzięki temu, że jest oparta na C#. Stworzyłem aplikację testową, która po prostu wysyła dane do serwera. Napotkałem jeden interesujący moment. Dla zapytania z urządzenia, z ciekawości, zaimplementowałem jego realizację w HttpClient i wysłałem do serwera HttpRequestMessage, w którym znajdowały się dane z formularza logowania w formacie JSON. Nie oczekując nic szczególnego, otworzyłem log serwera i zobaczyłem tam żądanie z urządzenia ze wszystkimi danymi. Lekka konsternacja, zrozumienie wszystkiego, co zostało zrobione w ciągu ostatnich 3 tygodni męczących wieczorów. Aby sprawdzić poprawność wysłanych danych, zbudowałem testowy serwer na HttpListener. Otrzymując kolejne zapytanie już na nim, w zaledwie kilku linijkach kodu rozebrałem je na części, uzyskując KeyValuePair danych z formularza. Rozbiór zapytania sprowadził się do dwóch linijek.
Kontynuowałem testowanie dalej, wcześniej nie wspomniano, ale na poprzednim serwerze zrealizowałem również czat oparty na websocketach. Działał całkiem nieźle, ale sam proces interakcji przez TCP był przygnębiający, zbyt wiele zbędnego musiałem tworzyć, aby poprawnie zbudować interakcję dwóch użytkowników z rejestrowaniem logów rozmowy. To i parsowanie zapytania w celu zmiany połączenia oraz zbieranie odpowiedzi zgodnie z protokołem RFC 6455. Dlatego w testowym serwerze postanowiłem stworzyć proste połączenie websocket. Czysto z ciekawości.
Połączenie z czatem
prywatna statyczna asynchroniczna 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(); } } } } }I udało się. Serwer automatycznie konfigurował połączenie, generował klucz odpowiedzi. Nie musiałem nawet osobno konfigurować rejestracji serwera przez ssl, wystarczyło, że w systemie już był zainstalowany certyfikat na właściwym porcie.
Po stronie urządzenia i na stronie internetowej dwóch klientów wymieniało wiadomości, wszystko to było rejestrowane. Żadne ogromne parsery, spowalniające działanie serwera, nie były wymagane. Czas odpowiedzi skrócił się z 200 ms do 40–30 ms. I doszedłem do jedynego słusznego rozwiązania.
Pozbyć się obecnej implementacji serwera na Tcp i przepisać wszystko na Http. Projekt znajduje się teraz w fazie przemyślenia, ale już na zupełnie innych zasadach współpracy. Praca urządzeń i strony internetowej jest zsynchronizowana i dopracowana oraz ma wspólną koncepcję, z tą różnicą, że dla urządzeń nie trzeba generować stron html.
Wnioski
„Nie znając brodu, nie wpadaj do wody” Myślę, że przed rozpoczęciem prac powinienem był dokładniej określić cele i zadania, a także zgłębić potrzebne technologie oraz metody ich realizacji u różnych klientów. Projekt jest już bliski zakończenia, ale być może jeszcze powrócę, aby opowiedzieć o tym, jak znowu zawaliłem pewne rzeczy. W trakcie rozwoju nauczyłem się wielu rzeczy, ale jeszcze więcej muszę się nauczyć w przyszłości. Jeśli dotarłeś do tego miejsca, dziękuję ci za to.
Źródło: habr.com

