• 7.8.1 核心模块(续2)

    除了Buffer模块比较特殊, 放在包limax.node.js内, 其它核心模块均放置在包limax.node.js.modules内, 一个.java文件一个.js文件配对, 按照java习惯, 首字母大写, require时全小写。

    • Readline(逐行读取)

      太多的终端相关行为,用得不多,暂不实现。


    • REPL(交互式解释器)

      用得不多,暂不实现。


    • Stream(流)

      核心中的核心,纯js代码,完整实现,如同node.js文档所说,应该尽量使用flow模型。


    • String Decoder(字符串解码器)

      使用java.nio.charset.CharsetDecoder实现。


    • Timer(定时器)

      用处很多的工具,完整实现。


    • TLS/SSL

      Node.js的设计方式过于冗余,Limax版本直接在Net模块中实现。(实际上不存在tls这个模块),Limax版本的实现不但支持Node.js的TLSSocket方式,也支持某些实际网络协议使用的STARTTLS模式,允许在非安全连接上启停TLS。


      Net模块的扩充

      1. net.Server类

      1.1. 构造参数options中增加了options.tls属性,如果设置了该属性,服务器按TLSSocket服务器方式启动。(http模块的使用的方式)

      2. net.Socket类

      2.1. 构造参数options中增加了options.tls属性,如果设置了该属性,socket.connect时客户端按照TLSSocket客户端方式启动。(http模块使用的方式)

      2.2. 方法socket.starttls(tls),在非安全连接上使用tls配置信息启动TLS。starttls之后,允许立即发送数据,实际上数据将在tls握手完成以后被发送。

      2.3. 方法socket.stoptls([function()]),结束TLS会话,回到非安全会话状态。如果传入一个回调函数,该函数在tlsDown消息触发时被调用,详见tlsDown消息的说明。一般来说,如果需要设计STARTTLS类应用,必须正确设计STOPTLS协商机制,TLS结束握手完成以后再执行非安全连接状态下的会话。Limax实现上,保证不丢失任何数据,由于一些异步特性,如果stoptls之后立刻发送数据,这些数据可能在安全会话中发送,也可能在安全会话结束以后发送。

      2.4. 方法socket. tlsrenegotiate(),启动TLS重协商,该方法仅设置一个标记,指示在下一次socket活动时重新握手,所以不会返回任何错误。

      2.5. 消息tlsHandshaked,握手完成后,该消息被触发,参数模式为function(principal, cert0, cert1 …),也可以在触发后直接读取属性socket.tlsPeerPrincipal与socket.tlsPeerCertificateChain,获得对方的证书主题与证书链,其中socket.tlsPeerPrincipal为javax.security.auth.x500.X500Principal类型,socket.tlsPeerCertificateChain为一js数组,成员类型为java.security.cert.X509Certificate。

      2.6. 消息tlsDown, TLS会话结束后该消息被触发, 参数模式为function(err), if(err)成立,表示会话由某些异常导致,err.printStackTrace()可以获取异常信息, 发生异常以后, 用户应该自己socket.destroy(err),关闭网络连接。(可以参考http模块的实现)

      3. net.createTLS(function(c)),创建TLS配置,参数为一个callback函数,c为一个java对象,类型为limax.node.js.modules.tls.TLSConfig,所有的配置必须在该callback中调用c上的相应方法执行,这样可以最小化异常传播。

      3.1. 信任检测类配置,客户端必须配置,服务器如果需要验证客户端,也必须配置。

      3.1.1. c.addAllCA(),将jdk的cacerts文件中的受信任证书全部加入。

      3.1.2. c.addTrustCertificate(data),加入信任证书,data的类型可以是String,可以是Buffer,内容可以是文件路径,也可以是实际内容,实际内容的格式可以是PEM,可以是DER,可以有一个证书,也可以有多个证书,也可以是PKCS7证书。

      3.1.3. c.addCRL(data),加入CRL列表,data的解释同上。

      3.1.4. c.setRevocationEnabled(revocationEnabled),revocationEnabled为boolean类型,该方法配置是否允许CRL测试,如果不允许,通过上面的c.addCRL加入的CRL列表也没有意义。如果允许,则必须在启动虚拟机时加入虚拟机参数com.sun.security.enableCRLDP=true,否则需要通过网络进行的测试(比如OCSP)将会失败,CRL测试多数情况下极其费时,默认为false。

      3.1.5. c.setTrustChecker(function(chain, exception)),chain为java数组表示的证书链,exception为使用上面的配置检测失败以后抛出的异常,该方法返回true,表示接受证书链,TLS握手过程可以继续。通常,浏览器访问https网站时,发现某些证书异常,提示用户是否继续,就是使用这样的方式实现。复杂的证书链检测,应该将chain和exception转发给用户自己实现的java模块。

      3.1.6. c.setPositiveTrustChecker(function(chain, exception)),执行积极的证书链检测,参数解释同上,使用该方法配置,将忽略上面配置的检测,直接使用设置的检测器,上面配置的检测能力不符合要求的情况下可以使用该方法。另外,使用该方法设置永远返回true的检测器,可以辅助进行一些证书配置上调试。

      3.2. 服务器证书私钥配置,需要客户端验证的情况下,客户证书私钥的配置。

      3.2.1. c.addPKCS12(data, pass),加入PKCS12证书包,data的类型可以是String,可以是Buffer,内容可以是文件路径,可以是PKCS12证书包的实际内容;pass的类型可以是String,可以是Buffer,内容为PKCS12证书包的密码。

      3.2.2. c.addPrivateKeyAndCertificatePack(pkey, cert, pass),加入私钥和相应的证书链,实际上就是生成PKCS12证书包需要的信息。所有参数都允许是String或者Buffer,特别的,pkey关联的实际内容要求PEM格式表示的各种私钥格式,RSA,DSA,EC或者PKCS8,pass提供了私钥密码,如果私钥没有设置密码,pass被忽略,cert的要求与前面的c.addTrustCertificate(data)相同。

      3.3. TLS引擎配置

      3.3.1. c.setProtocol(protocol),protocol为String类型,允许SSLv3,TLSv1,TLSv1.1,TLSv1.2,默认TLSv1.2。

      3.3.2. c.setNeedClientAuth(enable),enable为boolean类型,默认为false,设置为true,将强制客户端提供证书进行验证。

      3.4. 虚拟服务器相关配置

      3.4.1. c.setSNIHostName(hostname),hostname为String类型或者Buffer类型,客户端调用该方法,设置请求的虚拟服务器名。

      3.4.2. c.addSNIServerName(type, name), type为int类型, name为Buffer类型, 客户端调用该方法,按类型添加请求的虚拟服务器名, c.setSNIHostName, 实际上提供了type=0, 所有这些type不能重复,否则抛出异常。

      3.4.3. 服务器没有使用JDK提供的SNIMatcher机制实现,采用的方法是,如果服务器端配置加入了多个私钥证书包,则启动虚拟服务器方式,SNIServerName使用证书Subject中CN设定的域名模板和SubjectAlternativeNames中的dNSName类型的域名模板进行模式匹配,选择正确的证书链和私钥,如果所有的匹配都不成功,由JDK的pkixKeyManager选择一对证书链和私钥。之所以这样实现,是因为JSSE提供的方案并不完备,存在逻辑bug。javax.net.ssl.ExtendedSSLSession.getRequestedServerNames()方法可以获取对方请求的服务器名,问题在于握手阶段选择证书时,该方法返回空集,直到握手完成,才返回客户端请求的证书。


      示例:

      服务器

      testtlsserver.js


      var net = require('net')
      var tls = net.createTLS(function(c) {
      	c.addPKCS12('/work/js.test/testtls.p12', '123456');
      	c.addTrustCertificate('/work/js.test/testtls.cer');
      	c.setNeedClientAuth(true);
      });
      var server = net.createServer(function(c) {
      	c.on('error', function(e){
      		console.log('error', e.stack)
      		e.printStackTrace();
      	});
      	c.once('tlsHandshaked', function() { 
      		console.log('tlsHandshaked:');
      		for (var i = 0; i < arguments.length; i++)
      			console.log(arguments[i].toString());
      	});
      	c.once('tlsDown', function(err) { 
      		console.log('tlsDown'); 
      		if (err)
      			client.destroy(err)
      	})
      	c.on('end', function() {
      		console.log('client disconnected');
      	});
      	c.starttls(tls);
      	c.write('hello\r\n');
      	c.pipe(c);
      	c.pipe(process.stdout)
      });
      server.on('error', function(err) {
      	throw err;
      });
      server.listen(8124, function() {
      	console.log('server bound');
      });
      

      客户端

      testtlsclient.js


      var net = require('net')
      var tls = net.createTLS(function(c) {
      	c.addPKCS12('/work/js.test/testtls.p12', '123456');
      	c.addTrustCertificate('/work/js.test/testtls.cer');
      });
      var client = net.createConnection({
      	port : 8124
      }, function() {
      	client.starttls(tls);
      	console.log('connected to server!');
      	client.write('world!\r\n', function() {
      		print('send done')
      		client.stoptls(function() {
      			print('stopped');
      			client.write('wwww?');
      		});
      	});
      });
      client.on('tlsHandshaked', function() {
      	console.log('tlsHandshaked:');
      	for (var i = 0; i < arguments.length; i++)
      		console.log(arguments[i].toString());
      });
      client.on('tlsDown', function(err) {
      	console.log('tlsDown')
      	if (err)
      		client.destroy(err)
      });
      client.on('data', function(data) {
      	data = data.toString();
      	console.log(data);
      	if (data[data.length - 1] == '?')
      		client.end();
      });
      client.on('close', function() {
      	console.log('disconnected from server');
      });
      client.on('error', function(e) {
      	console.log('error', e.stack)
      	e.printStackTrace();
      })
      

      1. 该例子通过对ECHO功能的服务器客户端进行TLS改造而来,要求客户端提供认证,客户端服务器简单使用同样的私钥和证书,testtls.p12,testtls.cer,可以按Node.js文档示例的方式制作。

      2. 客户端STARTTLS后,立即向服务器发送"world\r\n",可以明确该字符串在TLS会话中发送,发送完成后立即STOPTLS,停止完成后,发送字符串"wwww?",这里可以明确该字符串在非安全会话中发送,这两个串最终都会被服务器ECHO回来。

      3. 服务器accept客户端连接以后,立即在该连接上STARTTLS,随后向客户端发送"hello\r\n",可以明确该字符串在TLS会话中发送,收到"world\r\n",向客户端ECHO "world\r\n",这里注意到,服务器并不需要关心客户端是否STOPTLS,切换由底层完成,"wwww?"的ECHO,肯定在非安全会话中完成。逻辑上讲,尽管"world\r\n"在TLS会话中收到,ECHO回去的数据是否在TLS会话中发送,不应该作假设,毕竟客户端发送数据之后立即STOPTLS。这里可以看到设计STARTTLS类协议,应用层面必须仔细考虑STOPTLS的握手机制。

      4. 运行例子可以观察tlsHandshaked,tlsDown两个消息的效果。


    • TTY(终端)

      终端使用很少,不实现。


    • UDP/Datagram

      • 1. UDP服务器理论上不能提供Cluster能力, 如果用Cluster方式启动, 后续服务器bind时会产生地址已经使用异常。目前UDP最大的用途还是局域网环境内进行组播实现服务器间协同,不需要承载太重的负载。重负荷的情况可以设计为一个虚拟机收发报文,通过线程间通讯将任务分发给服务Cluster。

      • 2. socket.addMembership(multicastAddress[, multicastInterface])的实现与node.js描述有差异,Limax在实现上,缺省multicastInterface的行为是查找所有已经UP并且有组播能力的接口进行绑定。不同系统接口名差异太大,选择正确的名字都是问题,很难保证正确性;现在的计算机网络接口太多,让操作系统自己选择一个接口也是不可靠行为,谁知道能选中一个自己需要的?所以java.nio.channels.MulticastChannel接口也不提供自动选择能力。

      • 3. socket.setTTL(ttl)与socket.setMulticastTTL(ttl) 等同,因为DatagramChannel上仅提供一个TTL设置方法。


    • URL

      完整实现,仔细研究node.js文档可以发现问题,HTTP模块会用到这个URL解析器,这个解析器返回的urlObject.host并不是HTTP模块需要的host,HTTP模块文档需要的host实际上是这里的urlObject.hostname,基本数据字典都没有统一,不知道怎么规划的?


    • Util(实用工具)

      提供util.format(), util.inherits(), util.inspect()三个方法。其中, util.format()使用java.lang.String.format格式化参数, 格式参数与sprintf格式参数有稍许差异;inspect用于递归展示对象数据, 不支持显示颜色之类的特性。实际上console.log输出的串就是inspect展开的。


    • V8

      Nashorn,没V8啥事。


    • VM(虚拟机)

      • 1. 所有方法的options,都是V8相关的,忽略。

      • 2. DebugContext是V8特性,所以vm.runInDebugContext(),作为vm.runInNewContext()的别名实现。

      • 3. 不建议使用已有sandbox创建Context, 最好sandbox = vm.createContext(), 然后初始化sandbox, 有利于提高性能。这是Nashorn的限制, 非全局对象设置到Context上访问,会在该对象上添加一个nashorn.global成员作为真正的全局对象执行访问,为了保证正确性,执行完成之后必须执行一次拷贝。


    • ZLIB(压缩)

      node.js使用zlib实现,limax使用java.util.zip实现

      • 1. java.util.zip没有提供太多配置用常量,多数情况不需要使用options,默认实现即可。

      • 2. node.js文档中时间推送服务器的例子,flush的使用与文档本身定义的原型不符,原型中callback参数必选,例子没有。

      • 3. 实验时间推送服务器例子,最好把gzip算法改为deflate,java.util.zip.GZIPOutputStream的cache太大,直接的结果是感觉服务器长时间不响应。


上一页 下一页