ブラウザ戦略「歴史の道」。 プロジェクトのアーキテクチャと進化

この記事では、ブラウザベースのゲーム「 History Paths 」の技術的な部分の開発と進化についてお話します。

プログラミング言語、データベース、テクノロジー、アーキテクチャの選択に注意を払います。 ホスティングについて説明します。



Ways of Historyは、ブラウザベースの大規模な戦略ゲームです。 このプロジェクトは、一人の熱意から始まり、多くの聴衆のいる深刻なプロジェクトに成長しました。



エンジンを開発するために、3つの理由からC ++が選択されました。

  1. 彼は速いです。これはこのプロジェクトにとって重要です。
  2. いくつかの機会を最適に実現できる柔軟性があります。
  3. 私は彼を他の適切な人よりもよく知っています。


エンジンの本質は、リクエストを受信し、ページを生成して送信することです。

MySQLは非常に人気があり、同様のプロジェクトがMySQLを使用して行われることが多いため、データベースを選択しました。 その瞬間、私はデータベースの経験がありませんでした。

すぐに建築の問題が生じました。 次のモデルが選択されました。

エンジンは2つの部分に分かれています(D1、D2と呼びましょう)。

D1は要求を受信し、8つの空きスレッドのいずれかに送信します。 ストリームは要求を分析し、データベースに必要なデータを要求し、ページを形成して返します。 D1は、データベースに変更を加える方法を知りません。 データベースへのクエリの数を減らすために、多くのデータがD1にキャッシュされます。



場合によっては、D1は世界の状態の変更のリクエストを受け取ります(プレイヤーの都市で建造物が注文される、軍隊が送られるなど)。 この場合、D1は要求をD2に送信します(ソケットでの通信)。 複数のD1をD2に接続できます(各スレッドには8つのスレッドがあるため、同時に8つの命令をD2に送信できます)。 D2は一度に1つの命令のみを実行し、残りはインラインで待機します。 データベースに対する1つのステートメントの実行は、単一のトランザクションとして実行されます。 命令が正常に完了すると、データベースに変更が加えられ、命令が無効である場合、トランザクションはキャンセルされ、そのすべての変更がロールバックされます。 無効な指示はD1でも切断されることに注意してください) システム全体はD2なしでも機能しますが、読み取り専用モードでのみ可能です。変更はできず、すべてのイベントタイマーはオンになりますが、最後にはハングします。 その後D2をオンにすると、システムは障害がないかのように回復し、すべてのイベントが正しい順序で処理されます。



最初は、Apache Webサーバーが使用されていました。 人気があり、Windowsでビルドされているため、選択されました。 ISAPIテクノロジーを使用して、つまりdllライブラリとしてApacheに接続されたD1。 Apacheはリクエストを受け入れ、自身に接続されたライブラリに転送しました。 Apache自体は非常に低速でした。 したがって、ある時点で、プロジェクトはnginx + FastCGIの束に移行されました。



nginx Webサーバーは、設定と使用の両方で非常に便利です。 ページのレンダリング速度が向上しました。 さらに、nginxは静的コンテンツを非常に迅速に配信します。

FastCGIはどのように機能しますか? dllライブラリのエンジン自体は、スタンドアロンアプリケーションに再作成されました。 アプリケーションは、ソケットを介してWebサーバーからの要求を受け入れ、処理し、ページを生成し、同じソケットを介してWebサーバーにページを返します。 同時に、ソケットは開いたままで、新しいリクエストが入ります。 FastCGIプロトコルを使用したC ++開発の詳細については、 こちらをご覧ください



ホスティングについて。

プロジェクトを開始する前は、すべてが通常の家庭用コンピューターで通常の家庭用ケーブルインターネット上で機能していました。

当時、データセンターでサーバーをレンタルする経済的な機会はなかったため、最初のゲームの世界はすべて同じホームコンピューターで開始されました。 これにより、多くの不便が生じました。ネットワークへのアクセスが不安定で、家の照明が消灯する場合があり、プロバイダーが申告された料金でトラフィックを発行せず、しばしば技術的な作業を実行します。 プロジェクトはプレイヤーでいっぱいになり始め、負荷が増大しました。 別のインターネット接続を備えたもう1台のコンピューターが、第一世界のサービスに配信されました。 現在、1つの世界は1番目のサーバーで簡単に動作しますが、すべてが最適化されたわけではなく、使用するコンピューターは脆弱でした。



次の世界はすぐに発見されました。 さらに2台のコンピューターと2つのネットワーク接続が使用されました。 すでに世界3を立ち上げる準備をしています。これらのコンピューターはすべて、自宅のリビングルームにありました。 サーバーの数が増えると、問題の数も増えました。 何かが絶えず落ちていたので、もう家を出ることができませんでした。 不可抗力の問題を強制することに加えて、定期的なバグもありました。 誤った状況が発生した場合、アプリケーションはアサートですぐにクラッシュし、どうにかこの状況から抜け出そうとしませんでした。 このソリューションは特別に選ばれました。 これにより、私は常にすべてのバグの戦いを第一に考え、開発期間全体を通してバグを引きずりませんでした。

プロジェクトは収入を生み出し始め、データセンターにサーバーがレンタルされました。 ゲームのサイトと両方の世界がそこに移されました。 システムの管理ははるかに簡単になりましたが、コストが増加しました。 3番目の世界もこのサーバーで起動されましたが、DDoS攻撃の後、最初の2つの世界が危険にさらされないように別のサーバーに転送しました。



開発はWindows OS上で実施およびテストされました。 しかし、コードはこのOSに縛られることなくすぐに書かれており、将来、FreeBSDでコードを修正してプロジェクトをコンパイルするのに1日しかかかりませんでした。

ストリームを処理するために、POSIXライブラリが選択されました。 画像ファイルを作成するために、 FreeImageライブラリを使用しました



システム監視。

最初は、システム監視はモニターを使用していました! サーバーの「クラッシュ」は、エラーのあるウィンドウの形で検出されるか、チャート上に発信トラフィックがありません。 夜でも何度か目を覚まし、すべてのモニターを見て回らなければなりませんでした。

これはそれほど長く続くことができず、サーバーを常にポーリングし、サーバーからステータスデータを収集し、必要に応じて電子メールまたはSMSメッセージを電話に送信する特別なphpスクリプトが作成されました。 このスクリプトは無料ホスティングで起動され、今日まで機能しています。 彼のおかげで、問題をすばやく発見し、可能であればすぐに解消することが常に可能です。



以下の記事では、アイデアからリリースまでのプロジェクトの開発、データベースのデータを保存するためのエンジンと形式の技術的ソリューション、データのバックアップと攻撃に対する保護、ページ作成のメカニズムについて説明します。



根拠D1:

void* operateRequest(void* listen_socket) { // FCGX_Request request; assert(!FCGX_InitRequest(&request, *(int*)listen_socket, 0)); Session* s = new Session; //      while(FCGX_Accept_r(&request) == 0) { stringstream out; stringstream header; header << "Content-type: text/html"; //   string query; string addr; string referer; string post; string cookie; string agent; int content_lenght = 0; for(char** envp = request.envp; *envp; ++envp) { string v = *envp; string::size_type e = v.find('='); string p = v.substr(0, e); if(p == "REQUEST_URI") query = v.substr(e + 2, v.length()); if(p == "REMOTE_ADDR") addr = v.substr(e + 1, v.length()); if(p == "HTTP_COOKIE") cookie = v.substr(e + 1, v.length()); if(p == "HTTP_REFERER") referer = v.substr(e + 1, v.length()); if(p == "CONTENT_LENGTH") content_lenght = toInt(v.substr(e + 1, v.length())); if(p == "HTTP_USER_AGENT") agent = v.substr(e + 1, v.length()); } //   maximize(content_lenght, 9999); char p[10000]; FCGX_GetStr(p, content_lenght, request.in); p[content_lenght] = 0; post = p; // .  header   s->work(header, out, addr, cookie, referer, query, post); //    header << "\r\n\r\n" << out.str(); FCGX_PutStr(header.str().c_str(), int(header.str().length()), request.out); FCGX_Finish_r(&request); } return 0; } int main() { assert(initSocketSystem()); assert(!FCGX_Init()); int listen_socket = FCGX_OpenSocket(":8000", 400); assert(listen_socket >= 0); //  for(int i = 0; i < threads; ++i) { pthread_t thread; assert(pthread_create(&thread, 0, operateRequest, (void*)&listen_socket) == 0); } while(true) sleep(1000); return 0; }
      
      







ベースD2:

 void operateCommand(asComType com, Socket& sock) { // .        pthread_mutex_lock(&ascs); bool res; //     assert(sql.put("BEGIN")); switch(com) { case ASC_TOWNUPDATE: { //      int id = sock.readVal<int>(); res = asUpdateTown(id); } break; //  //… //… //… } //          assert(sql.put(res ? "COMMIT" : "ROLLBACK")); //  sock.sendVal(res); //   pthread_mutex_unlock(&ascs); } void* clientThread(void* client_socket) { Socket& sock = *(Socket*)client_socket; asComType com; int bytes; //    while((bytes = sock.readVal(com)) && bytes >= 0) { //  operateCommand(com, sock); } delete &sock; return 0; } int main() { while(Socket* client = sock.listen()) { //        pthread_t thread; assert(pthread_create(&thread, 0, clientThread, (void*)client) == 0); } return 0; }
      
      





明確にするために、提示されたコードは一部の場所で特別に簡略化されています。 一部のクラスと関数は省略されています。



All Articles