Analyzing Disconnects
當您建立一個在線多人遊戲時,您必須意識到,有時客戶端和伺服器之間的連接會失敗。
斷線可能是由軟件或硬件造成的。
如果連接的任何一個環節出現故障,信息會被延遲、丟失或損壞,連接需要被關閉。
如果這種情況經常發生,您可以做一些處理。
斷開連接的原因
客戶端SDK提供斷開連接的回調和斷開連接的原因。
使用這些來調查您所遇到的意外斷開連接的情況。
這裡我們列出了主要的斷開原因,以及它們是在客戶端還是在伺服器端造成。
由客戶端引起的斷開連接
- 客戶端超時:沒有/過晚收到伺服器的ACK。詳情見"超時斷連"。
- 客戶端套接字異常(連接遺失)。
- 客戶端連接在接收時失敗(緩沖區滿,連接遺失)。參見"流量問題和緩沖區滿"。
- 客戶端連接在發送時失敗(緩沖區滿,連接遺失)。參見"流量問題和緩沖區滿"。
由伺服器斷開連接
超時斷開連接
Unlike plain UDP, Photon's reliable UDP protocol establishes a connection between server and clients:
Commands within a UDP package have sequence numbers and a flag if they are reliable.
If so, the receiving end has to acknowledge the command.
Reliable commands are repeated in short intervals until an acknowledgement arrives.
If it does not arrive, the connection is timed out.
Both sides monitor this connection independently from their perspective.
Both sides have their rules to decide if the other is still available.
If a timeout is detected, a disconnect happens on that side of the connection.
As soon as one side thinks the other side does not respond anymore, no message is sent to it.
This is why timeout disconnects are one sided and not synchronous.
檢查您所發送的數據量。
如果出現峰值或您的信息/秒率非常高,這可能會影響連接質量。
閱讀"少發送"檢查您是否能在其他硬件和其他網絡上重現這個問題。
請看"嘗試另一個連接"。您可以調整重發的數量和時間。
見"調整重發"。
。如果您正在制作一個移動應用程序,請閱讀移動背景應用程序。
如果您想用斷點和所有的東西來調試您的遊戲,閱讀這個。
流量問題和緩沖區已滿
Photon伺服器和客戶端通常會在一些命令真正被放入軟件包並通過互聯網發送之前進行緩沖。
這使得我們可以將多個命令匯總到(較少的)包中。
如果某些方面產生了大量的命令(例如,通過發送大量的大事件),那麼緩沖區可能會用完。
填充緩沖區也會造成額外的滯後。
您會注意到,事件需要更長的時間才能索取另一方。
操作響應沒有平時那麼快。
閱讀"Send Less"。
第一項援助
檢查日誌
這是您需要做的第一個檢查。
所有的客戶端都有一些回調,以提供關於內部狀態變化和問題的日誌信息。
您應該記錄這些信息,並在出現問題時訪問它們。
如果沒有什麼有用的東西出現,您通常可以在一定程度上增加日誌記錄。
查閱API參考資料,了解如何做到這一點。
如果您定制了伺服器,檢查那裡的日誌。
啟用記錄器
The SupportLogger
is a tool that logs the most commonly needed info to debug problems with Photon, like the (abbreviated) AppId, version, region, server IPs and some callbacks.
{% if PUN_v1 %}
In PUN Classic, the SupportLogger is a component.
Create an empty GameObject in a scene that's enabled before you connect.
Add the SupportLogger component and you're done.
The console or log file will contain the new log entries.
In PUN 2, the SupportLogger can be enabled with a checkbox in the PhotonServerSettings.
The console or log file will contain the new log entries.
嘗試另一個項目
所有Photon的客戶端SDK都包括一些範例程序。
在您的目標平台上使用其中的一個。
如果範例也失敗了,那麼更可能是連接的問題。
嘗試其他伺服器或地區
使用Photon Cloud,您也可以輕鬆使用另一個地區。
自己主持?比起虛擬機,更喜歡物理機。
用靠近伺服器的客戶(但不在同一台機器或網絡上)測試最小滯留(往返時間)。
考慮在靠近客戶的地方增加伺服器。
嘗試另一種連接
在某些情況下,特定的硬件會使連接失敗。
嘗試另一個WiFi、路由器等。
檢查另一個設備是否運行得更好。
嘗試其他端口
從2018年初開始,我們在所有Photon Cloud部署中支援一個新的端口范圍。
不再使用5055到5058,而是從27000開始。
改變端口聽起來不應該有什麼不同,但它可以產生非常正向的影響。
到目前為止,反饋皆很正向。
在PUN中,您可以在連接前設置PhotonNetwork.UseAlternativeUdpPorts = true
。
啟用 CRC 檢測
有時,軟件包在客戶端和伺服器之間的路上會被損壞。
當路由器或網絡特別繁忙時,這種情況更容易發生。
有些硬件或軟件有明顯的錯誤,損壞可能隨時發生。
Photon有一個可選的CRC檢測。
由於這需要一定的性能,我們沒有默認激活。
您在客戶端啟用CRC檢測,但是當您這樣做的時候,伺服器也會發送一個CRC。
在連接前設置PhotonNetwork.CrcCheckEnabled = true
。
{% endif %}
Photon客戶端會追蹤有多少包裹因啟用CRC檢測而被丟棄。
您可以監控 "Photon.LoadBancingPer.CrcEnabled"。
您可以監控PhotonNetwork.PacketLossByCrcCheck
。
微調
檢測流量統計
在一些客戶端平台上,您可以直接在Photon中啟用 Traffic Statistics
。
這些數據可以跟蹤各種重要的性能指標,並且可以很容易地記錄下來。
在C#中,流量統計可以在LoadBalancingPeer類中作為 TrafficStatsGameLevel
屬性使用。
這提供了一個最有趣值的概述。
例如,使用TrafficStatsGameLevel.LongestDeltaBetweenDispatching
來檢測連續的DispatchIncomginCommands
調用之間最長的時間。
如果這個時間超過幾毫秒,您可能有一些局部滯留。
檢測LongestDeltaBetweenSending
以確保您的客戶端頻繁發送。
TrafficStatsIncoming
和TrafficStatsOutgoing
屬性提供了更多的輸入和輸出字節、命令和整包的統計數據。
調整再發送
{% if PUN%}}PUN有兩個屬性,可以讓您對其進行管理。
PUN有兩個屬性,允許您調整重發時間:
QuickResends
PhotonNetwork.QuickResends
可以加速重復那些沒有被接收端確認的可靠命令。
其結果是,如果某些信息被丟棄,則會有更多的流量,但延遲時間更短。
MaxResendsBeforeDisconnect
PhotonNetwork.MaxResendsBeforeDisconnect
定義了客戶端重復單個可靠信息的頻率。
如果客戶端重復的速度更快,它也應該重復的更頻繁。
在某些情況下,當設置PhotonNetwork.QuickResends
為3,PhotonNetwork.MaxResendsBeforeDisconnect
為7時,您會看到更好的效果。
{% endif %}
不過更多的重復並不能保証更好的連接,但會允許更長的延遲。
檢測重發的可靠命令
您應該開始監控ResentReliableCommands
。
這個計數器在每次重發可靠命令時都會上升(因為伺服器的確認沒有及時到達)。
C#
PhotonNetwork.ResentReliableCommands
如果這個值過高,說明連接不穩定,UDP數據包不能正常通過(任何來源)。
少發送
You can usually send less to avoid traffic issues.
Doing so has a lot of different approaches:
Don't Send More Than What's Needed
Exchange only what's totally necessary.
Send only relevant values and derive as much as you can from them.
Optimize what you send based on the context.
Try to think about what you send and how often.
Non critical data should be either recomputed on the receiving side based on the data synchronized or with what's happening in game instead of forced via synchronization.
Examples:
In an RTS, you could send "orders" for a bunch of units when they happen.
This is much leaner than sending position, rotation and velocity for each unit ten times a second.
Good read: 1500 archers.In a shooter, send a shot as position and direction.
Bullets generally fly in a straight line, so you don't have to send individual positions every 100 ms.
You can clean up a bullet when it hits anything or after it travelled "so many" units.
No need to instantiate and destroy each bullet.Don't send animations. Usually you can derive all animations from input and actions a player does.
There is a good chance that a sent animation gets delayed and playing it too late usually looks awkward anyways.Use delta compression. Send only values when they changes since last time they were sent.
Use interpolation of data to smooth values on the receiving side.
It's preferable over brute force synchronization and will save traffic.
Don't Send Too Much
Optimize exchanged types and data structures.
Examples:
Make use of bytes instead of ints for small ints, make use of ints instead of floats where possible.
Avoid exchanging strings at all costs and prefer enums/bytes instead.
Avoid exchanging custom types unless you are totally sure about what get sent.
Typically avoid exchanging a
Vector3
for rotation of your character as most likely it only rotates around the vertical axis.
So make extra code to extract the vertical rotation and only send this you save 2/3 of the size just for that parameter.Don't try to combine data into arrays.
Split everything into individual usingstream.SendNext()
for every single variable.Be careful of over-using Custom Properties.
If you set a lot of them in a long running game, joining players will have a lot to catch up with.
When your clients drop a lot while joining a room, check this.
Use another service to download static or bigger data (e.g. maps).
Photon is not built as content delivery system.
It's often cheaper and easier to maintain to use HTTP-based content systems.
Anything that's bigger than the Maximum Transfer Unit (MTU) will be fragmented and sent as multiple reliable packages (they have to arrive to assemble the full message again).
Don't Send Too Often
Lower the send rate, you should go under 10 if possible.
This depends on your gameplay of course.
This has a major impact on traffic.
You can also use adaptive or dynamic send rate based on the user's activity or the exchanged data, this is also helping a lot.Send unreliable when possible.
You can use unreliable messages in most cases if you have to send another update as soon as possible.
Unreliable messages never cause a repeat.
Example: In an FPS, player position can usually be sent unreliable.
嘗試降低MTU
通過在客戶端的設置,您可以強迫伺服器和客戶端使用比平時更小的最大包。
降低MTU意味著您需要更多的包來發送一些信息,但如果沒有其他幫助的話,試試這個是有意義的。
這樣做的結果是未經驗証的,我們希望聽到您的意見,如果這能改善情況。
設置 PhotonNetwork.NetworkingClient.LoadBalancingPeer.MaximumTransferUnit = 520;
。
工具
Wireshark
這個網絡協議分析器和記錄器對於發現您的遊戲的網絡層到底發生事件非常有用。
有了這個工具,我們可以看清事狀況(網絡方面的)。
Wireshark可能有點嚇人,但當我們要求您記錄我們遊戲的流量時,您只需要做一些設置。
安裝並開始。
第一個工具欄的圖標將打開(網絡)接口的列表。
您可以勾選有流量的接口旁邊的方框。
如有疑問,可記錄一個以上的接口。
接下來,點擊 "選項"。
我們不想要您所有的網絡流量,所以您必須為每個被選中的接口設置一個過濾器。
在下一個對話框("捕獲選項")中,找到選中的接口並雙擊它。
這將打開另一個對話框 "接口設置"。
在這裡您可以設置一個過濾器。
記錄任何與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)
當您按下 "開始 "時,當您連接時就會開始記錄。
在您重現一個問題後,停止日誌記錄(第三個工具欄按鈕)並保存它。
在最好的情況下,您還包括對您所做工作的描述,如果錯誤經常發生,在這種情況下發生的頻率和時間(日誌中有時間戳)。
也要附上客戶控制台的日誌。
把.pcap
和其他文件郵寄給我們,我們會查看。
平台特定信息
Unity
PUN在間隔時間內為您實現了Service
調用。
然而,Unity不會在加載場景和資產時或在您拖動一個獨立玩家的窗口時調用Update
。
為了在加載場景時保持連接,您應該設置PhotonNetwork.IsMessageQueenRunning = false
。
暫停消息列有兩個效果。
- 一個後台線程將被用來調用
SendOutgoingCommands
,而Update
不被調用。
這將保持連接的活力,只發送確認,但不發送事件或操作(RPC或同步更新)。
傳入的數據不會被這個線程執行。 - 所有傳入的更新都是排隊的。既不調用RPC,也不更新觀察對象。
當您改變級別時,這就避免了在前一個級別中調用RPC。
如果您使用我們的Photon Unity SDK,您可能在一些MonoBehaviour Update
方法中進行Service
調用。
為了確保Photon客戶端的SendOutgoingCommands
在您加載場景時被調用,請實現一個後台線程。
這個線程應該在每次調用之間暫停100或200毫秒,所以它不會奪走所有的性能。
##從意外的斷開連接中恢復過來
Disconnects will happen, they can be reduced but they can't be avoided.
So it's better to implement a recovery routine for when those unexpected disconnects occur especially mid-game.
When To Reconnect
First you need to make sure that the disconnect cause can be recovered from.
Some disconnects may be due to issues that cannot be resolved or bypassed by a simple reconnect.
Instead those cases should be treated separately and handled case by case.
Quick Rejoin (ReconnectAndRejoin)
Photon client SDKs offer a way to rejoin rooms as soon as possible after being disconnected while joined to a room.
This is called "Quick Rejoin".
Photon client locally caches the authentication token, the room name and the game server address.
So when disconnected mid-game, the client can do a shortcut: connect directly to the game server, authenticate using the saved token and rejoin the room.
In PUN, this is done using PhotonNetwork.ReconnectAndRejoin()
.
Check the return value of this method to make sure the quick rejoin process is initiated.
In order for the reconnect and rejoin to succeed, the room needs to have PlayerTTL != 0.
But this is not a guarantee that the rejoin will work.
If the reconnection and authentication is successful, rejoin can fail with one of the following errors:
- GameDoesNotExist (32758): the room was removed from the server while disconnected. This probably means that you were the last actor leaving the room when disconnected and that 0 <= EmptyRoomTTL < PlayerTTL or PlayerTTL < 0 <= EmptyRoomTTL.
- JoinFailedWithRejoinerNotFound (32748): the actor was removed from the room while disconnected. This probably means that PlayerTTL is too short and expired, we suggest at least a value of 12000 milliseconds to allow a quick rejoin.
- PluginReportedError (32752): this probably means that you use webhooks and that PathCreate returns ResultCode other than 0.
- JoinFailedFoundActiveJoiner (32746): this is very unlikely to happen but it may. It means that another client using the same UserId managed to rejoin the room while you were disconnected.
You can catch these in the OnJoinRoomFailed
callback.
Reconnect
If the client got disconnected outside of a room or if quick rejoin failed (ReconnectAndRejoin
returned false) you could still do a Reconnect only.
The client will reconnect to the master server and reuse the cached authentication token there.
In PUN, this is done using PhotonNetwork.Reconnect()
.
Check the return value of this method to make sure the quick rejoin process is initiated.
It could be useful in some cases to add:
- check if connectivity is working as expected (internet connection available, servers/network reachable, services status)
- reconnect attempts counter: max. retries
- backoff timer between retries
Sample (C#)
C#
using Photon.Pun;
using UnityEngine;
using Photon.Realtime;
public class RecoverFromUnexpectedDisconnectSample : MonoBehaviourPunCallbacks
{
public override void 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 (!PhotonNetwork.ReconnectAndRejoin())
{
Debug.LogError("ReconnectAndRejoin failed, trying Reconnect");
if (!PhotonNetwork.Reconnect())
{
Debug.LogError("Reconnect failed, trying ConnectUsingSettings");
if (!PhotonNetwork.ConnectUsingSettings())
{
Debug.LogError("ConnectUsingSettings failed");
}
}
}
}
}
Back to top