본문 바로가기
Server

14. 유저 세션(3)

by Doromi 2018. 1. 18.
728x90
반응형

6-2. 구현하기

 

유저가 로그인을 성공하면 유저 세션 역할을 하는 프로세스를 생성할 것이다.

점수를 저장하는 API는 핸들러에서 처리시 해당 세션 프로세스에게 메시지를 전달해서 결과값을 받아 오도록 한다.

 

6-2-1. 유저 세션 프로세스 생성

 

mon_users 모듈에 new_session/1, loop/1, make_session_key/2 함수를 만든다.

프로세스 생성 부분만 만들고, 나머지는 함수 껍데기만 작성하겠다.

 

%% API
-export([join/2, login/2]).

join(Id, Password) ->
F = fun() ->
case mnesia:read(users, Id) of
[] ->
%% 해당 Id로 가입된 데이터가 없으면 저장한다
Users = #users{id=Id, password=Password},
ok = mnesia:write(Users); %% 가입완료
_ ->
fail %% 가입 실패
end
end,
mnesia:activity(transaction, F).

login(Id, Password) ->
F = fun() ->
case mnesia:read(users, Id) of
[U = #users{password=Password}] ->
%% Id, Password 일치, 로그인 성공
SessionKey = new_session(Id),
{ok, SessionKey};
_ ->
%% 일치하는 데이터 없음, 로그인 실패
fail
end
end,
mnesia:activity(transaction, F).

%% 유저 세션 프로세스 생성
new_session(Id) ->
Pid = spawn(mon_users, loop, [Id]),
make_session_key(Id, Pid).

%% session loop
loop(Id, Time) ->
receive
_ -> pass
end.

%% 세션 키 생성 및 저장
make_session_key(Id, Pid) ->
pass.

login() 함수 안에 로그인 성공 후 new_session(Id) 함수를 호출해서 세션 프로세스를 생성하도록 했고,

session loop는 지금은 동작하지 않는 코드다.

 

6-2-2. 세션 키

 

세션 키를 저장하기 위해 ETS를 이용해보겠다.

session_list라는 이름의 ETS 테이블을 생성한다.

 

  %% Code reloader 실행
mon_reloader:start(),

%% Session Table 생성
ets:new(session_list, [public, named_table]),

case mon_sup:start_link() of
{ok, Pid} ->
io:format("start ok~n"),
{ok, Pid};
Error ->
Error
end.

mon_app.erl을 수정하였다.

 

세션 키를 만드는 것은 서버에서 중복되지 않는 고유한 키여야 한다.

얼랭에서는 해시 키를 만드는 함수로 erlang:phase2(Term)을 사용할 수 있고, 랜덤 값을 얻을 때는

random:seed(A1, A2, A3)로 seed를 만들고, random:uniform(N) 함수로 1부터 N까지의 숫자 중 하나를 랜덤으로

얻을 수 있다.

A1, A2, A3는 integer 형식이어야 한다.

 

%% 세션 키 생성 및 저장
make_session_key(Id, Pid) ->
%% 시드 초기화
{A1, A2, A3} = now(),
random:seed(A1, A2, A3),

%% 1~10000까지 숫자중 하나를 랜덤 선택
Num = random:uniform(10000),

%% Id를 이용한 Hash 생성
Hash = erlang:phash2(Id),

%% 두개의 값을 16진수로 조합하여 session key 생성
List = io_lib:format("~.16B~.16B", [Hash, Num]),
SessionKey = list_to_binary(lists:append(List)),

%% 세션 키 저장 및 리턴
ets:insert(session_list, {SessionKey, Pid}),
SessionKey.

mon_users.erl 세션 키 생성 및 저장 함수 작성해준다.

 

 

따로 json 라이브러리를 사용하지 않았는데, 클라이언트에게 세션 키를 전달하기 위해 return data 구조가 복잡해지므로 적당한 json 라이브러리를 추가하도록 하겠다.

 

{jsx, ".*", {git, "git://github.com/talentdeficit/jsx.git", {tag, "v2.1.1"}}}

rebar.config 어플리케이션에 추가한다.

 

-pa ./ebin
-pa ./deps/cowboy/ebin
-pa ./deps/cowlib/ebin
-pa ./deps/ranch/ebin
-pa ./deps/jsx/ebin
-eval "application:start(mon)"
-sname node1
-mnesia dir '"./db"'

vm.args 도 이렇게 수정한다.

 

다 끝나면 get-deps를 실행해서 패키지를 다운 받아 온 후 컴파일해서 실행하도록 하겠다.

 

jsx의 사용법은 간단한데 json 데이터를 생성하기 위해서는 jsx:encode.

그 반대로 json 데이터를 파싱해서 얼랭 형식으로 변환하려면 jsx:decode를 사용한다.

 

 

 

 

 

jsx module을 사용해서 mon_http 모듈의 JSON 형식을 사용하는 부분을 모두 수정한다.

SessionKey도 전달하도록 코드를 추가한다.

 

 

 

curl 을 이용해서 테스트를 해보겠다.

 

정상적으로 세션 키를 받아 오는 것을 확인하였다.

 

 

6-2-3. 포인트 저장 기능 추가

 

mon_http 모듈에 API를 추가한다.

join 아래 부분에 추가

mon_http.erl에 /users/point api를 추가한다.

handle(<<"users">>, <<"point">>, _, Data) ->
SessionKey = proplists:get_value(<<"session_key">>, Data),
Point1 = proplists:get_value(<<"point">>, Data),
Point = binary_to_integer(Point1),
case mon_users:point(SessionKey, Point) of
ok ->
jsx:encode([{<<"result">>, <<"ok">>}]);
fail ->
jsx:encode([{<<"result">>, <<"fail">>}])
end;

mon_users:point 함수를 호출하는 것이 전부.

나머지는 mon_users 모듈에서 생성한 프로세스를 통해 저장 작업을 수행해야 한다.

 

%% API
-export([join/2, login/2]).

join(Id, Password) ->
F = fun() ->
case mnesia:read(users, Id) of
[] ->
%% 해당 Id로 가입된 데이터가 없으면 저장한다
Users = #users{id=Id, password=Password},
ok = mnesia:write(Users); %% 가입완료
_ ->
fail %% 가입 실패
end
end,
mnesia:activity(transaction, F).

login(Id, Password) ->
F = fun() ->
case mnesia:read(users, Id) of
[U = #users{password=Password}] ->
%% Id, Password 일치, 로그인 성공
SessionKey = new_session(Id),
{ok, SessionKey};
_ ->
%% 일치하는 데이터 없음, 로그인 실패
fail
end
end,
mnesia:activity(transaction, F).

point(SessionKey, Point) ->
case ets:lookup(session_list, SessionKey) of
[{SessionKey, Pid}] ->
Ref = make_ref(),
Pid ! {self(), Ref, save_point, Point},
receive
{Ref, saved} ->
ok;
_ ->
fail
after 3000 ->
fail
end;
_ ->
fail
end.

%% 유저 세션 프로세스 생성
new_session(Id) ->
Pid = spawn(mon_users, loop, [Id]),
make_session_key(Id, Pid).

%% session loop
loop(Id, Time) ->
receive
{Pid, Ref, save_point, Point} ->
save_point(Id, Point),
Pid ! {Ref, saved};
_ ->
Time
end,
loop(Id).

%% 세션 키 생성 및 저장
make_session_key(Id, Pid) ->
%% 시드 초기화
{A1, A2, A3} = now(),
random:seed(A1, A2, A3),

%% 1~10000까지 숫자중 하나를 랜덤 선택
Num = random:uniform(10000),

%% Id를 이용한 Hash 생성
Hash = erlang:phash2(Id),

%% 두개의 값을 16진수로 조합하여 session key 생성
List = io_lib:format("~.16B~.16B", [Hash, Num]),
SessionKey = list_to_binary(lists:append(List)),

%% 세션 키 저장 및 리턴
ets:insert(session_list, {SessionKey, Pid}),
SessionKey.

%% 유저 점수 저장
save_point(Id, Point) ->
F = fun() ->
case mnesia:read(users, Id) of
[U] ->
%% 유저 점수 저장
Users = U#users{point=Point},
ok = mnesia:write(Users);
_ ->
fail %% 저장 실패
end
end,
mnesia:activity(transaction, F).

mon_users.erl에 점수 저장 기능을 추가하였다.

point(SessionKey, Point) 함수에서 이미 저장한 세션 키에 해당하는 프로세스의 Pid를 가져온다.

그리고 해당 Pid로 점수를 저장하라는 메시지인 {self(), Ref, save_point, Point}를 전송한다.

Ref는 메시지에 고유 번호를 첨부했다고 보면 된다.

해당 Pid로 여러 개의 메시지가 동시에 도착할 수 있기 때문에 메시지를 구분하는 고유한 번호를 make_ref() 함수로

만들어서 추가한 것이다.

 

프로세스에서 메시지를 받아서 처리하는 함수인 loop(Id)에서 메시지 패턴 {Pid, Ref, save_point, Point}을 만들고

save_point() 함수를 호출하도록 하였다.

save_point() 함수 내부에서는 지난 번에 공부했던 방식대로 mnesia 데이터베이스의 users 테이블에서 해당 유저의

데이터를 읽고 그 안에 포인트를 저장한다.

 

6-2-4. 자동 로그아웃

 

유저 프로세스는 로그인 후 만들어지고, 유저 세션이 유지되는 동안 유저 프로세스는 종료되지 않는다.

유저가 무한정 접속하는 것이 아니기 때문에 일정 시간이 지나면 자동으로 세션을 끝내고, 유저 프로세스를 종료해야 한다.

 

제일 먼저 할 일은 유저로부터 명령이 도착했을 때의 시간을 저장하는 것이다.

그리고 그 시간과 현재 시간을 체크해서 일정 시간이 지났다고 판단하면 스스로 종료하는 코드를 넣으면 될 것이다.

 

erlang:send_after(Time, Dest, Msg) -> TimeRef

 

이 함수는 Time에는 밀리 세컨드 단위의 시간을, Dest에는 Pid을 입력한다.

Msg에는 보내고자 하는 메시지를 적으면 Time 시간이 지난 후에 해당 프로세스로 메시지가 전송된다.

 

erlang:now() -> Timestamp

 

Timestamp 리턴 값은 {MegaSecs, Secs, MicroSecs}라는 3가지 숫자를 담은 튜플을 리턴

now()의 경우는 유니크한 값을 리턴한다는 점이다.

 

mon_users.erl에 추가한다.

spawn할 때에 Id와 함께 Timestamp를 전달한다.

그리고 1초 간격으로 {check} 메시지를 보내도록 한다.

 

 

 

프로세스가 실행하는 loop 함수도 Time을 추가하고 명령을 받으면 Time을 갱신하고,

{check} 메시지를 받았을 때는 현재 시간과 체크하는 기능을 추가한다.

시간 비교 함수는 timer:now_diff 함수 사용, 마이크로 세컨드 단위로 계산되므로 주의

 

%% API
-export([join/2, login/2, point/2, loop/2, make_session_key/2]).
%% session loop
loop(Id, Time) ->
Time1 =
receive
{Pid, Ref, save_point, Point} ->
save_point(Id, Point),
Pid ! {Ref, saved},
now();
{check} ->
Diff = timer:now_diff(now(), Time),
%% Diff는 마이크로 세컨드 단위이다.
%% 10초가 지났으면 세션 종료
if (Diff > 10000000) -> delete_session_key(self());
true -> erlang:send_after(1000, self(), {check})
end,
Time;
_ ->
Time
end,
loop(Id, Time1).

mon_users.erl 파일에 추가한다.

이는 명령 없이 10초가 지나면 유저 세션을 종료하고, 아니라면 다시 1초 후에 {check} 메시지를 전송하도록 한다.

 

 

ets 테이블에서 세션키를 제거하고,

프로세스를 종료하는 함수 delete_session_ket(Pid)를 만든다.

 

Pid는 Key가 아니기 때문에 ets:lookup 함수를 사용하지 않고, 패턴 매칭으로 검색하는 match_object 함수를 사용한다.

패턴으로 {'_',Pid}를 넣은 것은 Key 부분은 아무 값이나 매칭하고, 뒤에 value 부분이 Pid인 object를 찾기 위한 것이다.

 

 

10000000dl 10초이므로 0을 더 넣어서 10분으로 해서 테스트 해보았다.

728x90
반응형

'Server' 카테고리의 다른 글

CI/CD  (0) 2024.04.08
Kubernetes  (0) 2024.04.01
13. 유저 세션(2)  (0) 2018.01.15
12. 유저 세션(1)  (0) 2018.01.15
11. 데이터베이스(2)  (0) 2018.01.13