[TIL] Relay와 Transport

muchan
muchan
∙∙조회 5

Relay가 필요한 이유

대부분의 클라이언트는 공인 IP 없이 공유기 뒤 사설 IP만 가지고 있다. 그래서 상대방이 어디로 접속해야 할지 알 수가 없다. 게다가 방화벽은 기본적으로 inbound(들어오는) 연결을 막아버리기 때문에, 일반 유저에게 포트포워딩을 요구할 수도 없는 노릇이다.

이 문제를 해결하는 방식이 Relay다. 두 클라이언트가 직접 연결(P2P)하는 대신, 양쪽 모두 outbound(나가는) 방향으로 Relay 서버에 먼저 접속하고, 이후 모든 트래픽은 Relay가 중계해준다. Outbound는 방화벽이 기본적으로 허용하기 때문에 이 방식이 가능한 것이다.

물론 트레이드오프도 있다. A→Relay→B로 우회하다 보니 지연이 늘어날 수 있어서, 가까운 리전을 선택하는 게 중요하다. 그리고 Relay는 어디까지나 연결을 중계해주는 "우체국" 역할일 뿐, 게임 로직이나 권한은 여전히 host가 담당한다.

용어 정리

  • 방화벽: 네트워크 트래픽을 규칙에 따라 필터링하는 보안 문지기
  • Inbound/Outbound: 각각 들어오는/나가는 트래픽 방향
  • 포트포워딩: 공유기에 "이 포트로 들어오는 트래픽은 내 PC로 전달해줘"라고 수동 설정하는 것. 설정이 번거롭고 보안 구멍이 생길 수 있어서 일반 유저에게 요구하기 어렵다.

Transport란?

Transport는 흐름이 아니라 실제로 패킷을 주고받는 통신 계층(컴포넌트) 자체다.

  • NGO(Netcode for GameObjects): 게임 로직 담당 — "이 데이터를 상대한테 보내야 해" (뭘 보낼지 결정)
  • Transport (UnityTransport): 실제 전송 담당 — "그 데이터를 네트워크 소켓에 실어서 전송" (어떻게 보낼지 담당)

즉 Transport는 순서가 있는 절차가 아니라 하나의 컴포넌트/부품이다.

Relay는 "어디로 연결할지" 정보를 제공하고, Transport는 그 정보를 받아서 실제로 데이터를 전송하는 담당자다. 둘 다 전체 흐름의 구성요소일 뿐, Transport 자체가 흐름은 아니다.

Allocation과 JoinCode

  • Allocation: 호스트가 Relay에 "내 방용 중계 슬롯 잡아줘"라고 요청한 결과물. maxConnections(최대 접속자 수)를 지정해서 생성한다.
  • JoinCode: 그 Allocation에 들어올 수 있는 짧은 공유용 코드. 호스트가 발급하고 채팅/음성 등 게임 밖 수단으로 클라이언트에게 전달한다.
  • 총 인원 = maxConnections + 1 (호스트를 제외한 추가 인원 수라서)

선행 조건: Auth

Relay 호출 전에 UGS 초기화 + 익명 로그인이 끝나 있어야 한다. 안 하면 보통 403 에러가 뜬다.

csharp
await UnityServices.InitializeAsync();
if (!AuthenticationService.Instance.IsSignedIn)
    await AuthenticationService.Instance.SignInAnonymouslyAsync();

로컬에서 host/client를 한 머신에서 같이 테스트할 때는 주의가 필요하다. 익명 신원이 per-device + per-profile로 캐시되기 때문에, 프로필을 분리하지 않으면 같은 신원으로 묶여버린다.

수동 흐름 (Relay + UTP 와이어링)

호스트 쪽:

csharp
Allocation alloc = await RelayService.Instance.CreateAllocationAsync(maxConnections);
string joinCode = await RelayService.Instance.GetJoinCodeAsync(alloc.AllocationId);

var utp = NetworkManager.Singleton.GetComponent<UnityTransport>();
utp.SetRelayServerData(AllocationUtils.ToRelayServerData(alloc, "dtls"));
NetworkManager.Singleton.StartHost();

클라이언트 쪽:

csharp
JoinAllocation alloc = await RelayService.Instance.JoinAllocationAsync(joinCode);

var utp = NetworkManager.Singleton.GetComponent<UnityTransport>();
utp.SetRelayServerData(AllocationUtils.ToRelayServerData(alloc, "dtls"));
NetworkManager.Singleton.StartClient();

주의할 점 하나: Relay를 쓸 때는 SetConnectionData(IP/port 직접 입력)를 절대 같이 쓰면 안 된다. 섞어 쓰면 합류가 실패한다.

connectionType (dtls / udp / wss)

호스트와 클라이언트가 반드시 동일한 값을 써야 한다.

타입특징주 대상
dtlsUDP + 암호화데스크톱/모바일 (사실상 기본값처럼 씀)
udp평문, 암호화 없음오버헤드를 줄이고 싶을 때
wssWebSocket SecureWebGL/웹 빌드는 사실상 필수

자동으로 채워주는 디폴트는 없기 때문에 직접 명시해야 한다.