서버 런타임 예시 #

서버 대 서버 #

클라이언트 호출과 달리 서버 간 호출에는 사용자 ID가 없으므로 클라이언트에서 액세스하지 못하게 하려는 함수의 경우 컨텍스트에서 사용자 ID가 발견되면 오류를 반환하면 됩니다.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func ServerRPC(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
	userId, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)

	if ok && userId != "" {
		logger.Error("rpc was called by a user")
		return "", runtime.NewError("rpc is only callable via server to server", 7)
	}

	// Valid server to server RPC call, continue executing the RPC...
	return "<JsonResponse>", nil
}
Server
1
2
3
4
5
6
7
8
9
const serverRpc : nkruntime.RpcFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string) : string | void {
  if (ctx.userId != "") {
    logger.error("rpc was called by a user");
    return null;
  }
  
  // Valid server to server RPC call, continue executing the RPC...
  return "<JsonResponse>";
}
Server
1
2
3
4
5
6
7
8
9
local server_rpc = function(context, payload)
    if context.user_id and not context.user_id == "" then
        nk.logger_error("rpc was called by a user")
        return nil
    end

    -- Valid server to server RPC call, continue executing the RPC...
    return "<JsonResponse>"
end

또한, 웹 서비스에서 사용할 수 있고 사용자 지정 서버 환경에 쉽게 통합할 수 있는 HTTP REST 핸들러를 만드는 것이 유용할 수 있습니다.

이렇게 하려면 RPC 후크와 런타임 HTTP 키를 사용하여 서버를 인증하면 됩니다:

Server
1
2
3
4
5
6
7
8
9
local nk = require("nakama")

local function http_handler(context, payload)
  local message = nk.json_decode(payload)
  nk.logger_info(string.format("Message: %q", message))
  return nk.json_encode({["context"] = context})
end

nk.register_rpc(http_handler, "http_handler_path")
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func HttpHandler(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
		var message interface{}
		if err := json.Unmarshal([]byte(payload), &message); err != nil {
				return "", err
		}

		logger.Info("Message: %v", message)

		response, err := json.Marshal(map[string]interface{}{"message": message})
		if err != nil {
				return "", err
		}

		return string(response), nil
}

// Register as an RPC function, this call should be in InitModule.
if err := initializer.RegisterRpc("http_handler_path", HttpHandler); err != nil {
  logger.Error("Unable to register: %v", err)
  return err
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
let customFuncRpc: nkruntime.RpcFunction =
function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string) {
	logger.info('payload: %q', payload);

	if (ctx.userId) {
		// Reject non server-to-server call
		throw Error('Cannot invoke this function from user session');
	}

	let message = JSON.parse(payload);
	logger.info('Message: %q', message);

	return JSON.stringify({ message: message });
}

// Register as an after hook for the appropriate feature, this call should be in InitModule.
initializer.registerRpc("custom_rpc_func_id", customFuncRpc);

등록된 RPC 함수는 선택한 모든 HTTP 클라이언트로 호출할 수 있습니다. 예를 들어, cURL을 사용하면 다음과 같이 서버에서 이 함수를 실행할 수 있습니다.

1
2
3
4
curl "http://127.0.0.1:7350/v2/rpc/http_handler_path?http_key=defaulthttpkey" \
	-d '"{\"some\": \"data\"}"' \
	-H 'Content-Type: application/json' \
	-H 'Accept: application/json'

JSON 페이로드가 이스케이프되고 문자열 내부에 래핑됩니다. 이는 RPC API가 설계된 시점에 Protobuf 유형과 JSON 객체 간에 매핑되는 유형이 gRPC에 없기 때문에 설계된 것입니다. 이후 JSON 지원이 gRPC에 추가되었지만 API 계약을 위반하지 않고 호환성을 보장하기 위해 이 방식이 유지되었습니다.

페이로드의 원시 JSON 데이터로 RPC 함수를 호출할 수 있는 unwrap 쿼리 매개변수가 지원됩니다:

1
2
3
4
curl "http://127.0.0.1:7350/v2/rpc/http_handler_path?http_key=defaulthttpkey&unwrap" \
	-d '{"some": "data"}' \
	-H 'Content-Type: application/json' \
	-H 'Accept: application/json'

사용자 초기화 #

등록이 완료된 후 새 사용자에 대한 레코드를 작성하는 사용자 등록 후크.

"register_after" 후크는 “authenticaterequest_*” 메시지 유형과 함께 사용하여 메시지가 처리된 후에 서버가 기능을 실행하도록 전달합니다. 서버에서는 등록과 로그인 메시지가 구분되지 않기 때문에 조건부 작성을 사용하여 레코드를 저장합니다.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
local function initialize_user(context, payload)
  if payload.created then
    -- Only run this logic if the account that has authenticated is new.
    local changeset = {
      coins = 10,   -- Add 10 coins to the user's wallet.
      gems = 5      -- Add 5 gems to the user's wallet.
      artifacts = 0 -- No artifacts to start with.
    }
    local metadata = {}
    nk.wallet_update(context.user_id, changeset, metadata, true)
  end
end

-- change to whatever message name matches your authentication type.
nk.register_req_after(initialize_user, "AuthenticateDevice")
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func InitializeUser(ctx context.Context, logger Logger, db *sql.DB, nk NakamaModule, out *api.Session, in *api.AuthenticateDeviceRequest) error {
  if out.Created {
    // Only run this logic if the account that has authenticated is new.
    userID, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
    if !ok {
      return "", errors.New("Invalid context")
    }
    changeset := map[string]interface{}{
      "coins": 10,    // Add 10 coins to the user's wallet.
      "gems":  5,     // Add 5 gems to the user's wallet.
      "artifacts": 0, // No artifacts to start with.
    }
    var metadata map[string]interface{}
    if err := nk.WalletUpdate(ctx, userID, changeset, metadata, true); err != nil {
      // Handle error.
    }
  }
}

// Register as after hook, this call should be in InitModule.
if err := initializer.RegisterAfterAuthenticateDevice(InitializeUser); err != nil {
  logger.Error("Unable to register: %v", err)
  return err
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
let initializeUser : nkruntime.AfterHookFunction<nkruntime.Session, nkruntime.AuthenticateDeviceRequest> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, out: nkruntime.Session, data: nkruntime.AuthenticateDeviceRequest) : nkruntime.Session {
  const changeset = {
    "coins": 10, // Add 10 coins to the user's wallet
    "gems": 5, // Add 5 gems to the user's wallet
    "artifacts": 0 // No artifacts to start with
  };
  
  nk.walletUpdate(ctx.userId, changeset, null, true);
  return out;
};

// Register as after hook, this call should be in InitModule.
initializer.registerAfterAuthenticateDevice(initializeUser);

저장소 #

저장소에 쓰기 #

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func AuthoritativeWriteRPC(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
	userID, _ := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)

	data := map[string]interface{}{
		"achievementPoints": 100,
		"unlockedAchievements": []string{"max-level", "defeat-boss-2", "equip-rare-gear"},
	}

	bytes, err := json.Marshal(data)
	if err != nil {
		return "", runtime.NewError("error marshaling data", 13)
	}

	write := &runtime.StorageWrite{
		Collection:      "Unlocks",
		Key:             "Achievements",
		UserID:          userID,
		Value:           string(bytes),
		PermissionRead:  1, // Only the server and owner can read
		PermissionWrite: 0, // Only the server can write
	}

	_, err = nk.StorageWrite(ctx, []*runtime.StorageWrite{write})
	if err != nil {
		return "", runtime.NewError("error saving data", 13)
	}
	
	return "<JsonResponse>", nil
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
let authoritativeWriteRpc : nkruntime.RpcFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string) : string | void {
  const data = {
    achievementPoints: 100,
    unlockedAchievements: ['max-level', 'defeat-boss-2', 'equip-rare-gear']
  };

  const write : StorageWriteRequest = {
    collection: 'Unlocks',
    key: 'Achievements',
    userId: ctx.userId,
    value: data,
    permissionRead: 1, // Only the server and owner can read
    permissionWrite: 0 // Only the server can write
  };

  nk.storageWrite([write]);

  return "<JsonResponse>";
};
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
local authoritative_write_rpc = function(context, payload)
    local data = {
        ["achievementPoints"] = 100,
        ["unlockedAchievements"] = { "max-level", "defeat-boss-2", "equip-rare-gear" }
    }

    local write = {
        collection = "Unlocks",
        key = "Achievements",
        user_id = context.user_id,
        value = data,
        permission_read = 1,
        permission_write = 0
    }
    
    nk.storage_write({ write })

    return "<JsonResponse>"
end

Related Pages