how to resolve problems of mobile game server development and service maintenance
郭學聰 Hsueh-Tsung KuoFri, 09 Jun 2017
official website: https://www.rayark.com/g/voez/
teaser: https://youtu.be/Bh6gQyJHbxI
walkthrough wiki: http://voez.info/
Apple iTunes: https://itunes.apple.com/jp/app/voez/id1007929736
Google Play: https://play.google.com/store/apps/details?id=com.rayark.valkyrie
Nintendo Switch: https://ec.nintendo.com/JP/ja/titles/70010000000044
request
POST /api/leaderboard/song/add_score HTTP/1.1
Host: voez-api.rayark.net
Signature: cHl0aG9uaXN0YQ==
{
"token": "593a19bc593a19bc",
"player": "pythonista",
"timestamp": 1496979900,
"score": {
"total": 999999.0,
......
}
}
response
HTTP/1.1 200 OK
Server: nginx/1.9.12
Content-Type: application/json; charset=UTF-8
Content-Length: 22
Cache-Control: public,s-maxage=300
Date: Fri, 09 Jun 2017 10:20:30 GMT
{
"status": "ok"
}
API layout
client->server: request w/ signature
note right of server: verify signature
server->client: status
# client side def sign_message(private_key_string, message): private_key = Crypto.PublicKey.RSA.importKey(private_key_string) signer = Crypto.Signature.PKCS1_v1_5.new(private_key) message_hash = Crypto.Hash.SHA256.new(message) signature = signer.sign(message_hash) return base64.standard_b64encode(signature).decode('utf-8') # server side def verify_message(public_key_string, message, signature_b64): public_key = Crypto.PublicKey.RSA.importKey(public_key_string) verifier = Crypto.Signature.PKCS1_v1_5.new(public_key) message_hash = Crypto.Hash.SHA256.new(message) signature = base64.standard_b64decode(signature_b64.encode('utf-8')) return verifier.verify(message_hash, signature)
client->server: request w/ RSA.encrypt(merge(request.hash, nonce))
note right of server: verify request.hash
server->client: response w/ RSA.sign(merge(response.hash, nonce))
note left of client: verify signature
# client side def encrypt_message(public_key_string, message): public_key = Crypto.PublicKey.RSA.importKey(public_key_string) cipher = Crypto.Cipher.PKCS1_OAEP.new(public_key) encrypted_message = cipher.encrypt(message) return base64.standard_b64encode(encrypted_message).decode('utf-8') def encrypt_message_hash_with_nonce(public_key_string, message, nonce): message_hash_digest = Crypto.Hash.SHA256.new(message).digest() # this computation may take too long return encrypt_message(public_key_string, merge(message_hash_digest, nonce)) # server side def decrypt_message(private_key_string, encrypted_message_b64): private_key = Crypto.PublicKey.RSA.importKey(private_key_string) cipher = Crypto.Cipher.PKCS1_OAEP.new(private_key) encrypted_message = base64.standard_b64decode(encrypted_message_b64.encode('utf-8')) return cipher.decrypt(encrypted_message) def verify_encrypted_message_hash_and_extract_nonce(private_key_string, message, encrypted_message_hash_with_nonce_b64): message_hash_with_nonce = decrypt_message(private_key_string, encrypted_message_hash_with_nonce_b64) message_hash_digest = Crypto.Hash.SHA256.new(message).digest() # this computation may take too long (decrypted_message_hash_digest, nonce) = unmerge(message_hash_with_nonce) return (decrypted_message_hash_digest == message_hash_digest, nonce)
class Revision(mongo_engine.Document): # read this 1st, publish this 2nd, remove this 1st revision_id = mongo_engine.StringField(required=True) timestamp = mongo_engine.DateTimeField(required=True) name = mongo_engine.StringField(required=True) meta = { 'indexes': [ { 'fields': ['timestamp'], 'unique': True, }, ], } class SongAssetMeta(mongo_engine.Document): # read this 2nd, publish this 1st, remove this 2nd revision_id = mongo_engine.StringField(required=True) song_id = mongo_engine.StringField(required=True) asset_set_checksum = mongo_engine.StringField(required=True) song_cls = mongo_engine.StringField(required=True) song_pack_id = mongo_engine.StringField(required=False) song_title = mongo_engine.StringField(required=True) meta = { 'indexes': [ { 'fields': ['revision_id', 'song_id'], 'unique': True, }, ] }
class SongAssetMetaCache(object): def set_meta(self, revision_id, song_id, asset_set_checksum, song_cls, song_pack_id): meta_key = 'song_asset_meta:%s:%s' % (revision_id, song_id) song_meta = {'asset_set_checksum': asset_set_checksum, 'song_cls': song_cls, 'song_pack_id': song_pack_id} self.strict_redis.hmset(meta_key, song_meta) self.strict_redis.expire(meta_key, self.cache_expire_time) def get_meta(self, revision_id, song_id): return self.strict_redis.hmget(revision_id, song_id) class SongAssetMetaModel(object): def reconstruct_cache(self, revision_id, song_id): try: song_asset_meta = SongAssetMeta.objects.get(revision_id=revision_id, song_id=song_id) except SongAssetMeta.DoesNotExist: SongAssetMeta.delete(revision_id, song_id) else: self.__song_asset_meta_cache.set_meta(song_asset_meta.revision_id, song_asset_meta.song_id, song_asset_meta.asset_set_checksum, song_asset_meta.song_cls, song_asset_meta.song_pack_id) def get_song_asset_meta(self, revision_id, song_id): song_asset_meta = self.__song_asset_meta_cache.get_meta(revision_id, song_id) if song_asset_meta is None: self.reconstruct_cache(revision_id, song_id) song_asset_meta = self.__song_asset_meta_cache.get_meta(revision_id, song_id) return song_asset_meta
def consume(player_access_token): failure_count = 0 while True: try: purchase.consume_coin(player_access_token) except (requests.exceptions.RequestException, server_error.ServerError): failure_count += 1 if failure_count >= 3: raise else: break def gacha(player_access_token): selected_avatars = randomly_select_avatars(player_access_token) transaction = begin_appending_avatars_to_player_data(player_access_token, selected_avatars) try: consume(player_access_token) except: revert_appending_avatars_to_player_data(transaction) raise else: finish_appending_avatars_to_player_data(transaction)
"skip Python code execution
if you can"
Hsueh-Tsung KuoFri, 09 Jun 2017
def publish_asset_revision(revision): song_list = [process(song) for song in SongAssetMeta.objects(revision_id=revision.id)] song_list_json = json.dumps(song_list, ensure_ascii=False, default=bson.json_util.default) location = get_cloud_storage_location() with io.BytesIO(json_data) as fp: cloud_storage.upload_file(location, fp, 'application/json', len(json_data))
client->CDN: request 1
Note left of CDN: cache miss
CDN-->server: request 1
server-->CDN: response 1
CDN->client: response 1
client->CDN: request 2
Note right of CDN: cache hit
CDN->client: response 2 (same as response 1)
Cache-Control: public,s-maxage=300
Date: Fri, 09 Jun 2017 10:20:30 GMT
@app.route('/api/asset/asset_info/<directory>/<file_name>', methods=('GET',)) @cache_control(get_cdn_cache_maxage()) def get_asset_info(directory, file_name): # handle asset info return flask.Response(response=asset_info_json_data, mimetype='application/json') @app.route('/api/asset/song_list', methods=('GET',) template_response_headers() def get_song_list(): remaining_second = get_current_song_list_remaining_second() cache_maxage = min(remaining_second, get_cdn_cache_maxage()) # handle song list response = flask.Response(response=song_list_json_data, mimetype='application/json') add_precise_cache_control_to_headers(response.headers, cache_maxage) return response
def __add_cache_control_to_headers(headers, s_maxage): headers['Cache-Control'] = 'public,s-maxage=%d' % (s_maxage,) def add_precise_cache_control_to_headers(headers, s_maxage): __add_cache_control_to_headers(headers, s_maxage) def template_response_headers(headers={}): def decorator(func): @wraps(func) def decorated_function(*args, **kwargs): flask.g.current_unix_timestamp = time.time() response = flask.make_response(func(*args, **kwargs)) original_headers = response.headers for (header, value) in headers.items(): original_headers.setdefault(header, value) original_headers.setdefault('Date', werkzeug.http.http_date(flask.g.current_unix_timestamp)) return response return decorated_function return decorator def cache_control(s_maxage): headers = {} __add_cache_control_to_headers(headers, s_maxage) return template_response_headers(headers)
the incredible variable:
os.environ['TZ'] = 'Europe/Berlin' time.tzset()
def get_utc_offset_in_second(timestamp): struct_time = time.localtime(timestamp) return calendar.timegm(struct_time)-int(time.mktime(struct_time)) def get_utc_offset_in_timedelta(timestamp): return datetime.datetime.fromtimestamp(timestamp)-datetime.datetime.utcfromtimestamp(timestamp) def get_unix_epoch_day_in_day(timestamp, utc_offset_in_second, local_day_boundary_in_second=None): if local_day_boundary_in_second is None: local_day_boundary_in_second = 0 return (timestamp+utc_offset_in_second-local_day_boundary_in_second)//86400 def get_unix_epoch_day_in_datetime(datetime_obj, utc_offset_in_timedelta, local_day_boundary_in_timedelta=None): if local_day_boundary_in_timedelta is None: local_day_boundary_in_timedelta = datetime.timedelta() return (datetime_obj+utc_offset_in_timedelta-local_day_boundary_in_timedelta).replace(hour=0, minute=0, second=0, microsecond=0)
"don't repeat the same mistakes we did before!"
Hsueh-Tsung KuoFri, 09 Jun 2017