接続が切断された場合の調査
オンラインマルチプレイヤーゲームを構築する際には、クライアントとサーバー間の接続が失敗する場合を想定しておく必要があります。
切断の原因は、ソフトウェアまたはハードウェアです。 接続のいずれかのリンクが失敗すると、メッセージの遅延や、欠落、破損が発生し、接続をシャットダウンしなければなりません。
切断が頻繁に起きる場合は、いくつか解決策を試してみてください。
切断の理由
クライアントSDKは切断コールバックの用意があり、切断の要因となります。
予期せぬ切断が発生した場合はこれらの調査を行ってください。
主な切断の要因を並べてみました。これらはクライアント側かサーバー側どちらでも発生します。
クライアントによる切断
- クライアント側のタイムアウト: サーバーからのACKがない、または遅すぎることが原因。詳細はタイムアウトによる切断を参照してください。
- クライアントソケットの例外 (接続が失われます).
- 受信時のクライアントの接続不良(バッファフル、接続ロス)。「トラフィックの問題とバッファフル」を参照してください。
- 送信時のクライアント接続不良(バッファフル、接続ロス)「トラフィックの問題とバッファフル」を参照してください。
サーバーによる切断
- サーバー側のタイムアウト: クライアントからのACKがない、または遅すぎることが原因。詳細はタイムアウトによる切断を参照してください。
- サーバーのバッファフル送信(メッセージ過多)。「トラフィックの問題とバッファフル(#traffic_issues_and_buffer_full」 を参照してください。
- ライセンス、または購入CCUの上限に到達。
タイムアウトによる切断
通常のUDPと異なり、PhotonのリライアブルなUDPプロトコルはサーバーとクライアント間の接続を確立します:
UDPパッケージ内のコマンドはリライアブルである場合、連番とフラグがあります。
その場合、受信側はコマンドに肯定応答する必要があります。
リライアブルなコマンドは、肯定応答が届くまで短いインターバルで繰り返されます。
肯定応答が届かなければ、接続はタイムアウトします。
両者がそれぞれの立場から接続を監視します。もう片方が使用可能かを決定するのにそれぞれにルールがあります。
タイムアウトが検出されると、接続のタイムアウトが検出された側で切断が発生します。
一方が、片方がもう応答しないと判断した場合、何のメッセージも送信されません。
タイムアウト切断が片方でのみ発生して同期的でないのはそのためです。
タイムアウトによる切断は「まったく」接続できない問題を除けば、もっとも多く発生する問題です。
頻繁にタイムアウトが発生する場合、障害の発生箇所は1つではありません。問題の原因として考えられるシナリオや、修正方法は複数あります。
簡単なチェックリストを以下に用意しました。
- そのゲームをバックグラウンドで実行しているか確認します。
- Unityのアプリは、ロード時に常にメインループを実行しているわけではありません。
- 他のハードウェアやネットワーク上でも同じ問題が再度発生するかどうか確認します。
「別の接続を試す」を参照してください。 - 送信データ量が急増したり、メッセージ/秒率が非常に高い場合には、接続のクオリティに影響をおよぼす影響があります。
「送信量を減らす」を参照してください。 - 再送信する数とタイミングを調整します。「再送信の調整」を参照してください。
- ブレイクポイントを使用してゲームのデバッグを行う場合、こちらを参照してください。
トラフィックの問題とバッファフル
通常Photonサーバーおよびクライアントはパッケージに実際に置かれてインターネット経由で送信される前に複数のコマンドをバッファします。
バッファフルの問題は、「メモリ不足」の問題に似ています。 Photon Serverとクライアントは、実際にパッケージされインターネット経由で送信される前に、通常はいくつかのコマンドをバッファします。
これにより複数コマンドを(より少ない)パッケージに集約できるようになります。
いずれかの側が大量のコマンドを発生した場合(たとえば、大きなイベントを大量に送信するなど)、バッファ不足になる可能性があります
バッファを充填すると、さらにラグが生じる原因になります: イベントが反対側に受信されるまでに、通常より長く時間がかかることがわかります。
また、オペレーションのレスポンスが通常よりも遅くなります。
「送信料を減らす」を参照してください。
緊急処置
ログを確認する
これは、最初に確認すべき事項です。
すべてのクライアントには、内部ステートの変化や問題についてのログメッセージを提供する、なんらかのコールバックがあります。
これらのメッセージのログを作成し、問題が発生した場合にはログにアクセスしてください。
有用な情報が表示されない場合には、ロギングを増加することができます。 ロギングを増加させる方法は、APIリファレンスを確認してください。
サーバーをカスタマイズしている場合には、そのサーバーのログを確認してください。
SupportLoggerを有効化する
SupportLogger
は 頻繁に必要とされる情報をログしてPhotonの問題をデバッグするツールです。(短縮した)AppId、バージョン、リージョン、サーバーIP、一部のコールバックなどがその例です。
Unityでは、SupportLoggerはMonoBehaviourとなります。
PUNを使用しない場合、このコンポーネントはあらゆるGameObjectに追加して、そのLoadBalancingClientに設定できます。
追加したGameObjectのDontDestroyOnLoad()
を呼び出します。
Unityの他では、SupportLoggerは通常のクラスとなります。インスタンス化してLoadBalancingClientを設定し、コールバックに登録します。Debug.LogメソッドはそれぞれSystem.Diagnostics.Debug
にマッピングされます。
別のプロジェクトを使用
PhotonのすべてのクライアントSDKには、複数のデモが含まれています。
これらの中から、対象となるプラットフォームのデモを使用してください。
デモが失敗する場合には、接続に問題が生じている可能性があります。
別のサーバーまたはリージョンを使用
Photon Cloudを使用している場合には、別のリージョンを簡単に使用できます。
独自にホスティングをおこなう際は、仮想マシンではなく物理マシンを使用しましょう。
サーバーに近いクライアント(ただし同一マシン上やネットワーク上ではなく)で最小ラグ(ラウンドトリップタイム)をテストしましょう。
顧客の近くにサーバーを追加する事も検討してみてください。
別の接続を使用
特定のハードウェアが原因で接続に失敗する場合があります。
他のWifiやルーターなどを試してみてください。
別のデバイスで作動状況が改善するか確認してください。
代替のポート番号を使用
弊社のデフォルトのUDPポート番号範囲(5055から5058)がブロックされる場合があるため、すべてのPhoton Cloudデプロイで代替の番号範囲をサポートしています:
5055から5058を使用するのではなく、ポート番号は27000から開始します。
これらの「代替ポート」への切替は簡単におこなえます。
Realtime APIでは、接続前にLoadBalancingClient.ServerPortOverrides = PhotonPortDefinition.AlternativeUdpPorts
を割り当てます。
Name Serverはポート番号 27000 (以前は5058)、Master Serverは27001 (以前は5055)、Game Serverは27002 (以前は5056)になりました。
{% endif %}
CRCチェックを有効化
クライアントとサーバー間でパッケージが破損する場合があります。 ルーターまたはネットワークが非常にビジーな状態の場合に、
こうした破損が生じる可能性が高まります。 またハードウェアやソフトウェアによってはバグが多く、破損が発生する可能性が十分にあります。
hotonにはオプションでパッケージごとのCRC Checkがあります。 パフォーマンス上の問題が生じるため、デフォルトでは有効化されていません。
ただし有効化した場合にはサーバーもCRCを送信します。
C#
loadBalancingClient.LoadBalancingPeer.CrcEnabled = true
Photonクライアントは、CRC Checkの有効化によって何個のパッケージが欠落したかをトラッキングします。
Check:
C#
LoadBalancingPeer.PacketLossByCrc
微調整
トラフィックの統計を確認する
一部のクライアントプラットフォームでは、Photonで直接Traffic Statistics
を有効化できます。
トラフィックの統計では、様々なパフォーマンスインジケーターがトラッキングされ、ログを容易に作成できます。
トラフィックの統計は、C#ではLoadBalancingPeerクラス内のTrafficStatsGameLevel
プロパティとして利用可能です。
このプロパティによって興味深い値を参照できます。
たとえば、TrafficStatsGameLevel.LongestDeltaBetweenDispatching
を使用して連続するDispatchIncomginCommands
コール間の最長の間隔を確認してください。
もしこの間隔が数ミリ秒以上ならば、ローカルラグが発生している可能性があります。
LongestDeltaBetweenSending
を参照して、クライアントが頻繁に送信しているか確認してください。
TrafficStatsIncoming
プロパティとTrafficStatsOutgoing
プロパティは、送受信されるbyteやコマンド、パッケージについてより詳細な統計を提供します。
再送信を微調整
C#/.Net Photonライブラリには再送信のタイミングを微調整するプロパティが2つあり、以下で説明しています。
PhotonPeer.QuickResendAttempts
LoadBalancingPeer.QuickResendAttempts
は受信側が確認できない高信頼性コマンドを速く繰り返します。
結果として、いくつかのメッセージが欠落した場合にトラフィックが少し増加し、遅延が短くなります。
PhotonPeer.SentCountAllowance
デフォルトでは、Photonクライアントはそれぞれの高信頼性コマンドを最大6回まで送信します。
5回目の再送信後にACKがない場合には、接続は切断されます。
LoadBalancingPeer.SentCountAllowance
は、クライアントが個々の高信頼性メッセージをどのくらいの頻度で繰り返すかを定義します。
繰り返しの速度が速いならば、繰り返しの頻度も上げる必要があります。
QuickResendAttempts
を3に、SentCountAllowance
を7に設定することで状況が改善する場合があります。
繰り返しを増やすことで接続状況が改善されるとは限りません。また、遅延がより長くなる可能性があります。
再送信された高信頼性コマンドを確認する
ResentReliableCommands
. ResentReliableCommands
の監視を開始する必要があります。
このカウンターは高信頼性コマンドが送信されるたびに増加します(サーバーからの確認が時間内に受信されなかったため)。
C#
LoadBalancingPeer.ResentReliableCommands
もしこの値が上限を超えると接続は不安定になり、パケットは正常に送信されません(どちらの方向にも)。
送信量を減らす
トラフィックの問題を避けるには、送信量を減らします。
送信量を減らすには、いくつかの方法があります:
必要以上に送信しない
必要な分だけ通信してください。
関連する値のみを送信し、それらの値から出来る限り派生させてください。
状況に応じて、送信するものを最適化してください。
何を送信するべきか、また送信頻度を考慮するようにしましょう。
重要でないデータは同期によって強制的に再計算される場合を除き、同期されたデータ、またはゲームの進行内容にもとづいて受信側で再計算されるべきではありません。
例:
- RTSでは、発生時に複数のユニット向けに「オーダー」を送信できます。
これは1秒間に10回の頻度で各ユニットに位置、回転、速度を送信するよりもはるかに効率的です。
1500 archersを参照してください。
シューティングゲームでは、発射は位置と方向として送信してください。
銃弾は通常、直線で飛びます。このため、100ミリ秒ごとにそれぞれの位置を送信する必要はありません。
銃弾が何かに当たった場合、または銃弾が「非常に多くの」ユニットを通過した後には、銃弾をクリーンアップすることができます。アニメーションは送信しないでください。通常は、プレイヤーの入力やアクションからすべてのアニメーションを派生することができます。
アニメーションを送信すると遅延が発生する可能性が十分にあり、プレイが遅延すると非常に不自然な印象を与えます。デルタ圧縮を使用してください。前回の送信から変更があった値のみを送信してください。
受信側での値を平滑化するため、データ補間を使用してください。
この方法は無理に同期をおこなうよりも望ましく、トラフィックを軽減します。
送信量を抑える
やり取りする型とデータ構造を最適化してください。
例:
- 小さな整数の場合には、intではなくbyteを使用してください。可能であれば、floatではなくintを使用してください。
- stringのやり取りを避け、なるべくenumやbyteを使用してください。
- 送信されるものが明確でない限り、カスタムの型をやり取りしないでください。
静的なデータ、またはよりサイズの大きなデータをダウンロードするには、別のサービスを使用してください(例:マップ)。
Photonはコンテンツ配信システムとして構築されていません。
HTTPベースのコンテンツシステムを使用したほうがコストを抑えられ、管理も容易です。
最大転送単位(MTU)よりも大きなものはすべて分割され、複数の信頼性の高いパッケージとして送信されます(完全なメッセージへと再構成する必要があります)。
送信頻度を抑える
- 送信レートを下げてください。できれば、10未満に下げることを推奨します。
この設定は、当然のことながらゲームプレイに応じて異なります。
この変更はトラフィックに大きく影響します。
ユーザーのアクティビティや、やり取りするデータにもとづいて、適応送信レートや動的送信レートを使用できます。この設定もトラフィックに大きく影響します。
- 可能な場合には信頼性を低くして送信してください。
新たなアップデートをすぐに送信する必要がある場合、通常は信頼性の低いメッセージを使用できます。
信頼性の低いメッセージは、繰り返しを発生しません。
例:FPSでは、通常プレイヤーの位置は信頼性を低くして送信されます。
より低いMTUを試す
クライアント側で設定すれば、通常よりも最大パッケージサイズを小さくしてサーバーやクライアントで使用することができます。
MTUを低くするとメッセージ送信の際により多くのパッケージが必要ですが、他の方法でも改善しない場合にはこの方法を試してみてください。
この方法の結果について弊社では確認できていませんので、状況が改善したかどうかぜひ弊社にお知らせください。
C#
loadBalancingClient.LoadBalancingPeer.MaximumTransferUnit = 520;
ツール
Wireshark
このネットワークプロトコルアナライザーおよびロガーは、ゲームのネットワークレイヤーの状況を把握するのに非常に有用です。
このツールを使用すると、ネットワークの観点から詳細な状況が分かります。
Wiresharkは複雑そうに思えますが、実際にはゲームのトラフィックのログは非常に簡単な設定で取得できます。
インストールして起動します。
最初のツールバーアイコンから(ネットワーク)インターフェースのリストを開きます。
トラフィックのあるインターフェースの横のボックスにチェックマークをつけます。
不明な場合には、複数のインターフェースのログを取得してください。
次に「Options」をクリックします。
すべてのネットワークトラフィックは必要ではないため、チェックマークをつけたインターフェースごとにフィルタを設定する必要があります。
次のダイアログボックス(「Capture Options」)で、チェックマークをつけたインターフェースをダブルクリックします。
「Interface Settings」ダイアログボックスが開きます。 ここでフィルタを設定できます。
Photonに関するログを取得するフィルタは以下の通りです:
Plain Old Text
(udp || tcp) && (port 5055 || port 5056 || port 5057 || port 5058 || port 843 || port 943 || port 4530 || port 4531 || port 4532 || port 4533 || port 9090 || port 9091 || port 9092 || port 9093 || port 19090 || port 19091 || port 19093 || port 27000 || port 27001 || port 27002)
「Start」を押すと、接続後にロギングが開始されます。 問題を再現できたらロギングを停止し(ツールバーの3番目のボタン)保存します。
何をおこなったのか、そしてエラーが定期的に発生する場合には発生頻度や発生日時(ログにはタイムスタンプがあります)も記載してください。
クライアントソースログも添付してください。
.pcap
やその他のファイルをメールで送信いただければ、弊社で調査をおこないます。
プラットフォーム固有の情報
Unity
PUNは、自動的に接続を保持します。これを実現するため、PUNはUnityのメインループでDispatchIncomingCommands
とSendOutgoingCommands
を呼び出します。
ただしUnityは、シーンやアセットをローディングしている際や、スタンドアロンプレイヤーのウィンドウをドラッグしている際には
Update
を呼び出しません。
シーンのローディング中も接続を維持するには、PhotonNetwork.IsMessageQueueRunning = false
を設定する必要があります。
メッセージキューを一時停止することで2つの効果があります:
- バックグラウンドスレッドは
SendOutgoingCommands
を呼び出し、Update
は呼び出されません。
これにより、イベントや操作(RPCまたは同期アップデート)は送信されず肯定レスポンスのみが送信され、接続が維持されます。 受信中のデータはこのスレッドによって実行されません。 - 受信中のアップデートは全てキューされます。RPCは呼び出されず、アップデートされたオブジェクトの監視もされません。 レベルを変更している間、メッセージキューを一時停止していると前のレベルでのRPC呼び出しを防ぎます。
Photon Unity SDKを使用している場合、おそらくMonoBehaviour Update
メソッドでService
コールを行うことになります。
シーンのローディング中、PhotonクライアントのSendOutgoingCommands
が確実に呼ばれるようにするため、バックグラウンドスレッドを実装します。
このスレッドが一時停止するのは、各呼び出し間で100~200ミリ秒なので、パフォーマンスがすべて失われることはありません。
予期せぬ切断からの復旧
切断は起こり得るもので、発生可能性を下げることはできますが不可避なものです。
特にゲームの最中などに予期せぬ切断が発生したときのために、リカバリールーチンを実装しておくのがいいでしょう。
再接続のタイミング
まず切断原因がリカバリ可能なものかどうかの確認をします。
ただ単に再接続することで解決したりバイパスしたりできない障害による切断の場合もあります。
そのような場合は個別にケースバイケースで処理を行う必要があります。
Quick Rejoin (ReconnectAndRejoin)
「Quick Rejoin」は、プレイ中に(セッション、またはルームで)クライアントが切断された場合に使用できます。この場合、サーバーがこのクライアントの不在に気づかなくても、Photonクライアントは既存のPhotonトークン、ルーム名、ゲームサーバーアドレスを使用し、ルームに戻ることができます(プレイヤーはアクティブのままです)。
C# SDKでは、LoadBalancingClient.ReconnectAndRejoin()
を使用して行われます。
quick rejoinプロセスが初期化されたかどうか確認するにはこのメソッドの戻り値をみます。初期化されている場合には、正常な再参加はルーム参加の通常のコールバックとともに完了します:OnJoinedRoom
。
再接続と再参加を正常に行うには、ルームにPlayerTTL != 0を備えておく必要があります。
ただし、これで必ず再参加がうまくいくと保証されるわけではありません。
再接続が正常な場合、再参加は以下のいずれかのエラーによって失敗する可能性があります:
- GameDoesNotExist (32758): 切断されている間にルームがサーバーから削除された。切断時にルームを退出した最後のアクターが自分でった可能性および、0 <= EmptyRoomTTL < PlayerTTL or PlayerTTL < 0 <= EmptyRoomTTLを示唆する。
- JoinFailedWithRejoinerNotFound (32748): 切断されている間にアクターがルームから削除された。PlayerTTLが短すぎて失効した可能性を示唆する。正常なquick rejoinのためには少なくとも12000ミリ秒の値にすることが推奨される。
- PluginReportedError (32752): webhooksが使用されていること、およびPathCreateが0ではなくResultCodeを返した可能性を示唆する。
- JoinFailedFoundActiveJoiner (32746): 発生可能性は低いがあり得ることで、自分が切断されている間に他のクライアントが同じUserIdを使用し、異なるPhotonトークンを使用してルームに参加した可能性を示唆する。
OnJoinRoomFailed
コールバックでキャッチできます。
再接続
クライアントがルーム外で切断された場合やquick rejoinに失敗 (ReconnectAndRejoin
がfalseを返す)した場合、Reconnectのみ行うことができます。
クライアントはマスターサーバーに再接続し、そこでキャッシュされた認証トークンを再利用します。
C# SDKではLoadBalancingClient.ReconnectToMaster()
を使用して行われます。
quick rejoinプロセスが初期化されたかどうか確認するにはこのメソッドの戻り値をみます。
以下を追加しておくと便利な場合があります。
- 接続が予測通り機能しているかどうか確認する(インターネット接続が使用できているか、サーバー・ネットワークに到達しているか、サービスの状況はどうか)
- 再接続試行カウンタ:最大再試行数
- 試行間のバックオフタイマー
サンプル (C#)
C#
using System;
using Photon.Realtime;
public class RecoverFromUnexpectedDisconnectSample : IConnectionCallbacks
{
private LoadBalancingClient loadBalancingClient;
private AppSettings appSettings;
public RecoverFromUnexpectedDisconnectSample(LoadBalancingClient loadBalancingClient, AppSettings appSettings)
{
this.loadBalancingClient = loadBalancingClient;
this.appSettings = appSettings;
this.loadBalancingClient.AddCallbackTarget(this);
}
~RecoverFromUnexpectedDisconnectSample()
{
this.loadBalancingClient.RemoveCallbackTarget(this);
}
void IConnectionCallbacks.OnDisconnected(DisconnectCause cause)
{
if (this.CanRecoverFromDisconnect(cause))
{
this.Recover();
}
}
private bool CanRecoverFromDisconnect(DisconnectCause cause)
{
switch (cause)
{
// the list here may be non exhaustive and is subject to review
case DisconnectCause.Exception:
case DisconnectCause.ServerTimeout:
case DisconnectCause.ClientTimeout:
case DisconnectCause.DisconnectByServerLogic:
case DisconnectCause.DisconnectByServerReasonUnknown:
return true;
}
return false;
}
private void Recover()
{
if (!loadBalancingClient.ReconnectAndRejoin())
{
Debug.LogError("ReconnectAndRejoin failed, trying Reconnect");
if (!loadBalancingClient.ReconnectToMaster())
{
Debug.LogError("Reconnect failed, trying ConnectUsingSettings");
if (!loadBalancingClient.ConnectUsingSettings(appSettings))
{
Debug.LogError("ConnectUsingSettings failed");
}
}
}
}
#region Unused Methods
void IConnectionCallbacks.OnConnected()
{
}
void IConnectionCallbacks.OnConnectedToMaster()
{
}
void IConnectionCallbacks.OnRegionListReceived(RegionHandler regionHandler)
{
}
void IConnectionCallbacks.OnCustomAuthenticationResponse(Dictionary<string, object> data)
{
}
void IConnectionCallbacks.OnCustomAuthenticationFailed(string debugMessage)
{
}
#endregion
}