KOBACO( - neoclick환경부 분배출캠페읶 김준현 양상국 편(13.4) 산업통상자원부 에너지젃약캠페읶 이광수 편(13. 5) 기재부 핚미FTA 읶식개선 캠페읶(11.
Python 게임서버 안녕하십니까 : RPC framework 편
-
Upload
- -
Category
Technology
-
view
1.161 -
download
0
Transcript of Python 게임서버 안녕하십니까 : RPC framework 편
Python�게임서버�안녕하십니까?
RPC�Framework�편
스마트스터디�CTO�박준철
왜?NDC�발표�(Python�게임서버�안녕하십니까?�:�몬스터�슈퍼리그�게임�서버)��
준비중에�사내�리뷰�과정에서�“너굴”�님의�질문으로부터�시작
너굴�:�“게임�서버/클라�네트워킹�에서�RPC�framework�를�사용하지�않고�직접�구현하신�이유가�있나요?”�
준곰�:�“어쩌고�저쩌고…�그래서�어쩌고저쩌고”�
너굴�:�“네…”�
RPC�라는게�뭐길래?�게임�서버/클라에�쓸수�있나?
목표•RPC�framework�에�대한�정보�공유�
• RPC�framework�를�게임에�적용해보자�
•몬스터�슈퍼리그�에서�사용한�방식�공유�
•게임에�적합한�방식을�직접�만들어�보는�것
목차
•RPC�
• Thrift�,�gRPC�
• 몬스터�슈퍼리그�방식�
• 게임에�맞게�RPC�만들기�
•정리�&�생각해볼�만한�것들
마스터,�준비�되었나요?�이제�시작합니다.�졸면�안되요!
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.
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.
네트워크�상태나�콜�방식을�신경쓰지�않고�프로그래머가�원격의�함수를�실행하는�것
RPC1.�Procedure�name�
2.�Parameters��
3.�Networking��
4.�Protocol�(message)
RPC1.�Procedure�name�
2.�Parameters��
3.�Networking��
4.�Protocol�(message)
•IDL�(Interface�Definition�Language)�로�정의�
•IDL�은�RPC�framework�별로�다르지만�,�built-in�type�은�대부분�비슷하게�지원�
•단,�지원하는�container�의�차이,�signed,�unsigned�지원의�차이는�있음�
RPC1.�Procedure�name�
2.�Parameters��
3.�Networking��
4.�Protocol�(message)
•Networking�방식의�차이�따라�Procedure�Call�과�return�처리�방식이�달라짐�
•message�의�(de)serializer�차이에�따라�Protocol�의�성능이나�보안의�차이가�존재
게임에서�RPC�선택•IDL�정의를�서버/클라이언트가�코드�레벨에서�공유할�수�있나?�(�컴파일�
타임에�오류�확인이�가능한�방식을�선호,�코드를�생성해주는�RPC�
framework�의�IDL�)�
•클라이언트에서�async�call�을�지원해야�하며�return�의�형태나�return�의�
처리�과정에�개입할�수�있나?�
•Unity�(.Net�2.0,�.Net�3.5,�C#�4)�,�C++�지원하나?�
•json,�xml을�사용하지�않고�빠른�자체�message�protocol�지원하는가?
Thrift 마스터,�θrift�가�이�동네� 짱이라고�해요.�같이�싸워�봐요.�
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/�)
Thrift•Server,�Processor,�Protocol,�Transport�로�구성�
•Thrift�를�통해서�code�생성을�하면�RPC�Client�코드도�생성�
•서버�/�클라이언트�의�가장�큰�차이는�당연하게도�Processor�유무�
•Protocol,�Transport�는�각각�Serialization�과�Networking�을�담당
Thrift•일단,�Thrift�가�좋아보이니�이것으로�간단한�게임을�만들어보자.�
•PT�준비가�산으로…
PT가�산으로�가고�있냥!!
산으로�가는�김에�잠시�소개�합니다
준곰•스마트스터디의�CTO�로�몬스터�슈퍼리그�개발에�참여했습니다.��
•넥슨에서�게임을�즐겁게�만드는�방법을�배웠습니다.�
•엔씨소프트에서�게임을�잘�만드는�방법을�배웠습니다.�
•네오위즈게임즈에서�게임을�처음부터�만들고�끝까지�완성하는�
방법을�배웠습니다.�
•스마트스터디에서는�게임을�만들어�성공하는�방법을�배웠습니
다.
다시�게임으로�돌아갑시다
Othello�(오델로)•Reversi(리버시)�라고도�부르는�보드게임�
•두�명이�8x8�오델로�판�위에서�흑,�백�돌을�번갈아�놓으면서�진행
•처음에�판�가운데에�사각형으로�엇갈리게�배치된�돌�4개를�놓고�시작한다.�
•돌은�반드시�상대방�돌을�양쪽에서�포위하여�뒤집을�수�있는�곳에�놓아야�한다.�
•돌을�뒤집을�곳이�없는�경우에는�차례가�자동적으로�상대방에게�넘어가게�된다.�
•아래와�같은�조건에�의해�양쪽�모두�더�이상�돌을�놓을�수�없게�되면�게임이�끝나게�된다.�
• 64개의�돌�모두가�판에�가득�찬�경우�(가장�일반적)�
• 어느�한�쪽이�돌을�모두�뒤집은�경우�
• 한�차례에�양�쪽�모두�서로�차례를�넘겨야�하는�경우�
•게임이�끝났을�때�돌이�많이�있는�플레이어가�승자가�된다.�만일�돌의�개수가�같을�경우는�무승부가�된다.
wikipedia
IDL•Struct�
• User,�GameRoom��
•Service�
• User�
• Login,�Register�
• GameRoom�
• CreateGameRoom,�JoinGameRoom,�RandomJoin�
• Game�
• Put,�Exit,�GameOver,�Sync
서버•Data�Model�
• User�,�SecurityData��
• GameRoom�
•Python�3.6.1��
• SQLAlchemy��
• mysqlclient�
• asyncio�
• aiothrift�(�https://pypi.python.org/pypi/aiothrift�)
클라이언트•Intro�Scene��
• Register,�Login�
•Lobby�Scene�
• CreateGameRoom,�JoinGameRoom,�RandomJoin�
•Game�Scene�
• Put,�Exit,�GameOver,�Sync
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)}
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)}
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�가�생
성
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�사용
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
문제•Server�
•1�user�는�1개의�session�만�유지해야�하는�방법�필요�
•중복�요청�방지를�위한�방법이�필요�
•Client�
•C#�으로�생성된�Client�코드는�TSocket�을�사용�(blocked-io)�이는�
synchronous�
시도•인증�전�후로�사용�가능한�Procedure�를�분리하고�인증�전에는�session�
생성을�인증�후에는�session�체크를�하는��로직을�작성�
•Processor�에�session�token�생성,�체크�작성�
•Protocol�에�session�token�기본�포함,�생성된�코드에서�매번�session�
token�을�넣지�않도록�작성
시도•인증�전�후로�사용�가능한�Procedure�를�분리하고�인증�전에는�session�
생성을�인증�후에는�session�체크를�하는��로직을�작성�
•Processor�에�session�token�생성,�체크�작성�
•Protocol�에�session�token�기본�포함,�생성된�코드에서�매번�session�
token�을�넣지�않도록�작성
빠른�포기!!!
•IDL을�기준으로�코드�생성이�되므로�IDL�에�없는�상태에서�이를�반영하기�
위해서는�Protocol�을�수정할�필요가�있음�
•Protocol�에서의�인증�절차�등이�필요함
시도•C#�의�생성된�service�코드를�coroutine�으로�
•Thrift�가�생성한�코드는�async�처리가�불가능,�async�한�처리를�위해서�
Unity�coroutine�코드가�필요�
•TSocket�등�Transport�도�coroutine�으로�작성되어�있지�않음��
•생성된�코드기준으로�async,�await�등은�C#�5�이상�필요한�만큼�async�
call�을�사용할�수�없음
시도•C#�의�생성된�service�코드를�coroutine�으로�
•Thrift�가�생성한�코드는�async�처리가�불가능,�async�한�처리를�위해서�
Unity�coroutine�코드가�필요�
•TSocket�등�Transport�도�coroutine�으로�작성되어�있지�않음��
•생성된�코드기준으로�async,�await�등은�C#�5�이상�필요한�만큼�async�
call�을�사용할�수�없음
빠른�포기!!!
•TSocket,�Transport�를�coroutine�으로�새로�작성해야�함�
gRPC 마스터,�빠른�포기�다음에는�빠른�시도!�이번엔�gRPC와�
불어�보시죠.
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/�)
protobuf(Protocol�Buffers)•상세한�문서�!�(�https://developers.google.com/protocol-buffers/�)�
•signed,�unsigned�지원�
•uint32,�uint64�
•nested�type�지원�
•message�{�message�{�enum�{�}�}�}�
•repeated�(list),�map�container�지원�
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;}
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;}
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) {}}
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) {}}
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
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)
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
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
문제•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�를�써보려고�한�건�아닌데
문제•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�를�써보려고�한�건�아닌데
빠른�포기!!!
오델로�하나�만들지�못하고�끝나나!
몬스터�슈퍼리그�방식 마스터,�과거를�돌아봐요.�그때�그�코드�그가�당신을��
도와줄거에요.
몬스터�슈퍼리그
2017/04 NDC 발표 기준
몬스터�슈퍼리그
2017/04 NDC 발표 기준
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{ // ...}
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�에�공통으로�추가
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;}
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�기준으로�실행
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�를�처리하는�기본�로직
몬스터�슈퍼리그•클라이언트에서�Synchronous�Call�을�지원하지�않음�
•Procedure�Call�만�있을�뿐�return�은�Response�용�Procedure�가�클라
이언트에서�실행�되는�형태,�하지만�양방향통신�이라고�할�수는�없음�
•Connection�연결�유지가�필요없는�구조,�서버�클라�모두�부담이�적음�
•Protobuf�사용으로�인한�serialize�,�deserialize�비용이�낮음�
•게임�데이터�를�Protobuf�로�생성한�struct�사용
몬스터�슈퍼리그•클라이언트에서�Synchronous�Call�을�지원하지�않음�
•Procedure�Call�만�있을�뿐�return�은�Response�용�Procedure�가�클라
이언트에서�실행�되는�형태,�하지만�양방향통신�이라고�할�수는�없음�
•Connection�연결�유지가�필요없는�구조,�서버�클라�모두�부담이�적음�
•Protobuf�사용으로�인한�serialize�,�deserialize�비용이�낮음�
•게임�데이터�를�Protobuf�로�생성한�struct�사용
도전!!!
게임에�맞게�RPC�만들기
마스터,�그냥�대충해.
시도•게임�개발에�필요한�수준으로�직접�만들어보자�
•개발�난이도가�높은�IDL�과�message�의�serializer�는�기존�것을�선택하자�
•서버는�http�server�를�사용하고�RPC�설계는�몬스터�슈퍼리그�방식을�사
용하자
선택•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�을�사용
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;}
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
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;}
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 은 기존 몬스터 슈퍼리그 에서 사용한 것을 그대로 적용
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; }}
Client.cs•Server—>Client�RPC�실행은�OnPacket�으로�시작하는�handler�들이�순
차적으로�실행�
•동일한�Procedure�가�여럿�등록되어�실행�될�수�있음�
•data�처리�부분과�UI�처리부분을�분리하기�위함�
•항상�data�를�업데이트�Procedure�가�먼저�실행되어�데이터�업데이트
가�완료된�다음�UI�업데이트�Procedure�가�실행되어�UI�갱신�
•UI�업데이트의�경우�여러�UI�Component�에서�직접�UI�갱신
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
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�옵션을�유저에게�제공
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�를�그대로�다시�보냄
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" }}
othello�완성•Thrift,�gRPC�포기�후�몬슈
리�방식을�수정하여�도입�
•서버의�경우는�aiohttp�로�
새로�작성(몬슈리는�flask)�
•실제�코딩시간�40�시간�정
도�
•AWS�ECS�로�서비스�중�
•Android�앱으로�빌드�
(PlayStore�“준곰오셀로”)
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�
에서�처리
othello�python�client•github�:�https://goo.gl/Ws2qsn�
•간단한�random�play�를�하는�클라이언트�
•현재�서버에서는�총�5개의�auto_client�가�대기중�
•직접�protocol에�맞추어�클라이언트�개발을�해도�
되고�auto_client�의�GamePut�부분을�수정하여�
간단한�봇을�만들�수�있음�
•파이콘�기간�중에�실행해보시는�분들께�추첨을�통해�선물을�드립니다.
othello�python�client•github�:�https://goo.gl/Ws2qsn�
•간단한�random�play�를�하는�클라이언트�
•현재�서버에서는�총�5개의�auto_client�가�대기중�
•직접�protocol에�맞추어�클라이언트�개발을�해도�
되고�auto_client�의�GamePut�부분을�수정하여�
간단한�봇을�만들�수�있음�
•파이콘�기간�중에�실행해보시는�분들께�추첨을�통해�선물을�드립니다.
정리�&�생각해볼�것들이제�얼마�남지�않았어!
정리•대부분의�RPC�Framework�들은�Python�을�매우�잘�지원,�특히�Python�
3.6�의�asyncio�용�Library�들이�많음.�
•하지만,�게임의�특성에�따른�인증�절차�반영이�어려움�
•하지만,�게임엔진의�가장�큰�축인�Unity의�C#�지원이�잘�안됨,�또한�생성
된�Client�RPC�코드가�Unity�기준으로�가�Async�하지�않음�
•위와�같은�이유로�기본적으로�RPC가�게임�서버/클라이언트에�잘�맞지는�않음�
•다만,�각�RPC�framework�마다�IDL,�serializer�를�제공하니�이를�잘�이용하면�직접�개발하는데�큰�도움이�될�수�있음
생각해볼�것들•gRPC�의�stream�처럼�HTTP/2�를�지원하면�GameSync�등은�필요�없지�
않을까?�
•FlatBuffers�의�벤치마킹�자료를�보면�성능이�우월한데�이런�serialized�
data�structure�를�더�살펴볼�필요는�있지�않을까?�
•클라이언트에서�RPC�return�을�처리�방식에�coroutine�을�통한�async�
방식도�지원한다면?
생각해볼�것들•SMARTSTUDY�는�뭐하는�곳인가?�지금�뭐하고�있나?
오픈소스를�사랑하는�스마트스터디�기술본부는�Slack과�JIRA로�커뮤니케이션하고�
GitHub�Enterprise와�CircleCI�Enterprise로�개발�및�통합�테스트�후에�Terraform으
로�관리되는�AWS�위에서�Docker�기반으로�서비스를�운영하며�DataDog으로�모니터
링을,�오류�추적은�Sentry에서�받으며�Unity와�Python으로�만든�몬스터�슈퍼리그는�
글로벌�원�빌드로�게임을�즐기는�전�세계�친구들과�node.js채팅으로�대화를�나누고,�
React로�만든�관리도구를�통해�5개�국어�/�2,500편의�핑크퐁�콘텐츠는�준-페타급�스
토리지�안에서�Transcoding�되어�YouTube�등에�올라가�누적�시청�수가�25억이�넘지
만,�이런�것들보다�더�중요한,�가장�중요한�건…�재미있게�같이�개발할�실력�있는�동료
를�스마트스터디는�항상�찾고�있다는�것!
감사합니다