最初からMMO。 NettyとUnreal Engineを使用します。 パート1

みなさんこんにちは! いくつかの記事で、Unreal EngineとNettyを使用してMMOゲームの肖像を作成した経験を共有したいと思います。 おそらく、アーキテクチャと私の経験は誰かにとって便利であり、Photonなどのマルチプレイヤーゲームを開発するためのフレームワークに取って代わる、非現実的な専用サーバーではなく、独自のゲームサーバーの作成を開始するのに役立ちます。



最後に、ゲームにログインまたは登録し、ゲームルームを作成し、チャットを使用してゲームを開始し、接続が暗号化され、クライアントがサーバーを介して同期され、ゲームに武器が1つあります-レーザー、テストでチェックされるクライアントがありますサーバー。 私は美しいグラフィックスを作ろうとしませんでした、必要な最小限だけがあるでしょう、さらなる機能は類推によって追加されます。 サーバー上でロジックを簡単に拡張できます。たとえば、ランダムゲームやバランサーを追加できます。 MMOベースを作成し、本格的なモバイルMMOゲームを作成するために必要なものを把握することが重要でした。



パート1.全体像、ライブラリの構築、メッセージングのためのクライアントとサーバーの準備

パート2.ゲーム機能の拡張+ Diamond Squareアルゴリズム







一般的なアーキテクチャ、仕組み



最初に、一般的な用語で説明し、その後、すべてを段階的に記述します。 通信クライアントサーバーは、ソケット、Protobufメッセージング形式で構築され、ゲームに入った後の各メッセージは、クライアントのOpenSSLライブラリとサーバーのjavax.crypto *を使用するAESアルゴリズムを使用して暗号化され、キー交換はDiffie-Hellmanプロトコルを使用して実行されます。 Nettyは非同期サーバーとして使用されます。MySQLにデータを保存し、それを使用してHibernateを取得します。 Androidでゲームをサポートするという目標を設定したので、このプラットフォームへの移植に少し注意を払います。 私はSpikyプロジェクトを呼び出しました-引っかき傷があり、正当な理由があります:



主にC ++プログラマであるUnreal Engine 4は、開発するのに「楽しい」わけではありません。



私が何かを見逃した場合や何かが合わない場合は、ソースに連絡してください。



スパイキーソースコード



最終的に、これは私たちが得るものです:





クライアントとサーバー間の通信がどのように発生するかから始めましょう。 どちらにもMessageDecoderとDecryptHandlerがあり、これらはメッセージのエントリポイントです。パケットを読み取った後、メッセージが復号化され、タイプが決定され、タイプごとにプロセッサに送信されます。 出口点は、それぞれMessageEncoderとEncryptHandler、クライアントとサーバーです。 Nettyにメッセージを送信すると、EncryptHandlerを通過します。 ここで、暗号化するかどうか、およびラップする方法が決定されます。



各メッセージはWrapperプロトバフでラップされ、受信者はWrapper内でハンドラーを選択するためにチェックします。これはCryptogramWrapper-暗号化されたバイトまたはオープンメッセージです。 Wrapperメッセージは次のようになります(一部):



message CryptogramWrapper { bytes registration = 1; } message Wrapper { Utility utility = 1; CryptogramWrapper cryptogramWrapper = 2; }
      
      





すべてのメッセージングは​​Decoder-Encoderの原理に基づいて構築されているため、ゲームに新しいチームを追加する必要がある場合は、条件を更新する必要があります。 たとえば、クライアントが登録したい場合、メッセージはMessageEncoderに送られ、そこでメッセージが暗号化され、ラップされ、サーバーに送信されます。 サーバーでは、メッセージはDecryptHandlerに送信され、必要に応じて復号化され、メッセージフィールドの存在によってタイプが読み取られ、送信されます



 if(wrapper.hasCryptogramWrapper()) { if(wrapper.getCryptogramWrapper().hasField(registration_cw)) { byte[] cryptogram = wrapper.getCryptogramWrapper().getRegistration().toByteArray(); byte[] original = cryptography.Decrypt(cryptogram, cryptography.getSecretKey()); RegModels.Registration registration = RegModels.Registration.parseFrom(original); new Registration().saveUser(ctx, registration); } else if (wrapper.getCryptogramWrapper().hasField(login_cw)) {} }
      
      





.hasFieldを使用してメッセージ内のフィールドを見つけるには、一連の記述子(registration_cw、login_cw)が必要であり、それらをDescriptorsクラスに個別に保存します。



したがって、新しい機能が必要な場合は、



1.新しいタイプのProtobufメッセージを作成し、Wrapper / CryptogramWrapperに入れます

2.クライアントとサーバーの記述子でアクセスが必要なフィールドを宣言します

3.型を決定した後、メッセージを送信する論理クラスを作成します

4. Decode-Encoderクライアントおよびサーバーに新しいタイプを定義する条件を追加します

5.処理します



これは何度も繰り返さなければならない重要なポイントです。



このプロジェクトでは、TCPプロトコルを使用しました。もちろん、UDPを使用してアドオンを作成することをお勧めしますが、最初はそれを試みましたが、それから出てきたものはすべてTCPのように見えましたが、私の状況では、パケットの確認を無効にできないのはTCPのみで、TCPは確認を待っています送信を続ける前に遅延が発生し、ネットワーク経由の送信中にパケットが失われた場合、100未満のpingを達成することは困難です。ゲームは停止し、パケットが再配信されるまで待機します。 残念ながら、このTCPの動作を変更することはできません。TCPの意味なので、変更する必要はありません。 ソケットの種類の選択は、ゲームのジャンルに完全に依存します。アクションのジャンルのゲームでは、1秒前に起こったことではなく、ゲームの世界の最新の状態が重要です。 クライアントからサーバーにできるだけ早くデータが到着する必要があり、データが再び送信されるのを待ちたくありません。 そのため、マルチプレイヤーゲームにTCPを使用しないでください。



しかし、信頼できるudpを作成したい場合、困難が待っています。順序付け、配信確認をオフにする機能、チャネルの輻輳を制御する、1400バイトを超える大きなメッセージを送信する必要があります。 アクションゲームでは、UDPを使用する必要があります。詳細については、次の記事や書籍から始めることをお勧めします。



ゲーム開発者向けのネットワークプログラミング。 パート1:UDPと TCP

.Net用の信頼性の高いUdpプロトコルの実装

Joshua Glaser-マルチプレイヤーゲーム。 第7章遅延、変動および信頼性。



コマンド、暗号化されたメッセージ、ファイル(キャプチャ)を送信するには、信頼性の高いシリアル接続が必要でした。 TCPは、そのような機能をすぐに使用できるようにします。 プレイヤーの移動など、頻繁に更新され、あまり重要ではないゲームデータを転送するには、UDPが最適なオプションであり、完全性と開始場所についてUDPメッセージを送信する機能を追加しましたが、このプロジェクトではすべての通信がTCPを介して行われます。 たぶん、TCPとUDPを一緒に使うべきでしょうか? ただし、TCPが優先されるため、失われたUDPパケットの数が増加します。 UDPは、さらなる改善の分野に残りました。 この記事では、「問題が発生したときに改善する」という原則に従います。







サーバーはNettyに基づいており、ソケットの処理を引き継ぎ、便利なアーキテクチャを実装しています。 受信データ用に複数のハンドラーを接続できます。 最初のハンドラーでは、ProtobufDecoderを使用して受信メッセージを逆シリアル化し、ゲームデータを直接処理します。 同時に、ライブラリ自体の設定を柔軟に制御し、必要な数のスレッドまたはメモリをライブラリに割り当てることができます。 Nettyを使用すると、簡単に拡張および拡張できるクライアントサーバーアプリケーションをすばやく簡単に作成できます。 1つのスレッドでクライアントを処理するのに十分でない場合は、必要な数のスレッドをEventLoopGroupコンストラクターに渡すだけです。 プロジェクト開発のある段階で追加のデータ処理が必要な場合、コードを書き換える必要はありません。新しいハンドラーをChannelPipelineに追加するだけで、アプリケーションのサポートが大幅に簡素化されます。



Nettyを使用するときの一般的なアーキテクチャは次のようになります。



 public class ServerInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); /*  */ //pipeline.addLast(new LoggingHandler(LogLevel.INFO)); /*   */ // Decoders protobuf pipeline.addLast(new ProtobufVarint32FrameDecoder()); pipeline.addLast(new ProtobufDecoder(MessageModels.Wrapper.getDefaultInstance())); /*   */ // Encoder protobuf pipeline.addLast(new ProtobufVarint32LengthFieldPrepender()); pipeline.addLast(new ProtobufEncoder()); /*          30  */ pipeline.addLast(new IdleStateHandler(30, 0, 0)); /*    */ pipeline.addLast(new EncryptHandler()); /*    */ pipeline.addLast(new DecryptHandler()); } }
      
      





このアプローチの利点は、サーバーとハンドラーを異なるマシンに分割できることです。ゲームデータを計算するためのクラスターを受け取ると、かなり柔軟な構造が得られます。 負荷は小さいですが、すべてを1つのサーバーに保持できます。 負荷が増加すると、ロジックを別のマシンに分離できます。



ヒットをテストするために、ショットのパラメーターを取得し、ショット時の場所に基づいてオブジェクトをワールドに配置し、ヒットに関する情報をメインサーバーに返すショットをシミュレートする、特別なUnreal Engineクライアントを作成しました。逃した。



最初から始める



私は詳細に書き込もうとしましたが、ネタバレの下で多くをもたらしました。



コードを使用して空のプロジェクトを作成し、それをSpikyと呼びましょう。 まず、作成されたデフォルトのGameModeを削除します(これは現在のゲームのルールを定義するクラスです。さらに使用するため、特定のレベルごとに再定義できます。GameModeのインスタンスは1つのみです)-自動的に作成されたSpiky_ClientGameModeBaseを削除します。 次に、Spiky_Client.Build.csを開きます。これは、Unreal Build Systemの一部であり、さまざまなモジュール、サードパーティライブラリを接続し、デフォルトでは、バージョン4.16以降、SharedPCH(Sharing precompiled headers)モードが使用され、Include- What-You-Use(IWYU)。これにより、重いEngine.hヘッダーを含めることができなくなります。 アンリアルエンジンの以前のバージョンでは、エンジンの機能のほとんどは、Engine.hやUnrealEd.hなどのモジュールヘッダーファイルによって有効にされていました。コンパイル時間は、プリコンパイルヘッダー(PCH)を介してこれらのファイルをコンパイルできる速さに依存していました。 エンジンが成長するにつれて、これがボトルネックになりました。



IWYUリファレンスガイド



Spiky_Client.Build.csには



PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;







ssdを備えた高速マシンではうまく機能します(非現実的な作業をするには-別の頭痛が必要です、IntelliSenseを無効にして代わりにVisualAssistを使用することをお勧めします)が、利便性と速度のために、ssdマシンを使用しないでください。書き込みが少ない別のモードに切り替えることをお勧めしますこれは、PCHUsageMode.Defaultを有効にして、プリコンパイル済みヘッダーの生成を無効にすることで行います。



PCHUsageのすべての可能な値:



PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

PCHUsage = PCHUsageMode.UseSharedPCHs;

PCHUsage = PCHUsageMode.NoSharedPCHs;

PCHUsage = PCHUsageMode.Default;







これで、ファイルには次が含まれます。



Spiky_Client.Build.cs
 using UnrealBuildTool; public class Spiky_Client : ModuleRules { public Spiky_Client(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.Default; PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" }); PrivateDependencyModuleNames.AddRange(new string[] { }); } }
      
      







PublicDependencyModuleNamesとPrivateDependencyModuleNamesの違いは何ですか? Unrealプロジェクトでは、インターフェイスヘッダーとソースコードにSource / PublicおよびSource / Privateを使用することが望ましい場合、PublicDependencyModuleNamesはPublicおよびPrivateフォルダーで使用できますが、PrivateDependencyModuleNamesはPrivateフォルダーでのみ使用できます。 BuildConfiguration.xmlをオーバーライドすることにより、他のさまざまなアセンブリパラメーターを変更できます。すべてのパラメーターは次の場所にあります。



Unreal Build Systemの構成



便利なマイナーエディター設定
小さなアイコンをオンにし、フレームレートとメモリ消費を表示します。

一般->その他->パフォーマンス->フレームレートとメモリを表示

一般->ユーザーインターフェイス->小さなツールバーアイコンを使用



次に、ログイン、登録、およびメインメニュー画面にゲーム外GameModeを追加します。



SpikyGameModeを追加する
File-> New C ++ Class-> Game Mode BaseにSpikyGameModeという名前を付け、publicを選択してGameModesフォルダーを作成します。 最終パスは次のようになります。



Spiky / Spiky_Client / Source / Spiky_Client / Public / GameModes



SpikyGameModeのタスクは、世界への真の参照を作成することです。 ワールドは、アクターとコンポーネントが存在し視覚化されるマップを表すトップレベルのオブジェクトです。 後で、インターフェイスを制御するUObjectから継承したDiffrentMixクラスを作成し、UObjectクラスから取得できない現在の世界へのリンクを必要とするウィジェットを作成するため、DiffrentMixを初期化して世界にリンクを渡すGameModeを作成します。



インターフェイスについての別の言葉、これはクライアントアーキテクチャを指します。 SingleMingleMingleを介してすべてのウィジェットにアクセスできます。すべてのウィジェットはWidgetsContainer内に配置されます。これは、ウィジェットを深さを設定できるレイヤーに配置する必要があります。WidgetsContainerのルートはCanvasです。残念ながら、ビューポートを使用してウィジェットの順序を変更する方法は見つかりませんでした。 これは、たとえばチャットが他のすべての上にあることが保証される必要がある場合に便利です。 これを行うには、プログラムmainMenuChatSlot-> SetZOrder(10)でウィジェットを最大深度(優先度)に設定しますが、任意の優先度を設定できます。



すべてのオブジェクトの親UObject基本クラスであるDifferentMixクラスを追加し、新しいUtilsフォルダーに配置します。ここに、独自のクラスを作成するのに不必要な稀な機能であるウィジェットへのリンクを保存します。これはユーザーインターフェイスを制御するシングルトンです。



UGameInstanceクラスの派生物であるSpikyGameInstanceを追加します。これは、レベル間で転送されるデータを保存できる汎用UObjectです。 ゲームの作成時に作成され、ゲームが終了するまで存在します。 これを使用して、プレーヤーのログイン、ゲームセッションID、暗号化キーなどの一意のゲームデータを保存します。また、ストリームリスニングソケットを開始および停止し、それを介してDifferentMix関数にアクセスします。



新しいクラスの場所
Spiky_Client / Source / Spiky_Client / Private / GameModes / SpikyGameMode.h

Spiky_Client / Source / Spiky_Client / Private / Utils / DifferentMix.h

Spiky_Client / Source / Spiky_Client / Private / SpikyGameInstance.h



Spiky_Client / Source / Spiky_Client / Public / GameModes / SpikyGameMode.cpp

Spiky_Client /ソース/ Spiky_Client / Public / Utils / DifferentMix.cpp

Spiky_Client / Source / Spiky_Client / Public / SpikyGameInstance.cpp



おそらく、新しいクラスを追加した後のエディターからのゲームは収集を拒否します。これは、すべてのソースファイルで#include "Spiky_Client.h"の存在を必要とするモードに切り替えたためです。手動で追加し、スタジオを通じて収集します。エディターを介して新しいコードをコピーし、手動で編集し、Spiky_Client.uproject pcをクリックしてVisual Studioプロジェクトファイルを生成します。



エディターに戻って、Mapsフォルダーを作成し、その中に標準マップを保存し、後でMainMapと呼びます。後で回転ファーを配置します(または、多くのMMOのようにゲームキャラクターを選択します)。



プロジェクト設定→マップとモードを開き、作成されたGameMode / GameInstance / Mapを図のように設定します。







ネットワーク部



すべてを準備したら、ネットワーク部分からプロジェクトの作成を開始し、サーバーに接続し、失われたときに接続を復元し、着信メッセージのリスナーとサーバーの可用性をチェックするストリームを作成します。 ネットワークを操作し、ソケットを提供するクライアント上のメインオブジェクトは、UObjectの派生物であるSocketObjectと呼ばれ、Netフォルダーに追加します。 ネットワークを使用するため、Spiky_Client.Build.csにモジュール「Networking」、「Sockets」を追加する必要があります



 PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "Networking", "Sockets" });
      
      





デストラクタ、多数の自己記述型静的関数、SocketObjectヘッダーに必要なSocketSubsystemおよびNetworkingを追加します。



SocketObject.h
 // Copyright (c) 2017, Vadim Petrov - MIT License #pragma once #include "CoreMinimal.h" #include "UObject/NoExportTypes.h" #include "Networking.h" #include "SocketSubsystem.h" #include "SocketObject.generated.h" /** *   ,  ,   -  . */ UCLASS() class SPIKY_CLIENT_API USocketObject : public UObject { GENERATED_BODY() ~USocketObject(); public: // tcp static FSocket* tcp_socket; // tcp   static TSharedPtr<FInternetAddr> tcp_address; //   static bool bIsConnection; //     static void Reconnect(); //     static bool Alive(); // udp static FSocket* udp_socket; // udp   static TSharedPtr<FInternetAddr> udp_address; //       UDP  ,  unreal  FUdpSocketReceiver,       - static FUdpSocketReceiver* UDPReceiver; static void Recv(const FArrayReaderPtr& ArrayReaderPtr, const FIPv4Endpoint& EndPt); static void RunUdpSocketReceiver(); static int32 tcp_local_port; static int32 udp_local_port; //     ,  GameInstance static void InitSocket(FString serverAddress, int32 tcp_local_p, int32 tcp_server_port, int32 udp_local_p, int32 udp_server_port); };
      
      







ソースでは、まずInitSocketでソケットを作成し、バッファーを選択し、ローカルポートを割り当てます。ソケットを作成する2つの方法を知っています。そのうちの1つはビルダーです。



 tcp_socket = FTcpSocketBuilder("TCP_SOCKET") .AsNonBlocking() .AsReusable() .WithReceiveBufferSize(BufferSize) .WithSendBufferSize(BufferSize) .Build();
      
      





またはISocketSubsystemを介して:



 tcp_socket = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateSocket(NAME_Stream, TEXT("TCP_SOCKET"), false);
      
      





これらは、特定のプラットフォームに固有のさまざまなソケット固有のインターフェイスの基本的な抽象化です。 アドレスを構成ファイルまたはコードのどこかに行として設定するため、FIPv4Address :: Parseを使用して目的の形式にする必要があります。次に、bIsConnection = Alive();を接続して呼び出します。 このメソッドは、空のメッセージがサーバーに送信され、到達すると接続が確立されます。 最後に、FUdpSocketBuilderを使用してUDPソケットを作成します。結果のInitSocketは次のようになります。



USocketObject :: InitSocket
 void USocketObject::InitSocket(FString serverAddress, int32 tcp_local_p, int32 tcp_server_port, int32 udp_local_p, int32 udp_server_port) { int32 BufferSize = 2 * 1024 * 1024; tcp_local_port = tcp_local_p; udp_local_port = udp_local_p; // tcp /*  FTcpSocketBuilder tcp_socket = FTcpSocketBuilder("TCP_SOCKET") .AsNonBlocking() // Socket connect always success. Non blocking you say socket connect dont wait for response (Don?t block) so it will return true. .AsReusable() .WithReceiveBufferSize(BufferSize) .WithSendBufferSize(BufferSize) .Build(); */ tcp_socket = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateSocket(NAME_Stream, TEXT("TCP_SOCKET"), false); tcp_address = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); FIPv4Address serverIP; FIPv4Address::Parse(serverAddress, serverIP); tcp_address->SetIp(serverIP.Value); tcp_address->SetPort(tcp_server_port); tcp_socket->Connect(*tcp_address); bIsConnection = Alive(); // udp udp_address = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); FIPv4Address::Parse(serverAddress, serverIP); udp_address->SetIp(serverIP.Value); udp_address->SetPort(udp_server_port); udp_socket = FUdpSocketBuilder("UDP_SOCKET") .AsReusable() .BoundToPort(udp_local_port) .WithBroadcast() .WithReceiveBufferSize(BufferSize) .WithSendBufferSize(BufferSize) .Build(); }
      
      







ソケットを閉じて、デストラクタで削除します



 if (tcp_socket != nullptr || udp_socket != nullptr) { tcp_socket->Close(); delete tcp_socket; delete udp_socket; }
      
      







SocketObjectの現在の状態は次のとおりです。



SocketObject.cpp
 // Copyright (c) 2017, Vadim Petrov - MIT License #include "Spiky_Client.h" #include "SocketObject.h" FSocket* USocketObject::tcp_socket = nullptr; TSharedPtr<FInternetAddr> USocketObject::tcp_address = nullptr; bool USocketObject::bIsConnection = false; FSocket* USocketObject::udp_socket = nullptr; TSharedPtr<FInternetAddr> USocketObject::udp_address = nullptr; FUdpSocketReceiver* USocketObject::UDPReceiver = nullptr; int32 USocketObject::tcp_local_port = 0; int32 USocketObject::udp_local_port = 0; USocketObject::~USocketObject() { if (tcp_socket != nullptr || udp_socket != nullptr) { tcp_socket->Close(); delete tcp_socket; delete udp_socket; } } void USocketObject::InitSocket(FString serverAddress, int32 tcp_local_p, int32 tcp_server_port, int32 udp_local_p, int32 udp_server_port) { int32 BufferSize = 2 * 1024 * 1024; tcp_local_port = tcp_local_p; udp_local_port = udp_local_p; /* tcp_socket = FTcpSocketBuilder("TCP_SOCKET") .AsNonBlocking() // Socket connect always success. Non blocking you say socket connect dont wait for response (Don?t block) so it will return true. .AsReusable() .WithReceiveBufferSize(BufferSize) .WithSendBufferSize(BufferSize) .Build(); */ // tcp tcp_socket = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateSocket(NAME_Stream, TEXT("TCP_SOCKET"), false); // create a proper FInternetAddr representation tcp_address = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); // parse server address FIPv4Address serverIP; FIPv4Address::Parse(serverAddress, serverIP); // and set tcp_address->SetIp(serverIP.Value); tcp_address->SetPort(tcp_server_port); tcp_socket->Connect(*tcp_address); // set the initial connection state bIsConnection = Alive(); // udp udp_address = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); FIPv4Address::Parse(serverAddress, serverIP); udp_address->SetIp(serverIP.Value); udp_address->SetPort(udp_server_port); udp_socket = FUdpSocketBuilder("UDP_SOCKET") .AsReusable() .BoundToPort(udp_local_port) .WithBroadcast() .WithReceiveBufferSize(BufferSize) .WithSendBufferSize(BufferSize) .Build(); } void USocketObject::RunUdpSocketReceiver() { } void USocketObject::Recv(const FArrayReaderPtr& ArrayReaderPtr, const FIPv4Endpoint& EndPt) { } void USocketObject::Reconnect() { } bool USocketObject::Alive() { return false; }
      
      







Aliveメッセージの送信方法、メッセージ形式、およびサーバーを扱いましょう。 サーバーのコアでは、Javaで記述されたNetty非同期フレームワークを使用しました。 その主な利点は、ソケットに対する単純な読み取りと書き込みです。 Nettyはノンブロッキング非同期I / Oをサポートし、簡単にスケーリングできます。これは、システムが同時に何千もの接続を処理できるようにする必要がある場合、オンラインゲームにとって重要です。 そしてまた重要-Nettyは使いやすいです。



サーバーを作成しましょう。ここでは、IntelliJ IDEAを使用して、Mavenプロジェクトを作成します。



 <groupId>com.spiky.server</groupId> <artifactId>Spiky server</artifactId>
      
      





必要な依存関係Nettyを追加します



 <dependencies> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.8.Final</version> </dependency> </dependencies>
      
      





次に、メッセージのシリアル化の形式を理解しましょう。 Protobufを使用します。 メッセージサイズは非常に小さくなり、グラフから判断すると、すべての点でJSONよりも優れています。



サイズ比較




性能比較




* ここから取られた、良いもの、プロトバフの例とさまざまなメトリック



シリアル化可能なデータの構造を決定するには、この構造のソースコードを使用して.protoファイルを作成する必要があります。次に例を示します。



 syntax = "proto3"; message Player { string player_name = 1; string team = 2; int32 health = 3; PlayerPosition playerPosition = 4; } message PlayerPosition {}
      
      





その後、このデータ構造が特別なコンパイラprotocによってクラスにコンパイルされ、コンパイルコマンドは次のようになります。



./protoc --cpp_out=. --java_out=. GameModels.proto







Protobuffには、各フィールドの意味をよりよく理解するのに役立つ優れたドキュメントがあります。

Protobuffは、プロジェクトで使用されるJavaおよびC ++用に実装されています。 別の依存関係を追加します。



 <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>3.0.0-beta-4</version> </dependency>
      
      





Unrealでprotobuffのサポートを追加する必要があります。それほど単純ではありません。最初はgithubでブランチを取得します。 ここで正しく組み立てる必要があります。VisualStudioで組み立てる方法については、 こちらをご覧ください 。 anrialのリンクの種類を設定します。「構成プロパティへのフィルター処理> C / C ++>コード生成>ランタイムライブラリ、ドロップダウンリストからマルチスレッドDLL(/ MD)を選択」 ビルドシステムを使用した静的ライブラリのリンクとlibprotobuf.libのコンパイルを参照。 プロジェクトに追加した後、Libs and Includesを作成する必要があるルートにThirdParty / Protobufフォルダーを作成します。 /protobuf-3.0.0-beta-4/cmake/build/solution/Release/libprotobuf.libをLibsに配置します。 Includesに/ proto-install / include / googleを配置します。



私の目標はモバイルデバイスをサポートすることであったため、Android NDKを使用してAndroid用のライブラリを構築する必要がありますプロセス自体は次のようになり、Android NDKをインストールし、jniフォルダーを作成し、それらに2つのAndroid.mkおよびApplication.mkファイルを配置し、protobuf-3.0.0-beta-4 / srcからsrcをコピーしてndk-buildを使用するsrcを作成します。既製のApplication.mkおよびAndroid.mkファイル:



Application.mk
APP_OPTIM := release

APP_ABI := armeabi-v7a #x86 x86_64

APP_STL := gnustl_static



NDK_TOOLCHAIN_VERSION := clang



APP_CPPFLAGS += -D GOOGLE_PROTOBUF_NO_RTTI=1

APP_CPPFLAGS += -D __ANDROID__=1

APP_CPPFLAGS += -D HAVE_PTHREAD=1









Android.mk
LOCAL_PATH := $(call my-dir)



include $(CLEAR_VARS)



LOCAL_MODULE := libprotobuf



LOCAL_SRC_FILES :=\

src/google/protobuf/arena.cc \

src/google/protobuf/arenastring.cc \

src/google/protobuf/extension_set.cc \

src/google/protobuf/generated_message_util.cc \

src/google/protobuf/io/coded_stream.cc \

src/google/protobuf/io/zero_copy_stream.cc \

src/google/protobuf/io/zero_copy_stream_impl_lite.cc \

src/google/protobuf/message_lite.cc \

src/google/protobuf/repeated_field.cc \

src/google/protobuf/stubs/bytestream.cc \

src/google/protobuf/stubs/common.cc \

src/google/protobuf/stubs/int128.cc \

src/google/protobuf/stubs/once.cc \

src/google/protobuf/stubs/status.cc \

src/google/protobuf/stubs/statusor.cc \

src/google/protobuf/stubs/stringpiece.cc \

src/google/protobuf/stubs/stringprintf.cc \

src/google/protobuf/stubs/structurally_valid.cc \

src/google/protobuf/stubs/strutil.cc \

src/google/protobuf/stubs/time.cc \

src/google/protobuf/wire_format_lite.cc \

src/google/protobuf/any.cc \

src/google/protobuf/any.pb.cc \

src/google/protobuf/api.pb.cc \

src/google/protobuf/compiler/importer.cc \

src/google/protobuf/compiler/parser.cc \

src/google/protobuf/descriptor.cc \

src/google/protobuf/descriptor.pb.cc \

src/google/protobuf/descriptor_database.cc \

src/google/protobuf/duration.pb.cc \

src/google/protobuf/dynamic_message.cc \

src/google/protobuf/empty.pb.cc \

src/google/protobuf/extension_set_heavy.cc \

src/google/protobuf/field_mask.pb.cc \

src/google/protobuf/generated_message_reflection.cc \

src/google/protobuf/io/gzip_stream.cc \

src/google/protobuf/io/printer.cc \

src/google/protobuf/io/strtod.cc \

src/google/protobuf/io/tokenizer.cc \

src/google/protobuf/io/zero_copy_stream_impl.cc \

src/google/protobuf/map_field.cc \

src/google/protobuf/message.cc \

src/google/protobuf/reflection_ops.cc \

src/google/protobuf/service.cc \

src/google/protobuf/source_context.pb.cc \

src/google/protobuf/struct.pb.cc \

src/google/protobuf/stubs/mathlimits.cc \

src/google/protobuf/stubs/substitute.cc \

src/google/protobuf/text_format.cc \

src/google/protobuf/timestamp.pb.cc \

src/google/protobuf/type.pb.cc \

src/google/protobuf/unknown_field_set.cc \

src/google/protobuf/util/field_comparator.cc \

src/google/protobuf/util/field_mask_util.cc \

src/google/protobuf/util/internal/datapiece.cc \

src/google/protobuf/util/internal/default_value_objectwriter.cc \

src/google/protobuf/util/internal/error_listener.cc \

src/google/protobuf/util/internal/field_mask_utility.cc \

src/google/protobuf/util/internal/json_escaping.cc \

src/google/protobuf/util/internal/json_objectwriter.cc \

src/google/protobuf/util/internal/json_stream_parser.cc \

src/google/protobuf/util/internal/object_writer.cc \

src/google/protobuf/util/internal/proto_writer.cc \

src/google/protobuf/util/internal/protostream_objectsource.cc \

src/google/protobuf/util/internal/protostream_objectwriter.cc \

src/google/protobuf/util/internal/type_info.cc \

src/google/protobuf/util/internal/type_info_test_helper.cc \

src/google/protobuf/util/internal/utility.cc \

src/google/protobuf/util/json_util.cc \

src/google/protobuf/util/message_differencer.cc \

src/google/protobuf/util/time_util.cc \

src/google/protobuf/util/type_resolver_util.cc \

src/google/protobuf/wire_format.cc \

src/google/protobuf/wrappers.pb.cc



LOCAL_CPPFLAGS := -std=c++11

LOCAL_LDLIBS := -llog



ifeq ($(TARGET_ARCH),x86)

LOCAL_SRC_FILES := $(LOCAL_SRC_FILES) \

src/google/protobuf/stubs/atomicops_internals_x86_gcc.cc

endif



ifeq ($(TARGET_ARCH),x86_64)

LOCAL_SRC_FILES := $(LOCAL_SRC_FILES) \

src/google/protobuf/stubs/atomicops_internals_x86_gcc.cc

endif



LOCAL_C_INCLUDES = $(LOCAL_PATH)/src



include $(BUILD_SHARED_LIBRARY)









成功した場合、バイポッド/ android / proto / libs / armeabi-v7a-libprotobuf.soを取得します。プロジェクト/ Spiky / Spiky_Client / Source / Spiky_Client / armv7にコピーします。



起こりうる困難とエラー
:



ThirdParty/Protobuf/Includes\google/protobuf/arena.h(635,25) : error: cannot use typeid with -fno-rtti







arena.h



#define GOOGLE_PROTOBUF_NO_RTTI







, — error: "error C3861: 'check': identifier not found



, check (AssertionMacros.h), check (type_traits.h), check , check check_UnrealFix, , #undef check. unreal answers — Error C3861 (identifier not found) when including protocol buffers .



 template<typename B, typename D> struct is_base_of { typedef char (&yes)[1]; typedef char (&no)[2]; // BEGIN GOOGLE LOCAL MODIFICATION -- check is a #define on Mac. #undef check // END GOOGLE LOCAL MODIFICATION static yes check(const B*); static no check(const void*); enum { value = sizeof(check(static_cast<const D*>(NULL))) == sizeof(yes), }; };
      
      





type_traits.h :



 template<typename B, typename D> struct is_base_of { typedef char (&yes)[1]; typedef char (&no)[2]; // BEGIN GOOGLE LOCAL MODIFICATION -- check is a #define on Mac. //#undef check // END GOOGLE LOCAL MODIFICATION static yes check_UnrealFix(const B*); static no check_UnrealFix(const void*); enum { value = sizeof(check_UnrealFix(static_cast<const D*>(NULL))) == sizeof(yes), }; };
      
      







, OpenSSL . Android NDK ++ 11, chrono , , .



プロジェクトに追加する前に、Unrealの外部で個別に外部ライブラリの機能をテストすることをお勧めします。Unrealの方がはるかに高速です。



protobuf接続を据え置きながら、OpenSSLをコンパイルして、このトピックに戻らないようにし、繰り返さないようにします。OpenSSL-1.0.2kを使用しています。ライブラリを構築するには、このガイドを使用します(デバッグシンボルを使用した64ビットの静的ライブラリの構築)。問題がある場合のヒント:



  1. studio ml64.exeを含むフォルダーを検索し、OpenSSLを含むフォルダーにコピーします。NASMは使用しないでください-これはx32専用です
  2. クリーンソースを使用します(ビルドは試行しません)
  3. openssl fatal error LNK1112: module machine type 'x64' conflicts with target machine type 'X86'



    -VS2015の開発者コマンドプロンプトを開き、E:\ Program Files(x86)\ Microsoft Visual Studio 14.0 \ VCに移動して、vcvarsall.bat x64(sourceを実行します
  4. Unrealとの名前の競合は172行をコメントアウトします。 openssl/ossl_typ.h(172): error C2365: 'UI': redefinition; previous definition was 'namespace'







androidのコンパイルに関しては、これを行う最も簡単な方法は、プロジェクトソースにあるarmv7およびx86のスクリプトを使用して、Ubuntuの下から行うことです。



OpenSSL Android Android

プロジェクトに共有ライブラリ(.so)を追加する方法



起こりうる問題の解決策
, , :



E/AndroidRuntime( 1574): java.lang.UnsatisfiedLinkError: dlopen failed: could not load library "libcrypto.so.1.0.0" needed by "libUE4.so"; caused by library "libcrypto.so.1.0.0" not found





, Ubuntu :

rpl -R -e .so.1.0.0 "_1_0_0.so" /path/to/libcrypto.so







バイポッドをSource / Spiky_Client / armv7、ライブラリ、ThirdParty / OpenSSLのヘッダーにコピーしてコンパイルします。



Spiky_Client.Build.csのライブラリを接続します。便宜上、ModulePathとThirdPartyPathの2つの関数を追加します。最初の関数はプロジェクトへのパスを返し、2番目の関数はライブラリが接続されたフォルダーに戻ります。



 public class Spiky_Client : ModuleRules { private string ModulePath { get { return ModuleDirectory; } } private string ThirdPartyPath { get { return Path.GetFullPath(Path.Combine(ModulePath, "../../ThirdParty/")); } } ... }
      
      





プラットフォームごとに、ライブラリとヘッダーを追加します。コンパイル時に、プラットフォームに必要なライブラリが選択されます。



Spiky_Client.Build.cs
 // Copyright (c) 2017, Vadim Petrov - MIT License using UnrealBuildTool; using System.IO; using System; public class Spiky_Client : ModuleRules { private string ModulePath { get { return ModuleDirectory; } } private string ThirdPartyPath { get { return Path.GetFullPath(Path.Combine(ModulePath, "../../ThirdParty/")); } } public Spiky_Client(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.Default; PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "Networking", "Sockets" }); PrivateDependencyModuleNames.AddRange(new string[] { "UMG", "Slate", "SlateCore" }); string IncludesPath = Path.Combine(ThirdPartyPath, "Protobuf", "Includes"); PublicIncludePaths.Add(IncludesPath); IncludesPath = Path.Combine(ThirdPartyPath, "OpenSSL", "Includes"); PublicIncludePaths.Add(IncludesPath); if ((Target.Platform == UnrealTargetPlatform.Win64)) { string LibrariesPath = Path.Combine(ThirdPartyPath, "Protobuf", "Libs"); PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath, "libprotobuf.lib")); LibrariesPath = Path.Combine(ThirdPartyPath, "OpenSSL", "Libs"); PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath, "libeay32.lib")); } if (Target.Platform == UnrealTargetPlatform.Android) { string BuildPath = Utils.MakePathRelativeTo(ModuleDirectory, BuildConfiguration.RelativeEnginePath); AdditionalPropertiesForReceipt.Add(new ReceiptProperty("AndroidPlugin", Path.Combine(BuildPath, "APL.xml"))); PublicAdditionalLibraries.Add(BuildPath + "/armv7/libprotobuf.so"); PublicAdditionalLibraries.Add(BuildPath + "/armv7/libcrypto_1_0_0.so"); } } }
      
      







アセンブリにバイポッドを追加するには、ソースフォルダーにAPL.xml(AndroidPluginLanguage)ファイルを作成する必要があります。このファイルには、ライブラリをコピーする場所と場所、およびarmv7、x86のプラットフォームが記述されています。ここで例と他のパラメータを見つけることができます



APL
 <?xml version="1.0" encoding="utf-8"?> <root xmlns:android="http://schemas.android.com/apk/res/android"> <resourceCopies> <isArch arch="armeabi-v7a"> <copyFile src="$S(PluginDir)/armv7/libcrypto_1_0_0.so" dst="$S(BuildDir)/libs/armeabi-v7a/libcrypto_1_0_0.so" /> <copyFile src="$S(PluginDir)/armv7/libprotobuf.so" dst="$S(BuildDir)/libs/armeabi-v7a/libprotobuf.so" /> </isArch> </resourceCopies> </root>
      
      







テストhudを作成し、その中に(ソースではなく)ハッシュを表示することにより、WindowsおよびAndroid向けのOpenSSLの動作をテストできます。
 // OpenSSL tests #include <openssl/evp.h> #include <sstream> #include <iomanip> void ADebugHUD::DrawHUD() { Super::DrawHUD(); FString hashTest = "Hash test (sha256): " + GetSHA256_s("test", strlen("test")); DrawText(hashTest, FColor::White, 50, 50, HUDFont); } FString ADebugHUD::GetSHA256_s(const void * data, size_t data_len) { EVP_MD_CTX mdctx; unsigned char md_value[EVP_MAX_MD_SIZE]; unsigned int md_len; EVP_DigestInit(&mdctx, EVP_sha256()); EVP_DigestUpdate(&mdctx, data, (size_t)data_len); EVP_DigestFinal_ex(&mdctx, md_value, &md_len); EVP_MD_CTX_cleanup(&mdctx); std::stringstream s; s.fill('0'); for (size_t i = 0; i < md_len; ++i) s << std::setw(2) << std::hex << (unsigned short)md_value[i]; return s.str().c_str(); }
      
      







コンパイルされた.protoメッセージを追加すると、anrialはさまざまな警告を生成します。これは、エンジンのソースを理解するか、それらを抑制することで無効にできます。これを行うには、DisableWarnings.protoを作成してコンパイル./protoc --cpp_out=. --java_out=. DisableWarnings.proto



し、結果のDisableWarnings.pb.hヘッダーで警告を抑制します。各protoファイルにDisableWarningsを含めます。DisableWarnings.protoには、protobuffバージョン、javaパッケージの名前、生成されたクラスの名前の3行しかありません。



#define PROTOBUF_INLINE_NOT_IN_HEADERS 0

#pragma warning(disable:4100)

#pragma warning(disable:4127)

#pragma warning(disable:4125)

#pragma warning(disable:4267)

#pragma warning(disable:4389)







DisableWarnings.proto
syntax = "proto3";



option java_package = "com.spiky.server.protomodels";

option java_outer_classname = "DisableWarnings";









DisableWarnings.pb.h
 // Generated by the protocol buffer compiler. DO NOT EDIT! // source: DisableWarnings.proto #define PROTOBUF_INLINE_NOT_IN_HEADERS 0 #pragma warning(disable:4100) #pragma warning(disable:4127) #pragma warning(disable:4125) #pragma warning(disable:4267) #pragma warning(disable:4389) #ifndef PROTOBUF_DisableWarnings_2eproto__INCLUDED #define PROTOBUF_DisableWarnings_2eproto__INCLUDED #include <string> #include <google/protobuf/stubs/common.h> #if GOOGLE_PROTOBUF_VERSION < 3000000 #error This file was generated by a newer version of protoc which is #error incompatible with your Protocol Buffer headers. Please update #error your headers. #endif #if 3000000 < GOOGLE_PROTOBUF_MIN_PROTOC_VERSION #error This file was generated by an older version of protoc which is #error incompatible with your Protocol Buffer headers. Please #error regenerate this file with a newer version of protoc. #endif #include <google/protobuf/arena.h> #include <google/protobuf/arenastring.h> #include <google/protobuf/generated_message_util.h> #include <google/protobuf/metadata.h> #include <google/protobuf/repeated_field.h> #include <google/protobuf/extension_set.h> // @@protoc_insertion_point(includes) // Internal implementation detail -- do not call these. void protobuf_AddDesc_DisableWarnings_2eproto(); void protobuf_AssignDesc_DisableWarnings_2eproto(); void protobuf_ShutdownFile_DisableWarnings_2eproto(); // =================================================================== // =================================================================== // =================================================================== #if !PROTOBUF_INLINE_NOT_IN_HEADERS #endif // !PROTOBUF_INLINE_NOT_IN_HEADERS // @@protoc_insertion_point(namespace_scope) // @@protoc_insertion_point(global_scope) #endif // PROTOBUF_DisableWarnings_2eproto__INCLUDED
      
      







すべてのプロトバフをProtobufsフォルダー(Source / Spiky_Client / Protobufs)に配置しますが、-cpp_out =でフルパスを指定して、生成されたファイルの自動配置を設定する方が適切です。 --java_out =。



さらに進んで、Spikyサーバーを構成します!



com.spiky.serverパッケージを作成し、サーバーのエントリポイントであるServerMainクラスを追加します。ここでは、グローバル変数を保存し、tcpおよびudp接続用に2つのNettyサーバーを初期化して起動します(ただし、tcpのみがプロジェクトで使用されることに注意してください)。サーバーポート(ロジックサーバー-NettyおよびUnrealのテストサーバー)を格納できる構成ファイルと、暗号化をオフにする機能が必ず必要です。 Recourcesフォルダーで、configuration.propertiesを作成します。



サーバーの初期化をServerMainに追加し、設定ファイルを読み取ります。



 /*   */ private static final ResourceBundle configurationBundle = ResourceBundle.getBundle("configuration", Locale.ENGLISH); /*   */ private static final int tcpPort = Integer.valueOf(configurationBundle.getString("tcpPort")); private static final int udpPort = Integer.valueOf(configurationBundle.getString("udpPort")); private static void run_tcp() { EventLoopGroup bossGroup = new NioEventLoopGroup(); // 1 EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); // 2 b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) // 3 .childHandler(new com.spiky.server.tcp.ServerInitializer()) // 4 .childOption(ChannelOption.SO_KEEPALIVE, true) .childOption(ChannelOption.TCP_NODELAY, true); ChannelFuture f = b.bind(tcpPort).sync(); // 5 f.channel().closeFuture().sync(); // 6 } catch (InterruptedException e) { e.printStackTrace(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } }
      
      





, udp main()
 /* * Copyright (c) 2017, Vadim Petrov - MIT License */ package com.spiky.server; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import java.util.Locale; import java.util.ResourceBundle; public class ServerMain { /*   */ private static final ResourceBundle configurationBundle = ResourceBundle.getBundle("configuration", Locale.ENGLISH); /*   */ private static final int tcpPort = Integer.valueOf(configurationBundle.getString("tcpPort")); private static final int udpPort = Integer.valueOf(configurationBundle.getString("udpPort")); private static void run_tcp() { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new com.spiky.server.tcp.ServerInitializer()) .childOption(ChannelOption.SO_KEEPALIVE, true) .childOption(ChannelOption.TCP_NODELAY, true); ChannelFuture f = b.bind(tcpPort).sync(); f.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } private static void run_udp() { final NioEventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group).channel(NioDatagramChannel.class) .handler(new com.spiky.server.udp.ServerInitializer()); bootstrap.bind(udpPort).sync(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { new Thread(ServerMain::run_tcp).start(); new Thread(ServerMain::run_udp).start(); } }
      
      







  1. NioEventLoopGroup — , -. Netty EventLoopGroup . , NioEventLoopGroup. , «», . , «», , . , EventLoopGroup
  2. ServerBootstrap — , . . , ,
  3. NioServerSocketChannel,
  4. Handler, EventLoop. , , ,
  5. ,


エコーサーバーの簡単な例と説明付きのNettyの仕組みについては、ドキュメントを参照してくださいまた、Netty in Actionブックを読むことを強くお勧めします。



サーバーを起動する準備がほぼ整いました。両方のプロトコルにServerInitializerを追加します。



 /*  UDP  TCP*/ public class ServerInitializer extends ChannelInitializer<NioDatagramChannel> public class ServerInitializer extends ChannelInitializer<SocketChannel>
      
      





2つのパッケージを作成com.spiky.server.tcp



com.spiky.server.udp



、それぞれで、次の内容を持つServerInitializerクラス(優れたNioDatagramChannel / SocketChannelを含む)を作成します。



 package com.spiky.server.tcp; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; public class ServerInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); } }
      
      





パイプラインは、すべてのメッセージが通過するものであり、着信および発信メッセージを処理するChannelHandlersのリストが含まれています。たとえば、ハンドラーの1つは文字列データのみを受け入れ、もう1つのプロトバフは、write(string)を呼び出すと、行のハンドラーが呼び出され、そこでメッセージをさらに処理するか、新しいタイプに対応する他のハンドラーに送信するか、クライアントに送信するかを決定します。各ハンドラには、メッセージが着信か発信かを決定するタイプがあります。



ServerInitializerに標準のデバッグプロセッサを追加します。これは非常に便利で、受信メッセージのサイズと表示形式、受信者を確認できます。



 ... ChannelPipeline pipeline = ch.pipeline(); /*  */ pipeline.addLast(new LoggingHandler(LogLevel.INFO)); ...
      
      





TCPを介して送信されるprotobuffメッセージの処理はUDPを介して送信されるメッセージとは異なり、Nettyはprotobuffのハンドラーを用意していますが、TCPなどのストリーミング接続でのみ機能します。長さ、次に本体自体。 UDPから始めて、サーバーとクライアントによるメッセージの受信と送信を追加してテストします。デバッグハンドラーをServerInitializerに追加してから、パッケージcom.spiky.server.udp.handlersを作成します。パブリッククラスProtoDecoderHandlerを追加してSimpleChannelInboundHandlerを拡張します。 ChannelInboundHandlerAdapterを使用すると、特定の種類の着信メッセージのみを明示的に処理できます。たとえば、ProtoDecoderHandlerは、DatagramPacketタイプのメッセージのみを処理します。



ここにPackageHandlerを追加します-デコード(そしてデコードと復号化が必要です)後のprotobuff形式のメッセージがここに来ますpublic class PackageHandler extends SimpleChannelInboundHandler <MessageModels.Wrapper>



MessageModelsは暗号化を含むトップレベルのラッパークラスです暗号化されていないデータ。すべてのメッセージはその中にラップされています。ここにその最終形式があります。一部のタイプはまだ馴染みがありません:



 message Wrapper { Utility utility = 1; InputChecking inputChecking = 2; Registration registration = 3; Login login = 4; CryptogramWrapper cryptogramWrapper = 5; }
      
      





メッセージを送信すると、受信側はラッパーを読み取り、ラッパーが持っているフィールドを確認します。ログイン、登録?または、cryptogramWrapperの暗号化されたバイトですか?したがって、実行のスレッドを選択します。



気が散らないように、プロジェクトのすべてのプロトバフモデルを定義して説明しましょう。



DisableWarningsは、警告をオフにするだけのタスクを持つ空のプロトバフです。



DisableWarnings.proto
syntax = "proto3";



option java_package = "com.spiky.server.protomodels";

option java_outer_classname = "DisableWarnings";









MessageModels-メインのWrapperラッパーが含まれます。その中には、暗号化されていないメッセージUtility、InputChecking、Registration、Login、および暗号化されたCryptogramWrapperがあります。CryptogramWrapperには暗号化されたバイトが含まれます。たとえば、キーを交換してデータの暗号化を開始すると、このデータはCryptogramWrapperフィールドの1つとして割り当てられます。受信者は、受信し、暗号化されたデータがあるかどうかを確認し、復号化して、フィールド名でタイプを判別し、さらに処理するために送信しました。



MessageModels.proto
 syntax = "proto3"; option java_package = "com.spiky.server.protomodels"; option java_outer_classname = "MessageModels"; import "UtilityModels.proto"; import "RegLogModels.proto"; import "DisableWarnings.proto"; message CryptogramWrapper { bytes registration = 1; bytes login = 2; bytes initialState = 3; bytes room = 4; bytes mainMenu = 5; bytes gameModels = 6; } message Wrapper { Utility utility = 1; InputChecking inputChecking = 2; Registration registration = 3; Login login = 4; CryptogramWrapper cryptogramWrapper = 5; }
      
      







UtilityModelsは、アライブメッセージを送信するこのモデルの唯一のタスクです。



UtilityModels.proto
 syntax = "proto3"; option java_package = "com.spiky.server.protomodels"; option java_outer_classname = "UtilityModels"; import "DisableWarnings.proto"; message Utility { bool alive = 1; }
      
      







RegLogModels-登録とログイン、およびユーザー入力のチェックとサーバーからのキャプチャの受信に必要なモデルが含まれています。



RegLogModels.proto
 syntax = "proto3"; option java_package = "com.spiky.server.protomodels"; option java_outer_classname = "RegistrationLoginModels"; import "DisableWarnings.proto"; import "GameRoomModels.proto"; message InputChecking { string login = 1; string mail = 2; string captcha = 3; bool getCaptcha = 4; bytes captchaData = 5; oneof v1 { bool loginCheckStatus = 6; bool mailCheckStatus = 7; bool captchaCheckStatus = 8; } } message Login { string mail = 1; string hash = 2; string publicKey = 3; oneof v1 { int32 stateCode = 4; } } message Registration { string login = 1; string hash = 2; string mail = 3; string captcha = 4; string publicKey = 5; oneof v1 { int32 stateCode = 6; } } message InitialState { string sessionId = 1; string login = 2; repeated CreateRoom createRoom = 3; }
      
      







MainMenuModels-メインメニューに必要なデータは、チャットのみです。



MainMenuModels.proto
 syntax = "proto3"; option java_package = "com.spiky.server.protomodels"; option java_outer_classname = "MainMenuModels"; import "DisableWarnings.proto"; message ChatMessage { int64 time = 1; string name = 2; string text = 3; } message Chat { int64 time = 1; string name = 2; string text = 3; oneof v1 { bool subscribe = 4; } repeated ChatMessage messages = 5; } message MainMenu { Chat chat = 1; }
      
      







GameRoomModels-ゲームルームの作成と更新に必要なものすべて。



GameRoomModels.proto
 syntax = "proto3"; option java_package = "com.spiky.server.protomodels"; option java_outer_classname = "GameRoomModels"; import "DisableWarnings.proto"; import "MainMenuModels.proto"; message Room { CreateRoom createRoom = 1; RoomsListUpdate roomsListUpdate = 2; SubscribeRoom subscribeRoom = 3; RoomUpdate roomUpdate = 4; bool startGame = 5; string roomName = 6; } message CreateRoom { string roomName = 1; string mapName = 2; string gameTime = 3; string maxPlayers = 4; string creator = 5; } message RoomsListUpdate { bool deleteRoom = 1; bool addRoom = 2; string roomName = 3; string roomOwner = 4; } message SubscribeRoom { oneof v1 { bool subscribe = 1; } string roomName = 2; int32 stateCode = 3; RoomDescribe roomDescribe = 4; string player = 5; string team = 6; } message RoomDescribe { repeated TeamPlayer team1 = 1; repeated TeamPlayer team2 = 2; repeated TeamPlayer undistributed = 3; string roomName = 4; string mapName = 5; string gameTime = 6; string maxPlayers = 7; string creator = 8; Chat chat = 9; } message TeamPlayer { string player_name = 1; } message RoomUpdate { RoomDescribe roomDescribe = 1; string targetTeam = 2; string roomName = 3; }
      
      







GameModels-ゲームのモデル、プレーヤーの位置、ショットパラメーター、初期状態、ping。



GameModels.proto
 syntax = "proto3"; option java_package = "com.spiky.server.protomodels"; option java_outer_classname = "GameModels"; import "DisableWarnings.proto"; message GameInitialState { bool startGame = 1; repeated Player player = 2; } message Player { string player_name = 1; string team = 2; int32 health = 3; PlayerPosition playerPosition = 4; } message PlayerPosition { Location loc = 1; Rotation rot = 2; message Location { int32 X = 1; int32 Y = 2; int32 Z = 3; } message Rotation { int32 Pitch = 1; int32 Roll = 2; int32 Yaw = 3; } string playerName = 3; int64 timeStamp = 4; } message Ping { int64 time = 1; } message Shot { Start start = 1; End end = 2; PlayerPosition playerPosition = 3; message Start { int32 X = 1; int32 Y = 2; int32 Z = 3; } message End { int32 X = 1; int32 Y = 2; int32 Z = 3; } int64 timeStamp = 4; string requestFrom = 5; string requestTo = 6; string roomOwner = 7; oneof v1 { bool result_hitState = 8; } string result_bonename = 9; } message GameData { GameInitialState gameInitialState = 1; PlayerPosition playerPosition = 2; Ping ping = 3; Shot shot = 4; }
      
      







すべてのモデルはSpiky / Spiky_Protospaceにあります。



メッセージのタイプとその処理方法を決定するために、名前付きフィールドが含まれていることを学びます:そして、コードを乱雑にしないために、一連の記述子で個別のクラスを作成し、クライアントとサーバーでUtilsにDescriptorsクラスを追加します。



// java

if(wrapper.getCryptogramWrapper().hasField(registration_cw)) // -

// cpp

if (wrapper->cryptogramwrapper().GetReflection()->HasField(wrapper->cryptogramwrapper(), Descriptors::registration_cw)) // -











Descriptors.java
 // Copyright (c) 2017, Vadim Petrov - MIT License package com.spiky.server.utils; import com.spiky.server.protomodels.*; /** *         * */ public class Descriptors { public static com.google.protobuf.Descriptors.FieldDescriptor registration_cw = MessageModels.CryptogramWrapper.getDefaultInstance().getDescriptorForType().findFieldByName("registration"); public static com.google.protobuf.Descriptors.FieldDescriptor login_cw = MessageModels.CryptogramWrapper.getDefaultInstance().getDescriptorForType().findFieldByName("login"); public static com.google.protobuf.Descriptors.FieldDescriptor initialState_cw = MessageModels.CryptogramWrapper.getDefaultInstance().getDescriptorForType().findFieldByName("initialState"); public static com.google.protobuf.Descriptors.FieldDescriptor room_cw = MessageModels.CryptogramWrapper.getDefaultInstance().getDescriptorForType().findFieldByName("room"); public static com.google.protobuf.Descriptors.FieldDescriptor mainMenu_cw = MessageModels.CryptogramWrapper.getDefaultInstance().getDescriptorForType().findFieldByName("mainMenu"); public static com.google.protobuf.Descriptors.FieldDescriptor gameModels_cw = MessageModels.CryptogramWrapper.getDefaultInstance().getDescriptorForType().findFieldByName("gameModels"); public static com.google.protobuf.Descriptors.FieldDescriptor getCaptcha_ich = RegistrationLoginModels.InputChecking.getDefaultInstance().getDescriptorForType().findFieldByName("getCaptcha"); public static com.google.protobuf.Descriptors.FieldDescriptor login_ich = RegistrationLoginModels.InputChecking.getDefaultInstance().getDescriptorForType().findFieldByName("login"); public static com.google.protobuf.Descriptors.FieldDescriptor mail_ich = RegistrationLoginModels.InputChecking.getDefaultInstance().getDescriptorForType().findFieldByName("mail"); public static com.google.protobuf.Descriptors.FieldDescriptor captcha_ich = RegistrationLoginModels.InputChecking.getDefaultInstance().getDescriptorForType().findFieldByName("captcha"); public static com.google.protobuf.Descriptors.FieldDescriptor login_reg = RegistrationLoginModels.Registration.getDefaultInstance().getDescriptorForType().findFieldByName("login"); public static com.google.protobuf.Descriptors.FieldDescriptor mail_reg = RegistrationLoginModels.Registration.getDefaultInstance().getDescriptorForType().findFieldByName("mail"); public static com.google.protobuf.Descriptors.FieldDescriptor captcha_reg = RegistrationLoginModels.Registration.getDefaultInstance().getDescriptorForType().findFieldByName("captcha"); public static com.google.protobuf.Descriptors.FieldDescriptor publicKey_reg = RegistrationLoginModels.Registration.getDefaultInstance().getDescriptorForType().findFieldByName("publicKey"); public static com.google.protobuf.Descriptors.FieldDescriptor publicKey_log = RegistrationLoginModels.Login.getDefaultInstance().getDescriptorForType().findFieldByName("publicKey"); public static com.google.protobuf.Descriptors.FieldDescriptor subscribe_chat = MainMenuModels.Chat.getDefaultInstance().getDescriptorForType().findFieldByName("subscribe"); public static com.google.protobuf.Descriptors.FieldDescriptor chat_mm = MainMenuModels.MainMenu.getDefaultInstance().getDescriptorForType().findFieldByName("chat"); public static com.google.protobuf.Descriptors.FieldDescriptor deleteRoom_room = GameRoomModels.RoomsListUpdate.getDefaultInstance().getDescriptorForType().findFieldByName("deleteRoom"); public static com.google.protobuf.Descriptors.FieldDescriptor startGame_room = GameRoomModels.Room.getDefaultInstance().getDescriptorForType().findFieldByName("startGame"); public static com.google.protobuf.Descriptors.FieldDescriptor requestTo_shot_gd = GameModels.Shot.getDefaultInstance().getDescriptorForType().findFieldByName("requestTo"); }
      
      







Descriptors.h /Descriptors.pp
 // Copyright (c) 2017, Vadim Petrov - MIT License #pragma once #include <google/protobuf/descriptor.h> class Descriptors { public: static const google::protobuf::FieldDescriptor* captchaDataField_ich; static const google::protobuf::FieldDescriptor* loginCheckStatus_ich; static const google::protobuf::FieldDescriptor* mailCheckStatus_ich; static const google::protobuf::FieldDescriptor* captchaCheckStatus_ich; static const google::protobuf::FieldDescriptor* publicKey_reg; static const google::protobuf::FieldDescriptor* stateCode_reg; static const google::protobuf::FieldDescriptor* publicKey_log; static const google::protobuf::FieldDescriptor* stateCode_log; static const google::protobuf::FieldDescriptor* registration_cw; static const google::protobuf::FieldDescriptor* login_cw; static const google::protobuf::FieldDescriptor* initialState_cw; static const google::protobuf::FieldDescriptor* room_cw; static const google::protobuf::FieldDescriptor* mainMenu_cw; static const google::protobuf::FieldDescriptor* gameModels_cw; static const google::protobuf::FieldDescriptor* chat_mm; static const google::protobuf::FieldDescriptor* nameField_chat; static const google::protobuf::FieldDescriptor* player_sub; static const google::protobuf::FieldDescriptor* player_team; static const google::protobuf::FieldDescriptor* chat_room; }; // Copyright (c) 2017, Vadim Petrov - MIT License #include "Spiky_Client.h" #include "Descriptors.h" #include "Protobufs/RegLogModels.pb.h" #include "Protobufs/MessageModels.pb.h" #include "Protobufs/MainMenuModels.pb.h" const google::protobuf::FieldDescriptor* Descriptors::captchaDataField_ich = InputChecking::default_instance().descriptor()->FindFieldByName("captchaData"); const google::protobuf::FieldDescriptor* Descriptors::loginCheckStatus_ich = InputChecking::default_instance().descriptor()->FindFieldByName("loginCheckStatus"); const google::protobuf::FieldDescriptor* Descriptors::mailCheckStatus_ich = InputChecking::default_instance().descriptor()->FindFieldByName("mailCheckStatus"); const google::protobuf::FieldDescriptor* Descriptors::captchaCheckStatus_ich = InputChecking::default_instance().descriptor()->FindFieldByName("captchaCheckStatus"); const google::protobuf::FieldDescriptor* Descriptors::publicKey_reg = Registration::default_instance().descriptor()->FindFieldByName("publicKey"); const google::protobuf::FieldDescriptor* Descriptors::stateCode_reg = Registration::default_instance().descriptor()->FindFieldByName("stateCode"); const google::protobuf::FieldDescriptor* Descriptors::publicKey_log = Login::default_instance().descriptor()->FindFieldByName("publicKey"); const google::protobuf::FieldDescriptor* Descriptors::stateCode_log = Login::default_instance().descriptor()->FindFieldByName("stateCode"); const google::protobuf::FieldDescriptor* Descriptors::registration_cw = CryptogramWrapper::default_instance().descriptor()->FindFieldByName("registration"); const google::protobuf::FieldDescriptor* Descriptors::login_cw = CryptogramWrapper::default_instance().descriptor()->FindFieldByName("login"); const google::protobuf::FieldDescriptor* Descriptors::initialState_cw = CryptogramWrapper::default_instance().descriptor()->FindFieldByName("initialState"); const google::protobuf::FieldDescriptor* Descriptors::room_cw = CryptogramWrapper::default_instance().descriptor()->FindFieldByName("room"); const google::protobuf::FieldDescriptor* Descriptors::mainMenu_cw = CryptogramWrapper::default_instance().descriptor()->FindFieldByName("mainMenu"); const google::protobuf::FieldDescriptor* Descriptors::gameModels_cw = CryptogramWrapper::default_instance().descriptor()->FindFieldByName("gameModels"); const google::protobuf::FieldDescriptor* Descriptors::chat_mm = MainMenu::default_instance().descriptor()->FindFieldByName("chat"); const google::protobuf::FieldDescriptor* Descriptors::nameField_chat = Chat::default_instance().descriptor()->FindFieldByName("name"); const google::protobuf::FieldDescriptor* Descriptors::player_sub = SubscribeRoom::default_instance().descriptor()->FindFieldByName("player"); const google::protobuf::FieldDescriptor* Descriptors::player_team = SubscribeRoom::default_instance().descriptor()->FindFieldByName("team"); const google::protobuf::FieldDescriptor* Descriptors::chat_room = RoomDescribe::default_instance().descriptor()->FindFieldByName("chat");
      
      







クライアントからaliveフィールドのみを含むUtilityサーバーに常にtrueを送信する必要があります。boolタイプでは最小メッセージサイズを使用できます。falseフィールドでメッセージを送信するには、v1 {bool alive = 1;のいずれかでラップする必要があります。}値がfalseまたは0のフィールドが存在しないと見なされる場合、メッセージを受信し、それが生きているかどうかを知りたい場合、それを見つけることはできませんfalseまたは単にフィールドがありません(フィールドがない場合、これはいくつかのアクションのシグナルです。たとえば)。また、常にDisableWarningsをインポートして警告を無効にします。各protobuffメッセージには独自のクラスがあるため、変更を加えてすべてを再コンパイルする必要はありません。次のコマンドでクラスを生成します。



./protoc --cpp_out=c:/Spiky/Spiky_Client/Source/Spiky_Client/Protobufs --java_out=c:/Spiky/Spiky_Server/src/main/java *.proto







DisableWarningsヘッダーが更新されました。エラー抑制を再度追加してください。(add.txtファイルから)。



add.txt
#define PROTOBUF_INLINE_NOT_IN_HEADERS 0



#pragma warning(disable:4100)

#pragma warning(disable:4127)

#pragma warning(disable:4125)

#pragma warning(disable:4267)

#pragma warning(disable:4389)



スタジオ内のプロジェクトを更新して新しいファイルを表示するには、.uprojectを右クリックし、「Visual Studioプロジェクトファイルを生成する」を選択します。PackageHandlerクラスは大丈夫です。SimpleChannelInboundHandler<MessageModels.Wrapper>が見つかり、必要に応じてchannelRead0を再定義します。これは、すべての着信メッセージを処理するメソッドです。



 @Override protected void channelRead0(ChannelHandlerContext ctx, MessageModels.Wrapper wrapper) throws Exception { }
      
      





ハンドラーをパイプラインServerInitializerに追加します。



 public class ServerInitializer extends ChannelInitializer<NioDatagramChannel> { @Override protected void initChannel(NioDatagramChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); /*  */ pipeline.addLast(new LoggingHandler(LogLevel.INFO)); /* proto  */ pipeline.addLast(new ProtoDecoderHandler()); /*   */ pipeline.addLast(new PackageHandler()); }
      
      





ProtoDecoderHandlerを開いてexceptionCaughtを追加します。これは、エラーが発生した場合に呼び出されるメソッドです。チャネルまたはデータベース接続とchannelReadCompleteを閉じて、書き込み後にストリームをクリアすると便利です。すぐにchannelRead0を更新し、パッケージを読み取り、それをparseDelimitedFromを使用してメッセージで収集するバイト配列に変換します。長さを読み取り、次にメッセージ自体を読み取ります。ハンドラーに沿ってさらに送信するのではなく、エコーを使用してメッセージを送り返します。



プロトデコーダハンドラー
 /* * Copyright (c) 2017, Vadim Petrov - MIT License */ package com.spiky.server.udp.handlers; import com.spiky.server.protomodels.MessageModels; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.DatagramPacket; import java.io.ByteArrayInputStream; public class ProtoDecoderHandler extends SimpleChannelInboundHandler<DatagramPacket> { @Override protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket datagramPacket) throws Exception { ByteBuf buf = datagramPacket.content(); byte[] bytes = new byte[buf.readableBytes()]; int readerIndex = buf.readerIndex(); buf.getBytes(readerIndex, bytes); ByteArrayInputStream input = new ByteArrayInputStream(bytes); MessageModels.Wrapper wrapper = MessageModels.Wrapper.parseDelimitedFrom(input); System.out.println("udp: "); System.out.println(wrapper.toString()); System.out.println(datagramPacket.sender().getAddress() + " " + datagramPacket.sender().getPort()); //   () ctx.write(new DatagramPacket(Unpooled.copiedBuffer(datagramPacket.content()), datagramPacket.sender())); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
      
      







SocketObjectクラスで、クライアントにリスナーと送信者を実装します。新しい関数SendByUDPとReadDelimitedFromを追加する必要があります。残念ながら、javaとは異なり、c ++での実装はありません。



SocketObject.cpp
 #include "Spiky_Client.h" #include "SocketObject.h" #include "Protobufs/MessageModels.pb.h" #include <google/protobuf/message.h> #include <google/protobuf/io/zero_copy_stream_impl_lite.h> #include <google/protobuf/io/coded_stream.h> FSocket* USocketObject::tcp_socket = nullptr; TSharedPtr<FInternetAddr> USocketObject::tcp_address = nullptr; bool USocketObject::bIsConnection = false; FSocket* USocketObject::udp_socket = nullptr; TSharedPtr<FInternetAddr> USocketObject::udp_address = nullptr; FUdpSocketReceiver* USocketObject::UDPReceiver = nullptr; int32 USocketObject::tcp_local_port = 0; int32 USocketObject::udp_local_port = 0; USocketObject::~USocketObject() { if (tcp_socket != nullptr || udp_socket != nullptr) { tcp_socket->Close(); delete tcp_socket; delete udp_socket; } } void USocketObject::InitSocket(FString serverAddress, int32 tcp_local_p, int32 tcp_server_port, int32 udp_local_p, int32 udp_server_port) { int32 BufferSize = 2 * 1024 * 1024; tcp_local_port = tcp_local_p; udp_local_port = udp_local_p; // tcp tcp_socket = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateSocket(NAME_Stream, TEXT("TCP_SOCKET"), false); // create a proper FInternetAddr representation tcp_address = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); // parse server address FIPv4Address serverIP; FIPv4Address::Parse(serverAddress, serverIP); // and set tcp_address->SetIp(serverIP.Value); tcp_address->SetPort(tcp_server_port); tcp_socket->Connect(*tcp_address); // set the initial connection state bIsConnection = Alive(); // udp udp_address = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); FIPv4Address::Parse(serverAddress, serverIP); udp_address->SetIp(serverIP.Value); udp_address->SetPort(udp_server_port); udp_socket = FUdpSocketBuilder("UDP_SOCKET") .AsReusable() .BoundToPort(udp_local_port) .WithBroadcast() .WithReceiveBufferSize(BufferSize) .WithSendBufferSize(BufferSize) .Build(); } void USocketObject::RunUdpSocketReceiver() { FTimespan ThreadWaitTime = FTimespan::FromMilliseconds(30); UDPReceiver = new FUdpSocketReceiver(udp_socket, ThreadWaitTime, TEXT("UDP_RECEIVER")); UDPReceiver->OnDataReceived().BindStatic(&USocketObject::Recv); UDPReceiver->Start(); } void USocketObject::Recv(const FArrayReaderPtr& ArrayReaderPtr, const FIPv4Endpoint& EndPt) { GLog->Log("Reveived UDP data"); uint8_t * buffer = ArrayReaderPtr->GetData(); size_t size = ArrayReaderPtr->Num(); GLog->Log("Size of incoming data: " + FString::FromInt(size)); google::protobuf::io::ArrayInputStream arr(buffer, size); google::protobuf::io::CodedInputStream input(&arr); std::shared_ptr<Wrapper> wrapper(new Wrapper); ReadDelimitedFrom(&input, wrapper.get()); std::string msg; wrapper->SerializeToString(&msg); GLog->Log(msg.c_str()); } bool USocketObject::SendByUDP(google::protobuf::Message * message) { Wrapper wrapper; if (message->GetTypeName() == "Utility") { Utility * mes = static_cast<Utility*>(message); wrapper.set_allocated_utility(mes); } size_t size = wrapper.ByteSize() + 5; // include size, varint32 never takes more than 5 bytes uint8_t * buffer = new uint8_t[size]; google::protobuf::io::ArrayOutputStream arr(buffer, size); google::protobuf::io::CodedOutputStream output(&arr); output.WriteVarint32(wrapper.ByteSize()); wrapper.SerializeToCodedStream(&output); if (wrapper.has_utility()) { wrapper.release_utility(); } int32 bytesSent = 0; bool sentState = false; sentState = udp_socket->SendTo(buffer, output.ByteCount(), bytesSent, *udp_address); delete[] buffer; return sentState; } bool USocketObject::ReadDelimitedFrom(google::protobuf::io::CodedInputStream * input, google::protobuf::MessageLite * message) { // Read the size. uint32_t size; if (!input->ReadVarint32(&size)) return false; // Tell the stream not to read beyond that size. google::protobuf::io::CodedInputStream::Limit limit = input->PushLimit(size); // Parse the message. if (!message->MergeFromCodedStream(input)) return false; if (!input->ConsumedEntireMessage()) return false; // Release the limit. input->PopLimit(limit); return true; } void USocketObject::Reconnect() { } bool USocketObject::Alive() { return false; }
      
      







RunUdpSocketReceiver-新しいメッセージのチェック速度を設定し、受信Recvデータを委任します。Recv-サイズを読み取り、ReadDelimitedFromを使用してバイトを解析し、Wrapperラッパーを作成します。SendByUDP-UDPを介して送信し、さまざまな形式のメッセージを入力に送信し、内部の形式の種類を判別し、ラップし、シリアル化し、送信します。



SpikyGameModeを開き、Qを押してサーバーにメッセージを送信します。



 virtual void BeginPlay() override; virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; void TestSendUPDMessage();
      
      





BeginPlayで、次の設定によりユーザー入力に応答する機能を追加します。



 EnableInput(GetWorld()->GetFirstPlayerController()); InputComponent->BindAction("Q", IE_Pressed, this, &ASpikyGameMode::TestSendUPDMessage);
      
      





EndPlayで、ログメッセージを追加して、ゲームモードの終了を確認するか、別のモードに切り替えます。TestSendUPDMessage-Qキーを押したときに呼び出される関数。

 void ASpikyGameMode::TestSendUPDMessage() { GLog->Log("send ->>>"); std::shared_ptr<Utility> utility(new Utility); utility->set_alive(true); USocketObject::SendByUDP(utility.get()); }
      
      







SpikyGameMode.cpp
 // Copyright (c) 2017, Vadim Petrov - MIT License #include "Spiky_Client.h" #include "SpikyGameMode.h" #include "SocketObject.h" #include "Runtime/Engine/Classes/Engine/World.h" #include "Protobufs/UtilityModels.pb.h" void ASpikyGameMode::BeginPlay() { Super::BeginPlay(); GLog->Log("AClientGameMode::BeginPlay()"); EnableInput(GetWorld()->GetFirstPlayerController()); InputComponent->BindAction("Q", IE_Pressed, this, &ASpikyGameMode::TestSendUPDMessage); } void ASpikyGameMode::EndPlay(const EEndPlayReason::Type EndPlayReason) { Super::EndPlay(EndPlayReason); GLog->Log("AClientGameMode::EndPlay()"); } void ASpikyGameMode::TestSendUPDMessage() { GLog->Log("send ->>>"); std::shared_ptr<Utility> utility(new Utility); utility->set_alive(true); USocketObject::SendByUDP(utility.get()); }
      
      







SpikyGameInstanceを開き、ゲームの開始時にソケットを初期化し、ゲームの開始および終了時に呼び出される関数を追加します。



 virtual void Init() override; virtual void Shutdown() override;
      
      





サーバーにさまざまな静的設定を保存する別のConfigクラスが必要になります。親なしで作成し(Spiky_Client / Source / Spiky_Client / Public)、アドレス、ポートを配置し、暗号化がオンになっていることをフラグします(将来)。



Config.h / Config.cpp
 // .h // Copyright (c) 2017, Vadim Petrov - MIT License #pragma once #include <string> class Config { public: static std::string address; static size_t tcp_local_port; static size_t tcp_server_port; static size_t udp_local_port; static size_t udp_server_port; static bool bEnableCrypt; }; // .cpp // Copyright (c) 2017, Vadim Petrov - MIT License #include "Spiky_Client.h" #include "Config.h" bool Config::bEnableCrypt = true; std::string Config::address = "127.0.0.1"; size_t Config::tcp_local_port = 7678; size_t Config::tcp_server_port = 7680; size_t Config::udp_local_port = 7679; size_t Config::udp_server_port = 7681;
      
      







SpikyGameInstanceでソケットを初期化する:: Init()



 void USpikyGameInstance::Init() { GLog->Log("UClientGameInstance::Init()"); USocketObject::InitSocket(Config::address.c_str(), Config::tcp_local_port, Config::tcp_server_port, Config::udp_local_port, Config::udp_server_port); //    udp  USocketObject::RunUdpSocketReceiver(); }
      
      





エディターでキーストロークに対する反応を設定するだけです。これは、編集→プロジェクト設定→入力→アクションマッピングに進み、テキストフィールドで+を押し、コードで指定したQ名を記述し、Qボタンを追加します。



サーバーとクライアントを起動した後、LoggingHandlerのおかげでサーバーログのボタンをクリックすると、次のようなものが表示されます。Unreal Engineの場合:UDPトピックに戻りません。サーバーおよびクライアントUDPでの機能とudp_socketの作成を無効にします。現時点でのSocketObject ステータス:



udp:

utility {

alive: true

}



/127.0.0.1 7679

10, 2017 4:42:30 PM io.netty.handler.logging.LoggingHandler channelRead

INFO: [id: 0x89373e1f, L:/0:0:0:0:0:0:0:0:7681] RECEIVED: DatagramPacket(/127.0.0.1:7679 => /0:0:0:0:0:0:0:0:7681, PooledUnsafeDirectByteBuf(ridx: 0, widx: 5, cap: 2048)), 5B

+-------------------------------------------------+

| 0 1 2 3 4 5 6 7 8 9 abcdef |

+--------+-------------------------------------------------+----------------+

|00000000| 04 0a 02 08 01 |..... |

+--------+-------------------------------------------------+----------------+

10, 2017 4:42:30 PM io.netty.handler.logging.LoggingHandler write

INFO: [id: 0x89373e1f, L:/0:0:0:0:0:0:0:0:7681] WRITE: DatagramPacket(=> /127.0.0.1:7679, UnpooledUnsafeHeapByteBuf(ridx: 0, widx: 5, cap: 5)), 5B

+-------------------------------------------------+

| 0 1 2 3 4 5 6 7 8 9 abcdef |

+--------+-------------------------------------------------+----------------+

|00000000| 04 0a 02 08 01 |..... |

+--------+-------------------------------------------------+----------------+

10, 2017 4:42:30 PM io.netty.handler.logging.LoggingHandler flush

INFO: [id: 0x89373e1f, L:/0:0:0:0:0:0:0:0:7681] FLUSH









Reveived UDP data

Size of incoming data: 5











ServerMain

//new Thread(ServerMain::run_udp).start();

SpikyGameInstance

// udp

//USocketObject::RunUdpSocketReceiver();











SocketObject.cpp
 // Copyright (c) 2017, Vadim Petrov - MIT License #include "Spiky_Client.h" #include "SocketObject.h" #include "Protobufs/MessageModels.pb.h" FSocket* USocketObject::tcp_socket = nullptr; TSharedPtr<FInternetAddr> USocketObject::tcp_address = nullptr; bool USocketObject::bIsConnection = false; FSocket* USocketObject::udp_socket = nullptr; TSharedPtr<FInternetAddr> USocketObject::udp_address = nullptr; FUdpSocketReceiver* USocketObject::UDPReceiver = nullptr; int32 USocketObject::tcp_local_port = 0; int32 USocketObject::udp_local_port = 0; USocketObject::~USocketObject() { GLog->Log("USocketObject::~USocketObject()"); if (tcp_socket != nullptr || udp_socket != nullptr) { tcp_socket->Close(); //UDPReceiver->Stop(); delete tcp_socket; delete udp_socket; } } void USocketObject::InitSocket(FString serverAddress, int32 tcp_local_p, int32 tcp_server_port, int32 udp_local_p, int32 udp_server_port) { int32 BufferSize = 2 * 1024 * 1024; tcp_local_port = tcp_local_p; udp_local_port = udp_local_p; /* tcp_socket = FTcpSocketBuilder("TCP_SOCKET") .AsNonBlocking() // Socket connect always success. Non blocking you say socket connect dont wait for response (Don?t block) so it will return true. .AsReusable() .WithReceiveBufferSize(BufferSize) .WithSendBufferSize(BufferSize) .Build(); */ // tcp tcp_socket = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateSocket(NAME_Stream, TEXT("TCP_SOCKET"), false); // create a proper FInternetAddr representation tcp_address = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); // parse server address FIPv4Address serverIP; FIPv4Address::Parse(serverAddress, serverIP); // and set tcp_address->SetIp(serverIP.Value); tcp_address->SetPort(tcp_server_port); tcp_socket->Connect(*tcp_address); // set the initial connection state bIsConnection = Alive(); // udp udp_address = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); FIPv4Address::Parse(serverAddress, serverIP); udp_address->SetIp(serverIP.Value); udp_address->SetPort(udp_server_port); /* udp_socket = FUdpSocketBuilder("UDP_SOCKET") .AsReusable() .BoundToPort(udp_local_port) .WithBroadcast() .WithReceiveBufferSize(BufferSize) .WithSendBufferSize(BufferSize) .Build(); */ } void USocketObject::RunUdpSocketReceiver() { FTimespan ThreadWaitTime = FTimespan::FromMilliseconds(100); UDPReceiver = new FUdpSocketReceiver(udp_socket, ThreadWaitTime, TEXT("UDP_RECEIVER")); UDPReceiver->OnDataReceived().BindStatic(&USocketObject::Recv); UDPReceiver->Start(); } void USocketObject::Recv(const FArrayReaderPtr& ArrayReaderPtr, const FIPv4Endpoint& EndPt) { GLog->Log("Reveived UDP data"); uint8_t * buffer = ArrayReaderPtr->GetData(); size_t size = ArrayReaderPtr->Num(); GLog->Log("Size of incoming data: " + FString::FromInt(size)); google::protobuf::io::ArrayInputStream arr(buffer, size); google::protobuf::io::CodedInputStream input(&arr); std::shared_ptr<Wrapper> wrapper(new Wrapper); ReadDelimitedFrom(&input, wrapper.get()); std::string msg; wrapper->SerializeToString(&msg); GLog->Log(msg.c_str()); } bool USocketObject::SendByUDP(google::protobuf::Message * message) { Wrapper wrapper; if (message->GetTypeName() == "Utility") { Utility * mes = static_cast<Utility*>(message); wrapper.set_allocated_utility(mes); } size_t size = wrapper.ByteSize() + 5; // include size, varint32 never takes more than 5 bytes uint8_t * buffer = new uint8_t[size]; google::protobuf::io::ArrayOutputStream arr(buffer, size); google::protobuf::io::CodedOutputStream output(&arr); output.WriteVarint32(wrapper.ByteSize()); wrapper.SerializeToCodedStream(&output); if (wrapper.has_utility()) { wrapper.release_utility(); } int32 bytesSent = 0; bool sentState = false; sentState = udp_socket->SendTo(buffer, output.ByteCount(), bytesSent, *udp_address); delete[] buffer; return sentState; } void USocketObject::Reconnect() { } bool USocketObject::Alive() { return false; } bool USocketObject::ReadDelimitedFrom(google::protobuf::io::CodedInputStream * input, google::protobuf::MessageLite * message) { // Read the size. uint32_t size; if (!input->ReadVarint32(&size)) return false; // Tell the stream not to read beyond that size. google::protobuf::io::CodedInputStream::Limit limit = input->PushLimit(size); // Parse the message. if (!message->MergeFromCodedStream(input)) return false; if (!input->ConsumedEntireMessage()) return false; // Release the limit. input->PopLimit(limit); return true; }
      
      







TCPについても同じことをしましょう。Handlersパッケージを作成して、2つの空のDecryptHandler、EncryptHandlerクラスをそこに追加しましょう。タイプに応じて、すべてのメッセージが暗号化され、DecryptHandlerを通過して復号化され、さらに処理のために送信されます。ServerInitializerを開き、組み込みのNetty protobuffデコーダーを使用してメッセージを準備する必要があります。パイプラインプロトバフにエンコーダーとデコーダーを追加します。



 // Decoders protobuf pipeline.addLast(new ProtobufVarint32FrameDecoder()); pipeline.addLast(new ProtobufDecoder(MessageModels.Wrapper.getDefaultInstance())); // Encoder protobuf pipeline.addLast(new ProtobufVarint32LengthFieldPrepender()); pipeline.addLast(new ProtobufEncoder());
      
      





Extend DecryptHandlerは、MessageToMessageDecoder <MessageModels.Wrapper>を拡張し、decodeメソッドをオーバーライドして、後者をパイプラインに追加します。



 /*    */ pipeline.addLast(new DecryptHandler());
      
      





サーバー上でアライブメッセージを処理することは一切ありません。エコーをDecryptHandlerに送り返します。



 ctx.writeAndFlush(wrapper);
      
      





クライアントにメッセージを送信することに戻りましょう。個別のスレッドを使用して毎秒メッセージを送信することにより、サーバーのステータスを確認します。Unrealでストリームを作成する方法はいくつかありますが、最も簡単なのはtimerを作成することです。:小さなタスクのために作成したタスク、素数の検索との例もあります



UE4で実装マルチスレッド

マルチスレッド:タスクグラフシステム

エンジン/ソース/ランタイム/コア/公共/非同期/ AsyncWork.h



そして、あなたがインターフェイスを実装することができますFRunnableを私たちがやろうとしているものは、 。



マルチスレッド:UE4でスレッドを作成する方法



NetフォルダーにFRunnable親を持つServerStatusCheckingThクラスを作成します。



FServerStatusCheckingTh
 // Copyright (c) 2017, Vadim Petrov - MIT License #pragma once #include "Runtime/Core/Public/HAL/Runnable.h" #include "Runtime/Core/Public/HAL/RunnableThread.h" class SPIKY_CLIENT_API FServerStatusCheckingTh : public FRunnable { // Singleton instance, can access the thread any time via static accessor, if it is active! static FServerStatusCheckingTh* Runnable; // Thread to run the worker FRunnable on FRunnableThread* Thread; // The way to stop static bool bThreadRun; public: FServerStatusCheckingTh(); ~FServerStatusCheckingTh(); // FRunnable interface virtual bool Init(); virtual uint32 Run(); // Logics static FServerStatusCheckingTh* RunServerChecking(); // Shuts down the thread. Static so it can easily be called from outside the thread context static void Shutdown(); }; // Copyright (c) 2017, Vadim Petrov - MIT License #include "Spiky_Client.h" #include "ServerStatusCheckingTh.h" #include "SocketObject.h" FServerStatusCheckingTh* FServerStatusCheckingTh::Runnable = nullptr; bool FServerStatusCheckingTh::bThreadRun = false; FServerStatusCheckingTh::FServerStatusCheckingTh() { Thread = FRunnableThread::Create(this, TEXT("ServerStatusChecking"), 0, TPri_BelowNormal); } FServerStatusCheckingTh::~FServerStatusCheckingTh() { delete Thread; Thread = nullptr; } bool FServerStatusCheckingTh::Init() { bThreadRun = true; return true; } uint32 FServerStatusCheckingTh::Run() { while (bThreadRun) { FPlatformProcess::Sleep(1.f); //    if (!USocketObject::bIsConnection) //   ,  { USocketObject::Reconnect(); } else { USocketObject::bIsConnection = USocketObject::Alive(); //    ,    } //    //GLog->Log("Connect state (bIsConnection) = " + FString::FromInt((int32)USocketObject::bIsConnection) + " | FServerStatusCheckingTh::CheckServer"); } return 0; } FServerStatusCheckingTh* FServerStatusCheckingTh::RunServerChecking() { if (!Runnable && FPlatformProcess::SupportsMultithreading()) { Runnable = new FServerStatusCheckingTh(); } return Runnable; } void FServerStatusCheckingTh::Shutdown() { bThreadRun = false; GLog->Log("FServerStatusCheckingTh::Shutdown()"); if (Runnable) { delete Runnable; Runnable = nullptr; } }
      
      







Init、Run、Exitを実行するRunServerChecking()を呼び出してスレッドを開始します。シャットダウンを終了します()。 1秒ごとにAliveメッセージを送信し、メッセージが届かない場合は、Reconnectを呼び出して再接続を試みます。 USocketObjectにReconnectとAliveを実装します。再接続-ソケットを閉じ、アドレスを正規化し、ソケットを再度初期化します。 Alive-メッセージを作成し、すぐに送信します:



 void USocketObject::Reconnect() { tcp_socket->Close(); uint32 OutIP; tcp_address->GetIp(OutIP); FString ip = FString::Printf(TEXT("%d.%d.%d.%d"), 0xff & (OutIP >> 24), 0xff & (OutIP >> 16), 0xff & (OutIP >> 8), 0xff & OutIP); InitSocket(ip, tcp_local_port, tcp_address->GetPort(), udp_local_port, udp_address->GetPort()); } bool USocketObject::Alive() { std::shared_ptr<Utility> utility(new Utility); utility->set_alive(true); // Send  , : , ?  tcp? return UMessageEncoder::Send(utility.get(), false, true); }
      
      





Handlersフォルダーと、サーバーデコーダーおよびエンコーダーに似たUObjectから派生したMessageDecoderおよびMessageEncoderクラスを作成しましょう。これらのクラスは、受信/送信メッセージの復号化/暗号化および展開/ラッピングに関与します。#include“ MessageEncoder.h”をSocketObjectに追加してコンパイルします。



着信メッセージリスナが必要です。これを行うには、Netで、親FRunnableを持つ別のストリームTCPSocketListeningThを作成します。ここでは、接続を確認し、無駄に動作しないようにストリームの速度を設定し、読み取り、バイトをprotobufに変換し、メインゲームストリームで処理するために送信します。



FTCPSocketListeningTh
 // Copyright (c) 2017, Vadim Petrov - MIT License #pragma once #include "Runtime/Core/Public/HAL/Runnable.h" #include "Runtime/Core/Public/HAL/RunnableThread.h" class SPIKY_CLIENT_API FTCPSocketListeningTh : public FRunnable { FRunnableThread* Thread; static FTCPSocketListeningTh* Runnable; static bool bThreadRun; public: FTCPSocketListeningTh(); ~FTCPSocketListeningTh(); virtual bool Init(); virtual uint32 Run(); static FTCPSocketListeningTh* RunSocketListening(); static void Shutdown(); }; #include "Spiky_Client.h" #include "TCPSocketListeningTh.h" #include "SocketObject.h" #include "MessageDecoder.h" #include <google/protobuf/io/zero_copy_stream_impl_lite.h> #include <google/protobuf/io/coded_stream.h> #include "Protobufs/MessageModels.pb.h" #include "Async.h" FTCPSocketListeningTh* FTCPSocketListeningTh::Runnable = nullptr; bool FTCPSocketListeningTh::bThreadRun = false; FTCPSocketListeningTh::FTCPSocketListeningTh() { Thread = FRunnableThread::Create(this, TEXT("TCP_RECEIVER"), 0, TPri_BelowNormal); } FTCPSocketListeningTh::~FTCPSocketListeningTh() { delete Thread; Thread = nullptr; } bool FTCPSocketListeningTh::Init() { bThreadRun = true; return true; } uint32 FTCPSocketListeningTh::Run() { while (bThreadRun) { //    if (USocketObject::bIsConnection == false) //   { FPlatformProcess::Sleep(1.f); //   ,  } else { FPlatformProcess::Sleep(0.03f); if (!USocketObject::tcp_socket) return 0; //Binary Array! TArray<uint8> ReceivedData; uint32 Size; while (USocketObject::tcp_socket->HasPendingData(Size)) //     { ReceivedData.Init(FMath::Min(Size, 65507u), Size); int32 Read = 0; USocketObject::tcp_socket->Recv(ReceivedData.GetData(), ReceivedData.Num(), Read); } if (ReceivedData.Num() > 0) { GLog->Log(FString::Printf(TEXT("Data Read! %d"), ReceivedData.Num()) + " | FTCPSocketListeningTh::Run"); //    protobuf uint8_t * buffer = ReceivedData.GetData(); size_t size = ReceivedData.Num(); google::protobuf::io::ArrayInputStream arr(buffer, size); google::protobuf::io::CodedInputStream input(&arr); bool protosize = true; /*        , tcp      */ while (protosize) { std::shared_ptr<Wrapper> wrapper(new Wrapper); protosize = USocketObject::ReadDelimitedFrom(&input, wrapper.get()); /*      ,    */ AsyncTask(ENamedThreads::GameThread, [wrapper]() { UMessageDecoder * Handler = NewObject<UMessageDecoder>(UMessageDecoder::StaticClass()); Handler->SendProtoToDecoder(wrapper.get()); }); } } } } return 0; } FTCPSocketListeningTh* FTCPSocketListeningTh::RunSocketListening() { if (!Runnable && FPlatformProcess::SupportsMultithreading()) { Runnable = new FTCPSocketListeningTh(); } return Runnable; } void FTCPSocketListeningTh::Shutdown() { bThreadRun = false; GLog->Log("FTCPSocketListeningTh::Shutdown()"); if (Runnable) { delete Runnable; Runnable = nullptr; } }
      
      







SpikyGameInstanceで2つの新しいスレッドを有効にします。



 ... #include "ServerStatusCheckingTh.h" #include "TCPSocketListeningTh.h" ... //     USpikyGameInstance::Init() //      FServerStatusCheckingTh::RunServerChecking(); //    tcp  FTCPSocketListeningTh::RunSocketListening(); //     USpikyGameInstance::Shutdown() //     FServerStatusCheckingTh::Shutdown(); //    tcp  FTCPSocketListeningTh::Shutdown();
      
      





エンコーダを実装し、protobuffタイプのメッセージが到着します。暗号化、入力、Wrapperでのラップ、長さと本文のバッファへの書き込みが必要かどうかを判断する関数で、TCPまたはUDPチャネルを介して送信します。



MessageEncoder
 // Copyright (c) 2017, Vadim Petrov - MIT License #include "Spiky_Client.h" #include "MessageEncoder.h" #include "SocketObject.h" #include "Protobufs/MessageModels.pb.h" #include <google/protobuf/io/zero_copy_stream_impl_lite.h> #include <google/protobuf/io/coded_stream.h> bool UMessageEncoder::Send(google::protobuf::Message * message, bool bCrypt, bool bTCP) { Wrapper wrapper; //     if (bCrypt) { } else { if (message->GetTypeName() == "Utility") { Utility * mes = static_cast<Utility*>(message); wrapper.set_allocated_utility(mes); } } size_t size = wrapper.ByteSize() + 5; // include size, varint32 never takes more than 5 bytes uint8_t * buffer = new uint8_t[size]; google::protobuf::io::ArrayOutputStream arr(buffer, size); google::protobuf::io::CodedOutputStream output(&arr); //       buffer output.WriteVarint32(wrapper.ByteSize()); wrapper.SerializeToCodedStream(&output); //     utility if (wrapper.has_utility()) { wrapper.release_utility(); } int32 bytesSent = 0; bool sentState = false; if (bTCP) { //send by tcp sentState = USocketObject::tcp_socket->Send(buffer, output.ByteCount(), bytesSent); } else { //send by udp sentState = USocketObject::udp_socket->SendTo(buffer, output.ByteCount(), bytesSent, *USocketObject::udp_address); } delete[] buffer; return sentState; }
      
      







サーバーとクライアントを起動し、メッセージとエコーが届くことを確認します。サーバー

utility { alive: true }





ログ:クライアントログ:

Connect state (bIsConnection) = 1 | FServerStatusCheckingTh::CheckServer

Data Read! 5 | FTCPSocketListeningTh::Run








おわりに



必要な準備ができたら、完了です。その結果、protobuffメッセージ、AndroidおよびWindowsでコンパイルおよび接続されたライブラリ、およびその上に機能を構築し続ける初期アーキテクチャを通信するクライアントサーバーを取得しました。最後に、Nettyとオンラインゲームのアーキテクチャを理解するのに役立つ参考文献のリストを残します。



この場所を読んでくれてありがとう!







ノーマンマウラー「Netty in Action」-Nettyを使用すると、簡単に拡張およびスケーリングできるクライアントサーバーアプリケーションをすばやく簡単に作成できます。



Josh Glazer「マルチプレイヤーゲームプログラミング:ネットワークゲームの設計」-この実例の本では、オンラインゲームの開発の特徴と、信頼できるマルチユーザーアーキテクチャの構築の基本について説明しています。



グレンビルアーミテージ「ネットワークとオンラインゲーム:マルチプレイヤーインターネットゲームの理解とエンジニアリング」はかなり古い本ですが、マルチプレイヤーゲームの仕組みを説明した優れた本です。



All Articles