Introdução
Tudo começou quando um colega me propôs criar um pequeno serviço web. Isso deveria ser algo como um Tinder, mas para a comunidade de TI. A funcionalidade é extremamente simples, você se registra, preenche seu perfil e vai para o que realmente importa: encontrar um parceiro de conversa, expandir suas conexões e fazer novas amizades.
Aqui eu preciso fazer uma pausa e contar um pouco sobre mim, para que fique mais claro mais adiante por que tomei essas decisões no desenvolvimento.
Atualmente, sou Artista Técnico em um estúdio de jogos, e minha experiência em programação em C# se baseou apenas na criação de scripts e utilitários para o Unity, além da criação de plugins para o trabalho de baixo nível com dispositivos Android. Até agora, não saí desse pequeno mundo, e surgiu essa oportunidade.
Parte 1. Prototipando a estrutura
Decidindo o que esse serviço deveria ser, comecei a procurar opções para sua implementação. A maneira mais simples seria encontrar alguma solução pronta que pudesse adaptar nossas mecânicas e expor tudo isso ao julgamento público.
Mas isso não é interessante, não vejo nenhum desafio ou sentido nisso, por isso comecei a estudar tecnologias web e métodos de interação com elas.
Comecei o estudo lendo artigos e documentação sobre C# .Net. Aqui encontrei diversas maneiras de executar a tarefa. Existem muitos mecanismos de interação com a rede, desde soluções completas como ASP.Net ou serviços Azure até a interação direta com conexões TcpHttp.
Na minha primeira tentativa com ASP, eu imediatamente descartá-lo, pois achei que era uma solução muito pesada para nosso serviço. Não iremos utilizar sequer um terço das capacidades dessa plataforma, então continuei a busca. A escolha recaiu entre cliente-servidor TCP e Http. Aqui no Habr, encontrei um artigo sobre , que, após montar e testar, decidi optar pela interação com conexões TCP, pois de algum modo achei que o http não me permitiria criar uma solução multiplataforma.
A primeira versão do servidor incluía o processamento de conexões, servia conteúdo estático de páginas da web e continha um banco de dados de usuários. E para começar, decidi desenvolver a funcionalidade para trabalhar com o site, para que posteriormente pudesse integrar o processamento do aplicativo em Android e iOS.
Aqui está um pouco de código
O fluxo principal, em um loop infinito aceitando clientes:
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);
}
}
}O próprio manipulador de clientes:
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 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();
}
}
}
E o primeiro banco de dados construído em SQL local:usando System; usando System.Data.Linq; nome do espaço ClearServer { classe DatabaseWorker { privado estático DatabaseWorker instance; público estático DatabaseWorker GetInstance { obter { se (instance == null) instance = new DatabaseWorker(); return instance; } } privado DatabaseWorker() { string connectionStr = databasePath; usando (DataContext db = new DataContext(connectionStr)) { Table users = db.GetTable(); foreach (var item in users) { Console.WriteLine("{item.login} {item.password}"); } } } } }"{item.login} {item.password}"
}
}
}
}
}
Como pode ser notado, essa versão difere pouco da apresentada no artigo. Na verdade, aqui apenas foi adicionada a carga de páginas de uma pasta no computador e um banco de dados (que, aliás, nesta versão não funcionou, devido à arquitetura de conexão errada).Capítulo 2. Integrando as rodas
Após testar o funcionamento do servidor, cheguei à conclusão de que essa seria uma ótima solução (spoiler: não), para o nosso serviço, e assim o projeto começou a adquirir lógica.
Passo a passo, novos módulos começaram a surgir e a funcionalidade do servidor se expandiu. O servidor ganhou um domínio de teste e ssl criptografia de conexão.Mais um pouco de código, descrevendo a lógica de funcionamento do servidor e o tratamento de clientes.
Versão atualizada do servidor, incluindo o uso de certificado.usando System; usando System.Net; usando System.Net.Sockets; usando System.Reflection; usando System.Security; usando System.Security.Cryptography.X509Certificates; usando System.Security.Permissions; usando System.Security.Policy; usando 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("Iniciando o servidor.." + 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("Mudando para outro domínio"); 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(); } } }E também um novo manipulador de cliente com autorização SSL:
usando ClearServer.Core.Requester; usando System; usando System.Net.Security; usando 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|Erro: {e.Message}n|------------n|Falha na autenticação - fechando a conexão.n" +
"---------------------------------------------------------------------n");
SSlClientStream.Close();
Client.Close();
}
new RequestContext(SSlClientStream, Client);
}}
}
Mas como o servidor opera exclusivamente em conexão TCP, é necessário criar um módulo que possa reconhecer o contexto da solicitação. Eu decidi que um parser que dividisse a solicitação do cliente em partes separadas, com as quais eu pudesse interagir, seria o ideal para fornecer as respostas necessárias ao cliente.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}");n{DateTime.Now:g} Cliente 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 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 values = new List();
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;
}
}
}
A essência está em dividir a solicitação em partes usando expressões regulares. Recebemos uma mensagem do cliente, destacamos a primeira linha que contém o método e a URL da solicitação. Em seguida, lemos os cabeçalhos, que organizamos em um array do tipo NomeDoCabeçalho=Conteúdo, e também encontramos, se houver, o conteúdo suplementar (como querystring), que também organizamos em um array semelhante. Além disso, o parser verifica se o cliente atual está autenticado e armazena seus dados. Todas as solicitações de clientes autenticados contêm um hash de autorização, que é armazenado em cookies, permitindo assim separar a lógica de operação para dois tipos de clientes e fornecer as respostas corretas.E uma pequena, mas agradável, funcionalidade que valeria a pena separar em um módulo próprio é a transformação de solicitações do tipo "site.com/@UserName" em páginas de usuário geradas dinamicamente. Após o processamento da solicitação, os próximos módulos entram em ação.
Capítulo 3. Instalação do volante, lubrificação da corrente
Assim que o parser termina, o manipulador entra em ação, dando as diretrizes adicionais ao servidor e separando o gerenciamento em duas partes.
Manipulador simples
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); }; } } }Basicamente, aqui há apenas uma verificação da autorização do usuário, após a qual o processamento da solicitação começa.
Controladores de clientes
Se o usuário não estiver autenticado, suas funcionalidades se baseiam apenas na exibição de perfis de usuário e na janela de registro/autenticação. O código para o usuário autenticado é aproximadamente o mesmo, portanto não vejo sentido em duplicá-lo.Usuário não autenticado
usando ClearServer.Core.Requester; usando System.IO; usando System.Net.Security; namespace ClearServer.Core.UserController { classe interna 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 ="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;}
}
}
}
E, claro, o usuário deve receber algum conteúdo das páginas, portanto, para as respostas, existe o seguinte módulo que responde às solicitações de recursos.WriterController
usando System; usando System.IO; usando System.Net.Security; usando 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":
returnquot;{Header} text/html";
case ".css":
returnquot;{Header} text/css";
case ".js":
returnquot;{Header} text/javascript";
case ".jpg":
case ".jpeg":
case ".png":
case ".gif":
returnquot;{Header} image/{extension}";
default:
if (extension.Length > 1)
{
returnquot;{Header} application/" + extension.Substring(1);
}
else
{
returnquot;{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();
}
}
}
Mas para mostrar ao usuário seu perfil e os perfis de outros usuários, decidi usar o RazorEngine, ou melhor, uma de suas partes. Ele também inclui o tratamento de solicitações inválidas e a emissão de códigos de erro apropriados.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(); } } } }E, claro, para que a verificação de usuários autorizados funcione, é necessária a autenticação. O módulo de autenticação interage com o banco de dados. Os dados obtidos dos formulários no site são extraídos do contexto, o usuário é salvo e recebe em troca cookies e acesso ao serviço.
Módulo de autenticação
usando ClearServer.Core.Cookies; usando ClearServer.Core.Requester; usando ClearServer.Core.Security; uso System; usando System.Linq; usando System.Net.Security; usando 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("Usuário pronto");
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);
}
}
}
}
E assim funciona o tratamento do banco de dados:Banco de dados
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 = @"caminho/para/o/banco"; 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("Usuário {user.name} com id {user.uid} adicionado");Console.WriteLine("Usuário {user.name} com id {user.uid} adicionado");
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 já existe");
return false;
}
return true;
}
public void UserUpdate(User user)
{
var UserToUpdate = users.FirstOrDefault(x => x.uid == user.uid);
UserToUpdate = user;
DataBase.SubmitChanges();
Console.WriteLine(Console.WriteLine("Usuário {UserToUpdate.name} com id {UserToUpdate.uid} atualizado");
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;
}
}
}
}
E tudo funciona como um relógio, a autenticação e o registro estão operacionais, o funcionalismo mínimo de acesso ao serviço já está disponível e chegou a hora de escrever o aplicativo e vincular tudo isso às funções principais, para as quais tudo está sendo feito.Capítulo 4. Construindo do zero
Para reduzir o tempo de desenvolvimento de dois aplicativos para duas plataformas, decidi criar uma solução multiplataforma com Xamarin.Forms, novamente por ser baseado em C#. Após desenvolver um aplicativo de teste que apenas envia dados para o servidor, encontrei um aspecto interessante. Para a solicitação do dispositivo, implementei por curiosidade usando HttpClient e enviei ao servidor um HttpRequestMessage que continha os dados do formulário de login no formato JSON. Sem grandes expectativas, abri o log do servidor e vi a requisição do dispositivo com todos os dados. Um leve atordoamento me atingiu, refletindo sobre tudo o que foi feito nas últimas três semanas em noites longas. Para verificar a precisão dos dados enviados, montei um servidor de teste com HttpListener. Após receber outra solicitação lá, com apenas algumas linhas de código, consegui decompor a solicitação em partes e obtive um KeyValuePair dos dados do formulário. A decomposição da solicitação se reduziu a duas linhas.
Continuei os testes, não mencionei antes, mas no servidor anterior, também implementei um chat baseado em websockets. Funcionava bastante bem, mas o próprio princípio de interação via TCP era frustrante, sendo necessário criar muito código adicional para construir corretamente a interação entre dois usuários com a manutenção do histórico da conversa. Isso incluía o parsing da solicitação para alternar a conexão e a coleta de respostas de acordo com o protocolo RFC 6455. Por isso, decidi criar uma simples conexão websocket no servidor de teste, apenas por curiosidade.
Conexão ao 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(); } } } } }E funcionou. O servidor configurou a conexão automaticamente, gerou a chave de resposta. Eu nem precisei configurar a registro do servidor via ssl separadamente, bastou que o certificado estivesse instalado na porta correta no sistema.
Do lado do dispositivo e do lado do site, dois clientes trocavam mensagens, tudo isso estava sendo registrado. Não foram necessários grandes parsers que desacelerassem o servidor, nada disso era necessário. O tempo de resposta foi reduzido de 200ms para 40-30ms. E cheguei à única solução correta.
Descartar a implementação atual do servidor em Tcp e reescrever tudo sob Http. Agora, o projeto está em fase de reestruturação, mas já com princípios de interação completamente diferentes. O funcionamento dos dispositivos e do site está sincronizado e ajustado, com um conceito comum, com a única diferença de que não é necessário gerar páginas html para os dispositivos.
Saída
"Quem não sabe, não se atire na água" Acho que, antes de começar o trabalho, deveria ter definido de forma mais clara os objetivos e as tarefas, assim como me aprofundar no estudo das tecnologias necessárias e métodos de sua implementação em diferentes clientes. O projeto já está próximo da conclusão, mas talvez eu ainda volte para contar como cometi novos erros. Aprendi muito durante o processo de desenvolvimento, mas ainda tenho muito a aprender no futuro. Se você leu até aqui, agradeço por isso.
Fonte: habr.com

