5. NEXライブラリ全般(基本型、ディスパッチ、非同期処理、データ正当性)

5.1. NEX で定義される基本型

NEX では int などの基本型を独自に定義していますが、SDK で定義される基本型を直接使用することも可能です。 Table 5.1 にNEX で定義される基本型と、SDK で定義される基本型との対応を示します。

Table 5.1 NEX で定義される基本型とSDKで定義される基本型との対応表
NEXで定義されている型名 SDKで対応する型名
nn::nex::qBool bool
nn::nex::qByte u8
nn::nex::qInt8 s8
nn::nex::qUnsignedInt8 u8
nn::nex::qInt16 s16
nn::nex::qUnsignedInt16 u16
nn::nex::qInt32 s32
nn::nex::qUnsignedInt32 u32
nn::nex::qInt Int
nn::nex::qUnsignedInt unsigned int
nn::nex::qFloat f32
nn::nex::qDouble f64
nn::nex::Real f32
nn::nex::qInt64 s64
nn::nex::qUnsignedInt64 u64
nn::nex::qChar8 char8
nn::nex::qChar16 char16
nn::nex::qWideChar char16
nn::nex::qChar char16

5.2. ディスパッチ処理の概要

アプリケーションが Scheduler::Dispatch() を定期的に実行することで NEX は 内部処理を進め、パケットの送受信を行います。 Initialize() の呼び出しなどで NEX が初期化されると NEX ライブラリ内で Scheduler オブジェクトが生成され、Scheduler::GetInstance() で取得できるようになります。 Scheduler オブジェクトが存在するときは定期的に Scheduler::Dispatch() を呼び出すようにしてください。 Finalize() の呼び出しなどで NEX が終了すると自動的に Scheduler オブジェクトも破棄されます。

CallContext::Wait() は内部で Scheduler::Dispatch() を呼び出しているため、 CallContext::Wait() でブロッキングしている間は Scheduler::Dispatch() を呼び出さなくても構いません。 スレッドセーフモードの場合は CallContext::Wait() と Scheduler::Dispatch() をそれぞれ並列に呼び出すことが可能です。

Scheduler::DispatchAll() は処理のディスパッチキュー内のすべてのジョブをディスパッチします。 ディスパッチされるべきジョブの全てに対し処理が終わると、このメソッドが終了します。

一方、Scheduler::Dispatch() はタイムアウトを指定することができ、タイムアウト時間が過ぎるか、ディスパッチキューのジョブが空になるまで処理されます。 uiDispatchTimeout パラメータでタイムアウト時間をミリ秒で指定します。 uiDispatchTimeout のデフォルト値は無限です (Scheduler::DispatchAll() と同等)。 Scheduler::GetReadyJobsSize() で Scheduler が実行可能なジョブの個数を取得できます。Scheduler::Dispatch() の後にこの関数を呼び出したときに 1 以上の数が連続して返り、増加傾向にある場合には、処理に対してタイムアウト時間が足りていません。タイムアウト時間を増加させるか、通信量を減らしてください。

スレッドモードに Core::ThreadModeInternalCore::ThreadModeInternalTransportBuffer を使用した場合は NEX 内部で自動的にディスパッチ処理が行われるため、 Scheduler::Dispatch() の呼び出しは不要です。

5.2.1. 通信レイテンシを最小化するディスパッチ処理の推奨使用例

以下に、通信のレイテンシを最小化するディスパッチ処理の推奨使用方法を示します。

  1. Scheduler::DispatchAll() / Scheduler::Dispatch() の実行(受信想定の処理)
  2. 受信データをアプリケーションでハンドリング。DOの場合は、すべて DO デュプリカのリフレッシュ(受信データの反映)
  3. ゲーム状態の変更(物理計算、AI など)
  4. データの送信。DOの場合はすべての DO マスターの更新(送信データの準備)
  5. Scheduler::DispatchAll() / Scheduler::Dispatch() の実行(送信想定の処理)
  6. レンダリングなどアプリケーション側の処理

NetZの複製オブジェクトを使用する場合、Scheduler::DispatchAll() / Scheduler::Dispatch() が毎フレームで実行され、複製オブジェクトのデータセットが更新(またはリフレッシュ)される前に、すべての複製オブジェクトが最新データを受信しておく必要があります。 Scheduler::DispatchAll() / Scheduler::Dispatch() を他のタイミングで実行することも、1フレームに 1 回だけ実行することもできますが、その場合はある程度のレイテンシが生じます。

ランキングやデータストアのみを使うなど NetZ の P2P 機能を使わないシーンやケースでも、ディスパッチ間隔は最大でも 100 msec 程度となるようにしてください。間隔が広いと通信の遅延が発生します。

5.3. 非同期処理

NEX でサーバーとの通信などで非同期処理を実行するには、非同期処理を行う関数の第一引数に、CallContext クラスやそのサブクラスである ProtocolCallContext クラスを渡します。 逆に言えば第一引数に CallContext もしくはそのサブクラスを取る関数は非同期処理を行う関数です。 CallContext を使うと非同期処理が完了したときに呼び出されるコールバックを設定したり、非同期処理が完了するまでブロッキングしたり、非同期処理の結果に関する情報を取得する事ができます。 また、キャンセルやタイムアウトの設定などもできます。 CallContext クラスは呼び出しコンテキストとも呼ばれます。

非同期処理が終了すると、CallContext の状態と結果が変更されます。状態は CallContext::GetState(), 結果は CallContext::GetOutcome() で取得できます。 状態は CallContext::State 列挙子で表され、 CallContext::AvailableCallContext::CallInProgressCallContext::CallSuccessCallContext::CallFailureCallContext::CallCancelled の 5つの状態のいずれかになります。 初期状態は CallContext::Availableです。非同期処理が呼び出されると、CallContext::CallInProgress 状態になり、その後 CallContext::CallSuccessCallContext::CallFailureCallContext::CallCancelled のいずれかの状態になります。 呼び出しコンテキストが CallContext::CallInProgress 状態の場合、CallContext::Cancel メソッドを使用して呼び出しコンテキストをキャンセルでき、この場合の状態は CallContext::CallCancelled になります。 状態が CallContext::CallSuccessCallContext::CallFailureCallContext::CallCancelled のいずれかの場合、CallContext::Reset() 関数を呼び出し、状態を CallContext::Available にリセットすることができます。 CallContext::Wait() 関数を使用すると、非同期処理が終了するまでスレッドが待機します。

../_images/Fig_Overall_CallContext.png

Figure 5.1 CallContext の状態遷移図

CallContext の完了と同時にユーザー定義のコールバックが呼び出されるようにできます。 完了コールバックを使用することによって、非同期処理の呼び出し後、完了しているかどうかを確認するために CallContext を連続してポーリングすることを回避できます。 コールバックは、CallContext::RegisterCompletionCallbackGeneric() を使用して、個々の CallContext に登録できます。

Code 5.1 コールバックの登録
void CallbackFunc(CallContext* pContext)
{
}

void Example()
{
    CallContext cc;
    cc.RegisterCompletionCallbackGeneric(&CallbackFunc);
    SomeAsyncCall(&cc, ...);
}

CallContext::RegisterCompletionCallbackGeneric() の第一引数には CallContext* を引数に取る関数ポインタ、関数オブジェクト、ラムダ式のいずれかを指定できます。(上記は関数ポインタを指定する例です。) コールバックの引数に渡される pContext はコールバックを紐づけた CallContext オブジェクトへのポインタです。 コールバックは Scheduler::Dispatch() を呼び出したスレッドから呼び出されます。

5.3.1. 非同期処理の返値

非同期処理を行う関数の多くは qBool を返します。これは非同期処理の開始に成功したかどうかを示します。 false が返された場合は、指定した CallContext の状態が CallContext::CallInProgress となっており非同期処理を開始出来ない事を示します。 これは他の非同期処理で使用中であることを示します。 プログラミングエラーであるため、このケースはリリースビルド以外では内部で Assert により停止します。

非同期処理を行う関数が qBool を返さない場合や、特記事項がある場合は API リファレンスの記載に従ってください。

5.3.2. 非同期処理のエラーハンドリング

非同期処理の結果は CallContext::GetOutcome() で取得できます。 qResult には、2 種類のマクロがあり、指定されたリターンコードを含む qResult クラスを生成する QRESULT_SUCCESS, QRESULT_ERROR マクロと、リターンコード自体を示す QSUCCESS, QERROR マクロがあります。qResult::GetReturnCode() でリターンコードを取得して QERROR マクロで生成したリターンコードと比較できるほか、 qResult にはリターンコードとの比較演算子が定義されているため、直接比較することも可能です。 qResult オブジェクトと QRESULT_ERROR マクロで生成した qResult オブジェクトとを比較することもできますが、QSUCCESS, QERROR マクロで生成したリターンコードと比較した方が処理は高速です。

もし、「アプリケーションがハンドリングするべきエラー一覧」にあるエラーが発生した場合には、アプリケーションはエラーをハンドリングして、適切な処理を行ってください。 詳細は、 API リファレンス、もしくは、各機能のエラーハンドリングの節を参考にしてください。

記載されていないエラーが発生した場合には、ErrorCodeConverter::ConvertToNetworkErrorCode() でネットワークエラーコードを取得し、エラー・EULAアプレットに渡した後に通信の終了処理を行ってください( Code 5.2 )。

5.3.3. 共通で発生するエラー

以下に、全ての API で共通で発生するエラーを記載します。

以下のエラーについては、アプリケーション側でハンドリングするか、エラー・EULAアプレットにエラーを渡すかを選択することができます。

Table 5.2 API で共通で発生するエラー(アプリケーション側でハンドリングできるもの)
エラー 発生条件や状況など
QERROR(Core, OperationAborted) ログアウトにより非同期処理が中断されました。
QERROR(RendezVous, ConnectionDisconnected) ゲームサーバーからの切断により非同期処理が中断されました。
QERROR(Transport, Unknown) 無線 LAN の切断など、NEX 自身では原因の分からないネットワーク層のエラーにより非同期処理に失敗しました。

以下のエラーについては、記載されていないエラーと同様に、ErrorCodeConverter::ConvertToNetworkErrorCode() でネットワークエラーコードを取得し、エラー・EULAアプレットに渡した後に通信の終了処理を行ってください( Code 5.2 )。

Table 5.3 API で共通で発生するエラー(記載されていないエラーと同様に扱うもの)
エラー 発生条件や状況など
QERROR(PythonCore, Exception) サーバーで予期しないエラーが発生しました。任天堂のサポート窓口に問い合わせてください。

5.3.4. アプリケーションがハンドリングするべきエラー一覧

  • マッチメイク
    • QERROR(RendezVous, SessionVoid) ギャザリングが存在しない。
    • QERROR(RendezVous, SessionFull) すでに最大参加人数に達している。
    • QERROR(RendezVous, PersistentGatheringCreationMax) 永続ギャザリングの登録上限を超過。
    • QERROR(RendezVous, PersistentGatheringParticipationMax) 永続ギャザリングの参加上限を超過。
    • QERROR(RendezVous, InvalidGatheringPassword) パスワード付永続ギャザリングでパスワードが合致していない。
    • QERROR(RendezVous, WithoutParticipationPeriod) 参加可能期間外。
    • QERROR(RendezVous, DeniedByParticipants) マッチメイクセッションの参加者が自分をブロックリストに登録している。
    • QERROR(RendezVous, ParticipantInBlockList) 自分のブロックリストの中にマッチメイクセッションの参加者が登録されている。
    • QERROR(RendezVous, NotParticipatedGathering) 指定したユーザーがギャザリングに参加していない
    • QERROR(RendezVous, MatchmakeSessionUserPasswordUnmatch) マッチメイクセッションユーザーパスワードが一致しない
    • QERROR(RendezVous, MatchmakeSessionSystemPasswordUnmatch) マッチメイクセッションシステムパスワードが一致しない
    • QERROR(RendezVous, UserIsOffline) 指定したユーザーがオフライン。
    • QERROR(RendezVous, NotFriend) フレンド関係がない。
    • QERROR(RendezVous, SessionClosed) ギャザリングが締め切られている。
    • QERROR(MatchmakeReferee, InvalidArgument) 引数が不正です。
    • QERROR(MatchmakeReferee, AlreadyExists) すでに存在します。
    • QERROR(MatchmakeReferee, NotParticipatedGathering) ギャザリングに参加していません。
    • QERROR(MatchmakeReferee, NotParticipatedRound) ラウンドに参加していません。
    • QERROR(MatchmakeReferee, StatsNotFound) 統計情報が見つかりません。
    • QERROR(MatchmakeReferee, RoundNotFound) ラウンドが見つかりません。
    • QERROR(MatchmakeReferee, RoundArbitrated) ラウンド集計処理が完了しています。
    • QERROR(MatchmakeReferee, RoundNotArbitrated) ラウンド集計処理が完了していません。

以下のエラーメッセージは、アプリケーションにてエラーをハンドリングするか、 エラー・EULAアプレットにエラーを渡すかは選択することができます。

  • QERROR(RendezVous, GameServerMaintence) ゲームサーバーのメンテナンスが開始される
  • QERROR(RendezVous, InvalidOperation) 不正な操作をしようとした。
  • QERROR(RendezVous, AlreadyParticipatedGathering) ギャザリングにすでに参加している。
  • QERROR(RendezVous, PermissionDenied) 権限がない操作をしようとした。
  • ランキング
    • QERROR(Ranking, NotFound) データが見つからない
  • ランキング 2
    • QERROR(Ranking2, InvalidScore) 不正なスコアが登録された。
  • データストア
    • QERROR(DataStore, NotFound) 指定したデータ ID や 永続化スロット にデータが見つからない。
    • QERROR(DataStore, PermissionDenied) 権限を持っていないデータに対してパスワードを指定せずに操作しようとした。
    • QERROR(DataStore, UnderReviewing) 審査中のため参照不可です。
    • QERROR(DataStore, InvalidPassword) 誤ったパスワードを指定して操作しようとした場合に発生します。
    • QERROR(DataStore, ValueNotEqual) 値が一致しないため、更新できなかった。
  • P2P通信機能

以下の2つのエラーメッセージは、アプリケーションの送信リトライ処理などにより通信の継続が可能です。ただし、開発中に起こる場合には、送信頻度を減らす、受信ポーリング回数を増やす、バッファサイズを見直す、周囲にある無線LANアクセスポイントの数が多すぎる場合には減らすなどの改善を検討してください。

  • QERROR(Transport, ReliableSendBufferFull) DirectStreamにおいてリライアブル通信のバッファがいっぱいになり送信できなかった。
  • QERROR(Transport, PacketBufferFull) パケットバッファ枯渇により送信できなかった

以下のエラーメッセージは、状況に応じて適切なハンドリングをしてください。

  • QERROR(Transport, InvalidStation) 宛先Stationが存在しなかった場合 (接続処理中であるか、すでに存在していません。)

以下のエラーは、複製オブジェクト使用時に、ステーションが離脱中や通信状態が悪い場合に発生するものです。アプリケーション側で無視することにより、他とのステーションとの通信の継続が可能です。

  • QERROR(DOCore, StationNotReached) 呼び出し対象のステーションに到達できない。
  • QERROR(DOCore, TargetStationDisconnect) 呼び出しは実行されましたが、応答が返る前に対象ステーションとの接続が途絶えた。
  • QERROR(DOCore, LocalStationLeaving) 呼び出しは実行されましたが、ローカルステーションはセッションを離脱中のため、応答を受け取れない。
  • QERROR(DOCore, CallTimeout) 呼び出しに設定されたタイムアウト時間内に処理を完了できなかったため、呼び出しは失敗した。
  • QERROR(DOCore, MigrationInProgress) オブジェクトのマイグレーションが進行中。
Code 5.2 エラーハンドリングの例
MatchmakeExtensionClient client;
ProtocolCallContext context;

// 非同期処理の呼び出し
client.JoinMatchmakeSession(&context, gid, NEX_T("join message"));

// ブロックして非同期処理の完了を待ちます
context.Wait();

if(context.GetState() != CallContext::CallSuccess)
{
    qResult result = context.GetOutcome();

    // アプリケーションでハンドリングすべきエラー
    switch (result.GetReturnCode())
    {
        case QERROR(RendezVous, SessionVoid):
            // マッチメイクセッションが存在しない
            break;
        case QERROR(RendezVous, SessionFull):
            // マッチメイクセッションが満室
            break;
        default:
        {
            // その他、アプリケーション側でハンドリングしなくて良い予期しないエラーはエラー表示を行ってください。

            u32 code = ErrorCodeConverter::ConvertToNetworkErrorCode(result);
        //エラー・EULAアプレットを起動



            // ログアウト・終了処理に移行
        }
    }
}

5.4. 長時間ブロックする可能性のあるAPI

CallContext なしのAPI(ID変換を行う等の簡単な処理を行うAPIやローカルの値を参照・更新するAPIを除く)は、長時間ブロックする可能性があるのでご注意ください。

ユーザースレッドから呼び出したNEX APIでブロック操作が発生すると、ブロック操作が完了するまで Scheduler::Dispatch() を自動的に実行します。 そのScheduler::Dispatch()から呼び出されたコールバックなどで、さらにブロック操作が必要となるNEX APIを呼び出した場合、NEX APIはエラーを返します。 これはブロック待ちによるデッドロックを回避するためと、また、Scheduler::Dispatch()が二重に呼び出されることでジョブが順番に実行できなくなる可能性を回避するためとなります。

以下にブロックする可能性のあるAPIとその条件を示します。 CallContext を使って非同期処理が可能なNEXのAPIについては記載していません。

Table 5.4 ブロックする可能性のあるAPIとその条件
API ブロックする条件
DuplicatedObject::Create() セッションマスター以外のステーションが呼び出した場合、ID生成のためにセッションマスターと通信するためブロックします。
DuplicatedObject::Publish() セッション参加前に DuplicatedObject::Create() をした場合、 DuplicatedObject::Publish() でIDを生成するためにセッションマスターと通信するためブロックします。
DOCallContext::Wait() この DOCallContext が参照するオブジェクトがまだ検出されていないか、呼び出しがまだ完了していない場合にブロックします。
DORef::Wait() この DORef が参照する DO がまだ検出されていない場合にブロックします。

5.5. コールバック内の処理についての注意点

コールバックは Scheduler::SystemLock() によるシステムロックが取得された状態で呼び出されます。 また、コールバック内から Scheduler::Dispatch()、Scheduler::DispatchAll()、CallContext::Wait() を呼び出さないでください。

コールバック、RMC、DataSet::OperationBegin() / DuplicatedObject::OperationBegin() / DataSet::OperationEnd() / DuplicatedObject::OperationEnd() などのメソッド内からブロック操作を呼び出すと、デッドロックの可能性がある場合、システムは SYSTEMERROR_GEN_INVALID_WAIT エラーを返します。

このような場合、CallMethodOperation クラスを使用して、ブロック操作から返される値を待つ間に元の呼び出しがデッドロックしないように保証することができます。 このクラスは、処理の準備ができていない場合に呼び出しを延期することができ、指定時間が経ってから再度同じメソッドを呼び出すことを可能にします。

コールバック、RMC、DataSet::OperationBegin() / DuplicatedObject::OperationBegin() / DataSet::OperationEnd() / DuplicatedObject::OperationEnd() メソッドをCallMethodOperation::PostponeOperation() メソッドを使用して、必要な遅延時間を設定できます。 このクラスの使用方法とコード例の詳細については、 複製オブジェクトの拡張機能 を参照してください。

5.6. NEX で使用するソケットのポート番号について

NEX はサーバーや他の端末との通信に UDP を使用します。また、一部のサーバーとの通信に HTTPS を使用します。 ルーターやファイアウォールの設定を行う場合、 UDP の全てのポートを送受信可能に、また、任意のサーバーに対する TCP 443 番ポートへの通信を許可してください。

5.7. NEX の通信におけるデータの正当性について

NEXでは、サーバーサービス・P2Pのどちらの通信においても改竄検知のため、「データのペイロードにセッション毎に異なる一時鍵等を付加したハッシュ値」をパケットに付与しています。 ここで言う「セッション毎」とは、サーバーサービスであればゲームサーバーへのログイン毎、P2Pであればマッチメイクセッション毎です。 また、ローカル通信の場合は、アプリケーション固有となります。 アプリケーション側でCRCチェック用のデータを付与する必要性はありません。ただし、アプリケーション側がハックされてしまう危険性を考慮してデータの範囲チェック等は別途行うようにして下さい。

データストアがデータのダウンロード・アップロードする場合は上記の通信ではなく、HTTPS通信を使用します。 通信経路上で改竄された場合、HTTPSの受信・送信処理でエラーが発生することになります(APIは通信エラーを返します)。

またNEXでは、受信したデータから想定されるハッシュ値が異なる場合、そのパケットをライブラリ内で破棄します。 不正なパケットがアプリケーション側に渡されることはありません。Reliable通信の場合は送信元に対してACKパケットを返しませんので、送信元がライブラリ内で自動的に再送することになります。 これは、通信経路上でパケットがドロップされたのと同じ挙動になります。 改竄されるとパケットを受信できなくなりますので、100%改竄された場合には、APIがタイムアウトするか、接続処理が失敗、もしくは、通信が切断されます。 アプリケーション側では通信経路上でのパケットの改竄について考慮する必要はありません。

5.8. NEX サーバサービスで提供される各種 ID のユニーク性について

NEX サーバサービスで発行する各種 ID (NEX ユニーク ID, セッション ID, ギャザリング ID, データ ID 等)にはゲームサーバーを跨いだユニーク性はありません。 環境、ゲームサーバー ID が異なる場合には、これらの ID は重複する可能性があります。例えば、開発環境で発行された NEX ユニーク ID は、ロットチェック環境の NEX ユニーク ID と重複します。 独自サーバを使用する場合等これらの ID がユニークであることを想定した設計にはしないでください。

また、ギャザリング ID は 0 から始まり、マッチメイクセッションや永続ギャザリング、公式永続ギャザリングを作成する毎にインクリメントされた値が付与されます。そのため、異なるマッチメイクセッションや永続ギャザリング間であってもギャザリング ID が重複することはありません。