저작권 안내
- 책 또는 웹사이트의 내용을 복제하여 다른 곳에 게시하는 것을 금지합니다.
- 책 또는 웹사이트의 내용을 발췌, 요약하여 강의 자료, 발표 자료, 블로그 포스팅 등으로 만드는 것을 금지합니다.
자동 확장 가능한 모바일 게임 서버 구축하기
이재홍 http://www.pyrasis.com 2014.03.24 ~ 2014.06.30
게임 서버 내용 둘러보기
게임 서버의 내용을 살펴보겠습니다. 먼저 Node.js의 express 모듈로 80번 포트에 웹 서버를 실행합니다.
- body-parser 모듈은 HTTP POST, PUT 메서드로 오는 데이터를 사용하기 편리하게 req.body에 넣어줍니다.
- express-validator 모듈은 HTTP POST, PUT 메서드로 받을 필수적인 데이터를 지정할 수 있습니다. 지정한 데이터가 빠졌을 때 에러를 발생시킵니다.
- app.use 함수로 body-parser, express-validator 모듈을 활성화합니다.
var express = require('express')
, bodyParser = require('body-parser')
, expressValidator = require('express-validator')
...
, http = require('http')
...
, app = express()
, server = http.createServer(app)
...
app.use(bodyParser.urlencoded())
app.use(bodyParser.json());
app.use(expressValidator());
...
server.listen(80);
서버 기본 설정 및 정의입니다.
- ElastiCache 캐시 노드 엔드포인트 주소(Redis), RDS DB 인스턴스 엔드포인트 주소(MySQL)와 연결 설정, DynamoDB 테이블 이름은 여러분들이 생성한 AWS 리소스의 정보를 입력합니다.
- 유저 테이블의 비밀번호 부분은 향후 로그인을 구현할 때 사용하면 됩니다. 요즘은 직접 로그인보다는 카카오톡, 라인, 페이스북을 연동을 많이 합니다. 카카오톡, 라인, 페이스북 연동에 필요한 데이터도 유저 테이블에 저장합니다.
- 테스트에 필요한 예제 유저 데이터도 생성합니다.
- 아이템 데이터는 보통 JSON 파일로 따로 빼서 정의합니다. 아래 코드는 예제이므로 JavaScript에서 오브젝트 형태로 직접 정의했습니다.
var redisEndpoint = {
host: 'examplegame.o5nouc.0001.apne1.cache.amazonaws.com',
port: 6379
};
var rdsEndpoint = {
host: 'examplegame.cnlconsezo7y.ap-northeast-1.rds.amazonaws.com',
port: 3306
};
var dynamoDbTable = 'ExampleGameLog';
var redisClient = redis.createClient(redisEndpoint.port, redisEndpoint.host);
// MySQL DB 이름, 계정, 암호
var sequelize = new Sequelize('examplegame', 'admin', 'adminpassword', {
host: rdsEndpoint.host,
port: rdsEndpoint.port
});
// 유저 테이블 정의
var User = sequelize.define('User', {
userId: { type: Sequelize.STRING, allowNull: false, unique: true },
//password: Sequelize.STRING,
topScore: Sequelize.INTEGER,
coin: Sequelize.INTEGER,
itemSlot1: Sequelize.STRING,
itemSlot2: Sequelize.STRING,
itemSlot3: Sequelize.STRING
});
// 예제 유저 데이터 생성
User.count().error(function (error) {
if (error.code == 'ER_NO_SUCH_TABLE') {
sequelize.sync().success(function () {
User.create({
userId: 'john',
topScore: 0,
coin: 1000,
});
User.create({
userId: 'maria',
topScore: 0,
coin: 1000,
});
});
}
});
// 아이템 데이터
var itemTable = {
'101': { name: 'Bomb', price: { coin: '100' } },
'102': { name: 'Time Bonus', price: { coin: '150' } }
};
DynamoDB에 로그를 저장하는 함수입니다.
- 고정 파라미터인 action과 비정형 데이터인 data를 받습니다.
- data는 DynamoDB 파라미터 형식에 맞게 변환합니다.
- AWS API를 이용하여 DynamoDB 테이블에 로그를 저장합니다.
// DynamoDB에 로그 저장
function writeLog(action, data) {
var params = {
Item: {
action: { S: action },
date: { S: moment().format('YYYY-MM-DD HH:mm:ss') }
},
TableName: dynamoDbTable
};
for (var key in data) {
var attribute = data[key];
if (isNaN(attribute))
params.Item[key] = { S: attribute };
else
params.Item[key] = { N: String(attribute) };
}
dynamodb.putItem(params, function (err, data) { });
}
앞의 예제들은 웹 서버이기 때문에 모두 인덱스 페이지가 있었습니다. 게임 서버는 따로 인덱스 페이지가 없으므로 ELB 로드 밸런서에서 헬스 체크를 할 수 있도록 /, /index.html 경로에서 빈 내용을 출력합니다.
// ELB 로드 밸런서 헬스 체크용
app.get(['/', '/index.html'], function (req, res) {
res.send();
});
유저의 정보를 얻는 API입니다.
- GET 메서드이며 userId를 받습니다.
- Sequelize 모듈로 MySQL에서 유저 정보를 가져와서 출력합니다.
- 클라이언트에는 꼭 필요한 정보만 전달합니다.
// 유저 정보 얻기
app.get('/users/:userId', function (req, res) {
req.assert('userId').notEmpty();
var errors = req.validationErrors();
if (errors) {
res.send({ error: -1, data: errors });
return;
}
var userId = req.params.userId;
User.find({ where: { userId: userId } }).success(function (user) {
var data = {};
data.userId = user.userId;
data.topScore = user.topScore;
data.coin = user.coin;
data.itemSlot1 = user.itemSlot1;
data.itemSlot2 = user.itemSlot2;
data.itemSlot3 = user.itemSlot3;
res.send({ error: '', data: data });
}).error(function (error) {
res.send({ error: 'db error' });
});
});
클라이언트에서 점수를 받는 API입니다
- POST 메서드이며 userId와 score를 받습니다.
- zadd 함수로 Redis의 Sorted Set에 점수를 저장합니다.
- Redis에 점수 저장이 성공하면 MySQL에서 유저 정보를 가져옵니다. 그리고 유저의 최고 점수와 현재 점수를 비교하여 현재 점수가 높으면 유저의 최고 점수를 업데이트합니다.
- writeLog 함수로 userId와 점수를 저장합니다.
// 클라이언트에서 점수 받기
app.post('/users/:userId/scores', function (req, res) {
req.assert('userId').notEmpty();
req.checkBody('score').notEmpty();
var errors = req.validationErrors();
if (errors) {
res.send({ error: -1, data: errors });
return;
}
var userId = req.params.userId;
var score = req.body.score;
redisClient.zadd('leaderboard', score, userId, function (err, reply) {
if (!err) {
User.find({ where: { userId: userId } }).success(function (user) {
if (score > user.topScore) {
user.topScore = score;
user.save().success(function () {
res.send({ error: '' });
}).error(function (error) {
res.send({ error: 'db error' });
});
}
else
res.send({ error: '' });
writeLog('game', {
category: 'score',
userId: userId,
score: score
});
});
}
else
res.send({ error: 'cache error' });
});
});
유저의 현재 순위와 전체 순위 정보를 얻는 API입니다.
- GET 메서드이며 userId를 받습니다.
- zrank 함수로 Redis에서 현재 유저의 순위를 얻습니다.
- zrevrange 함수로 처음(0)부터 끝(-1)까지 점수 순으로 정렬된 정보를 얻습니다. 0, -1 대신 특정 구간의 정보를 얻을 수도 있습니다. withscores를 설정하면 점수도 함께 얻습니다. zrevrange 함수에서 출력된 정보는 userId, 점수순으로 된 배열입니다.
// 유저 현재 순위 얻기
app.get('/users/:userId/rank', function (req, res) {
req.assert('userId').notEmpty();
var errors = req.validationErrors();
if (errors) {
res.send({ error: -1, data: errors });
return;
}
var userId = req.params.userId;
redisClient.zrank('leaderboard', userId, function (err, reply) {
if (!err)
res.send({ error: '', data: { rank: reply } });
else
res.send({ error: 'cache error' });
});
});
// 전체 순위 정보 얻기
app.get('/leaderboard', function (req, res) {
redisClient.zrevrange('leaderboard', 0, -1, 'withscores', function (err, reply) {
if (!err) {
var data = [];
for (var i = 0, rank = 1; i < reply.length; i += 2, rank++) {
data.push({ rank: rank, userId: reply[i], score: reply[i + 1] });
}
res.send({ error: '', data: data });
}
else {
res.send({ error: 'cache error' });
}
});
});
아이템을 구입하는 API입니다.
- POST 메서드이며 userId, itemId, itemSlot을 받습니다.
- MySQL에서 유저 정보를 가져온 뒤 유저가 가지고 있는 돈(coin)을 확인합니다.
- 아이템을 살 수 있으면 아이템 테이블에 설정된 대로 아이템 값을 차감하고, 유저의 아이템 슬롯에 아이템을 넣어준뒤 MySQL에 저장합니다.
- 클라이언트에는 아이템 구입 결과를 보냅니다.
- writeLog 함수로 userId, itemSlot, ItemId를 저장합니다.
// 아이템 구입하기
app.post('/users/:userId/items', function (req, res) {
req.assert('userId').notEmpty();
req.checkBody('itemId').notEmpty();
req.checkBody('itemSlot').notEmpty();
var errors = req.validationErrors();
if (errors) {
res.send({ error: -1, data: errors });
return;
}
var userId = req.params.userId;
var itemId = req.body.itemId;
var itemSlot = req.body.itemSlot;
User.find({ where: { userId: userId } }).success(function (user) {
if (user.coin > itemTable[itemId].price.coin) {
user['itemSlot' + itemSlot] = itemId;
user.coin -= itemTable[itemId].price.coin;
user.save().success (function () {
var data = {};
data['itemSlot' + itemSlot] = itemId;
res.send({ error: '', data: data });
writeLog('shop', {
category: 'item',
userId: userId,
itemSlot: itemSlot,
itemId: itemId
});
}).error(function (error) {
res.send({ error: 'db error' });
});
}
else
res.send({ error: 'not enough coin' });
}).error(function (error) {
res.send({ error: 'db error' });
});
});
저작권 안내
이 웹사이트에 게시된 모든 글의 무단 복제 및 도용을 금지합니다.- 블로그, 게시판 등에 퍼가는 것을 금지합니다.
- 비공개 포스트에 퍼가는 것을 금지합니다.
- 글 내용, 그림을 발췌 및 요약하는 것을 금지합니다.
- 링크 및 SNS 공유는 허용합니다.
Published
2014-09-30