Node.js를 사용한 게임 서버 및 카우치베이스 - 1부

요즘은 거의 모든 게임 스튜디오에서 플레이어가 전 세계의 친구 및 다른 플레이어와 상호 작용하고 협력할 수 있는 네트워크 게임을 개발하고 있는 것 같습니다. 이전에 이러한 서버를 구축한 경험이 있고 Couchbase가 이러한 시스템을 뒷받침하는 스토어에 적합하다는 점을 고려할 때 이 주제가 글을 쓰기에 좋은 주제라고 생각했습니다! 이 글은 여러 파트로 나누어 각 파트에서 게임 서버의 특정 측면을 구현할 것이며, PHP 클라이언트 라이브러리를 사용하여 동일한 튜토리얼을 작성하여 이를 보여줄 것입니다.

프로젝트 레이아웃

먼저 HTTP 요청을 보내고 받을 수 있도록 몇 가지 기본 사항을 설정하고 Couchbase 클러스터에 연결해야 합니다. 아직 이 작업을 수행하는 방법을 잘 모르시겠다면 제 이전 블로그 게시물 에서 아래보다 조금 더 자세히 설명하겠습니다. 게임 서버 코드를 정리하는 데 도움이 되는 몇 개의 추가 폴더가 있는 일반적인 Node.js 디렉토리 구조로 프로젝트를 시작하겠습니다.

/lib/
/lib/models/
/lib/app.js
/lib/database.js
/package.json

일반적인 Node.js 구조에 따라 모든 소스 파일을 보관하는 'lib' 폴더로 시작하여 메인 파일 역할을 하는 lib/server.js, 마지막으로 프로젝트 종속성 및 기타 메타 데이터를 설명하는 package.json을 추가합니다. 또한 데이터베이스 연결을 중앙에서 관리하여 각 요청에 대해 새 연결을 인스턴스화할 필요가 없도록 하는 database.js와 다양한 데이터베이스 모델 소스 코드를 저장하는 데 사용할 /lib/models/ 폴더를 추가합니다.

기본 사항

다음은 package.json에 대한 몇 가지 콘텐츠입니다. 프로젝트에 이름을 지정하고 기본 자바스크립트 파일을 가리킨 다음 나중에 필요한 몇 가지 필수 모듈을 정의합니다. 이 파일을 저장한 후 다음을 실행합니다. npm 설치 프로젝트 루트 디렉터리에 참조된 종속성을 설치해야 합니다.

{
"main": "./lib/app",
"라이선스" : "Apache2",
"name": "gameapi-couchbase",
"종속성": {
"couchbase": “~1.0.0”,
"express": “~3.4.0”,
"uuid": “~1.4.1”
},
"devDependencies": {
},
"버전": “0.0.1”
}

다음 단계는 게임 서버의 핵심을 설정하는 것입니다. 이것은 /lib/app.js에 배치됩니다. 이 파일의 섹션을 블록별로 살펴보고 각 섹션이 무엇을 하는지 설명하겠습니다.

먼저 이 파일에 필요한 모듈을 가져와야 합니다. 지금은 HTTP 라우팅 및 구문 분석을 위한 express 모듈만 필요하지만 이 튜토리얼의 뒷부분에서 더 많은 모듈을 추가할 예정입니다.

var express = require('express');

다음으로, express를 설정하고, JSON POST 및 PUT 본문을 구문 분석할 수 있도록 express의 bodyParser 하위 모듈을 추가로 첨부합니다. 이렇게 하면 나중에 게임 클라이언트에서 JSON 데이터 블록을 전달해야 할 때 도움이 됩니다.

var= express();
앱으로 이동합니다.사용(익스프레스.bodyParser());

데모 목적으로 서버 루트에 대한 요청을 처리하기 위해 HTTP 서버에 간단한 경로를 추가해 보겠습니다.

앱으로 이동합니다.get(‘/’, 함수(req, res, 다음) {
res.보내기({미니언: '나는 뿌리이니 내 앞에 절하라!'});
});

마지막으로 포트 3000에서 HTTP 서버가 수신 대기하도록 설정해 보겠습니다.

앱으로 이동합니다.듣기(3000, 함수 () {
콘솔.로그('포트 3000에서 수신 중');
});

다음은 지금까지 앱.js가 어떤 모습이어야 하는지에 대한 대략적인 아이디어입니다:

var express = require('express');

var= express();
앱으로 이동합니다.사용(익스프레스.bodyParser());

앱으로 이동합니다.get(‘/’, 함수(req, res, 다음) {
res.보내기({미니언: '나는 뿌리이니 내 앞에 절하라!'});
});

앱으로 이동합니다.듣기(3000, 함수 () {
콘솔.로그('포트 3000에서 수신 중');
});

프로젝트 기본 사항의 마지막 부분으로 데이터베이스 연결을 설정해 보겠습니다. 코드는 매우 간단합니다. 카우치베이스 모듈을 가져온 다음 로컬로 호스팅되는 서버와 'gameapi' 버킷에 대한 새 연결을 모듈의 프로퍼티를 통해 내보냅니다. 메인버킷.

var 카우치베이스 = require('couchbase');

// 카우치베이스 서버에 연결

모듈.수출.메인버킷 = new 카우치베이스.연결({버킷:'gameapi'}, 함수(){});

이 시점에서 프로젝트 루트에서 터미널을 열고 다음을 실행하면 노드 라이브러리/앱.js를 클릭하면 "포트 3000에서 수신 중"이라는 메시지가 표시됩니다. 이제 브라우저에서 http://localhost:3000 를 클릭하고 지금까지의 성과를 확인하세요.

이 시점에서 특정 HTTP 요청을 작성할 수 있는 애플리케이션을 설치하는 것이 좋습니다. 저는 개인적으로 구글 크롬용 POSTman 확장 프로그램을 좋아합니다. 나중에 단순한 GET 요청이 아닌 엔드포인트를 테스트하고 싶을 때 유용하게 사용할 수 있습니다!

계정 만들기 - 계정 모델

이제 기본 서버가 실행되었으니 게임 서버의 '게임' 부분에 대한 작업을 시작하겠습니다. 먼저 계정 생성 엔드포인트를 구현하여 게임 서버의 /사용자 URI. 이 프로세스를 시작하려면 먼저 엔드포인트 핸들러가 처리할 모델을 구축하여 데이터베이스 구현의 일부 세부 사항을 추상화해야 합니다. 이 모델에서 Couchbase Server와 대부분의 상호 작용이 이루어집니다.

먼저 새 파일을 만드는 것으로 시작하겠습니다. /lib/models 폴더에 'accountmodel.js'라는 파일을 만듭니다. accountmodel.js 파일을 준비하고 열었으면 필요한 몇 가지 모듈을 가져오는 것부터 시작해 보겠습니다.

var uuid = require('uuid');
var 카우치베이스 = require('couchbase');
var db = require('./../데이터베이스').메인버킷;

보시다시피 지금 당장 필요한 모듈은 4가지입니다. 데이터베이스 객체에 대한 UUID를 생성하기 위해 uuid 모듈을 사용할 것입니다. 많은 사람들이 Couchbase의 incr/decr 시스템을 사용하여 구현된 시퀀스 카운터를 사용하는 것을 보았지만, 저는 추가 데이터베이스 작업을 할 필요가 없기 때문에 여기서 사용할 UUID 방법을 훨씬 선호합니다. 다음으로 필요한 다양한 상수(주로 오류)에 액세스하는 데 사용할 카우치베이스 모듈을 가져옵니다. 마지막으로 데이터베이스 모듈을 가져와서 앞서 만든 게임에이파이 버킷에 대한 연결을 가져옵니다.

다음으로 모델에 필요하지만 나머지 서버에는 중요하지 않은 데이터베이스 수준 속성을 제거하는 데 도움이 되는 간단한 헬퍼 함수를 정의합니다. 지금은 'type' 프로퍼티가 유일하게 제거할 프로퍼티입니다. 이 속성은 나중에 맵 리덕션을 수행할 때 버킷의 특정 아이템이 어떤 종류의 오브젝트인지 식별하기 위해 gameapi에서 사용됩니다.

함수 cleanUserObj(객체) {
삭제 obj.유형;
반환 객체;
}

이제 AccountModel 클래스를 정의합니다.

함수 계정 모델() {
}

그리고 이 파일을 가져오는 다른 파일로 클래스를 내보냅니다. 특정 파일이 내보내는 내용을 식별하려고 할 때 쉽게 찾을 수 있도록 이 문장을 항상 파일 하단에 두는 것이 좋습니다.

모듈.수출 = 계정 모델;

이제 모델 상용구가 완성되었으므로 사용자 객체를 생성할 수 있는 create 함수를 작성할 수 있습니다. 이 함수를 더 쉽게 설명하기 위해 더 작은 덩어리로 나누겠습니다.

함수 자체의 정의부터 시작하겠습니다.

계정 모델.create = 함수(사용자, 콜백) {
};

다음으로, Couchbase 버킷에 삽입할 객체를 만들어 보겠습니다. 위에서 언급한 대로 나중에 사용할 객체의 유형을 지정합니다. 사용자를 전체적으로 참조하는 데 도움이 되는 UID를 생성합니다. 마지막으로 생성 함수에 전달된 사용자 세부 정보를 복사합니다. 모델에 전달되는 데이터에 대한 유효성 검사를 수행하지 않는 것을 볼 수 있는데, 이는 대부분의 경우 요청 처리 코드가 무엇을 수락하거나 수락하지 않을지 더 잘 알고 있고 모델은 데이터를 저장할 책임만 있기 때문입니다. 마지막으로 이 문서를 참조하는 데 사용할 키를 생성하며, 이를 위해 문서 유형과 사용자 UID를 사용합니다.

var 사용자 문서 = {
유형: 'user',
uid: uuid.v4(),
이름: 사용자.이름,
사용자 이름: 사용자.사용자 이름,
비밀번호: 사용자.비밀번호
};

var 사용자 문서 이름 = 'user-' + userDoc.uid;

나중에 사용자 아이디로 이 사용자를 찾을 수 있도록 하기 위해(사용자가 자신의 UID를 기억하게 하는 것은 좋은 생각이 아닙니다!) '참조 문서', 즉 사용자의 사용자 아이디를 기반으로 사용자의 문서를 다시 가리키는 키가 있는 문서를 만듭니다(사용자 아이디를 사용). 이렇게 하면 여러 사용자가 동일한 사용자 아이디를 사용하는 것을 방지할 수 있다는 추가적인 이점도 있습니다.

var refDoc = {
유형: 'username',
uid: userDoc.uid
};
var refDocName = '사용자 이름-' + userDoc.사용자 이름;

마지막으로, 이러한 문서를 Couchbase 버킷에 삽입해야 합니다. 먼저 참조 문서를 삽입하고 사용자에게 사용자 이름을 가져왔다는 메시지를 반환하고 다른 오류를 간단히 전달하여 keyAlreadyExists 오류를 구체적으로 처리합니다(모델 수준에서 Couchbase 오류를 래핑해야 하지만 이 시리즈의 이 시점에서는 중요하지 않습니다). 여기서 참조 문서를 먼저 삽입하는 것이 중요한 이유는 ++TODO++ 왜 다시 중요한가? -TODO-. 다음으로 사용자 문서 자체를 삽입하고 마지막으로 전달받은 콜백을 호출합니다. 먼저 데이터베이스 수준의 속성이 애플리케이션의 다른 계층으로 유출되지 않도록 앞서 만든 함수를 사용하여 반환된 객체를 위생 처리합니다. 또한 콜백을 통해 'cas' 값을 전달하고 있음을 알 수 있습니다. 이는 나중에 다음과 같은 작업을 수행해야 할 때 중요합니다. 낙관적 잠금 를 클릭합니다.

db.추가(refDocName, refDoc, 함수(err) {
만약 (err && err.코드 === 카우치베이스.오류.keyAlreadyExists) {
반환 콜백('지정한 사용자 아이디가 이미 존재합니다');
} else 만약 (err) {
반환 콜백(err);
}

db.추가(사용자 문서 이름, 사용자 문서, 함수(err, 결과) {
만약 (err) {
반환 콜백(err);
}

콜백(null, cleanUserObj(사용자 문서), 결과.cas);
});
});

지금까지 계정모델.js 파일의 모습은 다음과 같습니다:

var uuid = require('uuid');
var 카우치베이스 = require('couchbase');
var db = require('./../데이터베이스').메인버킷;

함수 cleanUserObj(객체) {
삭제 obj.유형;
반환 객체;
}

함수 계정 모델() {
}

계정 모델.create = 함수(사용자, 콜백) {
var 사용자 문서 = {
유형: 'user',
uid: uuid.v4(),
이름: 사용자.이름,
사용자 이름: 사용자.사용자 이름,
비밀번호: 사용자.비밀번호
};
var 사용자 문서 이름 = 'user-' + userDoc.uid;

var refDoc = {
유형: 'username',
uid: userDoc.uid
};
var refDocName = '사용자 이름-' + userDoc.사용자 이름;

db.추가(refDocName, refDoc, 함수(err) {
만약 (err && err.코드 === 카우치베이스.오류.keyAlreadyExists) {
반환 콜백('지정한 사용자 아이디가 이미 존재합니다');
} else 만약 (err) {
반환 콜백(err);
}

db.추가(사용자 문서 이름, 사용자 문서, 함수(err, 결과) {
만약 (err) {
반환 콜백(err);
}

콜백(null, cleanUserObj(사용자 문서), 결과.cas);
});
});
};

모듈.수출 = 계정 모델;

계정 생성 - 요청 처리

이제 계정 모델에서 생성 함수를 완성했으므로 이제 계정 생성 요청을 처리하고 이러한 요청을 함수에 전달하는 익스프레스 경로를 작성할 수 있습니다. 먼저 경로를 정의해야 합니다.

앱으로 이동합니다.post('/users', 함수(req, res, 다음) {
// 다음 비트는 여기에 들어갑니다!
});

그리고... 필요한 데이터가 엔드포인트에 전달되었는지 확인하기 위해 몇 가지 유효성 검사를 수행합니다.

만약 (!req.body.이름) {
반환 res.보내기(400, '이름을 지정해야 함');
}
만약 (!req.body.사용자 이름) {
반환 res.보내기(400, '사용자 아이디를 지정해야 합니다');
}
만약 (!req.body.비밀번호) {
반환 res.보내기(400, '비밀번호를 지정해야 합니다');
}

데이터가 *기침* "유효성 검사"*가 완료되면 사용자 비밀번호의 SHA1 해시를 생성한 다음(사용자의 비밀번호를 일반 텍스트로 저장하지 마세요!) 이 글의 앞부분에서 만든 생성 함수를 실행할 수 있습니다. 또한 사용자 객체에서 사용자 비밀번호를 제거한 후 클라이언트에 다시 전달하는 것을 볼 수 있습니다. 이 역시 보안을 위해 사용자의 비밀번호(어떤 형식이든)가 전송되는 것을 최대한 제한하기 위한 것입니다.

var newUser = req.body;
newUser.비밀번호 = 크립트.sha1(newUser.비밀번호);

계정 모델.create(req.body, 함수(err, 사용자) {
만약 (err) {
반환 다음(err);
}

삭제 사용자.비밀번호;
res.보내기(사용자);
});

요약하자면, 전체 계정 생성 경로는 다음과 같아야 합니다:

앱으로 이동합니다.post('/users', 함수(req, res, 다음) {
만약 (!req.body.이름) {
반환 res.보내기(400, '이름을 지정해야 함');
}
만약 (!req.body.사용자 이름) {
반환 res.보내기(400, '사용자 아이디를 지정해야 합니다');
}
만약 (!req.body.비밀번호) {
반환 res.보내기(400, '비밀번호를 지정해야 합니다');
}

var newUser = req.body;
newUser.비밀번호 = 크립트.sha1(newUser.비밀번호);

계정 모델.create(newUser, 함수(err, 사용자) {
만약 (err) {
반환 다음(err);
}

삭제 사용자.비밀번호;
res.보내기(사용자);
});
});

피날레

드디어 튜토리얼 1부가 끝났습니다. 기초적인 내용을 많이 다루었으므로 앞으로의 파트는 조금 더 짧아질 것입니다(그렇다고 약속할 수는 없습니다!). 이 시점에서 /users 엔드포인트에 POST 요청을 실행하고 다음과 같이 새 사용자를 생성할 수 있어야 합니다:

> POST /사용자
{
"이름": "브렛 로슨",
"username": "brett19",
"비밀번호": "성공!"
}
< 200 미만 확인
{
"uid": “b836d211-425c-47de-9faf-5d0adc078edc”,
"이름": "브렛 로슨",
"username": "brett19"
}

안타깝게도 현재로서는 새로 찾은 계정으로 할 수 있는 일이 많지 않아서 데이터베이스에 계정이 있다는 사실에 감탄하는 것 외에는 할 수 있는 일이 많지 않습니다. 이제 등록할 수 있는 사용자의 세션과 인증에 대해 소개하는 2부에서는 계속 지켜봐 주시기 바랍니다.

이 애플리케이션의 전체 소스는 여기에서 확인할 수 있습니다: https://github.com/brett19/node-gameapi

즐겨보세요! Brett

이 문서 공유하기
받은 편지함에서 카우치베이스 블로그 업데이트 받기
이 필드는 필수 입력 사항입니다.

작성자

게시자 브렛 로슨, 수석 소프트웨어 엔지니어, Couchbase

브렛 로슨은 카우치베이스의 수석 소프트웨어 엔지니어입니다. Brett은 Couchbase Node.js 및 PHP 클라이언트의 설계와 개발을 담당하고 있으며, C 라이브러리인 libcouchbase의 설계와 개발에도 참여하고 있습니다.

댓글 하나

  1. 이 작업을 하는 사람이라면 누구나 익스프레스 대신 하피를 사용하는 것을 추천합니다. https://github.com/spumko/hapi

    1. 이 라이브러리를 추천해주신 이유를 여쭤봐도 될까요? 잘 알려져 있고 안정적인 라이브러리이며 restify 및 기타 유사한 라이브러리에는 부족한 일부 기능이 있기 때문에 Express를 선택했습니다.
      건배, 브렛

      1. 에란이 만들고, 오스1에서 작업했으며, 오스2의 리드였습니다.
        그가 해결한 익스프레스 관련 문제가 있습니다. 이 동영상을 보셔야 합니다. https://www.youtube.com/watch?…

        1. 감사합니다. 꽤 흥미로운 영상이었어요 :) 안타깝게도 제 블로그 시리즈가 이미 시작되었기 때문에 이 시점에서 변경을 고려하는 것은 위험할 것 같습니다!

          1. 마음에 드셨다니 다행입니다! 다음에 또 뵙겠습니다 :)

  2. 안녕하세요! 매우 유용하고 초보자 친화적인 튜토리얼입니다. 제가 발견한 것은 버킷 게임에이파이가 존재하지 않고, 설명하기 전에 크립트 모듈을 사용하고 있으며, accountModel.create req.body가 아닌 newUser를 전달하고 있다는 것입니다. 시리즈 2부를 기대하겠습니다! 고마워요

  3. 비밀번호를 일반 텍스트로 포함하는 수정되지 않은 원래의 req.body 대신 (비밀번호가 암호화된) newUser를 accountModel.create()에 전달해야 하지 않을까요?

    1. 또한 기술적으로 적절한 데이터 저장에 필요한 '데이터 준비' 단계로 간주될 수 있으므로 모델 수준에서 비밀번호를 해시하는 것이 더 낫지 않을지 궁금합니다. 즉, 먼저 해시되지 않았거나 최소한 암호화되지 않은 비밀번호로 사용자 객체를 저장하고 싶지 않을 것입니다.

      따라서 accountmodel.js에서 userDoc.password 속성을 require(\'crypto\').createHash(\'sha1\').update(user.password).digest(\'base64\')로 설정한 다음 앱.js에서 튜토리얼에 표시된 대로 req.body를 accountModel.create()로 전달하여 newUser 변수를 완전히 없애는 것이 좋습니다.

      또한 계정모델.js에서 create() 메서드를 호출하기 전에 다음과 같이 초기화해야 합니다:

      var AccountModel = require(\'./모델/accountmodel.js\');

      마지막으로, 이 튜토리얼에서는 이 섹션을 읽으면 자연스럽게 app.js가 암시되므로 Couchbase 연결이 database.js에 있어야 한다고 표시하는 것이 좋으며, 이는 독자가 "데이터베이스 연결을 설정"하기 위해 database.js로 이동하라는 신호가 없었기 때문입니다.

      1. 앱에서 비밀번호 암호화를 처리한 이유는 사용자가 저장한 것을 되돌릴 수 있도록 모델을 설계하려고 했기 때문이며, 추가로 내 사용자 정의 모델을 ottoman과 같은 실제 Node.js ODM으로 교체하는 경우 (https://github.com/couchbasela..., 어쨌든 모델에서 비밀번호를 자동으로 암호화하는 기능은 없을 것입니다. 첫 번째 문제에 관해서는 맞습니다. 튜토리얼을 업데이트했습니다.
        건배, 브렛

  4. 사란쉬 모하파트라 2월 6, 2014에서 9:06 오후

    아주 사소한 질문입니다....그러나 사용자 이름이 고유해야 하는데 왜 사용자 이름에 \\'userDocName : 사용자 이름은 고유해야 하므로 \'사용자-사용자 이름\'을 저장할 수 없나요? 이렇게 하면 추가 데이터 쓰기와 읽기를 절약할 수 있습니다. 아니면 제가 놓치고 있는 어떤 이유 때문인가요?

    1. 안녕하세요 사란쉬,
      원래 구현에서는 물론이고 확장할 경우 이 구현에서도 마찬가지입니다. 계정에 사용자를 인증하는 방법은 여러 가지가 있습니다. 사용자 이름/비밀번호를 지원할 수도 있지만, 페이스북이나 구글 플레이 계정도 지원할 수 있습니다. 이 경우 각각에 대해 별도의 조회 키가 필요하지만 여전히 동일한 일반 계정 유형을 가리키도록 해야 합니다.
      건배, 브렛

  5. 간단한 질문이 있습니다. 저는 카우치베이스와 모든 NoSQL 데이터베이스를 처음 사용하므로 바보 같은 질문일 수도 있습니다.

    사용자 아이디와 이메일을 데이터베이스와 대조하여 동일한 이메일이나 사용자 아이디가 두 개가 없는지 확인하고 싶습니다. 사용자 이름에 대해서는 이미 참조 문서를 사용하여 확인했습니다. 이메일에 대해서도 같은 방법으로 확인하시겠습니까? 사용자가 10만 명이라면 데이터베이스가 부풀어 오르지 않을까요?
    가장 좋은 방법은 무엇일까요? 문서를 참조할까요? 모든 사용자를 파악하고 사용자 이름과 이메일을 확인할까요? 지도 축소?

    1. 안녕하세요 호세,
      참조 문서를 사용하는 것이 모든 종류의 고유 조회 테이블을 구현하는 가장 쉬운 방법입니다. 맵/축소를 사용할 수도 있지만, 인덱싱이 비동기적으로 이루어지기 때문에 사용자가 중복될 가능성이 있습니다. 공간 관점에서 보면 맵/축소 보기를 사용하거나 참조 문서를 사용하는 경우 비슷한 양의 공간을 사용할 가능성이 높습니다.
      건배, 브렛

  6. [...] 금주의 블로그: 금주의 블로그: Node.js를 사용한 게임 서버와 카우치베이스 - 1부 [...]

  7. [...] 이 시리즈의 1부를 아직 읽지 않았다면 기본 프로젝트 레이아웃과 기본 사용자 [...]를 설정하는 방법을 설명하는 1부를 읽어보시길 권합니다.

  8. [...] 금주의 블로그 포스트 #2a: Nodejs를 사용한 게임 서버와 카우치베이스 (1부) [...]

댓글 남기기

카우치베이스 카펠라를 시작할 준비가 되셨나요?

구축 시작

개발자 포털에서 NoSQL을 살펴보고, 리소스를 찾아보고, 튜토리얼을 시작하세요.

카펠라 무료 사용

클릭 몇 번으로 Couchbase를 직접 체험해 보세요. Capella DBaaS는 가장 쉽고 빠르게 시작할 수 있는 방법입니다.

연락하기

카우치베이스 제품에 대해 자세히 알고 싶으신가요? 저희가 도와드리겠습니다.