TOAST Hasteの紹介

はじめに

私たちは長い間、TCP(Transmission Control Protocol)を使って多くのアプリケーションを開発してきました。TCPはパケットの順序保証、フロー制御、混雑制御などの多くの役割を担い、開発者がより抽象的なレベルでネットワークを眺められるようにサポートします。これにより、開発者は端末間のネットワークをバイトストリームとみなして、簡単に開発することができます。

しかし、特定の分野(例えば、リアルタイム性が必要なもの)では、TCPの作業が負荷になって、パフォーマンスを発揮できないことがあります。最も代表的なものとして、Head of Line Blocking(以下HOL Blocking)がそうです。

では、TCPの代わりにUDPを使えば、どうでしょうか。UDPはベストエフォートで動作し、端末のアプリケーション間のポートを区別する役割を持ちます。見方を変えると、UDPは真っ白な画用紙のようなもので、その上にいくらでも機能を追加できます。実際にGoogleでも、ほとんどのサービスにUDP基盤のQUICプロトコルをオプションで使用しています。

UDPについては、UDPを使用するときに考慮すべきことが参考になるでしょう。

ゲームはどうしょうか?韓国のマルチプレイゲームはほとんどTCPを使って開発されています。先に説明したように、TCPはとても良いトランスポートプロトコルです。しかしUDPを使うと、マルチプレキシング、Wi-Fi Cellular handoverなどの点でメリットがあります。このようなメリットを得るために、UDPを使ってゲームを開発していくと、多くの課題に直面するでしょう。課題を解決するうちに、コンテンツ開発よりも、ネットワーク層の開発に多くの時間を費やすことになり、スケジュールに追われて、結局TCPに戻ることになります。UDPを使って早くゲームサーバーを開発することは不可能なのでしょうか。

私たちも同じような問題について、試行錯誤をしてきました。ネットワーク層の開発は毎回はできないので、再利用できるようにしておけば、次回はさらに簡単に作成できるのではないかと考えました。そして、そのように作られたフレームワークがTOAST Hasteです。

TOAST Haste


TOAST Hasteは非同期ゲームサーバーのフレームワークです。TOAST Hasteを使って開発すると、以下のようなメリットがあります。

  • 様々なQoSを提供
  • Multiplexing
  • Wi-Fi Cellular handover
  • オプションの暗号化(using Diffie-Hellman algorithm, AES)

1つずつ詳しく見ていきましょう。

TOAST Hasteのソースは、https://github.com/nhnent/toast-haste.frameworkから確認できます。
TOAST Haste .NET SDKは、https://github.com/nhnent/toast-haste.sdk.dotnetから確認できます。

様々なQoSを提供

UDPは基本的にベストエフォートで動作します。つまり、ポートでアプリケーションを分離する他は何もしません。したがって、パケットの順序保証、再送信を実装する必要があります。TOAST Hasteは、パケットの順序保証と再送信に対する実装がなされており、下記のようなQoSを提供しています。

  • Reliable sequenced:パケット順序を保証し、失効時に再送信を行う。
  • Unreliable sequenced:損失時の再送信は保証しないが、最新のパケット順序を保証する。パケット損失時の再送信は必要ないが、最新のデータが有効でなければならない場合に使用する。(例: プレイヤーの位置情報)
  • Reliable fragmented:IP階層の分断化を防ぐために、あらかじめMTUサイズに切り取って送受信する。大規模なデータ送受信に役立てられる。

TOAST HasteのQoS実装は、ENet(MIT License)とTCP実装の一部をもとに開発されました。

Multiplexing

TOAST Hasteは、論理的なチャネルを指定して送受信できるように設計されました。チャネル別に処理が実行されるので、データを役割に応じて適切にチャネル別に分けて送受信できます。適切なチャネル分散は、HOL Blockingの問題を最小限に抑えることができます。

Reliable sequenced(あるいはReliable fragmented)QoSで送受信する場合には、パケット損失時、チャネルごとにHOL Blockingの問題が発生します。
したがって、ドメイン別に適切なチャネル分配が必要です。

Wi-Fi Cellular handover

TCPの場合、連結基盤であるため、Source IP、Source Port、Destination IP、Destination Portの4つの情報を使って1つの連結を識別します。したがって、Wi-FiとCellular移動時、ネットワークが変更されてIPが変わると、新しい接続を試みる必要があります。TOAST Hasteは、独自の識別番号で接続を管理しているので、IPが変更されても再接続なく、接続が維持されます。

オプションの暗号化

ゲームだけでなく、すべてのアプリケーションにおいて、重要データの暗号化は不可欠です。しかしゲームの場合は、すべてのパケットを暗号化すると、リアルタイム性を確保するのが難しく、必要に応じて暗号化を行う必要があります。TOAST Hasteは、接続が成立したとき、暗号化に対する事前情報を送受信します。その後は、ユーザーが必要に応じて暗号化を行うことができます。
参考までに、TOAST Hasteはキーの交換にはDiffie-Hellman Algorithmを、データの暗号化にはAES256を使用しています。

Let’s Haste

簡単なEchoサーバーとクライアントを作成して、TOAST Hasteの使い方を覚えましょう。

Echoサーバー

  • TOAST Haste frameworkをチェックアウトすると、com.nhnent.haste.example.echoserverパッケージのEchoServerクラスのmain関数からEchoサーバーを実行できます。

1. GameServerBootstrapからサーバーを設定する

public class EchoServer {
    private static final int PORT = 5056;

    public static void main(String[] args) {
        GameServerBootstrap bootstrap = new GameServerBootstrap();

        bootstrap.application(new EchoServerApplication())
                .option(UDPOption.THREAD_COUNT, 2)
                .bind(PORT).start();
    }
}

2. EchoServerApplicationClientPeerの生成コードを追加する

  • ClientPeerは、クライアントが接続するときに生成されるPeerオブジェクトです。
public class EchoServerApplication extends ServerApplication {
    @Override
    protected void setup() {
    }

    @Override
    protected void tearDown() {
    }

    @Override
    protected ClientPeer createPeer(InitialRequest initialRequest, NetworkPeer networkPeer) {
        return new EchoPeer(initialRequest, networkPeer);
    }
}

3.実際に送受信するデータのEchoMessageクラスを実装する

  • データはMessageBridgeを継承して実装すると、FieldParameterアノテーションを利用して簡単に実装できます。
public class EchoMessage extends MessageBridge {
    public static final short MESSAGE = 0;

    public EchoMessage(RequestMessage request) {
        super(request);
    }

    @FieldParameter(Code = MESSAGE)
    public String message;
}

4.実際のクライアントとデータを送受信するEchoPeerクラスを実装する

public class EchoPeer extends ClientPeer {
    private static final Logger logger = LoggerFactory.getLogger(EchoPeer.class);

    public EchoPeer(InitialRequest initialRequest, NetworkPeer networkPeer) {
        super(initialRequest, networkPeer);
    }

    @Override
    protected void onReceive(RequestMessage request, SendOptions options) {
        EchoMessage message = new EchoMessage(request);

        logger.info(MessageFormat.format("Client message is \"{0}\"", message.message));

        ResponseMessage response = message.toResponse();
        this.send(response, options);
    }

    @Override
    protected void onDisconnect(DisconnectReason reason, String detail) {
    }
}

Echoクライアント

  • TOAST Haste SDK for .NETとEchoクライアントの全体ソースコードは、こちらから確認できます。

1. NetworkConnectionオブジェクトと設定オブジェクトを作成する

_connection = new NetworkConnection();
_config = new ConnectionConfig
{
    ChannelCount = 5,
    DisconnectionTimeout = 3000,
    IsCrcEnabled = false,
    MaxUnreliableCommands = 0,
    MTUSize = 1350,
    PingInterval = 1000,
    SentCountAllowance = 3,
    WarningQueueSize = 500,
};
_connection.Configure(_config);

2. NetworkConnectionオブジェクトに応答イベントを登録する

  • ClientPeerは、クライアントが接続するときに生成されるPeerオブジェクトです。
_connection.ResponseReceived += OnResponseReceived;
_connection.StatusChanged += OnStatusChanged;
...

const int MESSAGE_CODE = 0;

private static void OnResponseReceived(ResponseMessage response)
{
    if (response.Code == MESSAGE_CODE)
    {
        string message = string.Empty;
        if (response.Data.GetValue(MESSAGE_PARAM_CODE, out message))
        {
            Console.WriteLine("[OnResponseReceived] Server message is \"{0}\"", message);
        }
    }
}

3.接続して応答スレッドを開始する

IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5056);
_connection.Connect(remoteEndPoint, new Version(0, 1, 0), null);

Thread receiveThread = new Thread(() =>
{
    while (true)
    {
        _connection.NetworkUpdate();
    }
});
receiveThread.Start();

4.データ送信のメソッドを作成する

const int MESSAGE_PARAM_CODE = 0;

public void Send(string input)
{
    DataObject data = new DataObject();
    data.SetString(MESSAGE_PARAM_CODE, input);
    _connection.SendRequestMessage(MESSAGE_CODE, data, SendOptions.ReliableSend);
}

まとめ

TCPは現在においても優れたトランスポートプロトコルです。しかし、アプリケーションによっては、TCPのメリットが限界に達することもあります。代表的な例としてはリアルタイム性が必要なサービスで、そのうち代表的なものがゲームでしょう。
ゲームサーバーフレームワークが持つべき価値は、ネットワークがすべてではありません。TOAST Hasteは、様々なQoSのネットワークの実装だけでなく、最適なThreadモデルの選択とThread-safeなオブジェクトリサイクルなど、色々な技法を用いています。まだ道のりは長く、足りない部分が多いですが、より多く方に知識やコードを共有して向上させていきたいと考えています。

このようなことから、TOAST Hasteでは実装全体を公開しています。不便な部分や、素晴らしいアイデアがあれば、一緒に話し合いながら有用なフレームワークに発展させていきたいと思います。なおTOAST Hasteは、TOASTCloud Real-time multiplayerサービスの骨組みであり、今後もサービスで発生しているバグ修正や追加機能は、TOAST Hasteに着実に反映していく予定です。

興味のある方は、https://github.com/nhnent/toast-haste.frameworkからフレームワークのソースコードを、https://github.com/nhnent/toast-haste.sdk.dotnetから、.NET SDKのソースコードをご確認いただけます。

TOAST Meetup 編集部

TOASTの技術ナレッジやお得なイベント情報を発信していきます
pagetop