Python 게임서버 안녕하십니까 : RPC framework 편

78
Python 게임서버 안녕하십니까? RPC Framework 편 스마트스터디 CTO 박준철

Transcript of Python 게임서버 안녕하십니까 : RPC framework 편

Page 1: Python 게임서버 안녕하십니까 : RPC framework 편

Python�게임서버�안녕하십니까?

RPC�Framework�편

스마트스터디�CTO�박준철

Page 2: Python 게임서버 안녕하십니까 : RPC framework 편

왜?NDC�발표�(Python�게임서버�안녕하십니까?�:�몬스터�슈퍼리그�게임�서버)��

준비중에�사내�리뷰�과정에서�“너굴”�님의�질문으로부터�시작

너굴�:�“게임�서버/클라�네트워킹�에서�RPC�framework�를�사용하지�않고�직접�구현하신�이유가�있나요?”�

준곰�:�“어쩌고�저쩌고…�그래서�어쩌고저쩌고”�

너굴�:�“네…”�

Page 3: Python 게임서버 안녕하십니까 : RPC framework 편

RPC�라는게�뭐길래?�게임�서버/클라에�쓸수�있나?

Page 4: Python 게임서버 안녕하십니까 : RPC framework 편

목표•RPC�framework�에�대한�정보�공유�

• RPC�framework�를�게임에�적용해보자�

•몬스터�슈퍼리그�에서�사용한�방식�공유�

•게임에�적합한�방식을�직접�만들어�보는�것

Page 5: Python 게임서버 안녕하십니까 : RPC framework 편

목차

•RPC�

• Thrift�,�gRPC�

• 몬스터�슈퍼리그�방식�

• 게임에�맞게�RPC�만들기�

•정리�&�생각해볼�만한�것들

마스터,�준비�되었나요?�이제�시작합니다.�졸면�안되요!

Page 6: Python 게임서버 안녕하십니까 : RPC framework 편

RPC• Remote Procedure Call• wikipedia : In distributed computing, a remote procedure call (RPC)

is when a computer program causes a procedure (subroutine) to execute in another address space (commonly on another computer on a shared network), which is coded as if it were a normal (local) procedure call, without the programmer explicitly coding the details for the remote interaction.

Page 7: Python 게임서버 안녕하십니까 : RPC framework 편

RPC• Remote Procedure Call• wikipedia : In distributed computing, a remote procedure call (RPC)

is when a computer program causes a procedure (subroutine) to execute in another address space (commonly on another computer on a shared network), which is coded as if it were a normal (local) procedure call, without the programmer explicitly coding the details for the remote interaction.

네트워크�상태나�콜�방식을�신경쓰지�않고�프로그래머가�원격의�함수를�실행하는�것

Page 8: Python 게임서버 안녕하십니까 : RPC framework 편

RPC1.�Procedure�name�

2.�Parameters��

3.�Networking��

4.�Protocol�(message)

Page 9: Python 게임서버 안녕하십니까 : RPC framework 편

RPC1.�Procedure�name�

2.�Parameters��

3.�Networking��

4.�Protocol�(message)

•IDL�(Interface�Definition�Language)�로�정의�

•IDL�은�RPC�framework�별로�다르지만�,�built-in�type�은�대부분�비슷하게�지원�

•단,�지원하는�container�의�차이,�signed,�unsigned�지원의�차이는�있음�

Page 10: Python 게임서버 안녕하십니까 : RPC framework 편

RPC1.�Procedure�name�

2.�Parameters��

3.�Networking��

4.�Protocol�(message)

•Networking�방식의�차이�따라�Procedure�Call�과�return�처리�방식이�달라짐�

•message�의�(de)serializer�차이에�따라�Protocol�의�성능이나�보안의�차이가�존재

Page 11: Python 게임서버 안녕하십니까 : RPC framework 편

게임에서�RPC�선택•IDL�정의를�서버/클라이언트가�코드�레벨에서�공유할�수�있나?�(�컴파일�

타임에�오류�확인이�가능한�방식을�선호,�코드를�생성해주는�RPC�

framework�의�IDL�)�

•클라이언트에서�async�call�을�지원해야�하며�return�의�형태나�return�의�

처리�과정에�개입할�수�있나?�

•Unity�(.Net�2.0,�.Net�3.5,�C#�4)�,�C++�지원하나?�

•json,�xml을�사용하지�않고�빠른�자체�message�protocol�지원하는가?

Page 12: Python 게임서버 안녕하십니까 : RPC framework 편

Thrift 마스터,�θrift�가�이�동네� 짱이라고�해요.�같이�싸워�봐요.�

Page 13: Python 게임서버 안녕하십니까 : RPC framework 편

Thrift�(θrift)•“scalable�cross-language�services�development”�를�위해�Facebook�

에서�개발,�RPC�framework�로�사용됨�

•다양한�언어를�지원�(�https://thrift.apache.org/lib/�)��

•built-in�type�외에�다양한�container�지원�(�https://thrift.apache.org/docs/types�)�

•하지만,�부족한�문서는�가장�큰�단점�(�Thrift:�The�Missing�Guide�

https://diwakergupta.github.io/thrift-missing-guide/�)

Page 14: Python 게임서버 안녕하십니까 : RPC framework 편

Thrift•Server,�Processor,�Protocol,�Transport�로�구성�

•Thrift�를�통해서�code�생성을�하면�RPC�Client�코드도�생성�

•서버�/�클라이언트�의�가장�큰�차이는�당연하게도�Processor�유무�

•Protocol,�Transport�는�각각�Serialization�과�Networking�을�담당

Page 15: Python 게임서버 안녕하십니까 : RPC framework 편

Thrift•일단,�Thrift�가�좋아보이니�이것으로�간단한�게임을�만들어보자.�

•PT�준비가�산으로…

PT가�산으로�가고�있냥!!

Page 16: Python 게임서버 안녕하십니까 : RPC framework 편

산으로�가는�김에�잠시�소개�합니다

Page 17: Python 게임서버 안녕하십니까 : RPC framework 편

준곰•스마트스터디의�CTO�로�몬스터�슈퍼리그�개발에�참여했습니다.��

•넥슨에서�게임을�즐겁게�만드는�방법을�배웠습니다.�

•엔씨소프트에서�게임을�잘�만드는�방법을�배웠습니다.�

•네오위즈게임즈에서�게임을�처음부터�만들고�끝까지�완성하는�

방법을�배웠습니다.�

•스마트스터디에서는�게임을�만들어�성공하는�방법을�배웠습니

다.

Page 18: Python 게임서버 안녕하십니까 : RPC framework 편

다시�게임으로�돌아갑시다

Page 19: Python 게임서버 안녕하십니까 : RPC framework 편

Othello�(오델로)•Reversi(리버시)�라고도�부르는�보드게임�

•두�명이�8x8�오델로�판�위에서�흑,�백�돌을�번갈아�놓으면서�진행

•처음에�판�가운데에�사각형으로�엇갈리게�배치된�돌�4개를�놓고�시작한다.�

•돌은�반드시�상대방�돌을�양쪽에서�포위하여�뒤집을�수�있는�곳에�놓아야�한다.�

•돌을�뒤집을�곳이�없는�경우에는�차례가�자동적으로�상대방에게�넘어가게�된다.�

•아래와�같은�조건에�의해�양쪽�모두�더�이상�돌을�놓을�수�없게�되면�게임이�끝나게�된다.�

• 64개의�돌�모두가�판에�가득�찬�경우�(가장�일반적)�

• 어느�한�쪽이�돌을�모두�뒤집은�경우�

• 한�차례에�양�쪽�모두�서로�차례를�넘겨야�하는�경우�

•게임이�끝났을�때�돌이�많이�있는�플레이어가�승자가�된다.�만일�돌의�개수가�같을�경우는�무승부가�된다.

wikipedia

Page 20: Python 게임서버 안녕하십니까 : RPC framework 편

IDL•Struct�

• User,�GameRoom��

•Service�

• User�

• Login,�Register�

• GameRoom�

• CreateGameRoom,�JoinGameRoom,�RandomJoin�

• Game�

• Put,�Exit,�GameOver,�Sync

Page 21: Python 게임서버 안녕하십니까 : RPC framework 편

서버•Data�Model�

• User�,�SecurityData��

• GameRoom�

•Python�3.6.1��

• SQLAlchemy��

• mysqlclient�

• asyncio�

• aiothrift�(�https://pypi.python.org/pypi/aiothrift�)

Page 22: Python 게임서버 안녕하십니까 : RPC framework 편

클라이언트•Intro�Scene��

• Register,�Login�

•Lobby�Scene�

• CreateGameRoom,�JoinGameRoom,�RandomJoin�

•Game�Scene�

• Put,�Exit,�GameOver,�Sync

Page 23: Python 게임서버 안녕하십니까 : RPC framework 편

othello.thriftnamespace csharp othello

struct User { 1: optional i64 id=0; 2: optional string token=""; 3: optional string name=""; 4: optional i32 level=1; 5: optional i32 exp=0; 6: optional i32 win=0; 7: optional i32 lose=0; 8: optional i32 gold=0;}

enum PlatformType { CUSTOM = 1, GAME_CENTER = 2, GOOGLE_PLAY = 3, FACEBOOK = 4,}

// exceptionsexception ErrorUserNotRegistered {}exception ErrorUserNameAlreadyExists {}exception ErrorUserAlreadyExists {}exception ErrorUserInvalidName {}exception ErrorSystem { 1: optional i32 code; 2: optional string message;}

// servicesservice OthelloService { User Login(1:PlatformType platform_type, 2:string platform_token) throws (1: ErrorUserNotRegistered errUserNotRegistered),

User Register(1:PlatformType platform_type, 2:string platform_token, 3:string name) throws (1: ErrorUserNameAlreadyExists errUserNameAlreadyExists, 2: ErrorUserInvalidName errUserInvalidName, 3: ErrorSystem errSystem)}

Page 24: Python 게임서버 안녕하십니까 : RPC framework 편

othello.thriftnamespace csharp othello

struct User { 1: optional i64 id=0; 2: optional string token=""; 3: optional string name=""; 4: optional i32 level=1; 5: optional i32 exp=0; 6: optional i32 win=0; 7: optional i32 lose=0; 8: optional i32 gold=0;}

enum PlatformType { CUSTOM = 1, GAME_CENTER = 2, GOOGLE_PLAY = 3, FACEBOOK = 4,}

// exceptionsexception ErrorUserNotRegistered {}exception ErrorUserNameAlreadyExists {}exception ErrorUserAlreadyExists {}exception ErrorUserInvalidName {}exception ErrorSystem { 1: optional i32 code; 2: optional string message;}

// servicesservice OthelloService { User Login(1:PlatformType platform_type, 2:string platform_token) throws (1: ErrorUserNotRegistered errUserNotRegistered),

User Register(1:PlatformType platform_type, 2:string platform_token, 3:string name) throws (1: ErrorUserNameAlreadyExists errUserNameAlreadyExists, 2: ErrorUserInvalidName errUserInvalidName, 3: ErrorSystem errSystem)}

Page 25: Python 게임서버 안녕하십니까 : RPC framework 편

thrift�generate�code�$ thrift --gen py ./othello.thrift$ thrift --gen csharp ./othello.thrift$ find gen-pygen-pygen-py/__init__.pygen-py/othellogen-py/othello/__init__.pygen-py/othello/constants.pygen-py/othello/OthelloService-remotegen-py/othello/OthelloService.pygen-py/othello/ttypes.py

$ find gen-csharpgen-csharpgen-csharp/othellogen-csharp/othello/thriftgen-csharp/othello/thrift/ErrorSystem.csgen-csharp/othello/thrift/ErrorUserAlreadyExists.csgen-csharp/othello/thrift/ErrorUserInvalidName.csgen-csharp/othello/thrift/ErrorUserNameAlreadyExists.csgen-csharp/othello/thrift/ErrorUserNotRegistered.csgen-csharp/othello/thrift/OthelloService.csgen-csharp/othello/thrift/PlatformType.csgen-csharp/othello/thrift/User.cs

•thrift�--gen�[language]�[file]��

•gen-[language]�폴더에�code�가�생

Page 26: Python 게임서버 안녕하십니까 : RPC framework 편

othello.server.pyimport asyncioimport thriftpyfrom aiothrift.server import create_server# ...othello = thriftpy.load('othello.thrift', module_name='othello_thrift')# ...class OthelloServer: # ... def run_forever(self): self.loop = asyncio.get_event_loop() self.server = self.loop.run_until_complete( create_server(othello.OthelloService, Dispatcher(self), address=(self.ip, self.port), loop=self.loop, protocol_cls=TBinaryProtocol) self.loop.run_forever()

•aiothrift�로�Server�구성�(@asyncio.coroutine)�

• Server,�Processor,�Protocol,�Transport�재작성�

• asyncio�event_loop,�open_connection�사용

Page 27: Python 게임서버 안녕하십니까 : RPC framework 편

othello.server.pyclass Dispatcher: # ... @db_transaction def Login(self, platform_type, platform_token): # ... return user

@db_transaction def Register(self, platform_type, platform_token, name): # ... return user

def db_transaction(func): @wraps(func) def _impl(self, *args, **kwargs): ret = None try: self.db.begin_session() ret = func(self, *args, **kwargs) self.db.commit() except Exception as e: self.db.rollback() raise e finally: self.db.end_session()

return ret return _impl

•processor�handler�는�service�정의

대로�작성�

•db�transaction�을�processor�에�반

영하기�위해�decorator�

(db_transaction)�를�작성�

•RPC�는�오류가�있는�경우�raise�

Exception�을�하므로�이를�기준으로�

commit,�rollback

Page 28: Python 게임서버 안녕하십니까 : RPC framework 편

문제•Server�

•1�user�는�1개의�session�만�유지해야�하는�방법�필요�

•중복�요청�방지를�위한�방법이�필요�

•Client�

•C#�으로�생성된�Client�코드는�TSocket�을�사용�(blocked-io)�이는�

synchronous�

Page 29: Python 게임서버 안녕하십니까 : RPC framework 편

시도•인증�전�후로�사용�가능한�Procedure�를�분리하고�인증�전에는�session�

생성을�인증�후에는�session�체크를�하는��로직을�작성�

•Processor�에�session�token�생성,�체크�작성�

•Protocol�에�session�token�기본�포함,�생성된�코드에서�매번�session�

token�을�넣지�않도록�작성

Page 30: Python 게임서버 안녕하십니까 : RPC framework 편

시도•인증�전�후로�사용�가능한�Procedure�를�분리하고�인증�전에는�session�

생성을�인증�후에는�session�체크를�하는��로직을�작성�

•Processor�에�session�token�생성,�체크�작성�

•Protocol�에�session�token�기본�포함,�생성된�코드에서�매번�session�

token�을�넣지�않도록�작성

빠른�포기!!!

•IDL을�기준으로�코드�생성이�되므로�IDL�에�없는�상태에서�이를�반영하기�

위해서는�Protocol�을�수정할�필요가�있음�

•Protocol�에서의�인증�절차�등이�필요함

Page 31: Python 게임서버 안녕하십니까 : RPC framework 편

시도•C#�의�생성된�service�코드를�coroutine�으로�

•Thrift�가�생성한�코드는�async�처리가�불가능,�async�한�처리를�위해서�

Unity�coroutine�코드가�필요�

•TSocket�등�Transport�도�coroutine�으로�작성되어�있지�않음��

•생성된�코드기준으로�async,�await�등은�C#�5�이상�필요한�만큼�async�

call�을�사용할�수�없음

Page 32: Python 게임서버 안녕하십니까 : RPC framework 편

시도•C#�의�생성된�service�코드를�coroutine�으로�

•Thrift�가�생성한�코드는�async�처리가�불가능,�async�한�처리를�위해서�

Unity�coroutine�코드가�필요�

•TSocket�등�Transport�도�coroutine�으로�작성되어�있지�않음��

•생성된�코드기준으로�async,�await�등은�C#�5�이상�필요한�만큼�async�

call�을�사용할�수�없음

빠른�포기!!!

•TSocket,�Transport�를�coroutine�으로�새로�작성해야�함�

Page 33: Python 게임서버 안녕하십니까 : RPC framework 편

gRPC 마스터,�빠른�포기�다음에는�빠른�시도!�이번엔�gRPC와�

불어�보시죠.

Page 34: Python 게임서버 안녕하십니까 : RPC framework 편

gRPC•google�에서�개발�

• Transport�로�HTTP/2�지원��

•양방향�streaming�지원�

• IDL�로�google�Protocol�Buffers�사용�

•서버�클라이언트�모두�sync,�async�방식�제공�

• Protocol�레벨에서�인증�기능�제공�(�https://grpc.io/docs/guides/auth.html�)�

• 다양한�언어�지원�(�https://grpc.io/docs/quickstart/�)

Page 35: Python 게임서버 안녕하십니까 : RPC framework 편

protobuf(Protocol�Buffers)•상세한�문서�!�(�https://developers.google.com/protocol-buffers/�)�

•signed,�unsigned�지원�

•uint32,�uint64�

•nested�type�지원�

•message�{�message�{�enum�{�}�}�}�

•repeated�(list),�map�container�지원�

Page 36: Python 게임서버 안녕하십니까 : RPC framework 편

othello.proto

•unsigned�를�지원하므로�적절하게�이용�

•Exception�이�없으므로�ResultCode�를�만들어�사용�

•procedure�에�사용하는�parameter�와�return�value�는�별도로�정의

syntax = "proto3";

package othello;

message User { uint64 id = 1; string token = 2; string name = 3; uint32 level = 4; uint32 exp = 5; uint32 win = 6; uint32 lose = 7; uint32 gold = 8;}

enum PlatformType { UNKNOWN = 0; CUSTOM = 1; GAME_CENTER = 2; GOOGLE_PLAY = 3; FACEBOOK = 4;}enum ResultCode { Success = 0; ErrorUserNotRegistered = 100; ErrorUserNameAlreadyExists = 101; ErrorUserAlreadyExists = 102; ErrorUserInvalidName = 103; ErrorSystem = 200;}

Page 37: Python 게임서버 안녕하십니까 : RPC framework 편

othello.proto

•unsigned�를�지원하므로�적절하게�이용�

•Exception�이�없으므로�ResultCode�를�만들어�사용�

•procedure�에�사용하는�parameter�와�return�value�는�별도로�정의

syntax = "proto3";

package othello;

message User { uint64 id = 1; string token = 2; string name = 3; uint32 level = 4; uint32 exp = 5; uint32 win = 6; uint32 lose = 7; uint32 gold = 8;}

enum PlatformType { UNKNOWN = 0; CUSTOM = 1; GAME_CENTER = 2; GOOGLE_PLAY = 3; FACEBOOK = 4;}enum ResultCode { Success = 0; ErrorUserNotRegistered = 100; ErrorUserNameAlreadyExists = 101; ErrorUserAlreadyExists = 102; ErrorUserInvalidName = 103; ErrorSystem = 200;}

Page 38: Python 게임서버 안녕하십니까 : RPC framework 편

othello.proto

•procedure�에�사용하는�parameter�와�return�value�는�별도로�정의�

•return�값�에는�모두�Result�를�포함하도록�작성

message Result { ResultCode code = 1; string message = 2;}message ReqLogin { PlatformType platform_type = 1; string platform_token = 2;}message RspLogin { Result result = 1; User user = 2;}

service Othello { rpc Login(ReqLogin) returns (RspLogin) {} rpc Register(ReqRegister) returns (RspRegister) {}}

Page 39: Python 게임서버 안녕하십니까 : RPC framework 편

othello.proto

•procedure�에�사용하는�parameter�와�return�value�는�별도로�정의�

•return�값�에는�모두�Result�를�포함하도록�작성

message Result { ResultCode code = 1; string message = 2;}message ReqLogin { PlatformType platform_type = 1; string platform_token = 2;}message RspLogin { Result result = 1; User user = 2;}

service Othello { rpc Login(ReqLogin) returns (RspLogin) {} rpc Register(ReqRegister) returns (RspRegister) {}}

Page 40: Python 게임서버 안녕하십니까 : RPC framework 편

gRPC�generate�code

•pb2�,�grpc�2개의�파일이�생성�

•data�class�들은�othello_pb2.py�

•server,�client�class�들은�othello_pb2_grpc.py

$ python -m grpc_tools.protoc -I. --python_out=./gen-grpc --grpc_python_out=./gen-grpc ./othello.proto

$ find gen-grpcgen-grpcgen-grpc/__init__.pygen-grpc/othello_pb2.pygen-grpc/othello_pb2_grpc.py

Page 41: Python 게임서버 안녕하십니까 : RPC framework 편

othello.server.py

•Thrift�와�별�차이�없음�

•asyncio�잘�지원해주는�package�는�아직�없음

def run_forever(self): self.server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) othello_pb2_grpc.add_OthelloServicer_to_server( Dispatcher(self), self.server) self.server.add_insecure_port('[::]:{}'.format(self.port)) self.server.start() try: while True: time.sleep(60 * 60 * 24) except KeyboardInterrupt: self.server.stop(0)

Page 42: Python 게임서버 안녕하십니까 : RPC framework 편

othello.server.py

•Exception�을�지원하지�않고�있으므로�ResultCode�를�만들어�사용

class Dispatcher(othello_pb2_grpc.OthelloServicer): @db_transaction def Login(self, request, context): db_sec = self.session.query(SecurityData).filter( and_(SecurityData.platform_type==request.platform_type, SecurityData.platform_token==request.platform_token)).first()

if db_sec is None: return othello_pb2.RspLogin( result=othello_pb2.Result(code=othello_pb2.ErrorUserNotRegistered)) # ... rsp = othello_pb2.RspLogin( result=othello_pb2.Result(code=othello_pb2.Success), user=othello_pb2.User()) db_user.fill(rsp.user)return rsp

Page 43: Python 게임서버 안녕하십니까 : RPC framework 편

othello.server.py

•Exception�을�지원하지�않고�있으므로�ResultCode�를�만들어�사용

class Dispatcher(othello_pb2_grpc.OthelloServicer): @db_transaction def Login(self, request, context): db_sec = self.session.query(SecurityData).filter( and_(SecurityData.platform_type==request.platform_type, SecurityData.platform_token==request.platform_token)).first()

if db_sec is None: return othello_pb2.RspLogin( result=othello_pb2.Result(code=othello_pb2.ErrorUserNotRegistered)) # ... rsp = othello_pb2.RspLogin( result=othello_pb2.Result(code=othello_pb2.Success), user=othello_pb2.User()) db_user.fill(rsp.user)return rsp

Page 44: Python 게임서버 안녕하십니까 : RPC framework 편

문제•Python�Test�Client�작동�확인.�문제는�역시나�Unity�C#�지원�문제�

•Unity�Mono�는�.NET�4.5�가�아니라�.NET�2.0�지원�(Unity2017.1�버전�

부터는�.NET�3.5�지원)�

•Unity�에서�gRPC�사용을�위한�프로젝트들이�있기는�하지만�생각보다�복

잡하고�무거움�(�https://github.com/neuecc/MagicOnion�)��

•이럴려고�RPC�를�써보려고�한�건�아닌데

Page 45: Python 게임서버 안녕하십니까 : RPC framework 편

문제•Python�Test�Client�작동�확인.�문제는�역시나�Unity�C#�지원�문제�

•Unity�Mono�는�.NET�4.5�가�아니라�.NET�2.0�지원�(Unity2017.1�버전�

부터는�.NET�3.5�지원)�

•Unity�에서�gRPC�사용을�위한�프로젝트들이�있기는�하지만�생각보다�복

잡하고�무거움�(�https://github.com/neuecc/MagicOnion�)��

•이럴려고�RPC�를�써보려고�한�건�아닌데

빠른�포기!!!

Page 46: Python 게임서버 안녕하십니까 : RPC framework 편

오델로�하나�만들지�못하고�끝나나!

Page 47: Python 게임서버 안녕하십니까 : RPC framework 편

몬스터�슈퍼리그�방식 마스터,�과거를�돌아봐요.�그때�그�코드�그가�당신을��

도와줄거에요.

Page 48: Python 게임서버 안녕하십니까 : RPC framework 편

몬스터�슈퍼리그

2017/04 NDC 발표 기준

Page 49: Python 게임서버 안녕하십니까 : RPC framework 편

몬스터�슈퍼리그

2017/04 NDC 발표 기준

Page 50: Python 게임서버 안녕하십니까 : RPC framework 편

Protocol�Buffersmessage MsgUserItem{ optional fixed32 item_uid = 1; optional uint32 item_count = 2;}enum MonsterStatType { MS_None = 0; MS_Attack = 1; MS_Defence = 2; MS_Heal = 3; MS_Balance = 4; MS_Hp = 5;}

•unsigned,�signed�type�구분�

•Data�로�사용할�것은�Msg,�RPC�로�실행될�Procedure�정의는�Req,�Rsp�

message ReqUserLogin{ optional AccountPlatformType platform_type = 1; optional string platform_user_id = 2;}// response packet은 RspUserLogin 을 사용한다message ReqUserRegister{ // ...}

Page 51: Python 게임서버 안녕하십니까 : RPC framework 편

Protocol�Buffersmessage Request { optional uint32 protocolVersion = 1; optional int32 protocolId = 2; optional uint32 seqNo = 3; optional string token = 4; optional Ticket ticket = 5;

optional ReqUserLogin userLoginReq = 50; optional ReqUserRegister userRegisterReq = 55; // ...}

•Request�를�service�라고�정의,�optional�로�모든�Procedure�등록�

•인증에�필요한�protocol�version,�protocol�id,�seq�no,�token�을�

Request�Service�에�공통으로�추가

Page 52: Python 게임서버 안녕하십니까 : RPC framework 편

Protocol�Buffers

•RPC�return�역시�Server�—>�Client�RPC�라고�보고�Response�Service�

생성,�Response�Service�에�모든�Procedure�를�등록�

•인증에�필요한�정보를�Service�에�공통으로�추가

message Response{ optional uint32 protocolVersion = 1; optional int32 protocolId = 2; optional Result result = 3; optional Ticket ticket = 4; optional uint32 reqSeqNo = 5;

optional RspUserLogin userLoginRsp = 50; optional RspUserRegister userRegisterRsp = 55; // ...}

message MultipleResponse{ repeated Response responses = 1; optional uint32 reqSeqNo = 2; optional uint32 nextTicketNo = 3;}

Page 53: Python 게임서버 안녕하십니까 : RPC framework 편

server.route.pydef route(ext): def decorator(f): succ = False for field in request_pb2._REQUEST.fields: if field.message_type is not None and \ field.message_type.name == ext: route.route_protocol_map[field.number] = \ [field.name, f, ext] succ = True break if succ is False: raise Exception("Unknown Request Packet : %s" % ext) return f return decorator

def handle(userContext, req): if req.protocolId in route.route_protocol_map: field_name, handler, name = \ route.route_protocol_map[req.protocolId] if req.HasField(field_name): packet = getattr(req, field_name) newrelic.agent.set_transaction_name(field_name) return handler(userContext, packet)

•Procedure�는

@route(“Procedure�Name”)�

으로�선언�

•Request�가�오면�handle�에서�

등록되어있는�Procedure�를�

Procedure�Name�기준으로�실행

Page 54: Python 게임서버 안녕하십니까 : RPC framework 편

server.api.py@route('ReqUserLogin')def userLogin(reqUserLogin): # ...

@app.route('/api', methods=['POST'])def api(): req = request_pb2.Request.FromString(reqBody) # ... db_begin() try: rsp = handle(req) db_commit() except: db_rollback() finally: db_end() return rsp

•실제�몬슈리에서�Request�를�처리하는�기본�로직

Page 55: Python 게임서버 안녕하십니까 : RPC framework 편

몬스터�슈퍼리그•클라이언트에서�Synchronous�Call�을�지원하지�않음�

•Procedure�Call�만�있을�뿐�return�은�Response�용�Procedure�가�클라

이언트에서�실행�되는�형태,�하지만�양방향통신�이라고�할�수는�없음�

•Connection�연결�유지가�필요없는�구조,�서버�클라�모두�부담이�적음�

•Protobuf�사용으로�인한�serialize�,�deserialize�비용이�낮음�

•게임�데이터�를�Protobuf�로�생성한�struct�사용

Page 56: Python 게임서버 안녕하십니까 : RPC framework 편

몬스터�슈퍼리그•클라이언트에서�Synchronous�Call�을�지원하지�않음�

•Procedure�Call�만�있을�뿐�return�은�Response�용�Procedure�가�클라

이언트에서�실행�되는�형태,�하지만�양방향통신�이라고�할�수는�없음�

•Connection�연결�유지가�필요없는�구조,�서버�클라�모두�부담이�적음�

•Protobuf�사용으로�인한�serialize�,�deserialize�비용이�낮음�

•게임�데이터�를�Protobuf�로�생성한�struct�사용

도전!!!

Page 57: Python 게임서버 안녕하십니까 : RPC framework 편

게임에�맞게�RPC�만들기

마스터,�그냥�대충해.

Page 58: Python 게임서버 안녕하십니까 : RPC framework 편

시도•게임�개발에�필요한�수준으로�직접�만들어보자�

•개발�난이도가�높은�IDL�과�message�의�serializer�는�기존�것을�선택하자�

•서버는�http�server�를�사용하고�RPC�설계는�몬스터�슈퍼리그�방식을�사

용하자

Page 59: Python 게임서버 안녕하십니까 : RPC framework 편

선택•IDL�:�Protocol�Buffers�(serializer�포함,�service�정의는�사용하지�않음)�

•Procedure�는�IDL�에�정의한�message�를�활용한다�

•ReqLogin,�RspLogin��

•Request�에�실패한�경우에�대한�공통적인�처리를�작성한다�

•RPC�response�처리�효율화를�위해�Request�/�Respone�를�분리한다�

•서버는�aiohttp�,�Protobuf�3.3.0�(Python�3�지원)�을�사용�

•클라이언트는�Protobuf�2.6.1�,�protobuf-net�r668�을�사용

Page 60: Python 게임서버 안녕하십니까 : RPC framework 편

othello.proto•Procedure�를�protobuf�IDL�의�

message�로�정의�

•Client�—>�Server�의�Procedure�는�

Request�라는�message�내에�모두�

등록.�(Request�는�Service�임)

message ReqLogin { optional PlatformType platform_type = 1; optional string platform_token = 2;}message RspLogin { optional User user = 1; optional string platform_token = 2; optional string token = 3;}message Request { optional uint32 protocolVersion = 1; optional int32 protocolId = 2; optional uint32 seqNo = 3; optional string token = 4; optional ReqLogin loginReq = 100; optional ReqRegister registerReq = 101; optional ReqCreateGameRoom makeGameRoomReq = 102; optional ReqExitGameRoom exitGameRoomReq = 103; optional ReqGamePut gamePutReq = 104; optional ReqGameSync gameSyncReq = 105; optional ReqJoinGameRoom joinGameRoomReq = 106; optional ReqRandomJoin randomJoinReq = 107;}

Page 61: Python 게임서버 안녕하십니까 : RPC framework 편

othello.proto•Procedure�를�protobuf�IDL�의�

message�로�정의�

•Client�—>�Server�의�Procedure�는�

Request�라는�message�내에�모두�

등록.�(Request�는�Service�임)

message ReqLogin { optional PlatformType platform_type = 1; optional string platform_token = 2;}message RspLogin { optional User user = 1; optional string platform_token = 2; optional string token = 3;}message Request { optional uint32 protocolVersion = 1; optional int32 protocolId = 2; optional uint32 seqNo = 3; optional string token = 4; optional ReqLogin loginReq = 100; optional ReqRegister registerReq = 101; optional ReqCreateGameRoom makeGameRoomReq = 102; optional ReqExitGameRoom exitGameRoomReq = 103; optional ReqGamePut gamePutReq = 104; optional ReqGameSync gameSyncReq = 105; optional ReqJoinGameRoom joinGameRoomReq = 106; optional ReqRandomJoin randomJoinReq = 107;}

Procedure

Service

Page 62: Python 게임서버 안녕하십니까 : RPC framework 편

othello.proto•RPC�return�값은�Server�—>�Client�

Procedure�Call�로�정의하고�Response�

message�를�만들어�Procedure�를�모두�

등록�

•서버�기준에서�한번에�여러�Procedure�를�

순서대로�Call�할�수�있도록�

multipleResponse�message�를�추가�

ex)�Login�을�다시�했지만�이전�접속�때�

진행중인�게임이�있다면�Join��

�RspLogin�,�RspJoinGameRoom�두�

Response�가�return

message Response{ optional uint32 protocolVersion = 1; optional int32 protocolId = 2; optional Result result = 3; optional uint32 reqSeqNo = 5;

optional RspLogin loginRsp = 100; optional RspRegister registerRsp = 101; optional RspCreateGameRoom makeGameRoomRsp = 102; optional RspExitGameRoom exitGameRoomRsp = 103; optional RspGamePut gamePutRsp = 104; optional RspGameSync gameSyncRsp = 105; optional RspJoinGameRoom joinGameRoomRsp = 106;

optional RspInternalServerError internalServerErrorRsp = 200;}message MultipleResponse{ optional uint32 reqSeqNo = 1; repeated Response responses = 10;}

Page 63: Python 게임서버 안녕하십니까 : RPC framework 편

othello.server.py

•aiohttp�는�+_+b�좋음�

•health�는�ELB�target�group�의�health�check�용�

•api,�route,�handle�는�몬스터�슈퍼리그의�api�코드�참고

def run_forever(self): self.server = web.Application() self.server.router.add_get('/', self.home) self.server.router.add_get('/health', self.health) self.server.router.add_post('/api', self.api) web.run_app(self.server, host=self.ip, port=self.port)

# api , route, handle 은 기존 몬스터 슈퍼리그 에서 사용한 것을 그대로 적용

Page 64: Python 게임서버 안녕하십니까 : RPC framework 편

Client.cs

•Procedure�Call�(SendPacket)�하고�wait�하지�않음��

•Server—>Client�RPC�실행은�OnPacket�으로�시작하는�handler�들이�순

차적으로�실행

public void RequestLogin(PlatformType platform_type, string platform_token){ ReqLogin req = new ReqLogin(); req.platform_type = platform_type; req.platform_token = platform_token; SendRequest(req, typeof(RspLogin));}public void OnPacketRspLogin(HttpResponseCode httpCode, Result result, RspLogin rsp){ if (httpCode == HttpResponseCode.OK && result.code == ResultCode.Success) { DataManager.user = rsp.user; Othello.Client.instance.UserToken = rsp.token; }}

Page 65: Python 게임서버 안녕하십니까 : RPC framework 편

Client.cs•Server—>Client�RPC�실행은�OnPacket�으로�시작하는�handler�들이�순

차적으로�실행�

•동일한�Procedure�가�여럿�등록되어�실행�될�수�있음�

•data�처리�부분과�UI�처리부분을�분리하기�위함�

•항상�data�를�업데이트�Procedure�가�먼저�실행되어�데이터�업데이트

가�완료된�다음�UI�업데이트�Procedure�가�실행되어�UI�갱신�

•UI�업데이트의�경우�여러�UI�Component�에서�직접�UI�갱신

Page 66: Python 게임서버 안녕하십니까 : RPC framework 편

Client.cs•Server—>Client�RPC�실행은�OnPacket�으로�시작하는�handler�들이�순

차적으로�실행�

•동일한�Procedure�가�여럿�등록되어�실행�될�수�있음�

•data�처리�부분과�UI�처리부분을�분리하기�위함�

•항상�data�를�업데이트�Procedure�가�먼저�실행되어�데이터�업데이트

가�완료된�다음�UI�업데이트�Procedure�가�실행되어�UI�갱신�

•UI�업데이트의�경우�여러�UI�Component�에서�직접�UI�갱신

Server Client

DataManager

GameScene

OthelloBoard, DashBoard

RspGameSync

Page 67: Python 게임서버 안녕하십니까 : RPC framework 편

Client.cs•Procedure�Call�을�할�때�Networking�을�크게�고민하지�않도록�구현�

•HTTP�Status�Code�==�200�

•Procedure�Call�에�return�에�해당하는�Response�RPC�실행�

•Procedure�Call�로직내에서�검출되는�Error�는�resultCode�로�확인�

•HTTP�Status�Code�!=�200�

•Network�Error�또는�Server�Error�

•Networking�담당�코드에서�Retry,�Restart�옵션을�유저에게�제공

Page 68: Python 게임서버 안녕하십니까 : RPC framework 편

Client.cs•Procedure�Call�을�할�때�Networking�을�크게�고민하지�않도록�구현�

•HTTP�Status�Code�==�200�

•Procedure�Call�에�return�에�해당하는�Response�RPC�실행�

•Procedure�Call�로직내에서�검출되는�Error�는�resultCode�로�확인�

•HTTP�Status�Code�!=�200�

•Network�Error�또는�Server�Error�

•Networking�담당�코드에서�Retry,�Restart�옵션을�유저에게�제공

실패했던�http�request�를�그대로�다시�보냄

Page 69: Python 게임서버 안녕하십니까 : RPC framework 편

othello�완성$ python3 manage.py runserver --port 14500 local.cfginit session======== Running on http://127.0.0.1:14500 ========(Press CTRL+C to quit)begin_sessionprotocolVersion: 1protocolId: 100seqNo: 1loginReq { platform_type: CUSTOM platform_token: "1c8daa46-74ec-4f7f-9a6a-42dc9ecb9762"}

REQUEST : ReqLoginplatform_type: CUSTOMplatform_token: "1c8daa46-74ec-4f7f-9a6a-42dc9ecb9762"

commitend_sessionreqSeqNo: 1responses { protocolVersion: 1 protocolId: 100 result { code: Success } loginRsp { user { id: 5 name: "joongom3" level: 1 exp: 0 win: 0 lose: 0 gold: 0 } platform_token: "1c8daa46-74ec-4f7f-9a6a-42dc9ecb9762" token: "d68a4478-79e3-11e7-bf27-a45e60f1ced1" }}

Page 70: Python 게임서버 안녕하십니까 : RPC framework 편

othello�완성•Thrift,�gRPC�포기�후�몬슈

리�방식을�수정하여�도입�

•서버의�경우는�aiohttp�로�

새로�작성(몬슈리는�flask)�

•실제�코딩시간�40�시간�정

도�

•AWS�ECS�로�서비스�중�

•Android�앱으로�빌드�

(PlayStore�“준곰오셀로”)

Page 71: Python 게임서버 안녕하십니까 : RPC framework 편

test_client.pyclass Dispatcher: ... def RspLogin(self, result, rsp): if result.code == othello_pb2.ErrorUserNotRegistered: client.rpc(othello_pb2.ReqRegister( platform_type=othello_pb2.CUSTOM, platform_token=client.user_platform_token, name=client.user_name )) elif result.code == othello_pb2.Success: client.user = rsp.user client.token = rsp.token client.rpc(othello_pb2.ReqCreateGameRoom())class Client: ... def run(self): self.rpc(othello_pb2.ReqLogin(platform_type=othello_pb2.CUSTOM, platform_token=self.user_platform_token)) while len(self.rpc_queue) > 0: remote_procedure = self.rpc_queue.pop(0) status_code = self.__rpc(remote_procedure) print('status_code:{}'.format(status_code)) if status_code != 200: self.rpc_queue.insert(0, remote_procedure) time.sleep(5)

•Python�으로�구현한�Othello�

RPC�test�Client�

•Response�Service�의

procedure�name�으로�

handler�생성�

•Exception�대신�Result�Code�

사용�

•RPC�실제�실행은�main�loop�

에서�처리

Page 72: Python 게임서버 안녕하십니까 : RPC framework 편

othello�python�client•github�:�https://goo.gl/Ws2qsn�

•간단한�random�play�를�하는�클라이언트�

•현재�서버에서는�총�5개의�auto_client�가�대기중�

•직접�protocol에�맞추어�클라이언트�개발을�해도�

되고�auto_client�의�GamePut�부분을�수정하여�

간단한�봇을�만들�수�있음�

•파이콘�기간�중에�실행해보시는�분들께�추첨을�통해�선물을�드립니다.

Page 73: Python 게임서버 안녕하십니까 : RPC framework 편

othello�python�client•github�:�https://goo.gl/Ws2qsn�

•간단한�random�play�를�하는�클라이언트�

•현재�서버에서는�총�5개의�auto_client�가�대기중�

•직접�protocol에�맞추어�클라이언트�개발을�해도�

되고�auto_client�의�GamePut�부분을�수정하여�

간단한�봇을�만들�수�있음�

•파이콘�기간�중에�실행해보시는�분들께�추첨을�통해�선물을�드립니다.

Page 74: Python 게임서버 안녕하십니까 : RPC framework 편

정리�&�생각해볼�것들이제�얼마�남지�않았어!

Page 75: Python 게임서버 안녕하십니까 : RPC framework 편

정리•대부분의�RPC�Framework�들은�Python�을�매우�잘�지원,�특히�Python�

3.6�의�asyncio�용�Library�들이�많음.�

•하지만,�게임의�특성에�따른�인증�절차�반영이�어려움�

•하지만,�게임엔진의�가장�큰�축인�Unity의�C#�지원이�잘�안됨,�또한�생성

된�Client�RPC�코드가�Unity�기준으로�가�Async�하지�않음�

•위와�같은�이유로�기본적으로�RPC가�게임�서버/클라이언트에�잘�맞지는�않음�

•다만,�각�RPC�framework�마다�IDL,�serializer�를�제공하니�이를�잘�이용하면�직접�개발하는데�큰�도움이�될�수�있음

Page 76: Python 게임서버 안녕하십니까 : RPC framework 편

생각해볼�것들•gRPC�의�stream�처럼�HTTP/2�를�지원하면�GameSync�등은�필요�없지�

않을까?�

•FlatBuffers�의�벤치마킹�자료를�보면�성능이�우월한데�이런�serialized�

data�structure�를�더�살펴볼�필요는�있지�않을까?�

•클라이언트에서�RPC�return�을�처리�방식에�coroutine�을�통한�async�

방식도�지원한다면?

Page 77: Python 게임서버 안녕하십니까 : RPC framework 편

생각해볼�것들•SMARTSTUDY�는�뭐하는�곳인가?�지금�뭐하고�있나?

오픈소스를�사랑하는�스마트스터디�기술본부는�Slack과�JIRA로�커뮤니케이션하고�

GitHub�Enterprise와�CircleCI�Enterprise로�개발�및�통합�테스트�후에�Terraform으

로�관리되는�AWS�위에서�Docker�기반으로�서비스를�운영하며�DataDog으로�모니터

링을,�오류�추적은�Sentry에서�받으며�Unity와�Python으로�만든�몬스터�슈퍼리그는�

글로벌�원�빌드로�게임을�즐기는�전�세계�친구들과�node.js채팅으로�대화를�나누고,�

React로�만든�관리도구를�통해�5개�국어�/�2,500편의�핑크퐁�콘텐츠는�준-페타급�스

토리지�안에서�Transcoding�되어�YouTube�등에�올라가�누적�시청�수가�25억이�넘지

만,�이런�것들보다�더�중요한,�가장�중요한�건…�재미있게�같이�개발할�실력�있는�동료

를�스마트스터디는�항상�찾고�있다는�것!

Page 78: Python 게임서버 안녕하십니까 : RPC framework 편

감사합니다