ݺߣ

ݺߣShare a Scribd company logo
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억이 넘지
만, 이런 것들보다 더 중요한, 가장 중요한 건… 재미있게 같이 개발할 실력 있는 동료
를 스마트스터디는 항상 찾고 있다는 것!
감사니다

More Related Content

What's hot (20)

PDF
게임서버프로그래밍 #1 - IOCP
Seungmo Koo
PDF
중앙 서버 없는 게임 로직
Hoyoung Choi
PDF
게임서버프로그래밍 #2 - IOCP Adv
Seungmo Koo
PPTX
NDC 11 자이언트 서버의 비밀
승명 양
PDF
테라로 살펴본 MMORPG의 논타겟팅 시스템
QooJuice
PPTX
GPGPU(CUDA)를 이용한 MMOG 캐릭터 충돌처리
YEONG-CHEON YOU
PDF
[야생의 땅: 듀랑고] 서버 아키텍처 - SPOF 없는 분산 MMORPG 서버
Heungsub Lee
PDF
게임서버프로그래밍 #4 - 멀티스레드 프로그래밍
Seungmo Koo
PDF
게임서버프로그래밍 #0 - TCP 및 이벤트 통지모델
Seungmo Koo
PDF
잘 알려지지 않은 숨은 진주, Winsock API - WSAPoll, Fast Loopback
흥배 최
PDF
[IGC 2017] 아마존 구승모 - 게임 엔진으로 서버 제작 및 운영까지
강 민우
PDF
Multiplayer Game Sync Techniques through CAP theorem
Seungmo Koo
PDF
[NDC2017 : 박준철] Python 게임 서버 안녕하십니까 - 몬스터 슈퍼리그 게임 서버
준철 박
PDF
〈야생의 땅: 듀랑고〉 서버 아키텍처 Vol. 3
Heungsub Lee
PDF
Iocp advanced
Nam Hyeonuk
PDF
홍성우, 게임 서버의 목차 - 시작부터 출시까지, NDC2019
devCAT Studio, NEXON
PDF
MMOG Server-Side 충돌 및 이동처리 설계와 구현
YEONG-CHEON YOU
PDF
양승명, 다음 세대 크로스플랫폼 MMORPG 아키텍처, NDC2012
devCAT Studio, NEXON
PDF
[야생의 땅: 듀랑고] 서버 아키텍처 Vol. 2 (자막)
Heungsub Lee
PDF
Iocp 기본 구조 이해
Nam Hyeonuk
게임서버프로그래밍 #1 - IOCP
Seungmo Koo
중앙 서버 없는 게임 로직
Hoyoung Choi
게임서버프로그래밍 #2 - IOCP Adv
Seungmo Koo
NDC 11 자이언트 서버의 비밀
승명 양
테라로 살펴본 MMORPG의 논타겟팅 시스템
QooJuice
GPGPU(CUDA)를 이용한 MMOG 캐릭터 충돌처리
YEONG-CHEON YOU
[야생의 땅: 듀랑고] 서버 아키텍처 - SPOF 없는 분산 MMORPG 서버
Heungsub Lee
게임서버프로그래밍 #4 - 멀티스레드 프로그래밍
Seungmo Koo
게임서버프로그래밍 #0 - TCP 및 이벤트 통지모델
Seungmo Koo
잘 알려지지 않은 숨은 진주, Winsock API - WSAPoll, Fast Loopback
흥배 최
[IGC 2017] 아마존 구승모 - 게임 엔진으로 서버 제작 및 운영까지
강 민우
Multiplayer Game Sync Techniques through CAP theorem
Seungmo Koo
[NDC2017 : 박준철] Python 게임 서버 안녕하십니까 - 몬스터 슈퍼리그 게임 서버
준철 박
〈야생의 땅: 듀랑고〉 서버 아키텍처 Vol. 3
Heungsub Lee
Iocp advanced
Nam Hyeonuk
홍성우, 게임 서버의 목차 - 시작부터 출시까지, NDC2019
devCAT Studio, NEXON
MMOG Server-Side 충돌 및 이동처리 설계와 구현
YEONG-CHEON YOU
양승명, 다음 세대 크로스플랫폼 MMORPG 아키텍처, NDC2012
devCAT Studio, NEXON
[야생의 땅: 듀랑고] 서버 아키텍처 Vol. 2 (자막)
Heungsub Lee
Iocp 기본 구조 이해
Nam Hyeonuk

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

PPTX
20250307_Kit-Woks Team Study_gRPC_김현진.pptx
Wonjun Hwang
PPTX
Implementing remote procedure calls rev2
Sung-jae Park
PPTX
NDC 2011, 네트워크 비동기 통신, 합의점의 길목에서
tcaesvk
PDF
파이썬 생존 안내서 (자막)
Heungsub Lee
PDF
[NHN NEXT]실전프로젝트 밴드 게임 만들기 후기
flashscope
PPTX
고급시스템프로그래밍 Project#2 20123222_서기원
giwon seo
PDF
[2015-05월 세미나] 파이선 초심자의 Openstack
OpenStack Korea Community
PDF
하이퍼레저 패브릭 데이터 구조
Logpresso
PPTX
Golang Project Guide from A to Z: From Feature Development to Enterprise Appl...
Kyuhyun Byun
PPTX
잭블랙이블랙잭을한다 - 최종발표
Seokmin No
PPTX
아파치 쓰리프트 (Apache Thrift)
Jin wook
PPTX
KGC 2016: HTTPS 로 모바일 게임 서버 구축한다는 것 - Korea Games Conference
Xionglong Jin
PPT
Rhea_MMO_SNG_Convergence_Server_Architecture
Rhea Strike
PDF
잭블랙이블랙잭을한다 - 수업용 발표
Seokmin No
PPTX
Tcp server / client
문익 장
PDF
Gametech 2014: 모바일 게임용 PaaS/BaaS 구현 사례와 디자인 트레이드오프
Jinuk Kim
PDF
세션3 node.js의 의미와 자바의 대안
Lee Ji Eun
PDF
오픈소스 소프트웨어 성능 최적화 보고서 2장.
HyeonSeok Choi
PDF
1st-BE-sideproject-GDGonCampus_KyungHee_Univ.pdf
dpfls5645
PPT
2004_P2P chatting program
NakCheon Jung
20250307_Kit-Woks Team Study_gRPC_김현진.pptx
Wonjun Hwang
Implementing remote procedure calls rev2
Sung-jae Park
NDC 2011, 네트워크 비동기 통신, 합의점의 길목에서
tcaesvk
파이썬 생존 안내서 (자막)
Heungsub Lee
[NHN NEXT]실전프로젝트 밴드 게임 만들기 후기
flashscope
고급시스템프로그래밍 Project#2 20123222_서기원
giwon seo
[2015-05월 세미나] 파이선 초심자의 Openstack
OpenStack Korea Community
하이퍼레저 패브릭 데이터 구조
Logpresso
Golang Project Guide from A to Z: From Feature Development to Enterprise Appl...
Kyuhyun Byun
잭블랙이블랙잭을한다 - 최종발표
Seokmin No
아파치 쓰리프트 (Apache Thrift)
Jin wook
KGC 2016: HTTPS 로 모바일 게임 서버 구축한다는 것 - Korea Games Conference
Xionglong Jin
Rhea_MMO_SNG_Convergence_Server_Architecture
Rhea Strike
잭블랙이블랙잭을한다 - 수업용 발표
Seokmin No
Tcp server / client
문익 장
Gametech 2014: 모바일 게임용 PaaS/BaaS 구현 사례와 디자인 트레이드오프
Jinuk Kim
세션3 node.js의 의미와 자바의 대안
Lee Ji Eun
오픈소스 소프트웨어 성능 최적화 보고서 2장.
HyeonSeok Choi
1st-BE-sideproject-GDGonCampus_KyungHee_Univ.pdf
dpfls5645
2004_P2P chatting program
NakCheon Jung
Ad

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억이 넘지 만, 이런 것들보다 더 중요한, 가장 중요한 건… 재미있게 같이 개발할 실력 있는 동료 를 스마트스터디는 항상 찾고 있다는 것!