SlideShare une entreprise Scribd logo
1  sur  78
Télécharger pour lire hors ligne
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.
네트워크 상태나 콜 방식을 신경쓰지 않고
프로그래머가 원격의 함수를 실행하는 것
RPC
1. Procedure name
2. Parameters
3. Networking
4. Protocol (message)
RPC
1. Procedure name
2. Parameters
3. Networking
4. Protocol (message)
•IDL (Interface Definition Language) 로
정의
•IDL 은 RPC framework 별로 다르지만 ,
built-in type 은 대부분 비슷하게 지원
•단, 지원하는 container 의 차이, signed,
unsigned 지원의 차이는 있음
RPC
1. 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.thrift
namespace 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,
}
// exceptions
exception ErrorUserNotRegistered {}
exception ErrorUserNameAlreadyExists {}
exception ErrorUserAlreadyExists {}
exception ErrorUserInvalidName {}
exception ErrorSystem {
1: optional i32 code;
2: optional string message;
}
// services
service 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.thrift
namespace 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,
}
// exceptions
exception ErrorUserNotRegistered {}
exception ErrorUserNameAlreadyExists {}
exception ErrorUserAlreadyExists {}
exception ErrorUserInvalidName {}
exception ErrorSystem {
1: optional i32 code;
2: optional string message;
}
// services
service 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-py
gen-py
gen-py/__init__.py
gen-py/othello
gen-py/othello/__init__.py
gen-py/othello/constants.py
gen-py/othello/OthelloService-remote
gen-py/othello/OthelloService.py
gen-py/othello/ttypes.py
$ find gen-csharp
gen-csharp
gen-csharp/othello
gen-csharp/othello/thrift
gen-csharp/othello/thrift/ErrorSystem.cs
gen-csharp/othello/thrift/ErrorUserAlreadyExists.cs
gen-csharp/othello/thrift/ErrorUserInvalidName.cs
gen-csharp/othello/thrift/ErrorUserNameAlreadyExists.cs
gen-csharp/othello/thrift/ErrorUserNotRegistered.cs
gen-csharp/othello/thrift/OthelloService.cs
gen-csharp/othello/thrift/PlatformType.cs
gen-csharp/othello/thrift/User.cs
•thrift --gen [language] [file]
•gen-[language] 폴더에 code 가 생
성
othello.server.py
import asyncio
import thriftpy
from 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.py
class 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-grpc
gen-grpc
gen-grpc/__init__.py
gen-grpc/othello_pb2.py
gen-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 Buffers
message 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 Buffers
message 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.py
def 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.cfg
init session
======== Running on http://127.0.0.1:14500 ========
(Press CTRL+C to quit)
begin_session
protocolVersion: 1
protocolId: 100
seqNo: 1
loginReq {
platform_type: CUSTOM
platform_token: "1c8daa46-74ec-4f7f-9a6a-42dc9ecb9762"
}
REQUEST : ReqLogin
platform_type: CUSTOM
platform_token: "1c8daa46-74ec-4f7f-9a6a-42dc9ecb9762"
commit
end_session
reqSeqNo: 1
responses {
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.py
class 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억이 넘지
만, 이런 것들보다 더 중요한, 가장 중요한 건… 재미있게 같이 개발할 실력 있는 동료
를 스마트스터디는 항상 찾고 있다는 것!
감사합니다

Contenu connexe

Tendances

[야생의 땅: 듀랑고] 서버 아키텍처 - SPOF 없는 분산 MMORPG 서버
[야생의 땅: 듀랑고] 서버 아키텍처 - SPOF 없는 분산 MMORPG 서버[야생의 땅: 듀랑고] 서버 아키텍처 - SPOF 없는 분산 MMORPG 서버
[야생의 땅: 듀랑고] 서버 아키텍처 - SPOF 없는 분산 MMORPG 서버Heungsub Lee
 
TERA Server Architecture
TERA Server ArchitectureTERA Server Architecture
TERA Server Architectureujentus
 
홍성우, 게임 서버의 목차 - 시작부터 출시까지, NDC2019
홍성우, 게임 서버의 목차 - 시작부터 출시까지, NDC2019홍성우, 게임 서버의 목차 - 시작부터 출시까지, NDC2019
홍성우, 게임 서버의 목차 - 시작부터 출시까지, NDC2019devCAT Studio, NEXON
 
이승재, 실버바인 서버엔진 2 설계 리뷰, NDC2018
이승재, 실버바인 서버엔진 2 설계 리뷰, NDC2018이승재, 실버바인 서버엔진 2 설계 리뷰, NDC2018
이승재, 실버바인 서버엔진 2 설계 리뷰, NDC2018devCAT Studio, NEXON
 
테라로 살펴본 MMORPG의 논타겟팅 시스템
테라로 살펴본 MMORPG의 논타겟팅 시스템테라로 살펴본 MMORPG의 논타겟팅 시스템
테라로 살펴본 MMORPG의 논타겟팅 시스템QooJuice
 
NDC 11 자이언트 서버의 비밀
NDC 11 자이언트 서버의 비밀NDC 11 자이언트 서버의 비밀
NDC 11 자이언트 서버의 비밀승명 양
 
게임 프레임워크의 아키텍쳐와 디자인 패턴
게임 프레임워크의 아키텍쳐와 디자인 패턴게임 프레임워크의 아키텍쳐와 디자인 패턴
게임 프레임워크의 아키텍쳐와 디자인 패턴MinGeun Park
 
실시간 게임 서버 최적화 전략
실시간 게임 서버 최적화 전략실시간 게임 서버 최적화 전략
실시간 게임 서버 최적화 전략YEONG-CHEON YOU
 
오딘: 발할라 라이징 MMORPG의 성능 최적화 사례 공유 [카카오게임즈 - 레벨 300] - 발표자: 김문권, 팀장, 라이온하트 스튜디오...
오딘: 발할라 라이징 MMORPG의 성능 최적화 사례 공유 [카카오게임즈 - 레벨 300] - 발표자: 김문권, 팀장, 라이온하트 스튜디오...오딘: 발할라 라이징 MMORPG의 성능 최적화 사례 공유 [카카오게임즈 - 레벨 300] - 발표자: 김문권, 팀장, 라이온하트 스튜디오...
오딘: 발할라 라이징 MMORPG의 성능 최적화 사례 공유 [카카오게임즈 - 레벨 300] - 발표자: 김문권, 팀장, 라이온하트 스튜디오...Amazon Web Services Korea
 
[야생의 땅: 듀랑고] 서버 아키텍처 Vol. 2 (자막)
[야생의 땅: 듀랑고] 서버 아키텍처 Vol. 2 (자막)[야생의 땅: 듀랑고] 서버 아키텍처 Vol. 2 (자막)
[야생의 땅: 듀랑고] 서버 아키텍처 Vol. 2 (자막)Heungsub Lee
 
빌드관리 및 디버깅 (2010년 자료)
빌드관리 및 디버깅 (2010년 자료)빌드관리 및 디버깅 (2010년 자료)
빌드관리 및 디버깅 (2010년 자료)YEONG-CHEON YOU
 
〈야생의 땅: 듀랑고〉 서버 아키텍처 Vol. 3
〈야생의 땅: 듀랑고〉 서버 아키텍처 Vol. 3〈야생의 땅: 듀랑고〉 서버 아키텍처 Vol. 3
〈야생의 땅: 듀랑고〉 서버 아키텍처 Vol. 3Heungsub Lee
 
전형규, SilvervineUE4Lua: UE4에서 Lua 사용하기, NDC2019
전형규, SilvervineUE4Lua: UE4에서 Lua 사용하기, NDC2019전형규, SilvervineUE4Lua: UE4에서 Lua 사용하기, NDC2019
전형규, SilvervineUE4Lua: UE4에서 Lua 사용하기, NDC2019devCAT Studio, NEXON
 
김동건, 할머니가 들려주신 마비노기 개발 전설, NDC2019
김동건, 할머니가 들려주신 마비노기 개발 전설, NDC2019김동건, 할머니가 들려주신 마비노기 개발 전설, NDC2019
김동건, 할머니가 들려주신 마비노기 개발 전설, NDC2019devCAT Studio, NEXON
 
심예람, <프로젝트DH> AI 내비게이션 시스템, NDC2018
심예람, <프로젝트DH> AI 내비게이션 시스템, NDC2018심예람, <프로젝트DH> AI 내비게이션 시스템, NDC2018
심예람, <프로젝트DH> AI 내비게이션 시스템, NDC2018devCAT Studio, NEXON
 
새해 일어난 일
새해 일어난 일새해 일어난 일
새해 일어난 일Eunhyang Kim
 
조정훈, 게임 프로그래머를 위한 클래스 설계, NDC2012
조정훈, 게임 프로그래머를 위한 클래스 설계, NDC2012조정훈, 게임 프로그래머를 위한 클래스 설계, NDC2012
조정훈, 게임 프로그래머를 위한 클래스 설계, NDC2012devCAT Studio, NEXON
 
양승명, 다음 세대 크로스플랫폼 MMORPG 아키텍처, NDC2012
양승명, 다음 세대 크로스플랫폼 MMORPG 아키텍처, NDC2012양승명, 다음 세대 크로스플랫폼 MMORPG 아키텍처, NDC2012
양승명, 다음 세대 크로스플랫폼 MMORPG 아키텍처, NDC2012devCAT Studio, NEXON
 
C++20에서 리플렉션 기능 구현
C++20에서 리플렉션 기능 구현C++20에서 리플렉션 기능 구현
C++20에서 리플렉션 기능 구현Bongseok Cho
 
Next-generation MMORPG service architecture
Next-generation MMORPG service architectureNext-generation MMORPG service architecture
Next-generation MMORPG service architectureJongwon Kim
 

Tendances (20)

[야생의 땅: 듀랑고] 서버 아키텍처 - SPOF 없는 분산 MMORPG 서버
[야생의 땅: 듀랑고] 서버 아키텍처 - SPOF 없는 분산 MMORPG 서버[야생의 땅: 듀랑고] 서버 아키텍처 - SPOF 없는 분산 MMORPG 서버
[야생의 땅: 듀랑고] 서버 아키텍처 - SPOF 없는 분산 MMORPG 서버
 
TERA Server Architecture
TERA Server ArchitectureTERA Server Architecture
TERA Server Architecture
 
홍성우, 게임 서버의 목차 - 시작부터 출시까지, NDC2019
홍성우, 게임 서버의 목차 - 시작부터 출시까지, NDC2019홍성우, 게임 서버의 목차 - 시작부터 출시까지, NDC2019
홍성우, 게임 서버의 목차 - 시작부터 출시까지, NDC2019
 
이승재, 실버바인 서버엔진 2 설계 리뷰, NDC2018
이승재, 실버바인 서버엔진 2 설계 리뷰, NDC2018이승재, 실버바인 서버엔진 2 설계 리뷰, NDC2018
이승재, 실버바인 서버엔진 2 설계 리뷰, NDC2018
 
테라로 살펴본 MMORPG의 논타겟팅 시스템
테라로 살펴본 MMORPG의 논타겟팅 시스템테라로 살펴본 MMORPG의 논타겟팅 시스템
테라로 살펴본 MMORPG의 논타겟팅 시스템
 
NDC 11 자이언트 서버의 비밀
NDC 11 자이언트 서버의 비밀NDC 11 자이언트 서버의 비밀
NDC 11 자이언트 서버의 비밀
 
게임 프레임워크의 아키텍쳐와 디자인 패턴
게임 프레임워크의 아키텍쳐와 디자인 패턴게임 프레임워크의 아키텍쳐와 디자인 패턴
게임 프레임워크의 아키텍쳐와 디자인 패턴
 
실시간 게임 서버 최적화 전략
실시간 게임 서버 최적화 전략실시간 게임 서버 최적화 전략
실시간 게임 서버 최적화 전략
 
오딘: 발할라 라이징 MMORPG의 성능 최적화 사례 공유 [카카오게임즈 - 레벨 300] - 발표자: 김문권, 팀장, 라이온하트 스튜디오...
오딘: 발할라 라이징 MMORPG의 성능 최적화 사례 공유 [카카오게임즈 - 레벨 300] - 발표자: 김문권, 팀장, 라이온하트 스튜디오...오딘: 발할라 라이징 MMORPG의 성능 최적화 사례 공유 [카카오게임즈 - 레벨 300] - 발표자: 김문권, 팀장, 라이온하트 스튜디오...
오딘: 발할라 라이징 MMORPG의 성능 최적화 사례 공유 [카카오게임즈 - 레벨 300] - 발표자: 김문권, 팀장, 라이온하트 스튜디오...
 
[야생의 땅: 듀랑고] 서버 아키텍처 Vol. 2 (자막)
[야생의 땅: 듀랑고] 서버 아키텍처 Vol. 2 (자막)[야생의 땅: 듀랑고] 서버 아키텍처 Vol. 2 (자막)
[야생의 땅: 듀랑고] 서버 아키텍처 Vol. 2 (자막)
 
빌드관리 및 디버깅 (2010년 자료)
빌드관리 및 디버깅 (2010년 자료)빌드관리 및 디버깅 (2010년 자료)
빌드관리 및 디버깅 (2010년 자료)
 
〈야생의 땅: 듀랑고〉 서버 아키텍처 Vol. 3
〈야생의 땅: 듀랑고〉 서버 아키텍처 Vol. 3〈야생의 땅: 듀랑고〉 서버 아키텍처 Vol. 3
〈야생의 땅: 듀랑고〉 서버 아키텍처 Vol. 3
 
전형규, SilvervineUE4Lua: UE4에서 Lua 사용하기, NDC2019
전형규, SilvervineUE4Lua: UE4에서 Lua 사용하기, NDC2019전형규, SilvervineUE4Lua: UE4에서 Lua 사용하기, NDC2019
전형규, SilvervineUE4Lua: UE4에서 Lua 사용하기, NDC2019
 
김동건, 할머니가 들려주신 마비노기 개발 전설, NDC2019
김동건, 할머니가 들려주신 마비노기 개발 전설, NDC2019김동건, 할머니가 들려주신 마비노기 개발 전설, NDC2019
김동건, 할머니가 들려주신 마비노기 개발 전설, NDC2019
 
심예람, <프로젝트DH> AI 내비게이션 시스템, NDC2018
심예람, <프로젝트DH> AI 내비게이션 시스템, NDC2018심예람, <프로젝트DH> AI 내비게이션 시스템, NDC2018
심예람, <프로젝트DH> AI 내비게이션 시스템, NDC2018
 
새해 일어난 일
새해 일어난 일새해 일어난 일
새해 일어난 일
 
조정훈, 게임 프로그래머를 위한 클래스 설계, NDC2012
조정훈, 게임 프로그래머를 위한 클래스 설계, NDC2012조정훈, 게임 프로그래머를 위한 클래스 설계, NDC2012
조정훈, 게임 프로그래머를 위한 클래스 설계, NDC2012
 
양승명, 다음 세대 크로스플랫폼 MMORPG 아키텍처, NDC2012
양승명, 다음 세대 크로스플랫폼 MMORPG 아키텍처, NDC2012양승명, 다음 세대 크로스플랫폼 MMORPG 아키텍처, NDC2012
양승명, 다음 세대 크로스플랫폼 MMORPG 아키텍처, NDC2012
 
C++20에서 리플렉션 기능 구현
C++20에서 리플렉션 기능 구현C++20에서 리플렉션 기능 구현
C++20에서 리플렉션 기능 구현
 
Next-generation MMORPG service architecture
Next-generation MMORPG service architectureNext-generation MMORPG service architecture
Next-generation MMORPG service architecture
 

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

머신러닝 및 데이터 과학 연구자를 위한 python 기반 컨테이너 분산처리 플랫폼 설계 및 개발
머신러닝 및 데이터 과학 연구자를 위한 python 기반 컨테이너 분산처리 플랫폼 설계 및 개발머신러닝 및 데이터 과학 연구자를 위한 python 기반 컨테이너 분산처리 플랫폼 설계 및 개발
머신러닝 및 데이터 과학 연구자를 위한 python 기반 컨테이너 분산처리 플랫폼 설계 및 개발Jeongkyu Shin
 
[IGC 2017] 아마존 구승모 - 게임 엔진으로 서버 제작 및 운영까지
[IGC 2017] 아마존 구승모 - 게임 엔진으로 서버 제작 및 운영까지[IGC 2017] 아마존 구승모 - 게임 엔진으로 서버 제작 및 운영까지
[IGC 2017] 아마존 구승모 - 게임 엔진으로 서버 제작 및 운영까지강 민우
 
[야생의 땅: 듀랑고] 지형 관리 완전 자동화 - 생생한 AWS와 Docker 체험기
[야생의 땅: 듀랑고] 지형 관리 완전 자동화 - 생생한 AWS와 Docker 체험기[야생의 땅: 듀랑고] 지형 관리 완전 자동화 - 생생한 AWS와 Docker 체험기
[야생의 땅: 듀랑고] 지형 관리 완전 자동화 - 생생한 AWS와 Docker 체험기Sumin Byeon
 
스마트폰 온라인 게임에서 고려해야 할 것들
스마트폰 온라인 게임에서 고려해야 할 것들스마트폰 온라인 게임에서 고려해야 할 것들
스마트폰 온라인 게임에서 고려해야 할 것들Hyunjik Bae
 
Windows via C/C++ 06 스레드의 기본
Windows via C/C++ 06 스레드의 기본Windows via C/C++ 06 스레드의 기본
Windows via C/C++ 06 스레드의 기본ssuser0c2478
 
Quick & Dirty Tips for x64 hooks
Quick & Dirty Tips for x64 hooksQuick & Dirty Tips for x64 hooks
Quick & Dirty Tips for x64 hooks용환 노
 
GDB와 strace로 Hang 걸린 Python Process 원격 디버깅
GDB와 strace로 Hang 걸린 Python Process 원격 디버깅GDB와 strace로 Hang 걸린 Python Process 원격 디버깅
GDB와 strace로 Hang 걸린 Python Process 원격 디버깅Youngmin Koo
 
TCP/IP Protocol - JAVA
TCP/IP Protocol - JAVATCP/IP Protocol - JAVA
TCP/IP Protocol - JAVAcooddy
 
사설 서버를 막는 방법들 (프리섭, 더이상은 Naver)
사설 서버를 막는 방법들 (프리섭, 더이상은 Naver)사설 서버를 막는 방법들 (프리섭, 더이상은 Naver)
사설 서버를 막는 방법들 (프리섭, 더이상은 Naver)Seungmo Koo
 
Packer, Terraform, Vault를 이용해 만드는 
재현 가능한 게임 인프라
Packer, Terraform, Vault를 이용해 만드는 
재현 가능한 게임 인프라Packer, Terraform, Vault를 이용해 만드는 
재현 가능한 게임 인프라
Packer, Terraform, Vault를 이용해 만드는 
재현 가능한 게임 인프라MinKyu Kim
 
KGC 2016 오픈소스 네트워크 엔진 Super socket 사용하기
KGC 2016 오픈소스 네트워크 엔진 Super socket 사용하기KGC 2016 오픈소스 네트워크 엔진 Super socket 사용하기
KGC 2016 오픈소스 네트워크 엔진 Super socket 사용하기흥배 최
 
20160414 ROS 2차 강의 (for 아스라다 팀)
20160414 ROS 2차 강의 (for 아스라다 팀)20160414 ROS 2차 강의 (for 아스라다 팀)
20160414 ROS 2차 강의 (for 아스라다 팀)Yoonseok Pyo
 
오픈 소스 사용 매뉴얼
오픈 소스 사용 매뉴얼오픈 소스 사용 매뉴얼
오픈 소스 사용 매뉴얼Kenu, GwangNam Heo
 
2015 제2회 동아리 해커 세미나 - 병렬컴퓨팅 소개 (16기 김정현)
2015 제2회 동아리 해커 세미나 - 병렬컴퓨팅 소개 (16기 김정현)2015 제2회 동아리 해커 세미나 - 병렬컴퓨팅 소개 (16기 김정현)
2015 제2회 동아리 해커 세미나 - 병렬컴퓨팅 소개 (16기 김정현)khuhacker
 
web study 1day
web study 1dayweb study 1day
web study 1day준호 우
 
Gametech 2014: 모바일 게임용 PaaS/BaaS 구현 사례와 디자인 트레이드오프
Gametech 2014: 모바일 게임용 PaaS/BaaS 구현 사례와 디자인 트레이드오프Gametech 2014: 모바일 게임용 PaaS/BaaS 구현 사례와 디자인 트레이드오프
Gametech 2014: 모바일 게임용 PaaS/BaaS 구현 사례와 디자인 트레이드오프Jinuk Kim
 
나만의 엔진 개발하기
나만의 엔진 개발하기나만의 엔진 개발하기
나만의 엔진 개발하기YEONG-CHEON YOU
 
[KGC2014] 두 마리 토끼를 잡기 위한 C++ - C# 혼합 멀티플랫폼 게임 아키텍처 설계
[KGC2014] 두 마리 토끼를 잡기 위한 C++ - C#  혼합 멀티플랫폼 게임 아키텍처 설계[KGC2014] 두 마리 토끼를 잡기 위한 C++ - C#  혼합 멀티플랫폼 게임 아키텍처 설계
[KGC2014] 두 마리 토끼를 잡기 위한 C++ - C# 혼합 멀티플랫폼 게임 아키텍처 설계Sungkyun Kim
 
Kgc2014 one daylearnunitycryptography-f
Kgc2014 one daylearnunitycryptography-fKgc2014 one daylearnunitycryptography-f
Kgc2014 one daylearnunitycryptography-fSeungmin Shin
 
[IGC 2017] 넥스트플로어 김영수 - Protocol:hyperspace Diver 개발 포스트모템
[IGC 2017] 넥스트플로어 김영수 - Protocol:hyperspace Diver 개발 포스트모템[IGC 2017] 넥스트플로어 김영수 - Protocol:hyperspace Diver 개발 포스트모템
[IGC 2017] 넥스트플로어 김영수 - Protocol:hyperspace Diver 개발 포스트모템강 민우
 

Similaire à Python 게임서버 안녕하십니까 : RPC framework 편 (20)

머신러닝 및 데이터 과학 연구자를 위한 python 기반 컨테이너 분산처리 플랫폼 설계 및 개발
머신러닝 및 데이터 과학 연구자를 위한 python 기반 컨테이너 분산처리 플랫폼 설계 및 개발머신러닝 및 데이터 과학 연구자를 위한 python 기반 컨테이너 분산처리 플랫폼 설계 및 개발
머신러닝 및 데이터 과학 연구자를 위한 python 기반 컨테이너 분산처리 플랫폼 설계 및 개발
 
[IGC 2017] 아마존 구승모 - 게임 엔진으로 서버 제작 및 운영까지
[IGC 2017] 아마존 구승모 - 게임 엔진으로 서버 제작 및 운영까지[IGC 2017] 아마존 구승모 - 게임 엔진으로 서버 제작 및 운영까지
[IGC 2017] 아마존 구승모 - 게임 엔진으로 서버 제작 및 운영까지
 
[야생의 땅: 듀랑고] 지형 관리 완전 자동화 - 생생한 AWS와 Docker 체험기
[야생의 땅: 듀랑고] 지형 관리 완전 자동화 - 생생한 AWS와 Docker 체험기[야생의 땅: 듀랑고] 지형 관리 완전 자동화 - 생생한 AWS와 Docker 체험기
[야생의 땅: 듀랑고] 지형 관리 완전 자동화 - 생생한 AWS와 Docker 체험기
 
스마트폰 온라인 게임에서 고려해야 할 것들
스마트폰 온라인 게임에서 고려해야 할 것들스마트폰 온라인 게임에서 고려해야 할 것들
스마트폰 온라인 게임에서 고려해야 할 것들
 
Windows via C/C++ 06 스레드의 기본
Windows via C/C++ 06 스레드의 기본Windows via C/C++ 06 스레드의 기본
Windows via C/C++ 06 스레드의 기본
 
Quick & Dirty Tips for x64 hooks
Quick & Dirty Tips for x64 hooksQuick & Dirty Tips for x64 hooks
Quick & Dirty Tips for x64 hooks
 
GDB와 strace로 Hang 걸린 Python Process 원격 디버깅
GDB와 strace로 Hang 걸린 Python Process 원격 디버깅GDB와 strace로 Hang 걸린 Python Process 원격 디버깅
GDB와 strace로 Hang 걸린 Python Process 원격 디버깅
 
TCP/IP Protocol - JAVA
TCP/IP Protocol - JAVATCP/IP Protocol - JAVA
TCP/IP Protocol - JAVA
 
사설 서버를 막는 방법들 (프리섭, 더이상은 Naver)
사설 서버를 막는 방법들 (프리섭, 더이상은 Naver)사설 서버를 막는 방법들 (프리섭, 더이상은 Naver)
사설 서버를 막는 방법들 (프리섭, 더이상은 Naver)
 
Packer, Terraform, Vault를 이용해 만드는 
재현 가능한 게임 인프라
Packer, Terraform, Vault를 이용해 만드는 
재현 가능한 게임 인프라Packer, Terraform, Vault를 이용해 만드는 
재현 가능한 게임 인프라
Packer, Terraform, Vault를 이용해 만드는 
재현 가능한 게임 인프라
 
KGC 2016 오픈소스 네트워크 엔진 Super socket 사용하기
KGC 2016 오픈소스 네트워크 엔진 Super socket 사용하기KGC 2016 오픈소스 네트워크 엔진 Super socket 사용하기
KGC 2016 오픈소스 네트워크 엔진 Super socket 사용하기
 
20160414 ROS 2차 강의 (for 아스라다 팀)
20160414 ROS 2차 강의 (for 아스라다 팀)20160414 ROS 2차 강의 (for 아스라다 팀)
20160414 ROS 2차 강의 (for 아스라다 팀)
 
오픈 소스 사용 매뉴얼
오픈 소스 사용 매뉴얼오픈 소스 사용 매뉴얼
오픈 소스 사용 매뉴얼
 
2015 제2회 동아리 해커 세미나 - 병렬컴퓨팅 소개 (16기 김정현)
2015 제2회 동아리 해커 세미나 - 병렬컴퓨팅 소개 (16기 김정현)2015 제2회 동아리 해커 세미나 - 병렬컴퓨팅 소개 (16기 김정현)
2015 제2회 동아리 해커 세미나 - 병렬컴퓨팅 소개 (16기 김정현)
 
web study 1day
web study 1dayweb study 1day
web study 1day
 
Gametech 2014: 모바일 게임용 PaaS/BaaS 구현 사례와 디자인 트레이드오프
Gametech 2014: 모바일 게임용 PaaS/BaaS 구현 사례와 디자인 트레이드오프Gametech 2014: 모바일 게임용 PaaS/BaaS 구현 사례와 디자인 트레이드오프
Gametech 2014: 모바일 게임용 PaaS/BaaS 구현 사례와 디자인 트레이드오프
 
나만의 엔진 개발하기
나만의 엔진 개발하기나만의 엔진 개발하기
나만의 엔진 개발하기
 
[KGC2014] 두 마리 토끼를 잡기 위한 C++ - C# 혼합 멀티플랫폼 게임 아키텍처 설계
[KGC2014] 두 마리 토끼를 잡기 위한 C++ - C#  혼합 멀티플랫폼 게임 아키텍처 설계[KGC2014] 두 마리 토끼를 잡기 위한 C++ - C#  혼합 멀티플랫폼 게임 아키텍처 설계
[KGC2014] 두 마리 토끼를 잡기 위한 C++ - C# 혼합 멀티플랫폼 게임 아키텍처 설계
 
Kgc2014 one daylearnunitycryptography-f
Kgc2014 one daylearnunitycryptography-fKgc2014 one daylearnunitycryptography-f
Kgc2014 one daylearnunitycryptography-f
 
[IGC 2017] 넥스트플로어 김영수 - Protocol:hyperspace Diver 개발 포스트모템
[IGC 2017] 넥스트플로어 김영수 - Protocol:hyperspace Diver 개발 포스트모템[IGC 2017] 넥스트플로어 김영수 - Protocol:hyperspace Diver 개발 포스트모템
[IGC 2017] 넥스트플로어 김영수 - Protocol:hyperspace Diver 개발 포스트모템
 

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

  • 1. Python 게임서버 안녕하십니까? RPC Framework 편 스마트스터디 CTO 박준철
  • 2. 왜? NDC 발표 (Python 게임서버 안녕하십니까? : 몬스터 슈퍼리그 게임 서버) 준비중에 사내 리뷰 과정에서 “너굴” 님의 질문으로부터 시작 너굴 : “게임 서버/클라 네트워킹 에서 RPC framework 를 
 사용하지 않고 직접 구현하신 이유가 있나요?” 준곰 : “어쩌고 저쩌고… 그래서 어쩌고저쩌고” 너굴 : “네…”
  • 3. RPC 라는게 뭐길래? 게임 서버/클라에 쓸수 있나?
  • 4. 목표 • RPC framework 에 대한 정보 공유 • RPC framework 를 게임에 적용해보자 • 몬스터 슈퍼리그 에서 사용한 방식 공유 • 게임에 적합한 방식을 직접 만들어 보는 것
  • 5. 목차 • RPC • Thrift , gRPC • 몬스터 슈퍼리그 방식 • 게임에 맞게 RPC 만들기 • 정리 & 생각해볼 만한 것들 마스터, 준비 되었나요? 이제 시작합니다. 졸면 안되요!
  • 6. 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.
  • 7. 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. 네트워크 상태나 콜 방식을 신경쓰지 않고 프로그래머가 원격의 함수를 실행하는 것
  • 8. RPC 1. Procedure name 2. Parameters 3. Networking 4. Protocol (message)
  • 9. RPC 1. Procedure name 2. Parameters 3. Networking 4. Protocol (message) •IDL (Interface Definition Language) 로 정의 •IDL 은 RPC framework 별로 다르지만 , built-in type 은 대부분 비슷하게 지원 •단, 지원하는 container 의 차이, signed, unsigned 지원의 차이는 있음
  • 10. RPC 1. Procedure name 2. Parameters 3. Networking 4. Protocol (message) •Networking 방식의 차이 따라 Procedure Call 과 return 처리 방식이 달 라짐 •message 의 (de)serializer 차이에 따라 Protocol 의 성능이나 보안의 차이가 존재
  • 11. 게임에서 RPC 선택 •IDL 정의를 서버/클라이언트가 코드 레벨에서 공유할 수 있나? ( 컴파일 타임에 오류 확인이 가능한 방식을 선호, 코드를 생성해주는 RPC framework 의 IDL ) •클라이언트에서 async call 을 지원해야 하며 return 의 형태나 return 의 처리 과정에 개입할 수 있나? •Unity (.Net 2.0, .Net 3.5, C# 4) , C++ 지원하나? •json, xml을 사용하지 않고 빠른 자체 message protocol 지원하는가?
  • 12. Thrift 마스터, θrift 가 이 동네 
 짱이라고 해요. 
 같이 싸워 봐요.
  • 13. 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/ )
  • 14. Thrift •Server, Processor, Protocol, Transport 로 구성 •Thrift 를 통해서 code 생성을 하면 RPC Client 코드도 생성 •서버 / 클라이언트 의 가장 큰 차이는 당연하게도 Processor 유무 •Protocol, Transport 는 각각 Serialization 과 Networking 을 담당
  • 15. Thrift •일단, Thrift 가 좋아보이니 이것으로 간단한 게임을 만들어보자. •PT 준비가 산으로… PT가 산으로 가고 있냥!!
  • 16. 산으로 가는 김에 잠시 소개 합니다
  • 17. 준곰 • 스마트스터디의 CTO 로 몬스터 슈퍼리그 개발에 참여했습니다. • 넥슨에서 게임을 즐겁게 만드는 방법을 배웠습니다. • 엔씨소프트에서 게임을 잘 만드는 방법을 배웠습니다. • 네오위즈게임즈에서 게임을 처음부터 만들고 끝까지 완성하는 방법을 배웠습니다. • 스마트스터디에서는 게임을 만들어 성공하는 방법을 배웠습니 다.
  • 19. Othello (오델로) •Reversi(리버시) 라고도 부르는 보드게임 •두 명이 8x8 오델로 판 위에서 흑, 백 돌을 번갈아 놓으면서 진행 •처음에 판 가운데에 사각형으로 엇갈리게 배치된 돌 4개를 놓고 시작한다. •돌은 반드시 상대방 돌을 양쪽에서 포위하여 뒤집을 수 있는 곳에 놓아야 한다. •돌을 뒤집을 곳이 없는 경우에는 차례가 자동적으로 상대방에게 넘어가게 된다. •아래와 같은 조건에 의해 양쪽 모두 더 이상 돌을 놓을 수 없게 되면 게임이 끝 나게 된다. • 64개의 돌 모두가 판에 가득 찬 경우 (가장 일반적) • 어느 한 쪽이 돌을 모두 뒤집은 경우 • 한 차례에 양 쪽 모두 서로 차례를 넘겨야 하는 경우 •게임이 끝났을 때 돌이 많이 있는 플레이어가 승자가 된다. 만일 돌의 개수가 같 을 경우는 무승부가 된다. wikipedia
  • 20. IDL •Struct • User, GameRoom •Service • User • Login, Register • GameRoom • CreateGameRoom, JoinGameRoom, RandomJoin • Game • Put, Exit, GameOver, Sync
  • 21. 서버 •Data Model • User , SecurityData • GameRoom •Python 3.6.1 • SQLAlchemy • mysqlclient • asyncio • aiothrift ( https://pypi.python.org/pypi/aiothrift )
  • 22. 클라이언트 •Intro Scene • Register, Login •Lobby Scene • CreateGameRoom, JoinGameRoom, RandomJoin •Game Scene • Put, Exit, GameOver, Sync
  • 23. othello.thrift namespace 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, } // exceptions exception ErrorUserNotRegistered {} exception ErrorUserNameAlreadyExists {} exception ErrorUserAlreadyExists {} exception ErrorUserInvalidName {} exception ErrorSystem { 1: optional i32 code; 2: optional string message; } // services service 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) }
  • 24. othello.thrift namespace 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, } // exceptions exception ErrorUserNotRegistered {} exception ErrorUserNameAlreadyExists {} exception ErrorUserAlreadyExists {} exception ErrorUserInvalidName {} exception ErrorSystem { 1: optional i32 code; 2: optional string message; } // services service 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) }
  • 25. thrift generate code $ thrift --gen py ./othello.thrift $ thrift --gen csharp ./othello.thrift $ find gen-py gen-py gen-py/__init__.py gen-py/othello gen-py/othello/__init__.py gen-py/othello/constants.py gen-py/othello/OthelloService-remote gen-py/othello/OthelloService.py gen-py/othello/ttypes.py $ find gen-csharp gen-csharp gen-csharp/othello gen-csharp/othello/thrift gen-csharp/othello/thrift/ErrorSystem.cs gen-csharp/othello/thrift/ErrorUserAlreadyExists.cs gen-csharp/othello/thrift/ErrorUserInvalidName.cs gen-csharp/othello/thrift/ErrorUserNameAlreadyExists.cs gen-csharp/othello/thrift/ErrorUserNotRegistered.cs gen-csharp/othello/thrift/OthelloService.cs gen-csharp/othello/thrift/PlatformType.cs gen-csharp/othello/thrift/User.cs •thrift --gen [language] [file] •gen-[language] 폴더에 code 가 생 성
  • 26. othello.server.py import asyncio import thriftpy from 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 사용
  • 27. othello.server.py class 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
  • 28. 문제 •Server •1 user 는 1개의 session 만 유지해야 하는 방법 필요 •중복 요청 방지를 위한 방법이 필요 •Client •C# 으로 생성된 Client 코드는 TSocket 을 사용 (blocked-io) 이는 synchronous
  • 29. 시도 •인증 전 후로 사용 가능한 Procedure 를 분리하고 인증 전에는 session 생성을 인증 후에는 session 체크를 하는 로직을 작성 •Processor 에 session token 생성, 체크 작성 •Protocol 에 session token 기본 포함, 생성된 코드에서 매번 session token 을 넣지 않도록 작성
  • 30. 시도 •인증 전 후로 사용 가능한 Procedure 를 분리하고 인증 전에는 session 생성을 인증 후에는 session 체크를 하는 로직을 작성 •Processor 에 session token 생성, 체크 작성 •Protocol 에 session token 기본 포함, 생성된 코드에서 매번 session token 을 넣지 않도록 작성 빠른 포기!!! •IDL을 기준으로 코드 생성이 되므로 IDL 에 없는 상태에서 이를 반영하기 위해서는 Protocol 을 수정할 필요가 있음 •Protocol 에서의 인증 절차 등이 필요함
  • 31. 시도 •C# 의 생성된 service 코드를 coroutine 으로 •Thrift 가 생성한 코드는 async 처리가 불가능, async 한 처리를 위해서 Unity coroutine 코드가 필요 •TSocket 등 Transport 도 coroutine 으로 작성되어 있지 않음 •생성된 코드기준으로 async, await 등은 C# 5 이상 필요한 만큼 async call 을 사용할 수 없음
  • 32. 시도 •C# 의 생성된 service 코드를 coroutine 으로 •Thrift 가 생성한 코드는 async 처리가 불가능, async 한 처리를 위해서 Unity coroutine 코드가 필요 •TSocket 등 Transport 도 coroutine 으로 작성되어 있지 않음 •생성된 코드기준으로 async, await 등은 C# 5 이상 필요한 만큼 async call 을 사용할 수 없음 빠른 포기!!! •TSocket, Transport 를 coroutine 으로 새로 작성해야 함
  • 33. gRPC 마스터, 빠른 포기 다음에는 빠른 시도! 이번엔 gRPC와 불어 보시죠.
  • 34. 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/ )
  • 35. protobuf(Protocol Buffers) •상세한 문서 ! ( https://developers.google.com/protocol-buffers/ ) •signed, unsigned 지원 •uint32, uint64 •nested type 지원 •message { message { enum { } } } •repeated (list), map container 지원
  • 36. 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; }
  • 37. 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; }
  • 38. 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) {} }
  • 39. 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) {} }
  • 40. 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-grpc gen-grpc gen-grpc/__init__.py gen-grpc/othello_pb2.py gen-grpc/othello_pb2_grpc.py
  • 41. 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)
  • 42. 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
  • 43. 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
  • 44. 문제 •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 를 써보려고 한 건 아닌데
  • 45. 문제 •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 를 써보려고 한 건 아닌데 빠른 포기!!!
  • 46. 오델로 하나 만들지 못하고 끝나나!
  • 47. 몬스터 슈퍼리그 방식 마스터, 과거를 돌아봐요. 그때 그 코드 그가 당신을 도와줄거에요.
  • 50. Protocol Buffers message 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 { // ... }
  • 51. Protocol Buffers message 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 에 공통으로 추가
  • 52. 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; }
  • 53. server.route.py def 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 기준으로 
 실행
  • 54. 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 를 처리하는 기본 로직
  • 55. 몬스터 슈퍼리그 •클라이언트에서 Synchronous Call 을 지원하지 않음 •Procedure Call 만 있을 뿐 return 은 Response 용 Procedure 가 클라 이언트에서 실행 되는 형태, 하지만 양방향통신 이라고 할 수는 없음 •Connection 연결 유지가 필요없는 구조, 서버 클라 모두 부담이 적음 •Protobuf 사용으로 인한 serialize , deserialize 비용이 낮음 •게임 데이터 를 Protobuf 로 생성한 struct 사용
  • 56. 몬스터 슈퍼리그 •클라이언트에서 Synchronous Call 을 지원하지 않음 •Procedure Call 만 있을 뿐 return 은 Response 용 Procedure 가 클라 이언트에서 실행 되는 형태, 하지만 양방향통신 이라고 할 수는 없음 •Connection 연결 유지가 필요없는 구조, 서버 클라 모두 부담이 적음 •Protobuf 사용으로 인한 serialize , deserialize 비용이 낮음 •게임 데이터 를 Protobuf 로 생성한 struct 사용 도전!!!
  • 57. 게임에 맞게 RPC 만들기 마스터, 그냥 대충해.
  • 58. 시도 •게임 개발에 필요한 수준으로 직접 만들어보자 •개발 난이도가 높은 IDL 과 message 의 serializer 는 기존 것을 선택하자 •서버는 http server 를 사용하고 RPC 설계는 몬스터 슈퍼리그 방식을 사 용하자
  • 59. 선택 •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 을 사용
  • 60. 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; }
  • 61. 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
  • 62. 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; }
  • 63. 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 은 기존 몬스터 슈퍼리그 에서 사용한 것을 그대로 적용
  • 64. 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; } }
  • 65. Client.cs •Server—>Client RPC 실행은 OnPacket 으로 시작하는 handler 들이 순 차적으로 실행 •동일한 Procedure 가 여럿 등록되어 실행 될 수 있음 •data 처리 부분과 UI 처리부분을 분리하기 위함 •항상 data 를 업데이트 Procedure 가 먼저 실행되어 데이터 업데이트 가 완료된 다음 UI 업데이트 Procedure 가 실행되어 UI 갱신 •UI 업데이트의 경우 여러 UI Component 에서 직접 UI 갱신
  • 66. 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
  • 67. 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 옵션을 유저에게 제공
  • 68. 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 를 그대로 다시 보냄
  • 69. othello 완성 $ python3 manage.py runserver --port 14500 local.cfg init session ======== Running on http://127.0.0.1:14500 ======== (Press CTRL+C to quit) begin_session protocolVersion: 1 protocolId: 100 seqNo: 1 loginReq { platform_type: CUSTOM platform_token: "1c8daa46-74ec-4f7f-9a6a-42dc9ecb9762" } REQUEST : ReqLogin platform_type: CUSTOM platform_token: "1c8daa46-74ec-4f7f-9a6a-42dc9ecb9762" commit end_session reqSeqNo: 1 responses { 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" } }
  • 70. othello 완성 •Thrift, gRPC 포기 후 몬슈 리 방식을 수정하여 도입 •서버의 경우는 aiohttp 로 새로 작성(몬슈리는 flask) •실제 코딩시간 40 시간 정 도 •AWS ECS 로 서비스 중 •Android 앱으로 빌드 (PlayStore “준곰오셀로”)
  • 71. test_client.py class 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 에서 처리
  • 72. othello python client •github : https://goo.gl/Ws2qsn •간단한 random play 를 하는 클라이언트 •현재 서버에서는 총 5개의 auto_client 가 대기중 •직접 protocol에 맞추어 클라이언트 개발을 해도 되고 auto_client 의 GamePut 부분을 수정하여 간단한 봇을 만들 수 있음 •파이콘 기간 중에 실행해보시는 분들께 추첨을 통 해 선물을 드립니다.
  • 73. othello python client •github : https://goo.gl/Ws2qsn •간단한 random play 를 하는 클라이언트 •현재 서버에서는 총 5개의 auto_client 가 대기중 •직접 protocol에 맞추어 클라이언트 개발을 해도 되고 auto_client 의 GamePut 부분을 수정하여 간단한 봇을 만들 수 있음 •파이콘 기간 중에 실행해보시는 분들께 추첨을 통 해 선물을 드립니다.
  • 74. 정리 & 생각해볼 것들 이제 얼마 남지 않았어!
  • 75. 정리 •대부분의 RPC Framework 들은 Python 을 매우 잘 지원, 특히 Python 3.6 의 asyncio 용 Library 들이 많음. •하지만, 게임의 특성에 따른 인증 절차 반영이 어려움 •하지만, 게임엔진의 가장 큰 축인 Unity의 C# 지원이 잘 안됨, 또한 생성 된 Client RPC 코드가 Unity 기준으로 가 Async 하지 않음 •위와 같은 이유로 기본적으로 RPC가 게임 서버/클라이언트에 잘 맞지는 않음 •다만, 각 RPC framework 마다 IDL, serializer 를 제공하니 이를 잘 이용 하면 직접 개발하는데 큰 도움이 될 수 있음
  • 76. 생각해볼 것들 •gRPC 의 stream 처럼 HTTP/2 를 지원하면 GameSync 등은 필요 없지 않을까? •FlatBuffers 의 벤치마킹 자료를 보면 성능이 우월한데 이런 serialized data structure 를 더 살펴볼 필요는 있지 않을까? •클라이언트에서 RPC return 을 처리 방식에 coroutine 을 통한 async 방식도 지원한다면?
  • 77. 생각해볼 것들 •SMARTSTUDY 는 뭐하는 곳인가? 지금 뭐하고 있나? 오픈소스를 사랑하는 스마트스터디 기술본부는 Slack과 JIRA로 커뮤니케이션하고 GitHub Enterprise와 CircleCI Enterprise로 개발 및 통합 테스트 후에 Terraform으 로 관리되는 AWS 위에서 Docker 기반으로 서비스를 운영하며 DataDog으로 모니터 링을, 오류 추적은 Sentry에서 받으며 Unity와 Python으로 만든 몬스터 슈퍼리그는 글로벌 원 빌드로 게임을 즐기는 전 세계 친구들과 node.js채팅으로 대화를 나누고, React로 만든 관리도구를 통해 5개 국어 / 2,500편의 핑크퐁 콘텐츠는 준-페타급 스 토리지 안에서 Transcoding 되어 YouTube 등에 올라가 누적 시청 수가 25억이 넘지 만, 이런 것들보다 더 중요한, 가장 중요한 건… 재미있게 같이 개발할 실력 있는 동료 를 스마트스터디는 항상 찾고 있다는 것!