RPC(Remote Procedure Call)
- 서버에 데이터를 요청하여 응답받는 과정을 라이브러리에서 자동으로 처리
- RPC는 여러가지 방법을 사용하는데 그중에서 특히 JSON-RPC는 JSON 포맷으로 데이터를 주고 받을 수 있어 자바스크립트를 사용하는 노드 프로그램에서 훨씬 자연스럽게 사용할 수 있다.
- 데이터를 주고받을 때 사용하는 데이터 포맷은 XML이나 바이너리 포맷등이 있습닏나. JSON 포맷을 사용하는 경우에 JSON-RPC라 부릅니다. 표준으로 만든 JSON-RPC 프로토콜에대해 좀 더 자세히 알고싶으면
공식사이트 : https://www.jsonrpc.org/
Wiki 사이트 : https://en.wikipedia.org/wiki/JSON-RPC, https://ko.wikipedia.org/wiki/JSON-RPC
- JSON-RPC에는 어떤 OS와 언어를 사용하든 서로 데이터를 주고받을 수 있도록 다양한 언어로 작성된 라이브러리가 있습니다. 노드와 익스프레스 환경에서도 JSON-RPC를 사용할 수 있는데 여기에서는 jsyson 모듈을 사용합니다.
npm install jayson --save
- jsyson모듈을 사용하려면 우선 app.js 파일안에서 require() 메소드를 호출해서 모듈을 불러들어야 합니다. 클라이언트의 요청을 라우팅 하는 라우터 미들웨어 다음에 jayson 모듈을 사용하면 클라이언트가 특정 패스로 요청하는 경우에만 JSON-RPC로 처리되도록 만들 수 있습니다. 즉, 웹서버로 요청하는 패스 중 한가지 패스를 JSON-RPC로 실행하도록 만들 수 있습니다. JSON-RPC를 사용하기 위해 등록한 각 함수들은 보통 핸들러(Handler)라고 부릅니다.
- app.js
......
// JsonRpc 핸들러 로딩을 위한 파일 불러오기
var handler_loader = require('./handlers/handler_loader');
......
// JsonRpc 사용을 위한 jayson 모듈 불러오기
var jayson = require('jayson');
......
//===== jayson 미들웨어 사용 =====//
//JSON-RPC 핸들러 정보를 읽어들여 핸들러 설정
var jsonrpc_api_path = config.jsonrpc_api_path || '/api';
handler_loader.init(jayson, app, jsonrpc_api_path);
console.log('JSON-RPC를 [' + jsonrpc_api_path + '] 패스에서 사용하도록 설정함.');
......
- handler_loader.js
/*
* 핸들러 모듈을 로딩하여 설정
*
* 핸들러 모듈 파일에 대한 정보는 handler_info.js에 기록함
*/
var handler_loader = {};
var handler_info = require('./handler_info');
var utils = require('jayson/lib/utils');
handler_loader.init = function(jayson, app, api_path) {
console.log('handler_loader.init 호출됨.');
return initHandlers(jayson, app, api_path);
}
// handler_info에 정의된 핸들러 정보 처리
function initHandlers(jayson, app, api_path) {
var handlers = {};
var infoLen = handler_info.length;
console.log('설정에 정의된 핸들러의 수 : %d', infoLen);
for (var i = 0; i < infoLen; i++) {
var curItem = handler_info[i];
// 모듈 파일에서 모듈 불러옴
var curHandler = require(curItem.file);
console.log('%s 파일에서 모듈정보를 읽어옴.', curItem.file);
// 핸들러 함수 등록
//handlers[curItem.method] = curHandler;
handlers[curItem.method] = new jayson.Method({
handler: curHandler,
collect: true,
params: Array
});
console.log('메소드 [%s]이(가) 핸들러로 추가됨.', curItem.method);
}
// jayson 서버 객체 생성
var jaysonServer = jayson.server(handlers);
// app의 패스로 라우팅
console.log('패스 [' + api_path + ']에서 RPC 호출을 라우팅하도록 설정함.');
app.post(api_path, function(req, res, next) {
console.log('패스 [' + api_path + ']에서 JSON-RPC 호출됨.');
var options = {};
// Content-Type이 application/json이 아니면, 415 unsupported media type error
var contentType = req.headers['content-type'] || '';
if(!RegExp('application/json', 'i').test(contentType)) {
console.log('application/json 타입이 아님.');
return error(415);
};
// body 부분의 데이터가 없는 경우, 500 server error
if(!req.body || typeof(req.body) !== 'object') {
console.log('요청 body가 비정상임.');
return error(400, 'Request body must be parsed');
}
// RPC 함수 호출
console.log('RPC 함수를 호출합니다.');
jaysonServer.call(req.body, function(error, success) {
var response = error || success;
console.log(response);
// 결과 데이터를 JSON으로 만들어 응답
utils.JSON.stringify(response, options, function(err, body) {
if(err) return err;
if(body) {
var headers = {
"Content-Length": Buffer.byteLength(body, 'utf-8'),
"Content-Type": "application/json"
};
res.writeHead(200, headers);
res.write(body);
} else {
res.writeHead(204);
}
res.end();
});
});
// 에러 응답
function error(code, headers) {
res.writeHead(code, headers || {});
res.end();
}
});
return handlers;
}
module.exports = handler_loader;
- handler_info.js
/*
* 핸들러 모듈 파일에 대한 정보
*
*/
console.log('handler_info 파일 로딩됨.');
var handler_info = [
{file:'./echo', method:'echo'} // echo
,{file:'./echo_error', method:'echo_error'} // echo_error
,{file:'./add', method:'add'} // 더하기
,{file:'./multiply', method:'multiply'} // 곱하기
,{file:'./listuser', method:'listuser'} // 사용자 리스트
,{file:'./echo_encrypted', method:'echo_encrypted'} // 암호화된 echo
];
module.exports = handler_info;
- config.js
module.exports = {
server_port: 7001,
db_url: 'mongodb://localhost:27017/local',
db_schemas: [
{file:'./user_schema', collection:'users6', schemaName:'UserSchema', modelName:'UserModel'}
],
route_info: [
],
jsonrpc_api_path: '/api'
}
- app.js 메인 파일에서 handler_loader.js 파일의 init() 메소드를 호출하면 핸들러 정보를 읽어 들여 경로를 설정합니다. config.js 파일에 등록되어 있는 라우팅 함수 중에서 하나의 패스가 JSON-RPC로 처리되도록 설정되어 있습니다. 이 정보는 jsonrpc_api_path 속성에 들어 있으며, 여기에서 지정한 라우팅 패스로 들어오는 요청은 JSON-RPC로 처리됩니다. 기본값으로 /api 패스가 들어 있으나 필요에 따라 다른 패스로 바꿀 수도 있습니다.
- echo.js
// echo 함수
var echo = function(params, callback) {
console.log('JSON-RPC echo 호출됨.');
console.dir(params);
callback(null, params);
};
module.exports = echo;
- function(params, callback)에서 첫번째 파라미터는 클라이언트로 부터 전달받은것이며 배열 객체로 되어있다. 두뻔째 파라미터는 함수이며 클라이언트로 응답을 보낼 때 사용된다.
- callback(null, params)에서 첫번째 파라미터는 오류 전달을 위해 사용하고 두번째 파라미터는 정상적인 데이터를 전달할 때 사용
- 그러므로 echo.js는 클라이언트에서오는 데이터를 다시 params 객체를 그대로 넣어 보낸다.
- echo.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Echo 테스트</title>
<script src="./jquery-3.1.1.min.js"></script>
<script src="jquery.jsonrpc.js"></script>
<script>
$(function() {
$.jsonRPC.setup({
endPoint : 'http://localhost:7001/api',
namespace : ''
});
$("#requestButton").click(function() {
var message = $("#messageInput").val();
var method = 'echo';
$.jsonRPC.request(method, {
id: 1001,
params: [message],
success: function(data) {
println('정상 응답을 받았습니다.');
console.dir(data);
println(data.result);
},
error: function(data) {
println('에러 응답을 받았습니다.');
console.dir(data);
println(data.error.message);
}
});
println('[' + method + '] method로 요청을 보냈습니다.');
});
});
function println(data) {
$("#results").append('<p>' + data + '</p>');
}
</script>
</head>
<body>
<h3>JSON-RPC Echo 테스트</h3>
<br>
<textarea name="messageInput" id="messageInput"></textarea>
<br>
<input type="button" name="requestButton" id="requestButton" value="요청하기" />
<br>
<p>결과<p>
<div id="results"></div>
</body>
</html>
- jquery.jsonrpc.js 다운로드 : https://github.com/datagraph/jquery-jsonrpchttps://github.com/datagraph/jquery-jsonrpc
- jquery.jsonrpc.js 는 클라이언트에서 JSON-RPC 통신하기 위한 자바스크립트 파일
- $.jsonRPC.setup() 메소드를 호출하면 기본 설정
- .setup 속성에는 접속 URL 넣어주고 namespace는 비워둡니다.
- .request() 메소드는 서버에 요청할 때 사용, 첫번째 파라미터는 method 변수를 전달하고, 두번째 파라미터는 전달할 데이터와 콜백 함수가 들어있는 객체를 전달
- 서버에 요청할 때 전달되는 객체의 속성
속성이름 |
설명 |
id |
요청 ID를 지정할수 있다. 이 요청 ID는 서버로부터 받는 응답을 구별하는데 사용 |
params |
서버로 보낼 데이터를 넣는 배열 객체 |
success |
응답을 성공적으로 받았을 때 호출되는 콜백 함수 |
error |
오류 응답을 받았을 때 호출되는 콜백 함수 |
- success 성공 응답을 받았을 때 전달되는 객체의 속성
속성이름 |
설명 |
id |
요청할 때 전달한 id 값이 들어 있다 |
jsonrpc |
JSON-RPC 스펙의 버전을 표기. 여기에서는 2.0 |
result |
응답 데이터가 배열 객체로 들어 있다. |
- HTTP 상태코드
1XX 정보 100은 서버가 요청의 일부를 받았으며, 나머지 요청을 더 기다리고 있다는 것을 나타냅니다. 101은 http에서 https같이 프로토콜 전환이 일어났을 때 전환이 승인되었음을 알려줍니다. 저는 보통 웹소켓을 할 때 101을 본 것 같습니다. 2XX 성공 200은 여러분이 가장 좋아하는 숫자가 될 겁니다. 성공을 의미하거든요. 대부분은 200이고 몇 가지 다른 게 있습니다. 201은 새로운 컨텐츠 만들기에 성공했을 때 사용합니다. 새로운 포스트를 썼다든가, 새로운 댓글을 썼을 때 보내주면 됩니다. POST 메소드에 대한 응답으로 잘 어울립니다. 204는 요청이 성공은 했지만 응답할 콘텐츠가 없을 경우를 뜻합니다. 206은 스트리밍의 경우와 같이 요청에 대한 응답으로 일부만 먼저 전송한 경우 보내줍니다. 3XX 리다이렉션 300번대는 페이지를 이동 시킬 때 사용합니다. 특히 301 페이지는 영구적으로 주소가 바뀌었을 경우, 301 코드와 함께 새로운 주소로 이동시킵니다. 새로운 주소는 캐싱되기 때문에 속도가 빨라집니다. 반면, 302는 임시적으로 주소가 바뀌었을 경우 사용합니다. 또는 로그인 후 메인페이지로 이동한다든가 할 때에도 사용됩니다. 대부분의 리다이렉트는 302 코드를 사용합니다. 영구적으로 주소를 바꾸는 경우는 드물거든요. 304는 이전에 방문했을 때의 요청 결과와 다르지 않을 경우 표시됩니다. 즉 캐시된 페이지를 그대로 사용합니다. 307은 임시로 페이지를 리다이렉트하는 겁니다. 4XX 클라이언트 오류 400번대 부터는 에러입니다. 400은 서버가 요청을 이해하지 못 할 경우 발생합니다. 올바른 요청을 보냈는지 검사해야 합니다. 401은 로그인을 하지 않아 페이지를 열 권한이 없는 겁니다. 403은 금지된 페이지입니다. 로그인을 하든 안하든 상관없이 접근할 수 없는 페이지는 403을 전송합니다. 관리자 페이지가 이겁니다. 404는 찾을 수 없는 페이지입니다. 주소를 잘못 입력했거나 하면 404를 요청합니다. 403 대신에 404를 전송하는 경우도 많습니다. 왜냐하면, 403을 전송하면 금지되었긴 하지만 어쨌든 어떠한 페이지는 있는 것이기 때문에 해커들의 공격을 받을 수 있습니다. 이를 방지하고자 아예 404를 보내 없는 페이지처럼 위장하기도 합니다. 408은 요청 시간 초과입니다. 409는 서버가 요청을 처리하는 과정에서 충돌이 발생한 경우입니다. (회원가입을 했는데 이미 사용하고 있는 아이디인 경우) 410은 영구적으로 사용할 수 없는 페이지입니다. 451은 새로 생겼습니다. warning.or.kr처럼 법적으로 막힌 페이지를 표시할 때 451 코드를 전송합니다. 5XX 서버 오류 500번대는 서버 오류입니다. 요청은 제대로 전송되었지만 서버가 처리하지 못하는 경우입니다. 500은 내부 서버 에러(Internal Server Error)가 날 때 전송됩니다. 이것은 서버 상의 에러이기 때문에 해당 서버 관리자가 반드시 살펴봐야 합니다. 501은 서버에 아직 해당 요청을 처리하는 기능을 만들지 않았다는 뜻입니다. 502는 서버로 가는 요청이 중간에서 유실된 경우입니다. 503은 서버가 터졌거나(접속이 폭주 또는 Ddos 공격) 유지보수중일 때 전송합니다. 하지만 유지보수중일 때는 503을 보내기보다는 유지보수중이라는 것을 알려주는 페이지를 전송해주는 것이 좋습니다. 504는 서버 게이트웨이에 문제가 생겨 시간 초과가 된 경우입니다. 505는 HTTP 버전이 달라 요청을 처리할 수 없음을 뜻합니다. 이렇게 많은 코드들이 있습니다. 그런데 문제는 대부분의 경우 서버가 코드를 알아서 보내주지 않습니다. 여러분이 응답을 보낼 때 코드를 지정한 후에 보내는 겁니다. express 프레임워크는 5.0버전부터 status를 보내는 게 필수화되었습니다. ex) res.status(200).json({ completed: true }); 출처 : https://www.zerocho.com/category/NodeJS/post/579b4ead062e76a002648af7 |
- 데이터베이스에서 사용자 리스트 조회하기
- listuser.js
/*
* 사용자 기능 RPC 함수
*/
// 사용자 리스트 조회 함수
var listuser = function(params, callback) {
console.log('JSON-RPC listuser 호출됨.');
console.dir(params);
// 데이터베이스 객체 참조
// databases.js에서의 var database = {}; -> global.database = {}; 로 변경
var database = global.database;
console.dir(database);
if (database) {
console.log('database 객체 참조됨.');
} else {
console.log('database 객체 불가함.');
callback({
code: 410,
message: 'database 객체 불가함.'
}, null);
return;
}
if (database.db) {
// 1. 모든 사용자 검색
database.UserModel.findAll(function(err, results) {
//console.dir(results);
if (err) {
callback({
code: 410,
message: err.message
}, null);
return;
}
if (results) {
console.log('결과물 문서 데이터의 갯수 : %d', results.length);
var output = [];
for (var i = 0; i < results.length; i++) {
var curId = results[i]._doc.id;
var curName = results[i]._doc.name;
output.push({id:curId, name:curName});
}
console.dir(output);
callback(null, output);
} else {
callback({
code: 410,
message: '사용자 리스트 조회 실패'
}, null);
}
});
} else {
callback({
code: 410,
message: '데이터베이스 연결 실패'
}, null);
}
};
module.exports = listuser;
- listuser.html
// 요청을 위한 기본 함수
function sendRequest(method, id, params) {
$.jsonRPC.request(method, {
id: id,
params: params,
success: function(data) {
println('정상 응답을 받았습니다.');
console.dir(data);
processResponse(data);
},
error: function(data) {
println('에러 응답을 받았습니다.');
console.dir(data);
processError(data);
}
});
}
// 성공 응답을 받은 경우 호출되는 함수
function processResponse(data) {
if (Array.isArray(data.result)) {
println('사용자 수 : ' + data.result.length);
data.result.forEach(function(item, index) {
println('#' + index + ' : ' + item.id + ', ' + item.name);
});
} else {
println('결과 데이터가 배열 타입이 아닙니다.');
}
}
// 에러 응답을 받은 경우 호출되는 함수
function processError(data) {
println(data.error.code + ', ' + data.error.message);
}
function println(data) {
$("#results").append('<p>' + data + '</p>');
}
- 데이터 부분 암호화 : 노드에서 데이터를 암호화하는 crypto 모듈을 기본으로 제공하지만 웹 브라우저에서 웹 브라우저에서 암호화한 데이터와 호환되지 않는 경우도 있습니다. 그러므로 외장 모듈(crypto-js)을 사용해 암호화 작업을 실행
- 모듈 설치
npm install crypto-js --save
- echo_encrypted.html
<script src="./jquery-3.1.1.min.js"></script>
<script src="jquery.jsonrpc.js"></script>
<script src="cryptojs/aes.js"></script>
- crypto-js 공식사이트 : https://code.google.com/archive/p/crypto-js/
- crypto-js 다운로드 : https://code.google.com/archive/p/crypto-js/downloads
- echo_encrypted.html
$("#requestButton").click(function() {
var message = $("#messageInput").val();
// 암호화 테스트
var secret = 'my secret';
var encrypted = '' + CryptoJS.AES.encrypt(message, secret);
console.log(encrypted);
// 복호화 테스트
var decrypted = CryptoJS.AES.decrypt(encrypted, secret).toString(CryptoJS.enc.Utf8);
console.log(decrypted);
var method = 'echo_encrypted';
$.jsonRPC.request(method, {
id: 1001,
params: [encrypted],
success: function(data) {
println('정상 응답을 받았습니다.');
console.dir(data);
var secret = 'my secret';
var encrypted = data.result[0];
var decrypted = CryptoJS.AES.decrypt(encrypted, secret).toString(CryptoJS.enc.Utf8);
console.log(decrypted);
println(decrypted);
},
error: function(data) {
println('에러 응답을 받았습니다.');
console.dir(data);
println(data.error.message);
}
});
println('[' + method + '] method로 요청을 보냈습니다.');
});
- app.js (서버쪽.js)
/*
* echo RPC 함수
*/
var crypto = require('crypto-js');
// echo 함수
var echo = function(params, callback) {
console.log('JSON-RPC echo_encrypted 호출됨.');
console.dir(params);
try {
// 복호화 테스트
var encrypted = params[0];
var secret = 'my secret';
var decrypted = crypto.AES.decrypt(encrypted, secret).toString(crypto.enc.Utf8);
console.log('복호화된 데이터 : ' + decrypted);
// 암호화 테스트
var encrypted = '' + crypto.AES.encrypt(decrypted + ' -> 서버에서 보냄.', secret);
console.log(encrypted);
params[0] = encrypted;
} catch(err) {
console.dir(err);
console.log(err.stack);
}
callback(null, params);
};
module.exports = echo;
- AES 표준 알고리즘으로 암호화하거나 복호화할 수 있습니다.
- encrypt() 메소드를 사용하여 암호화시키며, decrypt() 메소드를 사용하여 복호화 합니다.
- 암호화할때 노출되지 않는 키 값을 사용하기 위해 secret 변수를 사용
'WEB > Node JS' 카테고리의 다른 글
Nodejs기초 - 15일차 정리(채팅 1대1) (0) | 2019.02.04 |
---|---|
Nodejs기초 - 14일차 정리(페이스북 활용) (2) | 2019.01.28 |
Nodejs기초 - 13일차 정리(패스포트 모듈 사용) (0) | 2019.01.27 |
Nodejs기초 - 12일차 정리(pug템플릿 사용 및 상속, 패스포트 정의) (0) | 2019.01.26 |
Nodejs기초 - 11일차 정리(Semantic UI 활용, MVC패턴, 응답 페이지 모듈화[ejs]) (0) | 2019.01.26 |