NDC Python 게임서버 안녕하십니까? : 몬스터 슈퍼리그 게임 서버 편의 후속으로 기획된 발표입니다. 사내 준비 도중 "너굴" 님의 질문에서 시작되었습니다.
이 발표는 잘 알려진 RPC Framework 인 Thrift, gRPC를 살펴보고 예시로 오델로 게임을 만들어보면서 기존 RPC framework 들이 게임의 서버/클라 구조에 잘 어울리지는 살펴보고 왜 몬스터 슈퍼리그에서 그런 선택을 했는지 살펴봅니다.
그리고 게임에 맞게 RPC 를 설계하고 이를 이용하여 온라인 오델로 게임을 완성해봅니다.
2. 왜?
NDC 발표 (Python 게임서버 안녕하십니까? : 몬스터 슈퍼리그 게임 서버)
준비중에 사내 리뷰 과정에서 “너굴” 님의 질문으로부터 시작
너굴 : “게임 서버/클라 네트워킹 에서 RPC framework 를
사용하지 않고 직접 구현하신 이유가 있나요?”
준곰 : “어쩌고 저쩌고… 그래서 어쩌고저쩌고”
너굴 : “네…”
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.
네트워크 상태나 콜 방식을 신경쓰지 않고
프로그래머가 원격의 함수를 실행하는 것
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 지원하는가?
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 을 담당
17. 준곰
• 스마트스터디의 CTO 로 몬스터 슈퍼리그 개발에 참여했습니다.
• 넥슨에서 게임을 즐겁게 만드는 방법을 배웠습니다.
• 엔씨소프트에서 게임을 잘 만드는 방법을 배웠습니다.
• 네오위즈게임즈에서 게임을 처음부터 만들고 끝까지 완성하는
방법을 배웠습니다.
• 스마트스터디에서는 게임을 만들어 성공하는 방법을 배웠습니
다.
19. Othello (오델로)
•Reversi(리버시) 라고도 부르는 보드게임
•두 명이 8x8 오델로 판 위에서 흑, 백 돌을 번갈아 놓으면서 진행
•처음에 판 가운데에 사각형으로 엇갈리게 배치된 돌 4개를 놓고 시작한다.
•돌은 반드시 상대방 돌을 양쪽에서 포위하여 뒤집을 수 있는 곳에 놓아야 한다.
•돌을 뒤집을 곳이 없는 경우에는 차례가 자동적으로 상대방에게 넘어가게 된다.
•아래와 같은 조건에 의해 양쪽 모두 더 이상 돌을 놓을 수 없게 되면 게임이 끝
나게 된다.
• 64개의 돌 모두가 판에 가득 찬 경우 (가장 일반적)
• 어느 한 쪽이 돌을 모두 뒤집은 경우
• 한 차례에 양 쪽 모두 서로 차례를 넘겨야 하는 경우
•게임이 끝났을 때 돌이 많이 있는 플레이어가 승자가 된다. 만일 돌의 개수가 같
을 경우는 무승부가 된다.
wikipedia
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 으로 새로 작성해야 함
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 를 써보려고 한 건 아닌데
빠른 포기!!!
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 기준으로
실행
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 사용
도전!!!
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 를 그대로 다시 보냄
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 부분을 수정하여
간단한 봇을 만들 수 있음
•파이콘 기간 중에 실행해보시는 분들께 추첨을 통
해 선물을 드립니다.
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억이 넘지
만, 이런 것들보다 더 중요한, 가장 중요한 건… 재미있게 같이 개발할 실력 있는 동료
를 스마트스터디는 항상 찾고 있다는 것!