> For the complete documentation index, see [llms.txt](/llms.txt)

# GS2-Realtime

리얼타임 통신 기능




게임 플레이어 간의 대전 기능을 구현하기 위해, 낮은 레이턴시로 높은 빈도의 통신이 필요한 경우에 사용할 수 있는 기능입니다.

GS2에서는 일반적으로 API 리퀘스트 횟수에 대해 요금이 발생하지만, 이 서비스는 게임 서버가 기동하면
해당 게임 서버의 가동 시간과 통신 용량에 대해 비용이 발생하며, 통신 횟수에 대해서는 비용이 발생하지 않습니다.

```mermaid
graph TD
  Match["GS2-Matchmaking 에서<br/>로비 성립"] --> Want["RoomWant 으로 룸 요청"]
  Want -- 핫 스탠바이에서 할당 --> Warm["Warm Start (1~3초)"]
  Want -- 신규 기동 --> Cold["Cold Start (40~60초)"]
  Warm --> Connect["플레이어가 접속"]
  Cold --> Connect
  Connect --> Play["패킷 릴레이로 대전 중 통신"]
  Play -- 1분간 무통신 또는 3시간 경과 --> Shutdown["룸 종료"]
```

## 서버 타입

현재 GS2-Realtime은 패킷 릴레이 기능만을 제공하고 있습니다.
이는 게임 서버로 메시지를 전송하면 그 메시지가 다른 플레이어에게 브로드캐스트되는 기능을 기본으로 하며,
플레이어ID를 지정하여 지정한 플레이어에게 메시지를 전달하는 기능만을 가지고 있습니다.

RPC나 오브젝트 동기화와 같은 기능은 SDK에 포함되어 있지 않으며, 단순한 바이너리 데이터의 송수신만을 수행할 수 있습니다.
페이로드의 바이너리 인코딩은 애플리케이션 측의 책임이므로, Protocol Buffers / FlatBuffers / MessagePack과 같은 임의의 인코딩을 채택할 수 있습니다.

### Unreal Engine Dedicated Server 호스팅

개발자가 Unreal Engine으로 빌드한 Dedicated Server를 호스팅할 수 있도록 하는 것을 검토하고 있습니다.
이 기능의 제공 시기는 미정입니다.

유상 지원 계약을 체결한 후 개발을 진행할 수 있는 강한 요구가 있다면 일정을 조정할 수 있으니 상담해 주시기 바랍니다.

## 서버 스펙

서버의 성능을 설정할 수 있습니다. 성능에 따라 1분당 이용 요금에 차이가 발생합니다.
가장 저렴한 realtime1.nano에서 8명의 플레이어가 1초에 3회 메시지를 주고받을 수 있음을 확인했습니다.

검토 중인 Unreal Engine Dedicated Server 호스팅에서는 요구 스펙이 이보다 높아질 것으로 예상됩니다.
개발 효율이 아닌 비용 효율이 가장 중요한 프로젝트에서는 Unreal Engine Dedicated Server 사용을 권장하지 않습니다.

서버 스펙은 Namespace마다 설정하며, `serverSpec` 필드에 `realtime1.nano`와 같은 식별자를 지정합니다.

## 라이프사이클

게임 서버는 기동 리퀘스트를 접수한 후부터 준비가 시작됩니다.

이때, 기동 리퀘스트가 빈번하게 발생하는 게임 서버에 대해서는 GS2가 핫 스탠바이를 준비합니다.
따라서 기동 리퀘스트를 접수한 직후에 할당이 이루어질 것으로 기대할 수 있지만, 기동 리퀘스트가 빈번하게 발생하지 않는 소규모 게임에서는 기동 리퀘스트부터 실제 할당까지 시간이 걸립니다.

핫 스탠바이에서 할당하는 경우를 웜 스타트, 서버를 새로 기동하여 할당하는 경우를 콜드 스타트라고 부릅니다.
콜드 스타트 시 할당에 필요한 예상 시간은 40초~60초 정도, 웜 스타트 시 할당에 필요한 시간은 1초~3초로 가정해 주시기 바랍니다.

기동 리퀘스트가 빈번하게 발생하지 않는 규모의 타이틀에서 콜드 스타트 시간을 허용할 수 없는 경우,
매월 고정 비용이 발생하는 것을 전제로 핫 스탠바이를 제공할 수 있습니다. 핫 스탠바이 계약은 5대부터 접수하고 있습니다.

핫 스탠바이를 계약했거나 기동 리퀘스트가 빈번하게 발생하는 상황이라도 콜드 스타트가 발생하지 않는다는 것을 보장할 수는 없습니다.
핫 스탠바이가 준비되는 것보다 빠르게 서버 기동 리퀘스트가 발생하거나, 게임 서버의 버전 업 시에는 콜드 스타트가 발생할 가능성이 있습니다.
따라서 최악의 경우 콜드 스타트가 발생하는 상황을 염두에 두고 시스템을 설계해 주시기 바랍니다.

### 스타트 종류

| 종류 | 소요 시간 | 설명 |
| -- | -- | -- |
| 웜 스타트 | 1~3초 | 핫 스탠바이에서 즉시 할당 |
| 콜드 스타트 | 40~60초 | 서버 프로세스를 새로 기동 |

### 룸 종료 조건

기동된 룸은 다음 조건에서 종료됩니다.

- 룸 생성 후 **5분간** 아무도 접속하지 않은 경우
- 룸에 참가하는 모든 플레이어로부터 **1분간** 무통신 상태가 지속된 경우
- 룸의 최대 가동 시간인 **3시간**이 경과한 경우

또한, 명시적으로 `RoomShutdown`을 호출하여 룸을 즉시 종료시킬 수도 있습니다.

또한, 핫 스탠바이의 룸에도 동일한 종료 처리가 적용되지만, 핫 스탠바이의 가동 시간은 이용 요금에 가산되지 않습니다.

## 룸 접속

GS2-Realtime은 다음 흐름으로 접속합니다.

```mermaid
sequenceDiagram
  participant Player
  participant Realtime as GS2-Realtime
  participant Server as GameServer
  Player ->> Realtime: Room(roomName).Model()
  Realtime -->> Player: IpAddress / Port / EncryptionKey
  Player ->> Server: RelayRealtimeSession.ConnectAsync()
  Server -->> Player: 접속 완료
  Note over Player,Server: 이후에는 게임 서버와 직접 WebSocket 통신
```

접속 정보는 다음 세 가지로 구성됩니다.

- `IpAddress`: 게임 서버의 IP 주소
- `Port`: 접속 대상 포트 번호
- `EncryptionKey`: 메시지 페이로드 암호화 키

접속 시에는 액세스 토큰과 플레이어의 프로필 초기값을 전달합니다.

## 프로필

릴레이 서버는 수신한 메시지를 다른 플레이어에게 전파할 뿐만 아니라, 플레이어별로 프로필 데이터를 가질 수 있습니다.

새로운 플레이어가 게임 서버에 접속했을 때, 참가 중인 다른 모든 플레이어의 프로필 데이터를 받을 수 있습니다.
플레이어의 기본 정보를 저장해 둠으로써, 신규 플레이어가 접속했을 때의 처리를 간소화할 수 있습니다.

프로필을 갱신하면 다른 플레이어에게 그 내용이 전송되므로 메시지 전송 대신 사용할 수도 있지만,
갱신 시 페이로드 전체가 송수신되는 프로필 갱신으로 높은 빈도의 메시징을 수행하는 것은 통신 효율 면에서 최적이 아닙니다.

프로필은 바이너리 페이로드로서 임의의 형식으로 인코딩할 수 있습니다.
플레이어의 캐릭터 정보, 장비, 레벨, 게임 내 통화 등의 스냅샷을 저장하는 데 적합합니다.

## 이벤트 핸들링

`RelayRealtimeSession`에는 다음 이벤트 핸들러를 설정할 수 있습니다.

| 핸들러 | 발생 시점 |
| -- | -- |
| `OnJoinPlayer` | 다른 플레이어가 룸에 참가했을 때 |
| `OnLeavePlayer` | 다른 플레이어가 룸에서 퇴장했을 때 |
| `OnRelayMessage` | 다른 플레이어로부터 릴레이 메시지를 수신했을 때 |
| `OnUpdateProfile` | 다른 플레이어가 프로필을 갱신했을 때 |
| `OnError` | 프로토콜 오류가 발생했을 때 |
| `OnGeneralError` | 통신 오류가 발생했을 때 |
| `OnClose` | 게임 서버와의 접속이 끊어졌을 때 |

각 이벤트에는 `MessageMetadata`를 포함하는 확장판(`OnRelayMessageWithMetadata` 등)도 준비되어 있어, 메시지의 메타 정보를 함께 취득할 수 있습니다.

## 푸시 알림

설정할 수 있는 주요 푸시 알림과 설정명은 다음과 같습니다.

- `createNotification`: 룸 할당 시 알림

`enableTransferMobileNotification`을 활성화하면 오프라인 단말로의 모바일 푸시 전송에도 대응합니다.

GS2-Matchmaking으로 모인 멤버 전원에게 룸 할당을 알림으로써, 대전 준비 완료를 실시간으로 알릴 수 있습니다.

## 마스터 데이터 운용

GS2-Realtime은 마스터 데이터를 필요로 하지 않습니다.
네임스페이스 설정(서버 타입·서버 스펙·푸시 알림 설정)만으로 운용할 수 있습니다.

## 트랜잭션 액션

GS2-Realtime에서는 트랜잭션 액션을 제공하지 않습니다.

## 다른 마이크로서비스와의 연계

GS2-Realtime은 단독으로 사용하기보다는 다음 마이크로서비스와 조합하여 사용하는 것이 일반적입니다.

| 서비스 | 용도 |
| -- | -- |
| GS2-Matchmaking | 대전 상대를 매칭하고, 성립 후 `RoomWant`를 호출 |
| GS2-Gateway | 푸시 알림 경로로 `createNotification`을 이용 |
| GS2-Account | 플레이어의 액세스 토큰을 발행 |

## 구현 예제

### 게임 서버 접속 정보 취득

룸 이름을 지정하여 접속 대상 정보를 취득합니다.
`RoomWant`를 경유하여 매치메이킹을 통해 룸이 준비된 경우에는 해당 `roomName`을 지정합니다.



**Unity**
```csharp

    var item = await gs2.Realtime.Namespace(
        namespaceName: "namespace-0001"
    ).Room(
        roomName: "room-0001"
    ).ModelAsync();

    var ipAddress = item.IpAddress;
    var port = item.Port;
    var encryptionKey = item.EncryptionKey;
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Realtime->Namespace(
        "namespace-0001" // namespaceName
    )->Room(
        "room-0001" // roomName
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Result = Future->GetTask().Result();

    const auto IpAddress = Result->GetIpAddress();
    const auto Port = Result->GetPort();
    const auto EncryptionKey = Result->GetEncryptionKey();
```
**Godot**
```gdscript

var domain = ez.realtime.namespace_(
        "namespace-0001"
    ).room(
        "room-0001"
    )

var async_result = await domain.model()
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


### 게임 서버에 접속



**Unity**
```csharp

    using (
      var session = new RelayRealtimeSession(
         accessToken, // 액세스 토큰
         ipAddress, // 게임 서버의 IP 주소
         port, // 게임 서버의 포트
         encryptionKey, // 암호화 키
         ByteString.CopyFromUtf8("my profile") // 프로필 초기값
      )
    )
    {
      // 이벤트 핸들러를 설정

      await session.ConnectAsync();

      // 세션이 유효한 스코프
    }
```
**Unreal Engine**
```cpp

    // Unreal Engine용 Realtime SDK는 현재 준비 중입니다.
```
**Godot**
```gdscript

var session = Gs2RelayRealtimeSession.new(
    game_session.access_token.token, ip_address, port, encryption_key,
    "my profile".to_utf8_buffer()
)
var async_result = await session.connect_session()
if async_result.error != null:
    push_error(str(async_result.error))
    return
session.dispatch()

```


### 다른 플레이어의 접속 핸들링



**Unity**
```csharp

    session.OnJoinPlayer += player => {
        var connectionId = player.ConnectionId;
        var profile = player.Profile;
        // 신규 참가자의 초기 표시 등을 수행
    };
```
**Unreal Engine**
```cpp

    // Unreal Engine용 Realtime SDK는 현재 준비 중입니다.
```
**Godot**
```gdscript

session.join_player.connect(func(player):
    var connection_id = player.connection_id
    var profile = player.profile
    # Initialize the newly joined player in the game UI.
)

```


### 다른 플레이어의 접속 해제 핸들링



**Unity**
```csharp

    session.OnLeavePlayer += player => {
        // 퇴장한 플레이어의 표시를 정리
    };
```
**Unreal Engine**
```cpp

    // Unreal Engine용 Realtime SDK는 현재 준비 중입니다.
```
**Godot**
```gdscript

session.leave_player.connect(func(player):
    # Remove the player who left from the game UI.
    pass
)

```


### 다른 플레이어로부터의 메시지 핸들링



**Unity**
```csharp

    session.OnRelayMessage += message => {
        var sourceConnectionId = message.ConnectionId;
        var payload = message.Data;
        // payload 를 애플리케이션 계층에서 복호화·해석
    };
```
**Unreal Engine**
```cpp

    // Unreal Engine용 Realtime SDK는 현재 준비 중입니다.
```
**Godot**
```gdscript

session.relay_message.connect(func(message):
    var source_connection_id = message.connection_id
    var payload = message.data
    # Decode and handle the payload in the application layer.
)

```


### 다른 플레이어의 프로필 갱신 핸들링



**Unity**
```csharp

    session.OnUpdateProfile += player => {
        // 프로필 변경 사항을 화면에 반영
    };
```
**Unreal Engine**
```cpp

    // Unreal Engine용 Realtime SDK는 현재 준비 중입니다.
```
**Godot**
```gdscript

session.update_profile_received.connect(func(player):
    # Reflect the changed profile in the game UI.
    pass
)

```


### 게임 서버로부터의 접속 해제 핸들링



**Unity**
```csharp

    session.OnClose += () => {
        // 접속 해제 시 재시도·화면 전환 처리
    };
```
**Unreal Engine**
```cpp

    // Unreal Engine용 Realtime SDK는 현재 준비 중입니다.
```
**Godot**
```gdscript

session.closed.connect(func():
    # Retry the connection or transition to an error screen.
    pass
)

```


### 프로필 갱신



**Unity**
```csharp

    await session.UpdateProfileAsync(
      ByteString.CopyFromUtf8("my profile2")
    );
```
**Unreal Engine**
```cpp

    // Unreal Engine용 Realtime SDK는 현재 준비 중입니다.
```
**Godot**
```gdscript

var async_result = await session.update_profile(
    "my profile2".to_utf8_buffer()
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

```


### 브로드캐스트 메시지 전송

`targetConnectionIds`를 생략하면 자신을 제외한 룸 내 모든 플레이어에게 배포됩니다.



**Unity**
```csharp

    await session.SendAsync(
      ByteString.CopyFrom(0x00, 0x01, 0x02)
    );
```
**Unreal Engine**
```cpp

    // Unreal Engine용 Realtime SDK는 현재 준비 중입니다.
```
**Godot**
```gdscript

var async_result = await session.send(
    PackedByteArray([0x00, 0x01, 0x02])
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

```


### 유니캐스트 메시지 전송

`targetConnectionIds`에 대상 플레이어의 `ConnectionId`를 지정합니다.
`ConnectionId`는 `OnJoinPlayer` 등에서 받을 수 있습니다.



**Unity**
```csharp

    await session.SendAsync(
      ByteString.CopyFrom(0x00, 0x01, 0x02),
      new uint[] { targetConnectionId1, targetConnectionId2 }
    );
```
**Unreal Engine**
```cpp

    // Unreal Engine용 Realtime SDK는 현재 준비 중입니다.
```
**Godot**
```gdscript

var async_result = await session.send(
    PackedByteArray([0x00, 0x01, 0x02]),
    [target_connection_id]
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

```


## 설계 시 주의사항

### 바이너리 페이로드 설계

GS2-Realtime은 바이너리 데이터를 그대로 중계할 뿐, 내용을 해석하지 않습니다.
따라서 다음 사항은 애플리케이션 측에서 설계해야 합니다.

- 메시지 종류를 식별하기 위한 헤더(선두 1바이트에 opcode를 넣는 등)
- 직렬화 형식(Protocol Buffers / FlatBuffers / MessagePack 등)
- 순서 보장의 필요성(필요한 경우 애플리케이션 계층에서 시퀀스 번호를 부여)

### 치트 대책

릴레이 서버는 페이로드의 내용을 해석하지 않으므로, 치트 행위 자체를 탐지할 수는 없습니다.
레이팅 계산 등에서는 투표를 통한 다수결로 결과를 결정합니다.

## 상세 레퍼런스

[GS2-Realtime API 레퍼런스](../../api_reference/realtime)



