前書き
それはすべて、同僚が私が小さなWebサービスを作成することを提案したときに始まりました。それは火口のようなものであるはずでしたが、ITの群衆にとっては。機能は非常にシンプルです。登録し、プロファイルに記入して、要点に進みます。つまり、対話者を見つけ、接続を拡大し、新しい知人を獲得します。
ここで私は逸脱して自分自身について少し話さなければなりません。そうすれば、将来、なぜ私がそのような開発のステップを踏んだのかがより明確になるでしょう。
現在、私はゲームスタジオでテクニカルアーティストの役職に就いています。C#プログラミングでの私の経験は、Unityのスクリプトとユーティリティの作成、およびこれに加えて、Androidデバイスでの低レベルの作業用のプラグインの作成にのみ基づいていました。私はまだこの小さな世界から抜け出していないので、そのような機会が訪れました。
パート1。フレームのプロトタイピング
このサービスがどのようなものになるかを決めた後、私は実装のオプションを探し始めました。最も簡単な方法は、地球上のフクロウのように、私たちのメカニックが引っ張ってすべてを公の非難にさらすことができる、ある種の既製の解決策を見つけることです。
しかし、これは面白くなく、これに挑戦や感覚は見られなかったので、私はWebテクノロジーとそれらとの相互作用の方法を研究し始めました。
私はC#.Netの記事とドキュメントを見ることから始めました。ここで私はタスクを達成するためのさまざまな方法を見つけました。 ASP.NetやAzureサービスなどの本格的なソリューションから、Tcp \ Http接続との直接的な対話まで、ネットワークと対話するための多くのメカニズムがあります。
ASPで最初の試みをした後、私はすぐにそれを却下しました。私の意見では、私たちのサービスにとって決定は難しすぎました。このプラットフォームの機能の3分の1も使用しないので、検索を続けました。TCPとHttpクライアントサーバーのどちらかを選択しました。ここ、Habréで、マルチスレッドサーバーに関する記事を見つけて収集してテストしたところ、TCP接続との相互作用に焦点を当てることにしました。何らかの理由で、httpではクロスプラットフォームソリューションを作成できないと思いました。
サーバーの最初のバージョンには、接続の処理、Webページでの静的コンテンツの提供、およびユーザーデータベースの組み込みが含まれていました。そして、最初に、サイトを操作するための機能を構築することにしました。これにより、後でandroidとiosでのアプリケーションの処理がここに添付されます。
ここにいくつかのコードがあります
, :
:
local SQL:
, , . ( , - ).
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("\r\n\r\n") >= 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();
}
}
}
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<User> users = db.GetTable<User>();
foreach (var item in users)
{
Console.WriteLine($"{item.login} {item.password}");
}
}
}
}
}
, , . ( , - ).
第2章ホイールを締める
サーバーの動作をテストした後、これは私たちのサービスにとって素晴らしい解決策(スポイラー:いいえ)であるという結論に達したので、プロジェクトはロジックを取得し始めました。
ステップバイステップで新しいモジュールが表示され始め、サーバーの機能が拡張されました。サーバーには、接続のテストドメインとssl暗号化があります。
サーバーとクライアントの処理のロジックを説明するもう少しコード
, .
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:\ssl\itinder.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" +
$"|{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($"\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;
}
}
}
その本質は、通常の式を使用してリクエストをパーツに分割するという事実にあります。クライアントからメッセージを受け取り、リクエストのメソッドとURLを含む最初の行を選択します。次に、HeaderName = Contentの形式の配列に駆動するヘッダーを読み取り、付随するコンテンツ(たとえば、querystring)も見つけます。これも、同様の配列に駆動します。さらに、パーサーは現在のクライアントが許可されているかどうかを確認し、そのデータを保存します。承認されたクライアントからのすべてのリクエストには、Cookieに保存されている承認ハッシュが含まれているため、2種類のクライアントの作業のさらなるロジックを分離して、正しい答えを与えることができます。
さて、そして別のモジュールで取り出されるべき小さくて素晴らしい機能、「site.com/@UserName」のようなリクエストの動的に生成されたユーザーページへの変換。リクエストを処理した後、次のモジュールが機能します。
第3章ハンドルバーの取り付け、チェーンの潤滑
パーサーが動作を終了するとすぐに、ハンドラーが機能し、サーバーにさらに指示を与え、制御を2つの部分に分割します。
シンプルなハンドラー
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);
};
}
}
}
実際、ユーザー認証のチェックは1つだけで、その後、要求の処理が開始されます。
クライアントコントローラー
, \. , .
, , , .
RazorEngine, . .
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 = $"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;
}
}
}
}
, , , .
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}\n\n";
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 OK\nContent-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);
}
else
{
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();
}
}
}
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();
}
}
}
}
そしてもちろん、許可されたユーザーの検証が機能するためには、許可が必要です。承認モジュールはデータベースと対話します。サイトのフォームから受け取ったデータはコンテキストから解析され、ユーザーは保存され、代わりにCookieとサービスへのアクセスを受け取ります。
承認モジュール
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\nLocation: /@{user.login}\nSet-Cookie: {cookies.AuthCookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnly\n\n");
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\nLocation: /@{user.login}\nSet-Cookie: {user.cookie}; Expires={DateTime.Now.AddDays(2):R}; Secure; HttpOnly\n\n");
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($"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($"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章バイクを捨てる
2つのプラットフォーム用に2つのアプリケーションを作成する人件費を削減するために、Xamarin.Formsでクロスプラットフォームを作成することにしました。繰り返しますが、それはC#にあるという事実のためです。サーバーにデータを送信するだけのテストアプリケーションを作成した後、1つの興味深い点に遭遇しました。デバイスからのリクエストについては、興味深いことに、HttpClientに実装し、json形式の認証フォームからのデータを含むサーバーHttpRequestMessageにスローしました。何も期待せずにサーバーログを開くと、すべてのデータを含むデバイスからの要求が表示されました。ちょっとした愚か者、怠惰な夜の最後の3週間に行われたすべての実現。送信されたデータの正確さを確認するために、HttpListnerでテストサーバーを収集しました。すでに次のリクエストを受け取ったので、数行のコードでそれをパーツに解析し、フォームからデータのKeyValuePairを受け取りました。クエリの解析は2行に短縮されました。
私はさらにテストを開始しました。これについては前述していませんが、以前のサーバーでは、Webソケット上に構築されたチャットを実装していました。それはかなりうまく機能しましたが、Tcpを介した対話の原則自体が気のめいるようで、通信ログを維持しながら2人のユーザーの対話を正しく構築するには、余分なものを作成する必要がありました。これは、接続スイッチの要求を解析し、RFC 6455プロトコルを使用して応答を収集するため、テストサーバーで、単純なWebソケット接続を作成することにしました。純粋に楽しみのために。
チャットに接続する
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を介してサーバー登録を個別に構成する必要はありませんでした。証明書は、システムの必要なポートに既にインストールされていれば十分でした。
デバイス側とサイト側では、2つのクライアントがメッセージを交換し、これはすべてログに記録されました。サーバーの速度を低下させる巨大なパーサーはありません。これは必要ありませんでした。応答時間は200msから40-30msに減少しました。そして、私は唯一の正しい決断に至りました。
現在のサーバー実装をTcpにスローし、すべてをHttpで書き直します。現在、プロジェクトは再設計の段階にありますが、すでに完全に異なる相互作用の原則に従っています。デバイスとサイトの操作は同期およびデバッグされ、共通の概念がありますが、デバイスのhtmlページを生成する必要がないという唯一の違いがあります。
出力
「フォードを知らないので、頭を水に突っ込まないでください」仕事を始める前に、目標と目的をより明確に定義し、さまざまなクライアントに必要な技術とその実装方法を研究する必要があると思います。プロジェクトはすでに完成に近づいていますが、どうやってまた何かを手に入れたのかについて話をするために戻ってくるかもしれません。開発の過程で多くのことを学びましたが、今後さらに学ぶべきことがあります。ここまで読んだら、ありがとうございます。
