Nakama는 실시간 게임 및 소셜 기능 구현을 위한 오픈 소스 서버 프레임워크입니다. 이 가이드에서는 Docker를 활용한 Nakama 서버 구축 방법과 C# 콘솔 애플리케이션을 이용한 기본 API 연동 테스트 방법을 다룹니다.
Nakama 서버 구축
Nakama 서버는 공식적으로 Docker를 사용하여 설치 및 실행하는 것을 권장합니다. 안정적인 환경을 위해 다음 단계를 따릅니다.
- 환경 준비: Docker Desktop이 시스템에 설치되어 있는지 확인합니다. Nakama 런타임 모듈 개발을 위해 Node.js, TypeScript, Lua, Go 등의 환경이 필요할 수 있으나, 기본 서버 실행에는 필수적이지 않습니다.
docker-compose.yml파일 생성: 프로젝트의 루트 디렉토리에 다음과 같은 내용의docker-compose.yml파일을 생성합니다. 이 파일은 Nakama 서버, 데이터베이스(CockroachDB), 그리고 모니터링 툴(Prometheus)을 한 번에 설정합니다.
version: '3'
services:
cockroachdb:
image: cockroachdb/cockroach:latest-v23.1
command: start-single-node --insecure --store=attrs=ssd,path=/var/lib/cockroach/
restart: "no"
volumes:
- data:/var/lib/cockroach
expose:
- "8080"
- "26257"
ports:
- "26257:26257"
- "8080:8080"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health?ready=1"]
interval: 3s
timeout: 3s
retries: 5
nakama:
image: registry.heroiclabs.com/heroiclabs/nakama:3.22.0
entrypoint:
- "/bin/sh"
- "-ecx"
- >
/nakama/nakama migrate up --database.address root@cockroachdb:26257 &&
exec /nakama/nakama --name nakama1 --database.address root@cockroachdb:26257 --logger.level DEBUG --session.token_expiry_sec 7200 --metrics.prometheus_port 9100
restart: "no"
links:
- "cockroachdb:db"
depends_on:
- cockroachdb
- prometheus
volumes:
- ./:/nakama/data
environment:
- NAKAMA_RUNTIME_PATH=/nakama/data/modules
expose:
- "7349"
- "7350"
- "7351"
- "9100"
ports:
- "7349:7349"
- "7350:7350"
- "7351:7351"
healthcheck:
test: ["CMD", "/nakama/nakama", "healthcheck"]
interval: 10s
timeout: 5s
retries: 5
prometheus:
image: prom/prometheus
entrypoint: /bin/sh -c
command: |
'sh -s <<EOF
cat > ./prometheus.yml <<EON
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ['localhost:9090']
- job_name: nakama
metrics_path: /
static_configs:
- targets: ['nakama:9100']
EON
prometheus --config.file=./prometheus.yml
EOF'
ports:
- '9090:9090'
volumes:
data:
</code>
- 서버 실행:
docker-compose.yml파일이 있는 디렉토리에서 명령 프롬프트나 터미널을 열고 다음 명령어를 실행합니다.
docker compose up
만약 Nakama 런타임 모듈을 수정했거나 빌드가 필요한 경우, 다음 명령어를 사용합니다.
docker compose up --build nakama
서버가 성공적으로 구동되면 Docker Desktop 대시보드에서 Nakama 서비스가 정상적으로 실행 중임을 확인할 수 있습니다. 만약 문제가 발생한다면, 일반적으로 필요한 라이브러리 누락이나 네트워크 연결 문제(방화벽, 타임아웃 등)가 원인일 수 있습니다. 공식 문서나 포럼을 참고하는 것이 좋습니다.
C# 애플리케이션을 이용한 Nakama API 연동 테스트
Nakama 서버가 정상 작동하는지 확인하기 위해 Unity 프로젝트가 아닌 일반 C# 콘솔 애플리케이션을 사용하여 간단한 API 연동 테스트를 진행합니다. 이 테스트는 기기 인증, 매치 목록 조회, 데이터 저장 시도, 세션 로그아웃 등의 기능을 다룹니다.
Nakama 서버와의 HTTP API 통신을 위해 HttpClient와 JSON 처리 라이브러리(Newtonsoft.Json)를 사용합니다. 프로젝트에 Newtonsoft.Json NuGet 패키지를 추가해야 합니다.
다음은 Nakama 서버와 상호작용하기 위한 C# 예제 코드입니다.
using Newtonsoft.Json.Linq;
using System.Text;
internal class NakamaApiTester
{
private const string BaseApiUrl = "http://127.0.0.1:7350";
private const string ApiKey = "defaultkey";
private static string _sessionToken = string.Empty;
static async Task Main(string[] args)
{
await AuthenticateDeviceAsync();
if (!string.IsNullOrEmpty(_sessionToken))
{
await RetrieveMatchListAsync();
await TryFetchStorageObjectAsync(); // 특정 키의 데이터가 없으면 404가 정상
await LogoutSessionAsync();
}
else
{
Console.WriteLine("Authentication failed, skipping further API calls.");
}
}
/// <summary>
/// 기기 인증을 통해 Nakama 세션 토큰을 획득합니다.
/// </summary>
static async Task AuthenticateDeviceAsync()
{
Console.WriteLine("\n--- Starting Device Authentication ---");
try
{
var authBytes = Encoding.UTF8.GetBytes($"{ApiKey}:");
var authHeader = Convert.ToBase64String(authBytes);
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("Authorization", $"Basic {authHeader}");
var deviceId = Guid.NewGuid().ToString(); // 고유한 기기 ID 생성
var postData = new { id = deviceId };
var jsonContent = Newtonsoft.Json.JsonConvert.SerializeObject(postData);
var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync($"{BaseApiUrl}/v2/account/authenticate/device?create=true", httpContent);
if (response.IsSuccessStatusCode)
{
var responseString = await response.Content.ReadAsStringAsync();
var responseJson = JObject.Parse(responseString);
Console.WriteLine("Authentication successful!");
if (responseJson.TryGetValue("token", out var tokenValue))
{
_sessionToken = tokenValue.ToString();
Console.WriteLine($"Session Token: {_sessionToken.Substring(0, 30)}..."); // 토큰의 일부만 표시
}
}
else
{
Console.WriteLine($"Authentication failed: {response.StatusCode}");
Console.WriteLine($"Response: {await response.Content.ReadAsStringAsync()}");
}
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred during authentication: {ex.Message}");
Console.WriteLine("Ensure Nakama server is running and accessible at the specified URL.");
}
}
/// <summary>
/// Nakama 서버에서 매치 목록을 조회합니다.
/// </summary>
static async Task RetrieveMatchListAsync()
{
Console.WriteLine("\n--- Retrieving Match List ---");
try
{
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_sessionToken}");
// 현재 활성화된 매치 목록을 최대 10개까지 조회 (최소 1명, 최대 10명 참가 가능)
var response = await httpClient.GetAsync($"{BaseApiUrl}/v2/match?limit=10&min_size=1&max_size=10");
if (response.IsSuccessStatusCode)
{
var responseString = await response.Content.ReadAsStringAsync();
var matchesJson = JObject.Parse(responseString);
if (matchesJson.TryGetValue("matches", out var matchesToken) && matchesToken is JArray matchesArray && matchesArray.Any())
{
Console.WriteLine("Active matches found:");
foreach (var match in matchesArray)
{
Console.WriteLine($" Match ID: {match["match_id"]}, Size: {match["size"]}");
}
}
else
{
Console.WriteLine("No active matches found or match list is empty.");
}
}
else
{
Console.WriteLine($"Failed to retrieve match list: {response.StatusCode}");
Console.WriteLine($"Response: {await response.Content.ReadAsStringAsync()}");
}
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred while fetching matches: {ex.Message}");
}
}
/// <summary>
/// Nakama 저장소에서 특정 객체를 조회합니다. (테스트용이므로 존재하지 않으면 404가 정상)
/// </summary>
static async Task TryFetchStorageObjectAsync()
{
Console.WriteLine("\n--- Attempting to Fetch Storage Object ---");
try
{
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_sessionToken}");
// 예제 경로: 컬렉션 'test', 키 '0ba39c8d-5b3d-429d-a570-85af6df61b49'
// 해당 객체가 존재하지 않으면 404 Not Found가 예상됩니다.
var response = await httpClient.GetAsync($"{BaseApiUrl}/v2/storage/test/0ba39c8d-5b3d-429d-a570-85af6df61b49");
if (response.IsSuccessStatusCode)
{
var responseString = await response.Content.ReadAsStringAsync();
var storageContent = JObject.Parse(responseString);
Console.WriteLine($"Storage object retrieved successfully: {storageContent}");
}
else
{
Console.WriteLine($"Failed to fetch storage object: {response.StatusCode}");
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
Console.WriteLine("This is expected if the storage object has not been created yet.");
}
Console.WriteLine($"Response: {await response.Content.ReadAsStringAsync()}");
}
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred while fetching storage: {ex.Message}");
}
}
/// <summary>
/// 현재 Nakama 세션을 로그아웃합니다.
/// </summary>
static async Task LogoutSessionAsync()
{
Console.WriteLine("\n--- Logging Out Session ---");
try
{
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_sessionToken}");
var response = await httpClient.PostAsync($"{BaseApiUrl}/v2/session/logout", null);
if (response.IsSuccessStatusCode)
{
Console.WriteLine("Session logged out successfully.");
}
else
{
Console.WriteLine($"Failed to log out session: {response.StatusCode}");
Console.WriteLine($"Response: {await response.Content.ReadAsStringAsync()}");
}
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred during logout: {ex.Message}");
}
}
}
API 연동 시 유의사항
- 포트 번호:
docker-compose.yml파일에서 볼 수 있듯이 Nakama는 여러 포트를 사용합니다. 기본 HTTP API 요청은7350번 포트를 통해 이루어지며, 필요에 따라 이 포트 번호를 변경할 수 있습니다. - 매치(Match) 유형: Nakama의 매치는 서버가 게임 로직을 직접 처리하는 '권한 매치(Authoritative Matches)'와 클라이언트 간 직접 통신하는 '비권한 매치(Non-Authoritative Matches)'로 구분됩니다. 매치 목록을 조회할 때 반환되는 정보에 차이가 있을 수 있으니 이를 고려해야 합니다.
- 데이터 저장소: Nakama는 휘발성 메모리 저장소와 영구 데이터베이스 저장소를 모두 지원합니다. 위의
TryFetchStorageObjectAsync예제는 특정 컬렉션과 사용자 ID에 대한 저장소 객체를 조회하는 것이며, 실제 데이터를 성공적으로 조회하려면 해당 경로에 데이터가 미리 저장되어 있어야 합니다. 테스트 환경에서는 404 응답이 정상일 수 있습니다.