1

node-soap客户端相关源码解读

node-soap结构——lib目录

源码学习初级阶段,着重在client端,并且先不考虑security设置。重点看以下几个方面:

  • wsdl解析完成后,如何关联services到client中

  • client中services调用时候,如何出发xml文档生成

  • http请求怎么处理

.
|____client.js    // webservice client我们关心的
|____http.js    //我们关心的
|____nscontext.js    //wsdl namespace 
|____security    // security
| |____BasicAuthSecurity.js
| |____BearerSecurity.js
| |____ClientSSLSecurity.js
| |____ClientSSLSecurityPFX.js
| |____index.js
| |____templates
| | |____wsse-security-header.ejs
| | |____wsse-security-token.ejs
| |____WSSecurity.js
| |____WSSecurityCert.js
|____server.js    // webservice server
|____soap.js    // api interface 我们关心的
|____utils.js    
|____wsdl.js    // wsdl xml2obj, obj2xml

createClient

createClient的使用方法

var soap = require('soap');
soap.createClient(url,function(err, client) {
    // do something with client
    console.log(client.describe())
    });

createClient的具体实现

  • _requestWSDL: 基于url获取wsdl内容,并构建wsdl对象。缓存wsdl对象

  • new Client(wsdl, endpoint, options),通过wsdl,服务endpoint,配置options构建client对象

"use strict";

var Client = require('./client').Client,
  Server = require('./server').Server,
  HttpClient = require('./http'),
  security = require('./security'),
  passwordDigest = require('./utils').passwordDigest,
  open_wsdl = require('./wsdl').open_wsdl,
  WSDL = require('./wsdl').WSDL;

var _wsdlCache = {};

function _requestWSDL(url, options, callback) {
  ... 
  // 通过wsdl URL从request或者本地cache中获取wsdl对象,然后调用callback,传回
  else {
    open_wsdl(url, options, function(err, wsdl) {
      if (err) {
        return callback(err);
      } else {
        _wsdlCache[url] = wsdl;
      }
      callback(null, wsdl);
    });
  }
}

function createClient(url, options, callback, endpoint) {
  if (typeof options === 'function') {
    endpoint = callback;
    callback = options;
    options = {};
  }
  endpoint = options.endpoint || endpoint;
    // 获取到WSDL对象,然后把wsdl和Client对象&&处理,通过callback传回,也就是createClient
    回调中的client参数
  _requestWSDL(url, options, function(err, wsdl) {
    callback(err, wsdl && new Client(wsdl, endpoint, options));
  });
}

function listen(server, pathOrOptions, services, xml) {
     // server 相关,先不关心
    ...
}

Client的构建

client最重要的三件事:

  • 初始化配置

  • 初始化服务(API)

  • 服务对象格式化XML文档

  • 发送XML文档,处理响应body


var Client = function(wsdl, endpoint, options) {
  events.EventEmitter.call(this);

  options = options || {};
  this.wsdl = wsdl;
  this._initializeOptions(options);
    // 重点看这个
  this._initializeServices(endpoint); 
  this.httpClient = options.httpClient || new HttpClient(options);
};

初始化Services _initializeServices

Client.prototype._initializeServices = function(endpoint) {
  var definitions = this.wsdl.definitions,
    services = definitions.services;
  for (var name in services) {
    this[name] = this._defineService(services[name], endpoint);
  }
};

// Port关联到service树结构中
Client.prototype._defineService = function(service, endpoint) {
  var ports = service.ports,
    def = {};
  for (var name in ports) {
    def[name] = this._definePort(ports[name], endpoint ? endpoint : ports[name].location);
  }
  return def;
};

// method关联到service树结构中
Client.prototype._definePort = function(port, endpoint) {
  var location = endpoint,
    binding = port.binding,
    methods = binding.methods,
    def = {};
  for (var name in methods) {
    def[name] = this._defineMethod(methods[name], location);
    this[name] = def[name];
  }
  return def;
};

// 最终function(args, callback, options, extraHeaders)作为叶子关联到services的树中,
// 安放到对应的port和method下. 
// client.runMethod(args, function(){})实际会调用这个叶子function
// 而这个函数,根本上,调用了_invoke
Client.prototype._defineMethod = function(method, location) {
  var self = this;
  var temp;
  return function(args, callback, options, extraHeaders) {
    ...
    self._invoke(method, args, location, function(error, result, raw, soapHeader)          {
      callback(error, result, raw, soapHeader);
    }, options, extraHeaders);
  };
};

小结

以上,完成了client的_initializeServices:

  1. 从wsdl解析definitions

  2. definitions中解析services

  3. services中解析ports

  4. ports中解析methods

  5. 为每一个methods定义一个函数,这个函数实际上就是_invoke函数。

  6. 最后,用户通过client.xxxxMethod(args, callback, options, extraHeaders)会调用这个invoke

  7. 剧透一下,invoke就负责把method名字,args参数,extraHeaders,options组装成xml内容,发送出去。响应结果解析后,通过callback的result参数传回。

_invoke

Client.prototype._invoke = function(method, args, location, callback, options, extraHeaders) {
  var self = this,
    name = method.$name,
    input = method.input,
    output = method.output,
    style = method.style,
    defs = this.wsdl.definitions,
    envelopeKey = this.wsdl.options.envelopeKey,
    ns = defs.$targetNamespace,
    encoding = '',
    message = '',
    xml = null,
    req = null,
    soapAction,
    alias = findPrefix(defs.xmlns, ns),
    
    // 设置headers,依据wsdl
    
    headers = {
      "Content-Type": "text/xml; charset=utf-8"
    },
    xmlnsSoap = "xmlns:" + envelopeKey + "=\"http://schemas.xmlsoap.org/soap/envelope/\"";

  if (this.wsdl.options.forceSoap12Headers) {
    headers["Content-Type"] = "application/soap+xml; charset=utf-8";
    xmlnsSoap = "xmlns:" + envelopeKey + "=\"http://www.w3.org/2003/05/soap-envelope\"";
  }

   ...

  options = options || {};

  //Add extra headers
  for (var header in this.httpHeaders ) { headers[header] = this.httpHeaders[header];  }
  for (var attr in extraHeaders) { headers[attr] = extraHeaders[attr]; }

  // Allow the security object to add headers
  if (self.security && self.security.addHeaders)
    self.security.addHeaders(headers);
  if (self.security && self.security.addOptions)
    self.security.addOptions(options);

    ...
    // message构建,主要是函数、参数;函数namespace设置等
    // stype == 'rpc' objectToRpcXML
    // style == 'document' objectToDocumentXML
  if ((style === 'rpc')&& ( ( input.parts || input.name==="element" ) || args === null) ) {
      assert.ok(!style || style === 'rpc', 'invalid message definition for document style binding');
      message = self.wsdl.objectToRpcXML(name, args, alias, ns,(input.name!=="element" ));
      (method.inputSoap === 'encoded') && (encoding = 'soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" ');
    } else if (typeof (args) === 'string') {
    message = args;
  } else {
    assert.ok(!style || style === 'document', 'invalid message definition for rpc style binding');
    // pass `input.$lookupType` if `input.$type` could not be found
    message = self.wsdl.objectToDocumentXML(input.$name, args, input.targetNSAlias, input.targetNamespace, (input.$type || input.$lookupType));
  }

//拼装xml
  
  xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
    "<" + envelopeKey + ":Envelope " +
    ...
    message +
    "</" + envelopeKey + ":Body>" +
    "</" + envelopeKey + ":Envelope>";

  if(self.security && self.security.postProcess){
    xml = self.security.postProcess(xml);
  }

  self.lastMessage = message;
  self.lastRequest = xml;
  self.lastEndpoint = location;

  self.emit('message', message);
  self.emit('request', xml);

  var tryJSONparse = function(body) {
    try {
      return JSON.parse(body);
    }
    catch(err) {
      return undefined;
    }
  };
 
// 1. 调用httpClient将xml打包,发送到webservice服务端,并将回包简单过滤处理
// 2. 调用wsdl将xml转为obj
  req = self.httpClient.request(location, xml, function(err, response, body) {
    var result;
    var obj;
    self.lastResponse = body;
    self.lastResponseHeaders = response && response.headers;
    self.lastElapsedTime = response && response.elapsedTime;
    self.emit('response', body, response);

    if (err) {
      callback(err);
    } else {

      try {
        obj = self.wsdl.xmlToObject(body);
      } catch (error) {
        //  When the output element cannot be looked up in the wsdl and the body is JSON
        //  instead of sending the error, we pass the body in the response.
        if(!output || !output.$lookupTypes) {
          debug('Response element is not present. Unable to convert response xml to json.');
          //  If the response is JSON then return it as-is.
          var json = _.isObject(body) ? body : tryJSONparse(body);
          if (json) {
            return callback(null, response, json);
          }
        }
        error.response = response;
        error.body = body;
        self.emit('soapError', error);
        return callback(error, response, body);
      }

      if (!output){
        // one-way, no output expected
        return callback(null, null, body, obj.Header);
      }

      if( typeof obj.Body !== 'object' ) {
        var error = new Error('Cannot parse response');
        error.response = response;
        error.body = body;
        return callback(error, obj, body);
      }

      result = obj.Body[output.$name];
      // RPC/literal response body may contain elements with added suffixes I.E.
      // 'Response', or 'Output', or 'Out'
      // This doesn't necessarily equal the ouput message name. See WSDL 1.1 Section 2.4.5
      if(!result){
        result = obj.Body[output.$name.replace(/(?:Out(?:put)?|Response)$/, '')];
      }
      if (!result) {
        ['Response', 'Out', 'Output'].forEach(function (term) {
          if (obj.Body.hasOwnProperty(name + term)) {
            return result = obj.Body[name + term];
          }
        });
      }

      callback(null, result, body, obj.Header);
    }
  }, headers, options, self);

  // Added mostly for testability, but possibly useful for debugging
  if(req && req.headers) //fixes an issue when req or req.headers is indefined
    self.lastRequestHeaders = req.headers;
};

总结

  1. 本文简单介绍node-soap的client实现。

  2. client实现重依赖wsdl作对象和文档的转化。所有namespace,xml,soap的格式解析都在wsdl中处理。后面,找机会再看一下wsdl

  3. client简单,清晰。并且提供事件监听


️22_️
25 声望1 粉丝

五行缺金又缺水~