Tuesday, March 31, 2009

关于keytool用法

做SSL以来,一直在使用keytool,下面把它用法整理如下,以备以后查看。
1,产生一个密钥对
keytool -genkey -alias mykeypair -keypass mykeypairpwd
过程如下:
liqingfeng@liqingfeng:~/WORK_APP/keytooltest$ keytool -genkey -alias mykeypair -keypass mykeypairpwd
输入keystore密码: 123456
您的名字与姓氏是什么?
[Unknown]: fingki
您的组织单位名称是什么?
[Unknown]: server
您的组织名称是什么?
[Unknown]: server
您所在的城市或区域名称是什么?
[Unknown]: bj
您所在的州或省份名称是什么?
[Unknown]: bj
该单位的两字母国家代码是什么
[Unknown]: CN
CN=fingki, OU=server, O=server, L=bj, ST=bj, C=CN 正确吗?
[否]: y

liqingfeng@liqingfeng:~/WORK_APP/keytooltest$
这样将产生一个keypair,同时产生一个keystore.默认名是.keystore,存放到user-home目录
假如你想修改密码,可以用:keytool -keypasswd -alias mykeypair -keypass mykeypairpwd -new newpass

2,产生一个密钥对,存放在指定的keystore中(加上-keystore 参数)
keytool -genkey -alias mykeypair -keypass mykeypairpwd -keystore mykeystore
过程与上面的相同。
执行完后,在当前目录下产生一个名为mykeystore的keystore,里面有一个别名为mykeypair的keypair。

3,检查一个keystore中的内容
keytool -list -v -alias mykeypair -keystore mykeystore
参数 -v指明要列出详细信息
-alias指明列出指定的别名为mykeypair的keypair信息(不指定则列出所有)
-keystore指明要列出名字为mykeystore的keystore中的信息
过程如下:
liqingfeng@liqingfeng:~/WORK_APP/keytooltest$ keytool -list -v -keystore mykeystore
输入keystore密码: 123456

Keystore 类型: jks
Keystore 提供者: SUN

您的 keystore 包含 1 输入

别名名称: mykeypair
创建日期: 2008-4-16
输入类型:KeyEntry
认证链长度: 1
认证 [1]:
Owner: CN=fingki, OU=server, O=server, L=bj, ST=bj, C=CN
发照者: CN=fingki, OU=server, O=server, L=bj, ST=bj, C=CN
序号: 48058c3c
有效期间: Wed Apr 16 13:18:52 GMT+08:00 2008 至: Tue Jul 15 13:18:52 GMT+08:00 2008
认证指纹:
MD5: FD:C3:97:DC:84:A0:D8:B2:08:6F:26:7F:31:33:C3:05
SHA1: A3:21:6F:C6:FB:5F:F5:2D:03:DA:71:8C:D3:67:9D:1C:E1:27:A5:11


*******************************************
*******************************************


liqingfeng@liqingfeng:~/WORK_APP/keytooltest$
4,Keystore的产生:
当使用-genkey 或-import或-identitydb命令添加数据到一个keystore,而当这个keystore不存在时,产生一个keystore.默认名是.keystore,存放到user-home目录.
当用-keystore指定时,将产生指定的keystore.
5,Keystore的实现:
Keytool 类位于java.security包下,提供一个非常好的接口去取得和修改一个keystore中的信息. 目前有两个命令行:keytool和jarsinger,一个GUI工具Policy 可以实现keystore.由于keystore是公开的,用户可以用它写一些额外的安全应用程序.
Keystore还有一个sun公司提供的內在实现.它把keystore作为一个文件来实现.利用了一个keystore类型(格式)"JKS".它用单独的密码保护每一个私有钥匙.也用可能不同的密码保护整个keystore的完整性.
支持的算法和钥匙大小:
keytool允许用户指定钥匙对和注册密码服务供应者所提供的签名算法.缺省的钥匙对产生算法是"DSA".假如私有钥匙是"DSA"类型,缺省签名算法是"SHA1withDSA",假如私有钥匙是"RSA"类型,缺省算法是"MD5withRSA".
当产生一个DSA钥匙对,钥匙必须在512-1024位之间.对任何算法的缺省钥匙大小是1024位.
6,关于证书
一个证书是一个实体的数字签名,还包含这个实体的公共钥匙值.
公共钥匙 :是一个详细的实体的数字关联,并有意让所有想同这个实体发生信任关系的其他实体知道.公共钥匙用来检验签名;
数字签名:是实体信息用实体的私有钥匙签名(加密)后的数据.这条数据可以用这个实体的公共钥匙来检验签名(解密)出实体信息以鉴别实体的身份;
签名:用实体私有钥匙加密某些消息,从而得到加密数据;
私有钥匙:是一些数字,私有和公共钥匙存在所有用公共钥匙加密的系统的钥匙对中.公共钥匙用来加密数据,私有钥匙用来计算签名.公钥加密的消息只能用私钥解密,私钥签名的消息只能用公钥检验签名。
实体:一个实体可以是一个人,一个组织,一个程序,一台计算机,一个商业,一个银行,或其他你想信任的东西.
实际上,我们用[1]中的命令已经生成了一个自签名的证书,没有指定的参数都使用的是默认值。
我们也可以用如下命令生成一个自签名的证书:
keytool -genkey -dname "CN=fingki,OU=server,O=server,L=bj,ST=bj,C=CN" -alias myCA -keyalg RSA -keysize 1024 -keystore myCALib -keypass 654321 -storepass 123456 -validity 3650
这条命令将生成一个别名为myCA的自签名证书,证书的 keypair的密码为654321,证书中实体信息为 "CN=fingki,OU=server,O=server,L=bj,ST=bj,C=CN",存储在名为myCALib的keystore中(如果 没有将自动生成一个),这个keystore的密码为123456,密钥对产生的算法指定为RSA,有效期为10年。
7,将证书导出到证书文件
keytool -export -alias myCA -file myCA.cer -keystore myCALib -storepass 123456 -rfc
使用该命令从名为myCALib的keystore中,把别名为myCA的证书导出到证书文件myCA.cer中。(其中-storepass指定keystore的密码,-rfc指定以可查看编码的方式输出,可省略)。

8,通过证书文件查看证书信息
keytool -printcert -file myCA.cer
9,密钥库中证书条目口令的修改
Keytool -keypasswd -alias myCA -keypass 654321 -new newpass -storepass 123456 -keystore myCALib
10,删除密钥库中的证书条目
keytool -delete -alias myCA -keystore myCALib
11,把一个证书文件导入到指定的密钥库
keytool -import -alias myCA -file myCA.cer -keystore truststore
(如果没有名为truststore的keystore,将自动创建,将会提示输入keystore的密码)
12,更改密钥库的密码
keytool -storepasswd -new 123456 -storepass 789012 -keystore truststore
其中-storepass指定原密码,-new指定新密码。

Refer to 'http://java.sun.com/j2se/1.5.0/docs/tooldocs/windows/keytool.html' for more information.

Tuesday, March 03, 2009

A introduction of NIO

FROM: GlassFish:开源的Java EE应用服务器 17.1

作为Java EE Web层面的最前端,HTTP引擎是负责接收客户请求的最开始的部分,这部分的性能在很大程度上决定了整个Java EE产品的性能和可扩展性。回顾现有的J2EE产品,大部分的HTTP引擎都不是用纯Java编写的。例如,Sun的JES应用服务器内置了一个用本地语 言(C/C++)开发Web服务器,JBoss的Web Server也不是纯Java的,它使用了大量与平台相关的运行库,只不过通过Apache的APR项目(http://apr.apache.org) 来维护跨平台的特性。而那些纯Java的J2EE服务器,在部署的时候也推荐前置一个其他的Web服务器,例如(Apache、IIS等)。

使用纯Java来构建具有扩展性很好的服务器软件, 一直是一个比较困难的事情,特别是在单个的Java虚拟机上(非集群的环境)。这是由Java的线程模型和网络IO的特性所决定的。在JDK 1.4以前,Java的网络IO的接口都是阻塞式的,这意味着网络的阻塞会引起处理线程的停止,因此每个用户请求的处理从开始到最后完成,需要单独的处理 线程。而Java的线程资源的分配和线程的调度都是有很大开销的,这使得在大量请求(数千个甚至上万个)同时到达的情况下,单个Java虚拟机很难满足大 并发性的需要。为了解决可扩展性的问题,一些解决方案使用了多个Java虚拟机或者多个机器节点进行集群来满足大并发的请求。

JDK 1.4版本(包括之后的版本)最显著的新特性就是增加了NIO(New IO),能够以非阻塞的方式处理网络的请求,这就使得在Java中只需要少量的线程就能处理大量的并发请求了。但是使用NIO不是一件简单的技术,它的一 些特点使得编程的模型比原来阻塞的方式更为复杂。

Grizzly作为GlassFish中非常重要的一个项目,就是用NIO的技术来实现应用服务器中的高性能纯Java的HTTP引擎。Grizzly还是一个独立于GlassFish的框架结构,可以单独用来扩展和构建自己的服务器软件。

本章重点:

l NIO的基本特点和编程方式

l Grizzly的基本结构

l Grizzly对NIO技术的运用手段

l Grizzly对性能上的考虑和优化

17.1 NIO简介

理解NIO是学习本章的重要前提,因为 Grizzly本身就是基于NIO的框架结构,所有的技术问题都是在NIO的技术上进行讨论的。如果读者对NIO不了解的话,建议首先了解NIO的基本概 念。对NIO的介绍和学习指南很多,本章不会对NIO做详细的讲解。下面仅对NIO做一个简单的介绍,并列出与本章内容相关的一些NIO特性。

17.1.1 NIO的基本概念

在JDK 1.4的新特性中,NIO无疑是最显著和鼓舞人心的。NIO的出现事实上意味着Java虚拟机的性能比以前的版本有了较大的飞跃。在以前的JVM的版本 中,代码的执行效率不高(在最原始的版本中Java是解释执行的语言),用Java编写的应用程序通常所消耗的主要资源就是CPU,也就是说应用系统的瓶 颈是CPU的计算和运行能力。在不断更新的Java虚拟机版本中,通过动态编译技术使得Java代码执行的效率得到大幅度提高,几乎和操作系统的本地语言 (例如C/C++)的程序不相上下。在这种情况下,应用系统的性能瓶颈就从CPU转移到IO操作了。尤其是服务器端的应用,大量的网络IO和磁盘IO的操 作,使得IO数据等待的延迟成为影响性能的主要因素。NIO的出现使得Java应用程序能够更加紧密地结合操作系统,更加充分地利用操作系统的高级特性, 获得高性能的IO操作。

NIO在磁盘IO处理和文件处理上有很多新的特性来提高性能,本文不作详细的解释,而仅仅介绍NIO在处理网络IO方面的新特点,这些特点是理解Grizzly的最基本的概念。

1. 数据缓冲(Buffer)处理

数据缓冲(Buffer)是IO操作的基本元 素。其实从本质上来说,无论是磁盘IO还是网络IO,应用程序所作的所有事情就是把数据放到相应的数据缓冲当中去(写操作),或者从相应的数据缓冲中提取 数据(读操作)。至于数据缓冲中的数据和IO设备之间的交互,则是操作系统和硬件驱动程序所关心的事情了。因此,数据缓冲在IO操作中具有重要的作用,是 操作系统与应用之间的IO桥梁。在NIO的包中,Buffer类是所有类的基础。Buffer类当中定义数据缓冲的基本操作,包括put、get、 reset、clear、flip、rewind等,这些基本操作是进行数据输入输出的手段。每一个基本的Java类型(boolean除外)都有相应的 Buffer类,例如CharBuffer、IntBuffer、DoubleBuffer、ShortBuffer、LongBuffer、 FloatBuffer和ByteBuffer。我们所关心的是ByteBuffer,因为操作系统与应用程序之间的数据通信最原始的类型就是Byte。

“Direct ByteBuffer”是一个值得关注的Buffer类型。在创建ByteBuffer的时候可以使用 ByteBuffer.allocateDirect()来创建一块直接(Direct)的ByteBuffer。这一块数据缓冲和一般的缓冲不一样。第 一,它是一块连续的空间。第二,它的实现不是纯Java的代码,而是本地代码,它内存的分配不在Java的堆栈中,不受Java内存回收的影响。这种直接 的ByteBuffer是NIO用来保证性能的重要手段。刚才提到,数据缓冲是操作系统和应用程序之间的IO接口。应用程序将需要“写出去”的数据放到数 据缓冲中,操作系统从这块缓冲中获得数据执行写的操作。当IO设备数据传进来的时候,操作系统就会将数据放到相应的数据缓冲中,应用程序从缓冲中“读进” 数据进行处理。一般的Java对象很难胜任这个直接的数据缓冲的工作。因为Java对象所占用的内存空间不一定是连续的,而且经常由于内存回收而改变地 址。而操作系统需要的是一片连续的不变动的地址空间,才能完成IO操作。在原来的Java版本中需要Java虚拟机的介入,将数据进行转换、拷贝才能被操 作系统所使用。而通过“Direct ByteBuffer”,应用程序能够直接与操作系统进行交流,大大减少了系统调用的次数,提高了执行的效率。

数据缓冲的另外一个重要的特点是可以在一个数据缓冲 上再建立一个或多个视图(View)缓冲。这个概念有些类似于数据库视图的概念:在数据库的物理表(Table)结构之上可以建立多个视图。同样,在一个 数据缓冲之上也可以建立多个逻辑的视图缓冲。视图缓冲的用处很多,例如可以将Byte类型的缓冲当作Int类型的视图,来进行类型转换。视图缓冲也可以将 一个大的缓冲看成是很多小的缓冲视图。这对提高性能很有帮助,因为创建物理的数据缓冲(特别是直接的数据缓冲)是非常耗时的操作,而创建视图却非常快。在 Grizzly中就有这方面的考虑。

2. 异步通道(Channel)

Channel(后文又称频道,译法仅暗示存在 多通道可选)是NIO的另外一个比较重要的新特点。Channel并不是对原有Java类的扩充和完善,而是完全崭新的实现。通过 Channel,Java应用程序能够更好地与操作系统的IO服务结合起来,充分地利用上文提到的ByteBuffer,完成高性能的IO操作。 Channel的实现也不是纯Java的,而是和操作系统结合紧密的本地代码。

Channel的一个重要的特点是在网络套接字频道(SocketChannel)中,可以将其设置为异步非阻塞的方式。

【例17.1】非阻塞方式的频道使用:

SocketChannel sc = SocketChannel.open();

sc.configureBlocking(false); // nonblocking

...

if (!sc.isBlocking()) {

doSomething(cs);

}

通过 SocketChannel.configureBlocking(false)就可以将网络套接字频道设置为异步非阻塞模式。一旦设置成非阻塞的方式, 从Socket中读和写就再也不会阻塞。虽然非阻塞只是一个设置问题,但是对应用程序的结构和性能却产生了天翻地覆的变化。

3. 有条件的选择(Readiness Selection)

熟悉UNIX的程序员对POSIX的select()或poll()函数应该比较熟悉。在现在大多数流行的操作系统中,都支持有条件地选择已经准备好的IO通道,这就使得只需要一个线程就能同时有效地管理多个IO通道。在JDK 1.4以前,Java语言是不具备这个功能的。

NIO通过几个关键的类来实现这种有条件的选择的功能:

(1) Selector

Selector类维护了多个注册的Channel以及它们的状态。Channel需要向Selector注册,Selector负责维护和更新Channel的状态,以表明哪些Channel是准备好的。

(2) SelectableChannel

SelectableChannel是可以被 Selector所管理的Channel。FileChannel不属于Selectable- Channel,而SocketChannel是属于这类的Channel。因此在NIO中,只有网络的IO操作才有可能被有条件地选择。

(3) SelectionKey

SelectionKey用于维护Selector 和SelectableChannel之间的映射关系。当一个Channel向Selector注册之后,就会返回一个SelectionKey作为注册 的凭证。SelectionKey中保存了两类状态值,一是这个Channel中哪些操作是被注册了的,二是有哪些操作是已经准备好的。

17.1.2 NIO之前的Server程序的架构

在NIO出现以前(甚至在NIO出现了很长时间的现 在),在用Java编写服务器端的程序时,服务请求的接收模块大多数都会采用以下的框架(例如在Tomcat中的连接接入 点:org.apache.tomcat.util.net.PoolTcpEndpoint就有相类似的结构)。

【例17.2】阻塞方式的server编程框架:

class Server implements Runnable {

public void run() {

try {

ServerSocket ss = new ServerSocket(PORT);

while (!Thread.interrupted())

new Thread(new Handler(ss.accept())).start();

} catch (IOException ex) { /* ... */ }

}

static class Handler implements Runnable {

final Socket socket;

Handler(Socket s) { socket = s; }

public void run() {

try {

byte[] input = new byte[MAX_INPUT];

socket.getInputStream().read(input);

byte[] output = process(input);

socket.getOutputStream().write(output);

} catch (IOException ex) { /* ... */ }

}

private byte[] process(byte[] cmd) { /* ... */ }

}

}

上面的结构比较简单:在主线程的run()方法中, 会有ServerSocket的accept()方法,它被循环地调用着,直到服务停止。accept()方法会被阻塞,直到新的连接请求的到来。当新的 连接请求进来以后,系统会使用另外的线程来处理这个请求。处理线程在socket端口进行read()调用,读取所有的请求数据。read()也是一个阻 塞的方法,一直到读取完所有的数据才会返回。数据经过处理以后,在同一个处理线程中将请求结果返回给客户端。在实际情况中,会比这个结构复杂得多,例如, 处理线程是从一个线程池中获取,而不是每次都产生一个新的线程。

这种结构在大多数情况下都可以获得很好的性能。例如 Tomcat在性能指标的测试中获得了很高的吞吐量测量值。但是在并发性很大的情况下,这种结构不具有很好的可扩展性。例如有2000个客户请求同时到 来,如果想要这2000个请求被同时处理,则需要2000个处理线程。这些线程在大多数的情况下可能都不在运行,而是阻塞在read()或write() 的方法上了。在一台机器或者一个Java虚拟机上运行上千个线程是个挑战,线程经常会阻塞,因此CPU会在这些线程之间来回调度和切换,这会引起大量的系 统调用和资源竞争,使得整个系统的扩展性能不高。

17.1.3 使用NIO来提高系统扩展性

NIO使用非阻塞的API,通过实现少量的线程就能 服务于大量的并发用户的请求。并且通过操作系统都支持的POSIX标准的select方式,来获得系统准备就绪的资源。使用这些手段,NIO就能够充分利 用每个活动的线程来服务于大量的请求,减少系统资源的浪费。通常来说,一个NIO的服务架构会采用以下的结构。

【例17.3】使用NIO的server编程框架:

public class Server {

public static void main(String[] argv) throws Exception {

ServerSocketChannel serverCh = ServerSocketChannel.open();

Selector selector = Selector.open();

ServerSocket serverSocket = serverCh.socket();

serverSocket.bind(new InetSocketAddress(80));

serverCh.configureBlocking(false);

serverCh.register(selector,SelectionKey.OP_ACCEPT);

while(true){

selector.select();

Iterator it = selector.selectedKeys().iterator();

while (it.hasNext()) {

SelectionKey key = (SelectionKey)it.next();

if (key.isAcceptable()) {

ServerSocketChannel server =

(ServerSocketChannel)key.channel();

SocketChannel channel = server.accept();

channel.configureBlocking(false);

channel.register(selector, SelectionKey.OP_READ);

}

if (key.isReadable()) {

readDataFromSocket(key);

}

it.remove();

}

}

}

}

上面的结构比起阻塞式的框架都复杂一些。具体说明如下:

l 通过ServerSocketChannel.open()获得一个Server的Channel对象。

l 通过Selector.open()来获得一个Selector对象。

l 从Server的Channel对象上可以获得一个Server的Socket,并让它在80端口监听。

l 通过ServerSocketChannel.configureBlocking(false)可以将当前的Channel配置成异步非阻塞的方式。如果没有这一步,那么Channel默认的方式跟传统的一样,是阻塞式的。

l 将当前的Channel注册到Selector对象中去,并告诉Selector当前的Channel关心的操作是OP_ACCEPT,也就是当有新的请求的时候,Selector负责更新此Channel的状态。

l 在循环当中调用selector.select(),如果当前没有任何新的请求过来,并且原来的连接也没有新的请求数据到达,这个方法会阻塞住,一直等到新的请求数据过来为止。

l 如果当前都请求的数据到达,那么selector.select()就会立刻退出,这时候可以从selector.selectedKeys()获得所有在当前selector注册过的并且有数据到达的这些Channel的信息(SelectionKey)。

l 遍历所有的这些SelectionKey来获得相关的信息。如果某个SelectionKey的操作是OP_ACCEPT,也就是isAcceptable,那么可以判定这是那个Server Channel,并且是有新的连接请求到达了。

l 当有新的请求来的时候,通过accept()方法可以获得新的channel服务于这个新来的请求。然后通过configureBlocking(false)可以将当前的Channel配置成异步非阻塞的方式。

l 接着将这个新的channel也注册到selector中,并告诉Selector当前的Channel关心的操作是OP_READ,也就是当前Channel有新的数据到达的时候,Selector负责更新此Channel的状态。

l 如果在循环当中发现某个SelectionKey的操作是OP_READ,也就是isReadable,那么可以判定这不是那个Server Channel,而是在循环内部注册的连接Channel,表明当前SelectionKey对应的这个Channel有数据到达了。

l 有数据到达之后的处理方式是下面要详细讨论的问题,在这里,我们简单地用一个方法readDataFromSocket(key)来表示,功能就是从这个Channel中读取数据。

从这个框架结构中可以看到,在一个线程中可以同时服 务于多个连接,包括Server的监听服务。在同一个时刻,并不是所有的连接都会有数据到达,因此为每一个连接分配单独的线程没有必要。使用异步非阻塞方 式,可以使用很少的线程,通过Select的方式来服务于多个连接请求,效率大大提高。

17.1.4 使用NIO来制作HTTP引擎的最大挑战

程序实例17.3使用了configureBlocking(false)方法来将一个Channel设置成非阻塞式的。如何使用这个非阻塞的特性,请参看下面的方法调用:

count = socketChannel.read(byteBuffer)); //非阻塞的方式

阻塞式的方法调用如下:

count = socket.getInputStream().read(input); //阻塞的方式

阻塞的方式下的read,会一直等到byte[]类 型的input被充满,或者InputStream遇到EOF(socket连接被关闭)的时候,这个函数调用才会被返回。而非阻塞的方式,立刻就返回 了,当前连接中有多少数据就读多少。正因为有了这种非阻塞的模式,当前的线程在读了某个通道的数据之后,可以接着再读另外一个通道的数据,线程的利用率大 大提高。

虽然线程的利用率提高了,却带来了一些其他的挑战。最大的挑战就在于:当一个请求过来的时候,很难判断什么时候所有请求的数据全部读进来了。因为每次非阻塞方式的read都可能只读了一部分数据,甚至什么也没有读到。例如,一个HTTP请求:

HTTP/1.1 206 Partial content

GET http://www.w3.org/pub/WWW/TheProject.html

所有的请求数据都是以文本方式传输。在非阻塞的方式 下,每一次对Channel进行读取的数据量大小不可预测,也许第一次读了“HTTP/1.1 206 Partial content”,第二次读取了“GET http://www.w3.org/pub/WWW”,第三次什么也没有读到。到底什么时候能把请求全部读完很难预测,在极端的情况下,也许最后几个字 符永远也读不到。在请求没有完全读到以前,一般不进行请求处理,因为请求还不完整。在阻塞的情况下,读取的函数会一直等到请求的数据全部到来并且连接关闭 以后才会返回,处理起来比较简单。但是非阻塞的方式就很复杂了。因为工作线程从一个连接读取完准备好的数据之后,又要为另一个连接服务。下次再转到先前连 接的时候,以前读取的数据还需要恢复。还需要判断到底所有的请求数据是否都读完,是否可以开始对该请求的处理了。

在本章的后面各节中,我们会看到Grizzly采用了一个有限状态机来解析HTTP请求的header信息,读取其中的content-length数值,以便预先判断什么时候到达请求的末尾。