자동 확장 가능한 모바일 게임 서버 구축하기

이재홍 http://www.pyrasis.com 2014.03.24 ~ 2014.06.30

Node.js로 게임 서버 작성하기

필요한 AWS 리소스를 생성하고 설정하였습니다. 이제 Node.js로 게임 서버를 작성해보겠습니다. 다음 예제 코드는 저의 GitHub 저장소에 있는 예제 코드를 받아서 사용합니다.

 

 

app.js

var express = require('express')
  , bodyParser = require('body-parser')
  , expressValidator = require('express-validator')
  , AWS = require('aws-sdk')
  , redis = require('redis')
  , Sequelize = require('sequelize')
  , moment = require('moment')
  , http = require('http')
  , fs = require('fs')
  , app = express()
  , server = http.createServer(app)
  , dynamodb = new AWS.DynamoDB({ region: 'ap-northeast-1' });

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' } }                    
};

app.use(bodyParser.urlencoded())
app.use(bodyParser.json());
app.use(expressValidator());

// 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) { console.log(err) });
}

// ELB 로드 밸런서 헬스 체크용
app.get(['/', '/index.html'], function (req, res) {
  res.send();
});

// 유저 정보 얻기
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' });
  });
});

// 클라이언트에서 점수 받기
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' });
  });
});

// 유저 현재 순위 얻기
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' });
    }
  });
});

// 아이템 구입하기
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' });
  });
});

server.listen(80);

다음은 app.js에서 사용한 모듈들의 버전을 정의한 파일입니다.

package.json

{
  "name": "ExampleGameServer",
  "version": "0.0.1",
  "description": "ExampleGameServer",
  "dependencies": {
    "express": "4.4.x",
    "express-validator": "2.3.x",
    "body-parser": "1.3.x",
    "aws-sdk": "2.0.x",
    "redis": "0.10.x",
    "sequelize": "1.7.x",
    "mysql": "2.3.2",
    "moment": "2.7.x"
  }
}

앞에서 생성한 <프로젝트 이름>.src S3 버킷에 ExampleGameServer라는 디렉터리를 생성하고 app.js, package.json 파일을 올립니다.


그림 33-10 S3 버킷에 웹 서버 Node.js 소스 올리기


저작권 안내

이 웹사이트에 게시된 모든 글의 무단 복제 및 도용을 금지합니다.
  • 블로그, 게시판 등에 퍼가는 것을 금지합니다.
  • 비공개 포스트에 퍼가는 것을 금지합니다.
  • 글 내용, 그림을 발췌 및 요약하는 것을 금지합니다.
  • 링크 및 SNS 공유는 허용합니다.