[Unity] NTPにより同期した日時を管理する

オンラインゲームなどでサーバ側と通信する際、日時管理が必要になったりします。
クライアントで設定されている日時(DateTime.Now)は各端末によって少しずれていたり不正目的で意図的に変更されたりする可能性があるため、信頼すべきではありません。
サーバと同期した日時を各クライアント端末で扱えるようにしたほうが望ましいです。

今回はUnityで開発するゲームアプリ内部でサーバと同期した日時を管理する方法を考えてみました。

■実装方針
サーバとの時刻同期にはNTPが一般的に用いられています。
サーバ側で設定されている時刻を通信遅延を考慮してクライアント側で取得できます。
この仕組みを用いてNTPで同期した時刻をゲームアプリ内部で保持するようにします。

NTPはTCPではなくUDPを使用するため、ゲームアプリ側からUDP接続する必要があります。
しかし、UnityにはUDPでパケット通信するAPIが(私が調査した限り)見当たらなかったため、.NET Frameworkのソケット通信を使って実現します。

ソケット通信の初期化は以下のようになります。

そして、RFC-2030にしたがってリクエストデータをUdpClient.Send()でNTPサーバに送信します。

NTPサーバはご自身の環境に合わせてください。
リクエストの送信に成功するとサーバからタイムスタンプが格納されたレスポンスが返ってきます。
UdpClient.Receive()でレスポンスデータを受け取ります。

しかし、上記の処理には問題があります。
Send()やReceive()の通信処理は同期で行われるため、通信完了するまで処理が返ってこないことです。
フレーム処理内でこれをやってしまうとゲームアプリが一時的にフリーズしてしまいます。

したがって、非同期で行うようするのが望ましいでしょう。
UdpClientクラスにSendAsync()やReceiveAsync()等の非同期メソッドが用意されています。
しかし、これらは別スレッドで処理が行われるのでスレッドセーフを意識しなければなりません。

結果として、マルチスレッドを用いて通信する方法がUnityのフォーラムに書かれていたのでこちらを参考にさせていただきました。

■スクリプト
起動時にNTPでサーバ時刻を取得し、1秒毎に同期した時刻をログに出力するスクリプトです。

Start()内の初期化処理にてNTPのリクエストを送信して日時を受け取る処理を別スレッドで実行します。

そして、コルーチンで通信が完了するまで待機し、正常な日時を取得できたらバイナリ形式のタイムスタンプをDateTime型に変換して保存します。

タイムスタンプは8バイトのバイナリデータで1900年1月1日を基準に何秒経過したかを表す値となっています。
上位32ビットは秒、下位32ビットは秒の小数点部分です。(固定小数点)
ビッグエンディアンで格納されているので注意が必要です。

ここで取得した日時はあくまでも同期時の日時なので、後でサーバと同期した日時を知りたい場合は同期時からの経過時間を加算する必要があります。
UnityのTime.realtimeSinceStartupはゲーム起動時からの経過時間で且つ時間操作できないのでこれを用います。

スクリプトを実行すると以下のようにクライアントの設定日時によらない日時が取得できます。

unity-ntp

流れは以上です。

今回のソケット通信を用いた方法はiPhoneやAndroidの端末では正しく実行できるかどうか分かりません。
これについては機会が出来たら確認したいです。

■参考サイト
Unity – Unity Manual
RFC-2030 日本語訳
NonSoft – NTPサーバの現在日時をシステム時計に設定するサンプル(C#.NET)
How can I receive UDP packets into Unity Webplayer? – Unity Answers

LEAVE A REPLY

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.