연결해제 분석

온라인 멀티 플레이어 게임을 구축할 때, 클라이언트와 서버 간의 연결이 가끔 끊어질 수 있다는 것을 염두에 두어야 합니다.

연결이 끊기는 원인은 소프트웨어 또는 하드웨어의 원인 일 수 있습니다. 연결이 끊기거나, 메시지가 지연거나 없어지거나 데이터가 이상해지면 연결을 셧다운 해야 할 수도 있습니다.

이런 상항이 자주 발생하면 무엇인가를 해주어야 합니다.

연결해제 원인

클라이언트가 연결은 전혀 할 수 없을 때에는 추가적인 요인이 있습니다(예, 서버에 접속할 수 없는 경우, 잘못된 서버 주소, DNS 사용 불가, 자체 호스팅 된 서버가 실행되지 않는 경우 등). 이 맥락에서는, 연결 해제로 간주되는 것이 아닌 '(초기) 연결 실패'로 간주됩니다.

클라이언트 SDK는 연결 해제 콜백과 연결 해제 원인이 제공됩니다. 이것을 이용하여 예상하지 못하게 발생된 연결 해제를 조사하게 됩니다. 다음은 연결 해제 원인의 주요 목록이며, 클라이언트 측 또는 서버 측의 원인입니다.

클라이언트에 의한 연결 해제

서버에 의한 연결 해제

  • 서버 측 타임아웃: 클라이언트에서 응답이 없거나 너무 늦은 ACK. 상세 내용은 "타임아웃 연결 해제" 를 참고하세요..
  • 서버 버퍼 풀 전송 (너무 많은 메시지). "트래픽 이슈와 버퍼 풀" 참조.
  • 라이선스 또는 구독 CCU 한도 초과.

타임아웃 연결해제

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 times 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 서버와 클라이언트는 명령어를 패키지에 넣고 인터넷을 통해 전송하기 전에 버퍼링 합니다. 이를 통해 여러개의 명령어를 통합하여(더 적게) 패키지에 넣을 수 있습니다.

어떤 한 측이 명령어가 많다면(예, 수많은 큰 이벤트의 전송), 버퍼가 모자를 수 있습니다.

버퍼를 채우는 것은 추가적인 지연 원인이 될 수 있습니다: 반대편에 이벤트 도달 속도가 더 오래 걸린다는 것을 알 수 있을 것입니다. 명령 응답은 평상시보다 빠르지 않게 됩니다.

"적게 보내기"를 읽어보세요.

응급 처치

로그 확인

가장 첫 번째로 확인해야 할 부분입니다.

모든 클라이언트는 내부 상태 변화와 이슈에 대한 로그 메시지를 제공하기 위한 콜백을 가지고 있습니다. 이 메시지를 로깅 해야 하고 문제 발생 시 접근해야 합니다.

필요한 정보가 없는 경우, 로깅 등급을 증가시킬 수 있습니다. 로깅 등급을 올리는 것을 API 레퍼런스를 확인해보십시오.

서버를 커스터마이징한 경우, 거기에서 로그를 확인해보세요.

SupportLogger 사용

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.

For Unity, the SupportLogger is a MonoBehaviour. When not using PUN, you can add this component to any GameObject. In your code, find the component (or reference it) to assign the LoadBalancingClient as Client. Call DontDestroyOnLoad() for the GameObject, if you switch scenes.

Outside of Unity, the SupportLogger is a regular class. Instantiate it and set the LoadBalancingClient, to make it register for callbacks. The Debug.Log method(s) get mapped to System.Diagnostics.Debug respectively.

다른 프로젝트 시도해보기

모든 Photon 용 클라이언트 SDK에는 일부 데모가 포함되어 있습니다. 타겟 플랫폼에 맞는 하나를 사용하세요. 데모도 역시 실패한다면, 연결에 대한 이슈일 가능성이 큽니다.

다른 서버 또는 지역 시도

Photon Cloud를 사용하면, 다른 지역을 쉽게 사용할 수 있습니다.

자체 호스팅을 하시나요? 가상 머신보다 물리 머신이 더 좋습니다. 서버와 가까이 있는(동일 머신 또는 동일 네트워크는 아님) 클라이언트로 최소 지연(라운드-트립 타임) 테스트하세요. 고객과 가까이 있는 곳에 서버 추가를 생각해보세요.

다른 연결 시도

어떤 경우에 있어서는, 특정 하드웨어가 연결 실패를 야기할 수 있습니다. 다른 WiFi, 라우터 등을 이용해보세요. 어떤 기기가 더 잘 동작하는지 확인해보세요.

다른 포트 시도

2018년도 초반부터, 모든 Photon Cloud 디플로이에 새로운 포트-범위를 제공했습니다: 5055에서 5058까지 사용하는 대신, 포트는 27000부터 시작합니다.

포트 변경으로 차이점이 없을 것 같지만, 매우 긍정적인 효과가 있습니다. 지금까지, 피드백은 정말 긍정적이었습니다.

일부 클라이언트 SDK에서, 서버로부터 나오는 주소-문자열에 있는 번호를 교체 해야할 수 도 있습니다. 네임 서버는 27000(이전 5058) 포트, 마스터 서버 27001(이전 5055)이며 게임 서버는 27002(이전 5056) 포트입니다. 이것은 간단한 문자열 변경으로 수행될 수 있습니다.

CRC Checks 사용하기

때로는 클라이언트와 서버 간 전송 시에 패키지가 변질될 수 있습니다. 라우터 또는 네트워크가 Busy 할 때 많이 발생합니다. 일부 하드웨어 또는 소프트웨어가 명백한 버그로 인한 변질로 인하여 언제든지 발생할 수 있습니다.

Photon에는 추가적인 패키지의 CRC 체크 기능이 있습니다. 이것으로 인하여 부하가 걸릴 수 있으므로 디폴트로는 활성화해 놓지는 않았습니다.

클라이언트에서 CRC 체크를 사용하도록 하지만 서버는 이렇게 하면 CRC를 보내 줄 것입니다.

C#

loadBalancingClient.LoadBalancingPeer.CrcEnabled = true

Photon 클라이언트는 CRC 체크 사용으로 해놓았기 때문에 얼마나 많은 패키지가 누락되었는지 추적할 수 있습니다.

체크:

C#

LoadBalancingPeer.PacketLossByCrc

미세 튜닝

트래픽 통계 확인

일부 클라이언트 플랫폼에서는 Photon에서 직접적으로 Traffic Statistics를 사용하도록 할 수 있습니다. 이러한 추적 자료는 성능의 중요한 지표이며 쉽게 기록할 수 있습니다.

C#에서는 트래픽 통계들은 LoadBalancingPeer 클래스의 TrafficStatsGameLevel 프로퍼티에서 사용 할 수 있습니다. 가장 관심 있는 값들에 대한 개요를 제공합니다.

예를 들어, 연속된 DispatchIncomginCommands 호출 시간 중 가장 긴 시간을 체크하기 위해서 TrafficStatsGameLevel.LongestDeltaBetweenDispatching을 사용합니다. 만약 이 시간이 수 밀리 세컨드보다 크면 로컬 지연이 있을 가능성이 있습니다. LongestDeltaBetweenSending 을 검토하여 클라이언트가 자주 전송하고 있는지 확인하시기 바랍니다.

TrafficStatsIncomingTrafficStatsOutgoing 프로퍼티들은 수신/발신 바이트, 커맨드와 패키지에 대한 통계를 제공합니다.

Tweak 재전송

C#/.Net Photon 라이브러리에는 재전송 타이밍을 조정할 수 있는 두 가지 속성이 있습니다:

PhotonPeer.QuickResendAttempts

LoadBalancingPeer.QuickResendAttempts는 수신 측에서 확인하지 못한 신뢰할 수 있는 명령어 반복 속도를 올려줍니다. 그 결과 일부 메시지가 삭제될 경우 더 짧은 지연을 위해 트래픽이 약간 더 많아집니다.

PhotonPeer.SentCountAllowance

기본적으로, Photon 클라이언트는 신뢰할 수 있는 명령어를 6번까지 전송합니다. 5번 재전송해도 ACK가 없는 경우, 연결은 해제됩니다.

LoadBalancingPeer.SentCountAllowance는 클라이언트가 개별적, 신뢰할 수 있는 메시지를 얼마나 자주 반복할지를 정의합니다. 클라이언트가 더 빨리 반복한다면, 더 자주 반복해야 합니다.

일부 경우에 있어서, 설정을 QuickResendAttempts을 3으로 SentCountAllowance는 7로 설정할 때 좋은 효과를 볼 수 있습니다.

그러나 반복 횟수가 많을수록 연결 상태가 개선되지 않으며 지연 시간이 길어집니다.

Check Resent Reliable Commands

ResentReliableCommands의 모니터링을 시작해야 합니다. 이 카운터는 신뢰 커맨드가 재전송 될 때마다 증가합니다(서버로 부터 ACK 가 제시간에 도착하지 않았기 때문에).

C#

LoadBalancingPeer.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.

  • 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.

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를 낮추는 것은 메시지에 대해 더 많은 패키지를 보내야 한다는 것을 의미하지만 다른 방법으로 해결되지 않을 때는 시도해 볼 만합니다.

이 결과는 아직 검증되지 않았으므로 이것을 통해 향상이 된 경우라면 연락 주시면 감사하겠습니다.

C#

loadBalancingClient.LoadBalancingPeer.MaximumTransferUnit = 520;

Wireshark

네트워크 프로토콜 분석기 및 로거로써 게임의 네트워크 레이어에서 어떤 일이 발생하고 있는지 알아볼 때 매우 유용합니다. 이 툴로 네트워킹 측면의 사실들을 볼 수 있습니다.

Wireshark는 복잡해 보이지만 게임의 트래픽의 로그를 기록할 때 몇 개만 설정하면 됩니다.

설치하고 시작합니다. 첫 번째 툴바 아이콘이 (네트워크) 인터페이스 목록에서 오픈될 것입니다.

photonserversettings in inspector
Wireshark 툴바

트래픽을 가지고 있는 인터페이스 옆의 박스를 체크할 수 있습니다. 확신이 서지 않으면 하나 이상의 인터페이스를 기록하세요. 다음에 "Options"을 클릭합니다.

photonserversettings in inspector
Wireshark - Capture Interfaces

모든 네트워크 트래픽에 대한 것을 다루지 않으므로 체크된 인터페이스별로 필터를 설정해야 합니다. 다음 다이얼로그 ("Capture Options")에서 체크된 인터페이스를 찾아 더블 클릭하세요. 이렇게 하면 "Interface Settings" 다이얼로그가 열리게 됩니다. 여기에서 필터를 설정할 수 있습니다.

photonserversettings in inspector
Wireshark - 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"를 누를 때, 연결시에 기록이 시작됩니다. 이슈가 발생 한 후에, 기록을 멈추고(세 번째 툴바 버튼) 저장합니다.

오류가 발생한 상황, 오류가 주기적으로 발생했는지, 얼마나 자주 발생했고 언제 발생했는지(로그에 타임스탬프가 포함되어 있습니다)를 포함 시켜 주는 것이 가장 좋습니다. 클라이언트 콘솔 로그도 첨부해 주세요.

저희에게 .pcap 파일과 다른 파일을 메일로 주시면 검토하겠습니다.

플랫폼별 정보

Unity

PUN은 간격을 두고 Service 호출을 구현합니다.

하지만 Unity는 씬을 불러오는 동거나 독립-플레이어의 윈도우로 드래그하는 동안 Update를 호출하지는 않습니다.

씬을 로딩하는 동안 연결을 유지하기 위해서 PhotonNetwork.IsMessageQueueRunning = false로 설정해야 합니다.

메시지 큐를 잠시 중단하는 것은 2가지 효과가 있습니다:

  • Update가 호출되지 않는 동안 백그라운드 스레드가 SendOutgoingCommands를 호출하기 위해서 사용됩니다. 이것으로 연결이 유지되고, ACK를 전송하지만 이벤트나 동작은 전송하지 않습니다 (RPC 또는 동기화 업데이트). 이 스레드가 수신 데이터는 실행되지 않습니다.
  • 들어오는 모든 업데이트가 큐에 들어갑니다. RPC는 호출되지 않으며 관찰된 개체가 업데이트되지도 않습니다. 레벨을 변경하는 동안 이전 RPC의 호출이 방지됩니다.

Photon Unity SDK를 사용하신다면, MonoBehaviour Update 메소드에서 Service 호출을 하셨을 수도 있습니다.

씬을 로드하는 동안 Photon 클라이언트의 SendOutgoingCommands가 호출되도록 하려면 백그라운드 스레드를 구현합니다. 이 스레드는 호출 사이에 100ms 또는 200ms를 일시 중지해야 하므로 모든 성능이 저하되지는 않습니다.

예기치 않은 연결 해제로 부터의 복구

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)

A "Quick Rejoin" can be used when a client got disconnected while playing (in a session/room). In that case, the Photon client uses an existing Photon Token, room name and game server address and can get back into the room, even if the server did not notice the absence of this client (the player still being active).

In C# SDKs, this is done using LoadBalancingClient.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 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 but a different Photon token joined the room while a client was 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 C# SDKs, this is done using LoadBalancingClient.ReconnectToMaster(). 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 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
}
Back to top