Session과 Actor 연결¶
가이드 홈 | 이전: STREAM | 다음: Location
다른 언어로 보기 — C++ · C#/.NET · Java · Kotlin · Node/TypeScript
이 장을 읽고 나면
외부 client의 연결 하나를 Actor 하나에 묶고, 그 Actor가 같은 연결로 알림을 보낼 수 있다. 이 장의 코드는 예제 저장소의 tutorial README에서 가져왔으며, README의 「내려받기와 설치」·「빌드」·「실행」 절을 따르면 아래 결과를 재현할 수 있다.
STREAM의 session은 연결이 끊기면 함께 끝난다. 플레이어의 상태는 그보다 오래 남아야 하고, 그 상태를 맡는 것이 Actor다. 이 장은 그 둘을 묶는다. 묶은 뒤에는 session이 다루지 않은 packet이 그 Actor로 간다. Actor는 같은 연결로 보낼 수 있다.
1. Actor와 session을 묶는 역할¶
Actor와 session을 묶으면 수명이 다른 연결과 Actor가 서로 이어진다. 같은 플레이어가 끊었다 다시 접속하면 연결은 새로 만들어지고 Actor는 이전 것이 그대로 남아 있다. 그래서 연결이 자기 Actor를 가리키게 해 두고, 그 뒤의 packet은 그 Actor가 받는다.
묶은 뒤에도 session은 사라지지 않는다. session이 아는 packet은 session이 먼저 처리하고, 모르는 것만 Actor로 간다.
2. 주고받을 packet을 정한다¶
인증 요청과 그 답, 그리고 Actor가 보내는 알림이다. 마지막 것은 답할 요청이 없다.
struct authenticate_t
{
static constexpr const char *packet_name = "Authenticate";
std::string player_id;
};
struct authenticated_t
{
static constexpr const char *packet_name = "Authenticated";
std::string player_id;
};
// Pushed by the player to its own connection, with no request to answer.
struct nickname_changed_t
{
static constexpr const char *packet_name = "NicknameChanged";
std::string nickname;
};
3. 받는 쪽 — 연결을 Actor에 묶는 node¶
받는 쪽 process
이 절의 코드는 STREAM에서 stream node를 등록한 process에 들어간다. 등록할 때 actor dispatch를 켜 두어야 이 장의 relay가 동작한다.
3.1 묶기¶
client가 자기 id를 보내면, 그 id의 Actor를 찾거나 만들어 이 연결에 묶는다.
// Each authenticate packet binds its player to this connection.
if (packet == authenticate_t::packet_name) {
const auto request = payload.parse_json<authenticate_t> ();
// A returning client finds its existing player rather than a new one.
auto &actors = stream.actors ();
auto located = actors.get_or_create (
"player", request.player_id, create_player_t{request.player_id});
if (!located)
throw fw::framework_exception_t (located.error_kind (),
"Player creation was rejected.");
auto bound = co_await actors.bind_or_get (located.value ().ref ()).async ();
stream
.reply_packet (
zlink::message_t::from_json (authenticated_t{std::string (bound.actor_id ())}))
.async ();
co_return;
}
같은 id로 다시 접속하면 있던 Actor에 묶인다. 그래서 만들기와 찾기를 한 호출로 처리한다.
3.2 남은 packet을 넘기기¶
session이 처리하지 못한 packet을 묶인 Actor로 보낸다. 대상은 packet마다 dispatch context의 Actor가 정한다 — client가 Actor handle로 보낸 packet은 그 Actor를 가리킨다. Actor handle 없이 보낸 packet은 Actor가 비어 있으므로, 이 연결에 묶인 Actor가 하나이면 그 Actor로 보낸다. 묶인 Actor가 없으면 보낼 곳이 없으므로 인증이 먼저라는 것이 여기서 강제된다. client 쪽 구분은 packet 수신 §8이 다룬다.
// The packet's Actor slot selects one binding on this connection.
if (dispatch.actor) {
co_await dispatch.actor->relay (packet, payload);
co_return;
}
auto bound = stream.actors ().bound ();
if (bound.size () != 1)
throw fw::framework_exception_t (
fw::framework_error_kind_t::invalid_operation,
"Specify an Actor when the session has no unique binding.");
co_await bound.front ().relay (packet, payload);
넘어간 packet은 그 Actor의 handler가 받는다 — Actor에서 사용한 것과 같은 handler다. 같은 handler가 mesh 호출로도 STREAM relay로도 호출된다.
3.3 Actor가 그 연결로 보내기¶
Actor는 자기에게 묶인 연결을 알고 있다. 응답이 아니라 스스로 보내는 알림이다.
// Pushes over the connection bound to this player. The same handler also runs
// on an HTTP path with no bound connection, where push ends with InvalidOperation.
// Rename is already complete, so only that failure is discarded.
try {
co_await player.context ()
.bound_session ()
.send (nickname_changed_t{player.nickname})
.async ();
}
catch (const fw::framework_exception_t &error) {
if (error.kind () != fw::framework_error_kind_t::invalid_operation)
throw;
}
묶인 연결이 없으면 push는 InvalidOperation으로 끝난다
같은 handler는 mesh 호출로도 불리며, 그때는 묶인 연결이 없다. 이 경우 push는 호출의 terminal에서
InvalidOperation으로 끝나며, 언어별 비동기 완료 수단으로 그 실패를 관찰한다. 위 코드는 rename을
이미 끝낸 뒤 push하므로 그 실패만 받아 버린다.
4. 접속하는 쪽 — mesh 밖의 client¶
접속하는 쪽 process
이 절의 코드는 STREAM에서 연결을 연 process에 들어간다. 앞 절과 짝을 이룬다.
인증하고, 알림을 받는다. 두 번째 호출은 답을 기다리지 않는 단방향인데도 뒤에 값이 온다 — 그것이 Actor가 보낸 알림이다.
auto bound_notice = connector.on_actor_bound ([] (const auto &actor) {
std::cout << "actor bound: " << actor->actor_id () << std::endl;
});
auto unbound_notice = connector.on_actor_unbound ([] (const auto &actor) {
std::cout << "actor unbound: " << actor->actor_id () << std::endl;
});
// With one Actor bound, the connector can send without an Actor handle.
const auto authenticated = value_of (
connector.request (authenticate_t{"p1"}).submit<authenticated_t> (), "authenticate");
std::cout << "bound player: " << authenticated.player_id << std::endl;
std::promise<sc::message_t<nickname_changed_t>> single_changed;
auto single_received = single_changed.get_future ();
{
auto single_notice = connector.on<nickname_changed_t> (
[&] (const auto &changed) { single_changed.set_value (changed); });
connector.send (change_nickname_t{"speedy"}).submit ();
if (single_received.wait_for (options.wait_timeout) != std::future_status::ready)
throw std::runtime_error ("NicknameChanged was not received for p1");
const auto pushed = single_received.get ();
std::cout << "pushed: " << pushed.payload.nickname
<< ", actor: " << pushed.actor_id.value_or ("none") << std::endl;
}
// A second Actor on the same connection calls for explicit handles.
const auto authenticated2 = value_of (
connector.request (authenticate_t{"p2"}).submit<authenticated_t> (), "authenticate");
std::cout << "bound player: " << authenticated2.player_id << std::endl;
auto player = connector.actor (authenticated.player_id);
auto player2 = connector.actor (authenticated2.player_id);
if (!player || !player2)
throw std::runtime_error ("Player Actor was not bound");
std::cout << "actor handle: " << player->actor_id () << std::endl;
std::cout << "actor handle: " << player2->actor_id () << std::endl;
std::promise<sc::message_t<nickname_changed_t>> changed1;
std::promise<sc::message_t<nickname_changed_t>> changed2;
auto received1 = changed1.get_future ();
auto received2 = changed2.get_future ();
auto player_notice = player->on<nickname_changed_t> (
[&] (const auto &changed) { changed1.set_value (changed); });
auto player2_notice = player2->on<nickname_changed_t> (
[&] (const auto &changed) { changed2.set_value (changed); });
// Connector-level callbacks can distinguish the same pushes by ActorId.
auto actor_id_notice = connector.on<nickname_changed_t> ([] (const auto &message) {
std::cout << "received actor id: " << message.actor_id.value_or ("none") << std::endl;
});
// Each handle addresses its own player on the shared connection.
player->send (change_nickname_t{"speedy-p1"}).submit ();
player2->send (change_nickname_t{"speedy-p2"}).submit ();
if (received1.wait_for (options.wait_timeout) != std::future_status::ready)
throw std::runtime_error ("NicknameChanged was not received for p1");
if (received2.wait_for (options.wait_timeout) != std::future_status::ready)
throw std::runtime_error ("NicknameChanged was not received for p2");
const auto pushed1 = received1.get ();
const auto pushed2 = received2.get ();
std::cout << "pushed: " << pushed1.payload.nickname
<< ", actor: " << pushed1.actor_id.value_or ("none") << std::endl;
std::cout << "pushed: " << pushed2.payload.nickname
<< ", actor: " << pushed2.actor_id.value_or ("none") << std::endl;
5. 실행 결과¶
tutorial README의 「실행」 절을 따라 띄운 상태에서 Server를 실행한 상태에서 아래 명령으로 StreamClient를 시작하면, StreamClient가 보낸 bind request와 받은 Actor 알림은 StreamClient process의 stdout에 나오고 session·Actor handler 기록은 Server process의 stdout 또는 server.log에 나온다.
dotnet run --project StreamClient/StreamClient.csproj
# connected: True
# round trip: 56ms
# bound player: p1
# pushed: speedy
pushed 줄은 client가 요구하지 않은 응답이다. client는 이름을 바꾸라고 보냈을 뿐 답을
요구하지 않았다. 돌아온 것은 player가 스스로 보낸 알림이다. 그 사이에 packet은 session을
지나 Actor로 갔고, Actor가 같은 연결로 되돌려 보냈다.
6. 연결이 끊긴 뒤 — session은 끝나고 Actor는 남는다¶
session은 끝나고 Actor는 남는다. 같은 id로 다시 접속하면 그 Actor에 다시 묶인다.
끊김을 Actor에게 알리는 callback, 묶음이 다른 연결로 옮겨 갈 때의 규칙, 그리고 Actor가 다른 node로 이동하는 중의 처리는 Session 묶음의 동작 원리가 다룬다.
7. 관련 문서¶
- 연결을 받는 자리 — STREAM
- 묶이는 대상 — Actor
- 묶음의 규칙 — Session 묶음의 동작 원리
- 이 장 코드의 실행본 — 예제 저장소의 tutorial README