이번 연재에서는 플레이어가 게임을 즐기는 동안 게임 상태를 저장할 수 있도록 게임 데이터 저장 시스템을 설정하겠습니다. 이를 위해 개별 상태 데이터 블록을 나타내는 /state 및 /states 엔드포인트를 만들겠습니다. 여러 개의 명명된 상태 블록을 허용하여 게임에서 상태 데이터를 개별적으로 업데이트 가능한 블록으로 나누어 한 부분만 변경되었을 때 많은 상태 블록을 작성할 필요가 없도록 할 것입니다.
아직 읽지 않았다면 1부 그리고 파트 2 이 시리즈와 앞으로의 모든 시리즈가 이 부분을 기반으로 만들어지기 때문에 꼭 읽어보시길 권해드립니다!
퀵 사이드 - 세션 갱신
이전 블로그 게시물에 포함되어야 했던 중요한 내용은 사용자가 접속할 때마다 사용자 세션을 갱신하는 것입니다. 이 기능이 없으면 플레이어가 계속 플레이하고 있는지 여부와 관계없이 세션이 60분 후에 만료됩니다. 이는 명백히 저희의 의도가 아니었으므로 수정하겠습니다!
먼저 세션모델에 새 함수를 추가해야 하므로 세션모델.js를 열고 다음 블록을 추가해 보겠습니다. 이 함수는 매우 간단한 함수로, 세션 ID를 가져와 이에 대해 터치 연산을 수행하여 만료 시간을 다시 3600으로 재설정합니다(키가 처음 삽입된 시점이 아니라 터치 실행 시점부터 시작).
세션 모델.터치 = 함수(sid, 콜백) {
var sessDocName = 'sess-' + sid;
db.터치(sessDocName, {만료: 3600}, 함수(err, 결과) {
콜백(err);
});
};
이제 세션을 업데이트하기 위한 모델 함수를 만들었으니 호출하기 좋은 곳을 찾아봅시다. 사용자가 인증되어야 하는 모든 엔드포인트에서 실행되는 authUser 메서드가 적합해 보입니다. 이제 그렇게 해봅시다. 다음은 터치 호출이 추가된 새로운 authUser 함수입니다.
req.uid = null;
만약 (req.헤더.권한 부여) {
var authInfo = req.헤더.권한 부여.분할(‘ ‘);
만약 (authInfo[0] === '무기명') {
var sid = authInfo[1];
세션 모델.get(sid, 함수(err, uid) {
만약 (err) {
다음('세션 ID가 유효하지 않습니다');
} else {
세션 모델.터치(sid, 함수(){});
req.uid = uid;
다음();
}
});
} else {
다음('이 엔드포인트에 액세스할 수 있는 권한이 있어야 합니다');
}
} else {
다음('이 엔드포인트에 액세스할 수 있는 권한이 있어야 합니다');
}
}
이번 세션 만료 수정의 일환으로, 이 블로그 게시 이전에 수정되었지만 최신 주기 릴리스 이후에 수정된 터치 구현의 버그로 인해 GitHub에서 직접 카우치노드 버전을 가져와야 할 수도 있습니다.
게임 상태 - 모델
이제 이전 파트의 작은 문제를 해결했으니 게임 상태 저장 구현으로 넘어가 보겠습니다! 위에서 말했듯이 네트워크 트래픽을 줄이기 위해 게임이 여러 상태 블록에 데이터를 저장할 수 있도록 할 것입니다. 스토리지 관점에서 이러한 모든 상태 블록을 하나의 Couchbase 문서에 저장할 것이며, 이 문서는 사용자의 첫 번째 정보 저장 요청 시 느리게 생성될 것입니다. 저장된 내용이 있을 때까지는 곧 보게 되겠지만 사용자의 빈 상태 목록을 에뮬레이트할 것입니다.
먼저 model/statemodel.js에서 표준 모델 파일 레이아웃을 설정해 보겠습니다. 필요한 모듈을 임포트하고 StateModel이라는 메서드가 없는 모델을 설정합니다.
var db = require('./../데이터베이스').메인버킷;
var 카우치베이스 = require('couchbase');
함수 StateModel() {
}
모듈.수출 = StateModel;
이제 모델에 대한 기본 사항을 갖추었으니 필요한 몇 가지 메서드를 구현해 보겠습니다. 새 상태 블록을 저장할 수 있는 모델부터 시작해 보겠습니다. 이 함수는 상태 블록의 생성과 업데이트를 모두 처리합니다. 이렇게 하면 API 수준에서 상태 블록이 이미 존재하는지 여부를 걱정할 필요가 없으므로 클라이언트 측 로직이 훨씬 간단해집니다. 저희는 각 상태 블록에 버전 번호가 저장되는 일종의 낙관적 잠금을 사용할 것입니다. 상태 블록이 업데이트될 때마다 서버가 새 데이터를 수락하기 전에 서버에 있는 기존 버전 번호를 전달해야 합니다. 이는 동시에 실행되는 여러 개의 게임 복사본이 서로의 데이터를 짓밟는 것을 방지하기 위한 것입니다. 이것은 또한 Couchbase의 낙관적 잠금 를 사용하여 두 개의 엔드포인트 호출에서 상태 객체를 동시에 변경하지 않도록 합니다.
저장 기능 프로토타입부터 시작하겠습니다.
};
첫 번째 실제 단계는 상태 저장소 문서의 이름을 만든 다음, 이 문서가 아직 존재하는지 확인하기 위해 Couchbase에 이 문서를 요청하는 것입니다.
db.get(stateDocName, 함수(err, 결과) {
// 아래 코드가 여기에 들어갑니다!
});
이제 기존 상태 문서를 요청하는 데 오류가 있는지 확인합니다. 오류가 발생하면 '찾을 수 없음' 오류가 아닌지 확인합니다. 문서를 찾을 수 없는 경우 이 오류를 무시하고 계속 진행하는데, 이는 상태 문서의 지연 생성 특성 때문입니다.
만약 (err.코드 !== 카우치베이스.오류.keyNotFound) {
반환 콜백(err);
}
}
다음으로 기존 상태 문서(또는 찾을 수 없는 경우 새 문서)를 별도의 변수로 이동하여 쉽게 액세스할 수 있도록 하고, 기존 문서와 새 문서를 동일한 방식으로 처리할 수 있습니다.
유형: 'state',
uid: uid,
상태: {}
};
만약 (결과.값) {
stateDoc = 결과.값;
}
이제 상태 블록 자체에 대해서도 동일한 작업을 수행합니다. stateBlock 변수가 실제 상태 문서의 상태 배열을 참조하도록 한 것을 볼 수 있습니다. 또 한 가지 언급할 만한 점은 기본 상태 블록 버전이 0이라는 것입니다. 즉, 처음으로 저장을 실행할 때 클라이언트가 새 상태 블록이라는 것을 명확히 하기 위해 버전 0을 지정할 것으로 예상됩니다.
버전: 0,
데이터: null
};
만약 (stateDoc.상태[이름]) {
stateBlock = stateDoc.상태[이름];
} else {
stateDoc.상태[이름] = stateBlock;
}
다음으로 호출자가 지정한 버전이 여전히 클러스터에 저장된 버전과 일치하는지 확인해야 합니다. 그렇지 않은 경우 클라이언트가 마지막으로 저장 데이터를 검색한 이후 다른 사용자가 변경한 것이 틀림없습니다. 버전이 일치하지 않는 경우 클라이언트는 새 데이터를 검색하고 필요한 병합을 수행한 다음 업데이트를 다시 시도할 것으로 예상됩니다.
반환 콜백('귀하의 버전이 서버 버전과 일치하지 않습니다.');
} else {
stateBlock.version++;
stateBlock.data = 데이터;
}
이 섹션의 시작 부분에서 언급했듯이, 상태 문서 쓰기가 순서대로 미리 수행되도록 하기 위해 Couchbase에 내장된 낙관적 잠금 기능을 사용할 것입니다. 이전에 가져오기를 수행한 다음 버전 비교를 수행하고 마지막으로 여기서 다시 쓰기를 수행하기 때문에 상태 저장 엔드포인트에 대한 다른 호출로 인해 원래 가져오기 이후 객체가 변경될 가능성이 있지만, 설정 전에 cas 값을 사용하는 낙관적 잠금을 사용하면 이를 방지할 수 있습니다. cas 값에 대한 자세한 내용은 CAS 값에 대한 카우치베이스 매뉴얼.
만약 (결과.값) {
setOptions.cas = 결과.cas;
}
마지막으로 이 특정 메서드의 경우 집합을 미리 만들고, 발생하는 모든 오류는 호출자에게 전파되며(앞서 언급한 것처럼 모델 수준에서 래핑되어야 함), 저장한 상태 블록으로 콜백이 호출됩니다.
db.set(stateDocName, stateDoc, 설정 옵션, 함수(err, 결과) {
만약 (err) {
반환 콜백(err);
}
콜백(null, stateBlock);
});
마지막으로 전체 저장 방법을 알려드리겠습니다. 꽤 길지만 비교적 이해하기 쉽기를 바랍니다!
StateModel.저장 = 함수(uid, 이름, preVer, 데이터, 콜백) {
var stateDocName = 'user-' + uid + '-state';
db.get(stateDocName, 함수(err, 결과) {
만약 (err) {
만약 (err.코드 !== 카우치베이스.오류.keyNotFound) {
반환 콜백(err);
}
}
var stateDoc = {
유형: 'state',
uid: uid,
상태: {}
};
만약 (결과.값) {
stateDoc = 결과.값;
}
var stateBlock = {
버전: 0,
데이터: null
};
만약 (stateDoc.상태[이름]) {
stateBlock = stateDoc.상태[이름];
} else {
stateDoc.상태[이름] = stateBlock;
}
만약 (stateBlock.버전 !== preVer) {
반환 콜백('사용 중인 버전이 서버 버전과 일치하지 않습니다.');
} else {
stateBlock.버전++;
stateBlock.데이터 = 데이터;
}
var 설정 옵션 = {};
만약 (결과.값) {
setOptions.cas = 결과.cas;
}
db.set(stateDocName, stateDoc, 설정 옵션, 함수(err, 결과) {
만약 (err) {
반환 콜백(err);
}
콜백(null, stateBlock);
});
});
};
다음으로 포함할 메서드는 findByUserId입니다. 이 메서드를 사용하면 특정 사용자에 대한 모든 상태 블록을 반환하는 엔드포인트를 구축할 수 있습니다. 이는 주로 여러 요청을 미리 수행하지 않고 모든 상태 블록을 한 번에 가져올 수 있도록 클라이언트 측에서 최적화한 것입니다. 이 기능은 매우 간단합니다. 저장 함수와 동일한 문서 이름을 사용하여 클러스터에서 상태 문서를 로드하려고 시도하고, 문서가 있는 경우 이 블록 내의 상태 목록을 반환하고, 문서가 없는 경우 빈 목록을 사용자에게 반환합니다. 다른 모든 오류는 호출자에게 전달됩니다.
StateModel.findByUserId = 함수(uid, 콜백) {
var stateDocName = 'user-' + uid + '-state';
db.get(stateDocName, 함수(err, 결과) {
만약 (err) {
만약 (err.코드 === 카우치베이스.오류.keyNotFound) {
반환 콜백(null, {});
} else {
반환 콜백(err);
}
}
var stateDoc = 결과.값;
콜백(null, stateDoc.상태);
});
};
마지막으로 빌드해야 할 모델 함수는 사용자의 단일 상태 블록에 액세스하는 메서드입니다. 이 함수는 전체 목록을 반환하는 대신 이름별로 특정 상태 블록을 추가로 드릴다운한다는 점을 제외하면 findByUserId 함수와 거의 동일합니다.
StateModel.get = 함수(uid, 이름, 콜백) {
var stateDocName = 'user-' + uid + '-state';
db.get(stateDocName, 함수(err, 결과) {
만약 (err) {
반환 콜백(err);
}
var stateDoc = 결과.값;
만약 (!stateDoc.상태[이름]) {
반환 콜백('이 이름의 상태 블록이 존재하지 않습니다.');
}
콜백(null, stateDoc.상태[이름]);
});
};
게임 상태 - 요청 처리
이제 모델을 모두 마무리하고 사용할 준비가 되었으니 이제 3개의 엔드포인트에 대한 요청 핸들러를 구축해 보겠습니다! 한 사용자의 모든 상태를 요청하는 엔드포인트, 특정 상태 블록을 요청하는 엔드포인트, 마지막으로 특정 상태 블록을 업데이트하는 엔드포인트를 구축할 것입니다.
요청 핸들러를 빌드하기 전에 먼저 앞서 생성한 statemodel.js 파일에 대한 참조를 추가해야 합니다!
이제 저장 엔드포인트부터 시작하겠습니다. 이전 부분과 마찬가지로 요청 핸들러는 매우 간단하며 요청의 관련 부분을 모델에 전달하기만 하면 됩니다. 상태 블록 이름은 URI의 일부로, 버전 번호는 쿼리의 일부로, 마지막으로 실제 상태 블록 데이터는 요청 본문에 포함될 것으로 예상합니다.
앱으로 이동합니다.put('/주/:이름', authUser, 함수(req, res, 다음) {
stateModel.저장(req.uid, req.매개변수.이름, parseInt(req.쿼리.preVer, 10),
req.body, 함수(err, 상태) {
만약 (err) {
반환 다음(err);
}
res.보내기(상태);
});
});
다음으로 이전에 저장한 상태 블록을 검색하는 기능이 필요합니다.
앱으로 이동합니다.get('/주/:이름', authUser, 함수(req, res, 다음) {
stateModel.get(req.uid, req.매개변수.이름, 함수(err, 상태) {
만약 (err) {
반환 다음(err);
}
res.보내기(상태);
});
});
마지막으로, 특정 사용자에 대해 저장된 모든 상태 블록을 요청하는 엔드포인트입니다. 앞서 말했듯이, 이는 주로 모든 상태 블록을 검색해야 하는 게임 로딩 시퀀스를 최적화하기 위한 것입니다.
앱으로 이동합니다.get('/states', authUser, 함수(req, res, 다음) {
stateModel.findByUserId(req.uid, 함수(err, 상태) {
만약 (err) {
반환 다음(err);
}
res.보내기(상태);
});
});
피니토!
이제 모델을 구축하고 필요한 요청 핸들러를 구현했으니, 이전 파트에서와 같이 앱을 시작하고 게임 서버에 대한 몇 가지 요청을 수행하여 노력의 성과를 확인할 수 있을 것입니다!
> POST /state/test?preVer=0
헤더(권한): Bearer 0e9dd36c-5e2c-4f0e-9c2c-bffeea72d4f7
{
"이름": "We Rock!",
"level": "13"
}
< 200 미만 확인
{
"버전": 1,
"데이터": {
"이름": "We Rock!",
"level": "13"
}
}
> GET /state/test
헤더(권한): Bearer 0e9dd36c-5e2c-4f0e-9c2c-bffeea72d4f7
< 200 미만 확인
{
"버전": 1,
"데이터": {
"이름": "We Rock!",
"level": "13"
}
}
> GET /states
헤더(권한): Bearer 0e9dd36c-5e2c-4f0e-9c2c-bffeea72d4f7
< 200 미만 확인
{
"test": {
"버전": 1,
"데이터": {
"이름": "We Rock!",
"level": "13"
}
}
}
성공!
이 애플리케이션의 전체 소스는 여기에서 확인할 수 있습니다: https://github.com/brett19/node-gameapi
즐겨보세요! Brett
인증 메시지를 표시하고 15ms마다 커서 위치를 저장하는 데모 클라이언트를 자바스크립트로 작성하여 사용자 지정 onMouseStop 리스너를 사용했습니다: https://gist.github.com/rdev5/...
멋지네요!
세션 유지 기능을 가져오기 및 터치 대신 터치 기능으로 만든 특별한 이유가 있나요?
세션 모델에서 할 수 있습니다:
db.get(sessDocName, {expiry: 3600}, function(err, result) {
...
아니요?
안녕하세요 호세,
이것은 실제로 최적화할 수 있지만, 안타깝게도 세션을 구현하는 첫 번째 부분에서 터치를 완전히 놓쳤기 때문에 단순화를 위해 별도의 터치로 붙였습니다.
건배, 브렛
알겠습니다. 고마워요, 멋진 튜토리얼입니다. 많은 것을 배웠습니다.
[...] 금주의 블로그 게시물: Node.js를 사용한 게임 서버와 카우치베이스 - 3부 [...]