Jsonrpc сервер

Материал из support.qbpro.ru

Код модуля сервера

 
 var sys = require('sys');
 var http = require('http');

//===----------------------------------------------------------------------===//
// Server Client
//===----------------------------------------------------------------------===//


var Client = function(port, host, user, password) {
  this.port = port;
  this.host = host;
  this.user = user;
  this.password = password;
 
  
  this.call = function(method, params, callback, errback, path) {
    var client = http.createClient(port, host);
    
    // First we encode the request into JSON
    var requestJSON = JSON.stringify({
      'jsonrpc': '2.0',
      'id': '' + (new Date()).getTime(),
      'method': method,
      'params': params
    });
    
    var headers = {};

    if (user && password) {
      var buff = new Buffer(this.user + ":" + this.password)
                           .toString('base64');
      var auth = 'Basic ' + buff;
      headers['Authorization'] = auth;
    }

    // Then we build some basic headers.
    headers['Host'] = host;
    headers['Content-Length'] = requestJSON.length;

    // Now we'll make a request to the server
    var request = client.request('POST', path || '/', headers);
    request.write(requestJSON);
    request.on('response', function(response) {
      // We need to buffer the response chunks in a nonblocking way.
      var buffer = '';
      response.on('data', function(chunk) {
        buffer = buffer + chunk;
      });
      // When all the responses are finished, we decode the JSON and
      // depending on whether it's got a result or an error, we call
      // emitSuccess or emitError on the promise.
      response.on('end', function() {
        var decoded = JSON.parse(buffer); // TODO: Check for invalid response from server
        if(decoded.hasOwnProperty('result')) {
          if (callback) 
            callback(null, decoded.result);
          
        } else {
          // Call error handler if it is set, otherwise call callback with error parameters
          if (errback) {
          	errback(decoded.error);
          } else if(callback) {
          	callback(decoded.error, null);
          }
       }
      });
    });
  };
}

//===----------------------------------------------------------------------===//
// Server
//===----------------------------------------------------------------------===//
function Server() {
  var self = this;
  this.functions = {};
  this.scopes = {};
  this.defaultScope = this;
  this.server = http.createServer(function(req, res) {
    Server.trace('<--', 'accepted request');
    if(req.method === 'POST') {
      self.handlePOST(req, res);
    }
    else {
      Server.handleNonPOST(req, res);
    }
  });
}


//===----------------------------------------------------------------------===//
// exposeModule
//===----------------------------------------------------------------------===//
Server.prototype.exposeModule = function(mod, object, scope) {
  var funcs = [];
  for(var funcName in object) {
    var funcObj = object[funcName];
    if(typeof(funcObj) == 'function') {
      this.functions[mod + '.' + funcName] = funcObj;
      funcs.push(funcName);

      if (scope) {
        this.scopes[mod + '.' + funcName] = scope;
      }
    }
  }
  Server.trace('***', 'exposing module: ' + mod + ' [funs: ' + funcs.join(', ') 
                + ']');
  return object;
}


//===----------------------------------------------------------------------===//
// expose
//===----------------------------------------------------------------------===//
Server.prototype.expose = function(name, func, scope) {
  Server.trace('***', 'exposing: ' + name);
  this.functions[name] = func;

  if (scope) {
    this.scopes[name] = scope;
  }
}


//===----------------------------------------------------------------------===//
// trace
//===----------------------------------------------------------------------===//
Server.trace = function(direction, message) {
  sys.puts('   ' + direction + '   ' + message);
}


//===----------------------------------------------------------------------===//
// listen
//===----------------------------------------------------------------------===//
Server.prototype.listen = function(port, host) { 
  this.server.listen(port, host);
  Server.trace('***', 'Server listening on http://' + (host || '127.0.0.1') + 
                ':' + port + '/'); 
}


//===----------------------------------------------------------------------===//
// handlePOST
//===----------------------------------------------------------------------===//
Server.prototype.handlePOST = function(req, res) {
  var buffer = '';
  var self = this;
  var handle = function (buf) {
    
    var decoded = "";
    try {
    	decoded = JSON.parse(buf);
    } catch (e) {
    	return Server.handleError(-32700, "Parse Error", null, req, res);
    }
    

    // Check for the required fields, and if they aren't there, then
    // dispatch to the handleError function.    
    if(!(decoded.method && decoded.params && decoded.id)) {
      
      if (typeof(id) == "undefined") {
   		var id = null;
   	  } 
   	  
      return Server.handleError(-32600, "Invalid Request", decoded.id, req, res);
    }

    if(!self.functions.hasOwnProperty(decoded.method)) {
      return Server.handleError(-32601, "Method not found", decoded.id, req, res);
    }

    // Build our success handler
    var onSuccess = function(funcResp) {
      Server.trace('-->', 'response (id ' + decoded.id + '): ' + 
                    JSON.stringify(funcResp));
	
	  var encoded = JSON.stringify({
        'jsonrpc': '2.0',
        'result': funcResp,
        'error': null,
        'id': decoded.id
      });
      
      res.writeHead(200, {'Content-Type': 'application/json',
                          'Content-Length': encoded.length});
      res.write(encoded);
      res.end();
    };

    Server.trace('<--', 'request (id ' + decoded.id + '): ' + 
                  decoded.method + '(' + decoded.params.join(', ') + ')');

    // Try to call the method, but intercept errors and call our
    // onFailure handler.
    var method = self.functions[decoded.method];
    var callback = function(result, errormessage) {
      if (errormessage) {
        Server.handleError(-32602, errormessage, decoded.id, req, res);
      } else {
        onSuccess(result);
      }
    };
    var scope = self.scopes[decoded.method] || self.defaultScope;

    // Other various information we want to pass in for the handler to be
    // able to access.
    var opt = {
      req: req,
      server: self
    };

    try {
      method.call(scope, decoded.params, opt, callback);
    } catch (err) {
      return Server.handleError(-32603, err, decoded.id, req, res);
    }

  } // function handle(buf)

  req.addListener('data', function(chunk) {
    buffer = buffer + chunk;
  });

  req.addListener('end', function() {
    handle(buffer);
  });
}

//===----------------------------------------------------------------------===//
// handleError
//===----------------------------------------------------------------------===//
Server.handleError = function(code, message, id, req, res) {
  
  var encoded = JSON.stringify({
  	'jsonrpc': '2.0',
    'error': {
    	'code':code,
    	'message':message
    },
    'id': id
  });
  
  res.writeHead(400, {'Content-Type': 'text/plain',
                      'Content-Length': encoded.length,
                      'Allow': 'POST'});
  
  res.write(encoded);
  res.end();
  
  Server.trace('-->', 'Failure: ' + code + ': ' + message);
}


//===----------------------------------------------------------------------===//
// handleNonPOST
//===----------------------------------------------------------------------===//
Server.handleNonPOST = function(req, res) {
  
  var encoded = JSON.stringify({
  	'jsonrpc': '2.0',
    'error': {
    	'code':-32600,
    	'message':"Only POST is allowed."
    },
    'id': null
  });
  
  res.writeHead(405, {'Content-Type': 'text/plain',
                      'Content-Length': encoded.length,
                      'Allow': 'POST'});
  res.write(encoded);
  res.end();
}


module.exports.Server = Server;
module.exports.Client = Client;


Пример использования в качестве сервера

var rpc = require('../src/jsonrpc');

var server = new rpc.Server();

/* Create two simple functions */ function add(args, opts, callback) {

 callback(args[0]+args[1]);

}

function multiply(args, opts, callback) {

 callback(args[0]*args[1]);

}

/* Expose those methods */ server.expose('add', add); server.expose('multiply', multiply);

/* We can expose entire modules easily */ var math = {

 power: function(args, opts, callback) {
   callback(Math.pow(args[0], args[1]));
 },
 sqrt: function(args, opts, callback) {
   callback(Math.sqrt(args[0]));
 }

} server.exposeModule('math', math);

/* Listen on port 8088 */ server.listen(8088, 'localhost');

/* By using a callback, we can delay our response indefinitely, leaving the

request hanging until the callback emits success. */

var delayed = {

 echo: function(args, opts, callback) {
   var data = args[0];
   var delay = args[1];
   setTimeout(function() {
     callback(data);
   }, delay);
 },
 add: function(args, opts, callback) {
   var first = args[0];
   var second = args[1];
   var delay = args[2];
   setTimeout(function() {
     callback(first + second);
   }, delay);
 }

}

server.exposeModule('delayed', delayed);


// We can also add error parameters to our callback // if something went wrong function wrong(arg, opts, callback) { callback(null, "This will ever go wrong.") } server.expose('wrong', wrong);

Пример использования в качестве клиента

var sys = require('sys'); var rpc = require('../src/jsonrpc');

var client = new rpc.Client(8088, 'localhost');

client.call('add', [1, 2], function (err, result) {

 sys.puts('  1 + 2 = ' + result);

});

client.call('multiply', [199, 2], function (err, result) {

 sys.puts('199 * 2 = ' + result);

});

// Accessing modules is as simple as dot-prefixing. client.call('math.power', [3, 3], function (err, result) {

 sys.puts('  3 ^ 3 = ' + result);

});

// We can handle errors the same way as anywhere else in Node client.call('wrong', [1, 1], function (err, result) {

 if (err) {
   sys.puts('RPC Error: '+ sys.inspect(err));
   return;
 }
 sys.puts(result);

});

// If you want to seperate the errors from your callback, // then define an extra error callback client.call('wrong', [1, 1], function (err, result) {

 sys.puts(result);

}, function(err){ sys.puts('RPC Error: ' + sys.inspect(err)); });

/* These calls should each take 1.5 seconds to complete. */ client.call('delayed.add', [1, 1, 1500], function (err, result) {

 sys.puts(result);

});

client.call('delayed.echo', ['Echo.', 1500], function (err, result) {

 sys.puts(result);

});

Анализ модуля

После анализа работы скрипта jsonrpc.js https://github.com/Philipp15b/node-jsonrpc2 было выявлено следущее:

1. Приложение по сути является http сервером, который анализирует входящие сообщения на предмет их соответствия стандарту jsonrpc 2.0 http://www.jsonrpc.org/specification и возвращает клиенту гарантированный jsonrpc ответ, в виде результатов выполненного метода. Сервер по протоколу http принимает запрос, проверяет запрос на валидность JSON, проверяет наличие вызываемого метода, выполняет этот метод с переданнми параметрами. В случае ошибок возвращает признак ошибки по стандарту jsonrpc 2.0

2. Приложение является монолитным модулем и не предоставляет возможности унификации кода для использования его как на сервере, так и на стороне браузера

3. Приложение подразумевает первоначальную загрузку всех методов (функций) в память.

4. Нет возможности использовать различные этапы проверки jsonrpc, что заставляет выполнять весь код целиком

5. Отсутствует проверка jsonrpc на стороне клиента.

Варианты решения:

Вариант 1.

1. Слелать модуль с возможностью проверки на соответствие jsonrpc на любом этапе. Это приводит к необходимости проверки jsonrpc ответа на возврат ошибки при каждом вызове, но позволяет не загружать весь код.

2. Позволяет вынести проверку наличия методов в отдельный модуль.

3. Позволяет выполнять методы в отдельном модуле.

4. Позволяет подключать модуль к любым серверам и слелать стандарт jsonrpc 2.0 стандартным форматом общения между различными компонентами системы в независимости от протокола обмена данными.

5. Позволит выполнять трассировку отдельных модулей

Недостатки: слишком много преобразований объектов JSON в строку и обратно, что занимает такты, слишком много приходится проверять наличие ошибки в каждом из ответов.

Вариант 2.

1. Максимально оставить все как есть.

2. Добавить в exports методы для наполнения, контроля и удаления набора вызываемых методов.

3. Убрать http сервер.

4. Перевести все этапы проверки на callback функции (уменьшает время работы на одном цикле event loop, увеличивает количество циклов event loop, это повышает отзывчивость модуля)

5. scope (переменная задающая область видимости при выполнении метода) привязать к каждому пользователю. Это увеличит расход памяти, но и сразу решит проблему с правами пользователей.

Анализ кода

Объявление внешних зависимостей

var sys = require('sys');
var http = require('http');

Функция Client

var Client = function(port, host, user, password) {
  
//присваиваем параметры запуска 
  this.port = port; 
  this.host = host;
  this.user = user;
  this.password = password;
  
//this.call - внутренний метод, аналог стандартной функция call
//Метод call может применяться для вызова функции в контексте нужного объекта
//подробнее http://javascript.ru/Function/call 
//method
//params
//callback - функция обработки результата (ответа сервера) 
//errback - функция обработки ошибки, если отсутствует, то ошибка передается на обработку в callback
//path - url запроса к серверу, если пустой то по умолчанию запрос на "/"

  this.call = function(method, params, callback, errback, path) {

//запуск клиента с параметрами, клиент создается заново каждый раз при вызове функции (это плохо)

    var client = http.createClient(port, host);
    
// First we encode the request into JSON 
//Во-первых мы формируем запрос в формате JSON 

    var requestJSON = JSON.stringify({
      'jsonrpc': '2.0',
      'id': '' + (new Date()).getTime(),
      'method': method,
      'params': params
    });
    
//Объявляем объект, для хранения заголовков http запроса

    var headers = {};

//Если заданы user и password в  вызове функции Client, то формируем заголовок Authorization

    if (user && password) {
      var buff = new Buffer(this.user + ":" + this.password)
                           .toString('base64');
      var auth = 'Basic ' + buff;
      headers['Authorization'] = auth;
    }

// Then we build some basic headers.
//Далее формируются некоторые простые заголовки ('Host', 'Content-Length')

    headers['Host'] = host;
    headers['Content-Length'] = requestJSON.length;

// Now we'll make a request to the server
// Формируем запрос к серверу

    var request = client.request('POST', path || '/', headers);
    
//Отправляем запрос
    request.write(requestJSON);

//Ожидаем событие ответа

    request.on('response', function(response) {

// We need to buffer the response chunks in a nonblocking way.
//Объявляем строковый буфер, для сбора частей ответа в неблокирующем стиле

      var buffer = '';

//по событию 'data' - добавляем часть ответа в буфер

      response.on('data', function(chunk) {
        buffer = buffer + chunk;
      });

// When all the responses are finished, we decode the JSON and
// depending on whether it's got a result or an error, we call
// emitSuccess or emitError on the promise.
//Когда все части ответа собраны, происходит событие 'end'
//мы декодируем ответ из формата JSON и в зависимости от того что в ответе 
//содержится, result или error вызывается обработка события emitSuccess или 
//emitError
      
      response.on('end', function() {
        var decoded = JSON.parse(buffer); // TODO: Check for invalid response from server

//Если есть свойство result

        if(decoded.hasOwnProperty('result')) {

//Если задана функция callback, вызываем её первым параметром передаем null - признак отсутствия ошибки
//вторым - результаты из декодированного JSON ответа 

          if (callback) 
            callback(null, decoded.result);

//Если нет свойства result - то значит ошибка
          
        } else {

// Call error handler if it is set, otherwise call callback with error parameters
//Вызываем заданный обработчик ошибки, если он задан. Иначе переедаем ошибку на обработку 
//в callback первым параметром с аргументами функции равными null
          if (errback) {
          	errback(decoded.error);
          } else if(callback) {
          	callback(decoded.error, null);
          }
       }
      });
    });
  };
}

Функция (объект) Server

function Server() {
  var self = this;     //непонятная манипуляция с областью видимости (контекстом)
  this.functions = {}; //объект (именованный массив) для текста вызываемых методов
  this.scopes = {};    //объект (именованный массив) для областей видимости (контекстов вызываемых методов)
  this.defaultScope = this; // область видимости (контекстов вызываемых методов) по умолчанию, если не задана специально
  this.server = http.createServer(function(req, res) { //создаем http сервер, в качестве параметра функция обработки  
                                                       //запроса и ответа
    Server.trace('<--', 'accepted request');           //трассировка принятия запроса
    if(req.method === 'POST') {                        //если запрос пришел по методу POST, то обрабатываем функцией  handlePOST
      self.handlePOST(req, res);
    }
    else {                                             //если не POST, то обрабатываем функцией handleNonPOST 
      Server.handleNonPOST(req, res);
    }
  });
}

Расширение функции Server методом exposeModule

Данный метод вызывается только при необходимости (в частности при инициализации), его производительность не критична для приложения в целом.

//Исходные методы, которые в последствии будут вызываться, можно передавать целыми модулями (в формате JSON)
//Например:
// var math = {
//   power: function(args, opts, callback) {
//     callback(Math.pow(args[0], args[1]));
//   },
//   sqrt: function(args, opts, callback) {
//     callback(Math.sqrt(args[0]));
//   }
//  }
//
//server.exposeModule('math', math);

//Данный метод добавляет в объект functions свойство из параметра mod (имя модуля), а в mod свойство из параметра
// funcName (имя фукнкции), а значение funcName присваивает из параметра  funcObj (тело функции)
// т.е. получается functions[mod.funcName] = funcObj;

Server.prototype.exposeModule = function(mod, object, scope) {
  var funcs = [];                         //массив, для наполнения названиями обработанных функций из переданного модуля.
                                          //после разбора выводится в трассировке работы функции

  for(var funcName in object) {           //перебираем все funcName(в примере это power и sqrt) в object (в примере это math)

    var funcObj = object[funcName];       //переменной funcObj присваиваем значение object[funcName]
                                          //например для object[power] это function(args, opts, callback) {
                                          //     callback(Math.pow(args[0], args[1]));}

    if(typeof(funcObj) == 'function') {   //если funcObj действительно является функцией
      this.functions[mod + '.' + funcName] = funcObj;   //добавляем funcObj в объект functions под соответствующим именем
      funcs.push(funcName);               //добавляем в конец массива funcs имя обработанной функции


      if (scope) {                        //если задан контекст, то его добавляем в объект scopes под соответствующим именем
        this.scopes[mod + '.' + funcName] = scope;
      }
    }
  }
  Server.trace('***', 'exposing module: ' + mod + ' [funs: ' + funcs.join(', ') //выводим трассировку
                + ']');
  return object;                          //возвращаем объект  (не понятно зачем)
}

Расширение функции Server методом expose

Данный метод вызывается только при необходимости (в частности при инициализации), его производительность не критична для приложения в целом.

//Данный метод добавляет в объект functions метод под именем name, т.е. functions[name] = func
//если указан контекст. то под тем же именем в объект scopes добавляется контекст scopes[name] = scope

//name - имя заносимого метода
//func - тело заносимого метода
//scope - контекст (область видимости) для данного метода

Server.prototype.expose = function(name, func, scope) {
  Server.trace('***', 'exposing: ' + name);
  this.functions[name] = func;

  if (scope) {
    this.scopes[name] = scope;
  }
} 

Расширение функции Server методом trace

Server.trace = function(direction, message) {

 sys.puts('   ' + direction + '   ' + message);

}

Расширение функции Server методом listen

Server.prototype.listen = function(port, host) { 
  this.server.listen(port, host);
  Server.trace('***', 'Server listening on http://' + (host || '127.0.0.1') + 
                ':' + port + '/'); 
}

Расширение функции Server методом handlePOST(основная часть Server)

Server.prototype.handlePOST = function(req, res) {
  var buffer = '';
  var self = this;
  var handle = function (buf) {
    

//первая проверка: валидность JSON вообще
//если JSON не валидный генерируем ошибку -32700 и прекращаем функцию 
//если JSON валидный дальше обрабатываем переменную decoded
    var decoded = "";
    try {
    	decoded = JSON.parse(buf);
    } catch (e) {
    	return Server.handleError(-32700, "Parse Error", null, req, res);
    }
    

// Check for the required fields, and if they aren't there, then
// dispatch to the handleError function.    
//вторая проверка: формат запроса по стандарту
//Наличие полей запроса (метод, параметры, id). id задания может отсутствовать в случае уведомления
//если id отсутствует, подставляем null всесто отсутствующего id
//При отсутствии полей запроса (метод, параметры) генерируем ошибку -32600
//Если нет ошибок - просто не останавливаем функцию и идем дальше

    if(!(decoded.method && decoded.params && decoded.id)) {
      
      if (typeof(id) == "undefined") {
   		var id = null;
   	  } 
   	  
      return Server.handleError(-32600, "Invalid Request", decoded.id, req, res);
    }

    
//третья проверка на наличие в functions указанного в запросе метода	
//в случае ошибки  генерируем ошибку -32601.

    if(!self.functions.hasOwnProperty(decoded.method)) {
      return Server.handleError(-32601, "Method not found", decoded.id, req, res);
    }

// Build our success handler
//
    var onSuccess = function(funcResp) {
        Server.trace('-->', 'response (id ' + decoded.id + '): '+ JSON.stringify(funcResp));
	var encoded = JSON.stringify({
            'jsonrpc': '2.0',
            'result': funcResp,
            'error': null,
            'id': decoded.id
            });
      
      res.writeHead(200, {'Content-Type': 'application/json',
                          'Content-Length': encoded.length});
      res.write(encoded);
      res.end();
    };

    Server.trace('<--', 'request (id ' + decoded.id + '): ' + 
                  decoded.method + '(' + decoded.params.join(', ') + ')');

    // Try to call the method, but intercept errors and call our
    // onFailure handler.
    var method = self.functions[decoded.method];
    var callback = function(result, errormessage) {
      if (errormessage) {
        Server.handleError(-32602, errormessage, decoded.id, req, res);
      } else {
        onSuccess(result);
      }
    };
    var scope = self.scopes[decoded.method] || self.defaultScope;

    // Other various information we want to pass in for the handler to be
    // able to access.
    var opt = {
      req: req,
      server: self
    };

    try {
      method.call(scope, decoded.params, opt, callback);
    } catch (err) {
      return Server.handleError(-32603, err, decoded.id, req, res);
    }

  } // function handle(buf)

  req.addListener('data', function(chunk) {
    buffer = buffer + chunk;
  });

  req.addListener('end', function() {
    handle(buffer);
  });
}

Расширение функции Server методом handleError

Server.handleError = function(code, message, id, req, res) {
  
  var encoded = JSON.stringify({
  	'jsonrpc': '2.0',
    'error': {
    	'code':code,
    	'message':message
    },
    'id': id
  });
  
  res.writeHead(400, {'Content-Type': 'text/plain',
                      'Content-Length': encoded.length,
                      'Allow': 'POST'});
  
  res.write(encoded);
  res.end();
  
  Server.trace('-->', 'Failure: ' + code + ': ' + message);
}

Расширение функции Server методом handleNonPOST

Server.handleNonPOST = function(req, res) {
  
  var encoded = JSON.stringify({
  	'jsonrpc': '2.0',
    'error': {
    	'code':-32600,
    	'message':"Only POST is allowed."
    },
    'id': null
  });
  
  res.writeHead(405, {'Content-Type': 'text/plain',
                      'Content-Length': encoded.length,
                      'Allow': 'POST'});
  res.write(encoded);
  res.end();
}

Активный экспорт переменных

module.exports.Server = Server;
module.exports.Client = Client;