http://blog.csdn.net/talent210/article/details/51829088
http://blog.csdn.net/talent210/article/details/51831271
http://www.cnblogs.com/Anker/p/3254269.html
IO在不同层次有不同的概念和单位。一次IO就是一次请求,对于磁盘来说,一个IO就是读或者写磁盘的某个或者某段扇区,读写完了,这个IO也就结束了。
寄存器和内存之间的数据通信不叫IO,寄存器,内存和外围设备的通信才叫IO.还有有些socket函数的操作,是内存和网卡之间的数据通信.所以也是IO操作。
IO操作速度慢,耗时.
而寄存器和内存之间传输数据则非常快,这两个不在一个数量级.
原来的 I/O 库(在 java.io.*中) 与 NIO 最重要的区别是数据打包和传输的方式。正如前面提到的,原来的 I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
面向流 的 I/O 系统一次一个字节地处理数据。
通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而 通道 可以用于读、写或者同时用于读写。
服务器在合理的时间之内处理大量客户机请求的能力取决于服务器使用 I/O 流的效率。同时为成百上千个客户机提供服务的服务器必须能够并发地使用 I/O 服务
端口地址在0~65535之间,其中0~1023之间的端口是用于一些知名的网络服务和应用,用户的普通网络应用程序应该使用1024以上的端口.
网络应用中基本上都是TCP(Transmission Control Protocol传输控制协议)和UDP(User Datagram Protocol用户数据报协议),TCP是面向连接的通信协议,UDP 是无连接的通信协议.
Socket连接套接字,Java分别为TCP和UDP提供了相应的类,TCP是java.net.ServerSocket(用于服务器端)和 java.net.Socket(用于客户端);UDP是java.net.DatagramSocket.
1,Java编写UDP网络程序
1.1,DatagramSocket
DatagramSocket有如下构造方法:
1,DatagramSocket() :构造数据报套接字并将其绑定到本地主机上任何可用的端口。
2,DatagramSocket(int port):创建数据报套接字并将其绑定到本地主机上的指定端口。
3,DatagramSocket(int port, InetAddress laddr):创建数据报套接字,将其绑定到指定的本地地址。即指定网卡发送和接收数据.
如果在创建DatagramSocket对象时,没有指定网卡的IP 地址,在发送数据时,底层驱动程序会自动选择一块网卡去发送,在接收数据时,会接收所有的网卡收到的与端口一致的数据.
发送信息时,可以不指定端口号,接收信息时,要指定端口号,因为要接收指定的数据.
发送数据使用DatagramSocket.send(DatagramPacket p)方法,接收数据使用DatagramSocket.receive(DatagramPacket p)方法.
1.2,DatagramPacket
DatagramPacket类有如下构造方法:
1,DatagramPacket(byte[] buf, int length):构造 DatagramPacket,用来接收长度为length的数据包。
2,DatagramPacket(byte[] buf, int length, InetAddress address, int port):构造数据报包,用来将长度为length的包发送到指定主机上的指定端口号。
接收数据时使用第一次构造方法,发送数据时使用第二种构造方法.
1.3,InetAddress
Java中对IP地址进行包装的类,
DatagramPacket.getAddress()可以获取发送或接收方的IP地址.DatagramPacket.getPort() 可以获取发送或接收方的端口.
1.4,UDP程序例子
发送程序:
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UdpSend {
public static void main(String[] args) throws Exception {
DatagramSocket ds = new DatagramSocket();
String str = "hello , world!";
DatagramPacket dp = new DatagramPacket(str.getBytes(),str.length(),InetAddress.getByName("192.168.0.105"),3000);
ds.send(dp);
ds.close(); //关闭连接
}
}
接收程序:
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UdpRecv {
public static void main(String[] args) throws Exception {
DatagramSocket ds = new DatagramSocket(3000);
byte[] buf = new byte[1024];
DatagramPacket dp = new DatagramPacket(buf,buf.length);
ds.receive(dp);
String str = new String(dp.getData(),0,dp.getLength());
System.out.println(str);
System.out.println("IP:" + dp.getAddress().getHostAddress() + ",PORT:" + dp.getPort());
ds.close();
}
}
public class UdpServer implements Runnable {
private int port;
public UdpServer(int port) {
this.port = port;
}
public void run() {
try {
// listening at port for udp request
DatagramSocket server = new DatagramSocket(new InetSocketAddress(port));
byte[] bs = new byte[4];
ByteBuffer bbuf = null;
DatagramPacket data = new DatagramPacket(bs, bs.length);
server.setSoTimeout(1000 * 10); // set timeout
while (true) { // 一直监听
server.receive(data);
bbuf = ByteBuffer.wrap(bs, 0, 4);
String s = "receive " + data.getData().length + " byte data,value: " + bbuf.getInt();
System.out.println(s);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new UdpServer(11111)).start();
}
}
public class UdpClient implements Runnable {
private int port;
public UdpClient(int port) {
this.port = port;
}
public void run() {
try {
DatagramSocket client = new DatagramSocket();
byte[] bs = new byte[4];
InetSocketAddress isa = new InetSocketAddress("localhost", port);
DatagramPacket data = new DatagramPacket(bs, bs.length);
data.setSocketAddress(isa);
for (int i = 0; i < 10; i++) {
data.setData(int2bytes(i));
client.send(data);
}
} catch (Exception e) {
e.printStackTrace();
}
}
static byte[] int2bytes(int num) {
byte[] b = new byte[4];
for (int i = 0; i < 4; i++) {
b[i] = (byte) (num >>> (24 - i * 8));
}
return b;
}
}
DatagramSocket只提供一对一的UDP服务。有时需要一对多的UDP服务,这时可以采用java的MulticastSocket。
IP协议为多点广播提供了这批特殊的IP地址,这些IP地址的范围是224.0.0.0至239.255.255.255。
通过Java实现多点广播时,MulticastSocket类是实现这一功能的关键,当 MulticastSocket把一个DatagramPacket发送到多点广播IP地址,该数据报将被自动广播到加入该地址的所有 MulticastSocket。MulticastSocket类既可以将数据报发送到多点广播地址,也可以接收其他主机的广播信息。
MulticastSocket有点像DatagramSocket,事实上MulticastSocket是 DatagramSocket的一个子类,也就是说MulticastSocket是特殊的DatagramSocket。若要发送一个数据报时,可使用随机端口创建MulticastSocket,也可以在指定端口来创建MulticastSocket。
MulticastSocket提供了如下三个构造器:
public MulticastSocket():使用本机默认地址、随机端口来创建一个MulticastSocket对象。
public MulticastSocket(int portNumber):使用本机默认地址、指定端口来创建一个MulticastSocket对象。
public MulticastSocket(SocketAddress bindaddr):使用本机指定IP地址、指定端口来创建一个MulticastSocket对象。
创建一个MulticastSocket对象后,还需要将该MulticastSocket加入到指定的多点广播地址,MulticastSocket使用jionGroup()方法来加入指定组;使用leaveGroup()方法脱离一个组。
joinGroup(InetAddress multicastAddr):将该MulticastSocket加入指定的多点广播地址。
leaveGroup(InetAddress multicastAddr):让该MulticastSocket离开指定的多点广播地址。
在某些系统中,可能有多个网络接口。这可能会对多点广播带来问题,这时候程序需要在一个指定的网络接口上监听,通过调用 setInterface可选择MulticastSocket所使用的网络接口;也可以使用getInterface方法查询 MulticastSocket监听的网络接口。
如果创建仅用于发送数据报的MulticastSocket对象,则使用默认地址、随机端口即可。但如果创建接收用的 MulticastSocket对象,则该MulticastSocket对象必须具有指定端口,否则发送方无法确定发送数据报的目标端口。
MulticastSocket用于发送、接收数据报的方法与DatagramSocket的完全一样。但 MulticastSocket比DatagramSocket多一个setTimeToLive(int ttl)方法,该ttl参数设置数据报最多可以跨过多少个网络,当ttl为0时,指定数据报应停留在本地主机;当ttl的值为1时,指定数据报发送到本地局域网;当ttl的值为32时,意味着只能发送到本站点的网络上;当ttl为64时,意味着数据报应保留在本地区;当ttl的值为128时,意味着数据报应保留在本大洲;当ttl为255时,意味着数据报可发送到所有地方;默认情况下,该ttl的值为1。
使用MulticastSocket进行多点广播时所有通信实体都是平等的,它们都将自己的数据报发送到多点广播IP地址,并使用MulticastSocket接收其他人发送的广播数据报。下面程序使用MulticastSocket实现了一个基于广播的多人聊天室,程序只需要一个MulticastSocket,两条线程,其中MulticastSocket既用于发送,也用于接收,其中一条线程分别负责接受用户键盘输入,并向MulticastSocket发送数据,另一条线程则负责从MulticastSocket中读取数据。
public class MulticastClient{
public static void main(String[] args) throws IOException{
MulticastSocket socket = new MulticastSocket(4446);
InetAddress address = InetAddress.getByName("230.0.0.1");
socket.joinGroup(address);
DatagramPacket packet;
//发送数据包
byte[] buf = "Hello,This is a member of multicast!".getBytes();
packet = new DatagramPacket(buf, buf.length,address,4445);
socket.send(packet);
//接收数据包并打印
byte[] rev = new byte[512];
packet = new DatagramPacket(rev, rev.length);
socket.receive(packet);
String received = new String(packet.getData()).trim();
System.out.println("received: " + received);
//退出组播组,关闭socket
socket.leaveGroup(address);
socket.close();
}
}
Socket conn = s.accept( ); 对 accept() 的调用将一直阻塞,直到服务器套接字接受了一个请求连接的客户机请求。
一旦建立了连接,服务器就使用 InputStream(实现类) 读取客户机请求。因为 InputStream(实现类) 要到缓冲区满(有的实现类只需要有可用数据)时才成批地读取数据,所以这个调用在读时阻塞。write 方法也一样
在 JDK 1.4 之前,自由地使用线程是处理阻塞问题最典型的办法。但这个解决办法会产生它自己的问题 ― 即线程开销,线程开销同时影响性能和可伸缩性。
Channel 类表示服务器和客户机之间的一种通信机制。表示连到一个实体(例如:硬件设备、文件、网络套接字或者能执行一个或多个不同 I/O 操作(例如:读或写)的程序组件)的开放连接。可以异步地关闭和中断 NIO 通道。所以,如果一个线程在某条通道的 I/O 操作上阻塞时,那么另一个线程可以将这条通道关闭。类似地,如果一个线程在某条通道的 I/O 操作上阻塞时,那么另一个线程可以中断这个阻塞线程。
文件锁机制主要是在多线程同时读写某个文件资源时使用。FileChannel提供了两种加锁机制,lock和tryLock,两者的区别在于,lock是同步的,直至成功才返回,tryLock是异步的,无论成不成功都会立即返回。
public void lock()
public boolean tryLock()
使用内存映射 FileChannel提供的的API为:MappedByteBuffer map(FileChannel.MapMode mode, long position, long size); 映射模式一个有三种:只读 读/写 专用
Selector 类是 Channel 的多路复用器。 Selector 类将传入客户机请求多路分用并将它们分派到各自的请求处理程序。
Selector 对多个 SelectableChannels 的事件进行多路复用。每个 Channel 向 Selector 注册事件。当事件从客户机处到来时, Selector 将它们多路分用并将这些事件分派到相应的 Channel 。
Selector 在 select() 调用时阻塞。接着,它开始等待,直到建立了一个新的连接,或者另一个线程将它唤醒,或者另一个线程将原来的阻塞线程中断。
SelectionKey用完后要调用cancel方法,或者关闭他的channel或selector,要不下次循环认为它是valid还会再次符合key.isAcceptable
在阻塞模式中,线程将在读或写时阻塞,一直到读或写操作彻底完成。如果在读的时候,数据尚未完全到达套接字,则线程将在读操作上阻塞,一直到数据可用。
在非阻塞模式中,线程将读取已经可用的数据(不论多少),然后返回执行其它任务。
Selector的选择方式有三种:select()、select(timeout)、selectNow()。
selectNow的选择过程是非阻塞的,与wakeup没有太大关系。
select(timeout)和select()的选择过程是阻塞的,其他线程如果想终止这个过程,就可以调用wakeup来唤醒。
close()操作限于通道,而且还是实现了InterruptibleChannel接口的通道,例如FileChannel就没有close操作。
一个可选择的通道,在创建之初会生成一个FileDescriptor,linux下即为fd,windows下即为句柄,这些都是系统资源,不能无限占用,当在不使用的时候,就应该将其释放,close即是完成这个工作的。
Selector的begin()中的Interruptible实现的interrupt中就调用了wakeup(),这样一来,当内核poll阻塞中,java线程执行 interrupt(),就会触发wakeup(),从而使得内核优雅的终止阻塞;
至于end(),就更好理解了,poll()结束后,就没有必要再wakeup了,所以就blockOn(null)了。
blockOn我们可以理解为,如果线程被中断,就附带执行我的这个interrupt方法吧。
我们要读取到数据,并没有直接和通道交互;只是和缓冲器交互,并把缓冲器派送到通道。通道要么从缓冲器获得数据,要么向缓冲器发送数据!
用户数据读写<-->ByteBuffer<--->FileChannel
缓冲区
特定基本类型元素的线性有限序列。除内容外,缓冲区的基本属性还包括容量、限制和位置.读写数据时候 本质就是从此缓冲区的“位置”处一直读到“限制”处!
FileChannel的read(ByteBuffer bf):将字节序列从FileChannel读入给定的缓冲区ByteBuffer bf 中!读完以后,bf的当前位置就是从FileChannel中读取的字节个数! 同理,我们应该可以很容易想象出write(ByteBuffer bf)它就是用来将字节序列从给定的缓冲区ByteBuffer bf写入通道FileChannel
ByteBuffer 和通道交换作用
通过告知分配多少存储空间来创建一个ByteBuffer对象
ByteBuffer的flip():反转此缓冲区,将限制设置为当前位置,然后将位置设置为 0.
public class Server extends Thread {
private Socket socket;
public Server(Socket socket) {
this.socket = socket;
}
public void run() {
try {
BufferedReader reader =
new BufferedReader(newInputStreamReader(socket.getInputStream()));
PrintWriter writer = newPrintWriter(socket.getOutputStream());
String data = reader.readLine();
writer.println(data);
writer.close();
socket.close();
} catch (IOException e) { }
}
public static void main(String[] args) throws Exception {
while (true) { new Server((new ServerSocket(8080)).accept()).start(); }
}
}
public class Client {
public static void main(String[] args) throws Exception {
Socket s = new Socket("localhost", 8080);
PrintWriter writer = newPrintWriter(s.getOutputStream());
BufferedReader reader=new BufferedReader(new InputStreamReader(s.getInputStream()));
writer.println("hello");
writer.flush();
System.out.println(reader.readLine());
s.close();
}
}
NIO Socket Client side:
public class Client {
public static void main(String[] args){
try {
InetAddress host = InetAddress.getLocalHost();
InetSocketAddress address = new InetSocketAddress(host, 9999);
SocketChannel channel = SocketChannel.open();
channel.connect(address);
channel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
WritableByteChannel out = Channels.newChannel(System.out);
while(channel.read(buffer) != -1){
buffer.flip();
out.write(buffer);
buffer.clear();
}
} catch (IOException e) {}
}
}
NIO Socket Server side:
public class Server {
public static void main(String[] args){
try {
InetAddress host = InetAddress.getLocalHost();
InetSocketAddress address = new InetSocketAddress(host, 9999);
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.socket().bind(address);
Selector selector = Selector.open();
SelectionKey keys = server.register(selector, SelectionKey.OP_ACCEPT);
while(true){
selector.select();
Set<SelectionKey> set = selector.selectedKeys();
Iterator<SelectionKey> it = set.iterator();
while(it.hasNext()){
SelectionKey key = it.next();
it.remove();
if(key.isAcceptable()){
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel client = serverChannel.accept();
client.configureBlocking(false);
SelectionKey key2 = client.register(selector, SelectionKey.OP_WRITE);
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(new byte[]{}, 0, 1024);
buffer.flip();
key2.attach(buffer);
}else if(key.isWritable()){
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
if(!buffer.hasRemaining()){
buffer.rewind();
int first = buffer.get();
buffer.rewind();
int position = first - ' ' + 1;
buffer.put(new byte[]{} , position, 1024);
buffer.flip();
}
client.write(buffer);
}else if(key.isReadable()){
}
key.cancel();
key.channel().close();
}
}
} catch (Exception e) { }
}
}
SSL(安全套接层)是 Netscape公司在1994年开发的,最初用于WEB浏览器,为浏览器与服务器间的数据传递提供安全保障,提供了加密、来源认证和数据完整性的功能。现在SSL3.0得到了普遍的使用,它的改进版TLS(传输层安全)已经成为互联网标准。SSL本身和TCP套接字连接是很相似的,在协议栈中,SSL可以被简单的看作是安全的TCP连接,但是某些TCP连接的特性它是不支持的,比如带外数据 (out-of-bound)。
在SSL通信协议中,首先服务端必须有一个数字证书,当客户端连接到服务端时,会得到这个证书,然后客户端会判断这个证书是否是可信的,如果是,则交换信道加密密钥,进行通信。如果不信任这个证书,则连接失败。
首先要为服务端生成一个数字证书。Java环境下,数字证书是用keytool生成的,这些证书被存储在store 的概念中,就是证书仓库。我们来调用keytool命令为服务端生成数字证书和保存它使用的证书仓库:
keytool -genkey -v -alias bluedash-ssl-demo-server -keyalg RSA -keystore ./server_ks -dname"CN=localhost,OU=cn,O=cn,L=cn,ST=cn,C=cn" -storepass server -keypass 123123
这样,我们就将服务端证书bluedash-ssl-demo-server保存在了server_ksy这个store文件当中。
SSL Socket Server Side:
public class SSLServer extends Thread {
private static String SERVER_KEY_STORE
="/Users/liweinan/projs/ssl/src/main/resources/META-INF/server_ks";
private static String SERVER_KEY_STORE_PASSWORD ="123123";
private Socket socket;
public SSLServer(Socket socket) {
this.socket = socket;
}
public void run() {
try {
BufferedReader reader
= new BufferedReader(newInputStreamReader(socket.getInputStream()));
PrintWriter writer = newPrintWriter(socket.getOutputStream());
String data = reader.readLine();
writer.println(data);
writer.close();
socket.close();
} catch (IOException e) {}
}
public static void main(String[] args) throws Exception {
System.setProperty("javax.net.ssl.trustStore", SERVER_KEY_STORE);
SSLContext context = SSLContext.getInstance("TLS");
KeyStore ks = KeyStore.getInstance("jceks");
ks.load(new FileInputStream(SERVER_KEY_STORE), null);
KeyManagerFactory kf = KeyManagerFactory.getInstance("SunX509");
kf.init(ks, SERVER_KEY_STORE_PASSWORD.toCharArray());
context.init(kf.getKeyManagers(), null, null);
ServerSocketFactory factory = context.getServerSocketFactory();
ServerSocket _socket = factory.createServerSocket(8443);
((SSLServerSocket) _socket).setNeedClientAuth(false);
while (true) {
new SSLServer(_socket.accept()).start();
}
}
}
●KeyStore ks=KeyStore.getInstance("JKS");
访问Java密钥库,JKS是keytool创建的Java密钥库,保存密钥。
● KeyManagerFactory kmf=KeyManagerFactory.getInstance("SunX509");
创建用于管理JKS密钥库的X.509密钥管理器。
● SSLContext sslContext=SSLContext.getInstance("SSLv3");
构造SSL环境,指定SSL版本为3.0,也可以使用TLSv1,但是SSLv3更加常用。
●sslContext.init(kmf.getKeyManagers(),null,null);
初始化SSL环境。第一个参数是要使用的 KeyManager 数组.第二个参数是TrustManager 对象数组.告诉JSSE使用的可信任证书的来源,设置为null是从javax.net.ssl.trustStore中获得证书。第三个参数是JSSE生成的随机数,这个参数将影响系统的安全性,设置为null是个好选择,可以保证JSSE的安全性。
SSL Socket Client Side:
有了SSL服务端,客户端必须要走SSL协议。由于服务端的证书是我们自己生成的,没有任何受信任机构的签名,所以客户端是无法验证服务端证书的有效性的,通信必然会失败。所以我们需要为客户端创建一个保存所有信任证书的仓库,然后把服务端证书导进这个仓库。这样,当客户端连接服务端时,会发现服务端的证书在自己的信任列表中,就可以正常通信了。
因此现在我们要做的是生成一个客户端的证书仓库,因为keytool不能仅生成一个空白仓库,所以和服务端一样,我们还是生成一个证书加一个仓库(客户端证书加仓库):
keytool -genkey -v -alias bluedash-ssl-demo-client -keyalg RSA -keystore ./client_ks -dname"CN=localhost,OU=cn,O=cn,L=cn,ST=cn,C=cn" -storepass client -keypass 456456
接下来,我们要把服务端的证书导出来,并导入到客户端的仓库。
第一步是导出服务端的证书:
keytool -export -alias bluedash-ssl-demo-server -keystore ./server_ks -file server_key.cer
然后是把导出的证书导入到客户端证书仓库:
keytool -import -trustcacerts -alias bluedash-ssl-demo-server -file ./server_key.cer -keystore ./client_ks
public class SSLClient {
private static String CLIENT_KEY_STORE
="/Users/liweinan/projs/ssl/src/main/resources/META-INF/client_ks";
public static void main(String[] args) throws Exception {
System.setProperty("javax.net.ssl.trustStore", CLIENT_KEY_STORE);
System.setProperty("javax.net.debug","ssl,handshake");
SSLClient client = new SSLClient();
Socket s = client.clientWithoutCert();
PrintWriter writer = newPrintWriter(s.getOutputStream());
BufferedReader reader
= new BufferedReader(newInputStreamReader(s.getInputStream()));
writer.println("hello");
writer.flush();
System.out.println(reader.readLine());
s.close();
}
private Socket clientWithoutCert() throws Exception {
//我们可以不在程序中指定SSL环境,而是在执行客户端程序时指定。需要注意的是
客户端并没有导入证书,而是采用了默认的工厂方法构造SSLSocket
SocketFactory sf = SSLSocketFactory.getDefault();
Socket s = sf.createSocket("localhost", 8443);
return s;
}
}
以上,我们便完成了SSL单向握手通信。即:客户端验证服务端的证书,服务端不认证客户端的证书。
我们来看一下SSL双向认证的全过程:
第一步: 客户端发送ClientHello消息,发起SSL连接请求,告诉服务器自己支持的SSL选项(加密方式等)。
ClientHello, TLSv1
第二步: 服务器响应请求,回复ServerHello消息,和客户端确认SSL加密方式:
ServerHello, TLSv1
第三步: 服务端向客户端发布自己的公钥。
第四步: 客户端与服务端的协通沟通完毕,服务端发送ServerHelloDone消息:
ServerHelloDone
第五步: 客户端使用服务端给予的公钥,创建会话用密钥(SSL证书认证完成后,为了提高
性能,所有的信息交互就可能会使用对称加密算法),并通过 ClientKeyExchange
消息发给服务器:ClientKeyExchange, RSA PreMasterSecret, TLSv1
第六步: 客户端通知服务器改变加密算法,通过ChangeCipherSpec消息发给服务端:
main, WRITE: TLSv1 Change Cipher Spec, length = 1
第七步: 客户端发送Finished消息,告知服务器请检查加密算法的变更请求:Finished
第八步:服务端确认算法变更,返回ChangeCipherSpec消息
main, READ: TLSv1 Change Cipher Spec, length = 1
第九步:服务端发送Finished消息,加密算法生效: Finished
那么如何让服务端也认证客户端的身份,即双向握手呢?其实很简单,在服务端代码中,把这一行:
((SSLServerSocket) _socket).setNeedClientAuth(false);
改成: ((SSLServerSocket) _socket).setNeedClientAuth(true);
就可以了。但是,同样的道理,现在服务端并没有信任客户端的证书,因为客户端的证书也是自己生成的。所以,对于服务端,需要做同样的工作:把客户端的证书导出来,并导入到服务端的证书仓库:
完成了证书的导入,还要在客户端需要加入一段代码,用于在连接时,客户端向服务端出示自己的证书:
public class SSLClient {
private static String CLIENT_KEY_STORE
="/Users/liweinan/projs/ssl/src/main/resources/META-INF/client_ks";
private static String CLIENT_KEY_STORE_PASSWORD ="456456";
public static void main(String[] args) throws Exception {
// Set the key store to use for validating the server cert.
System.setProperty("javax.net.ssl.trustStore", CLIENT_KEY_STORE);
System.setProperty("javax.net.debug","ssl,handshake");
SSLClient client = new SSLClient();
Socket s = client.clientWithCert();
PrintWriter writer = newPrintWriter(s.getOutputStream());
BufferedReader reader= new BufferedReader(new InputStreamReader(s.getInputStream()));
writer.println("hello");
writer.flush();
System.out.println(reader.readLine());
s.close();
}
private Socket clientWithCert() throws Exception {
SSLContext context = SSLContext.getInstance("TLS");
KeyStore ks = KeyStore.getInstance("jceks");
ks.load(new FileInputStream(CLIENT_KEY_STORE), null);
KeyManagerFactory kf = KeyManagerFactory.getInstance("SunX509");
kf.init(ks, CLIENT_KEY_STORE_PASSWORD.toCharArray());
context.init(kf.getKeyManagers(), null, null);
SocketFactory factory = context.getSocketFactory();
Socket s = factory.createSocket("localhost", 8443);
return s;
}
}
public class SSLNewServer {
private boolean handshakeDone = false;
private Selector selector;
private SSLEngine sslEngine;
private SSLContext sslContext;
private ByteBuffer appOut; // clear text buffer for out
private ByteBuffer appIn; // clear text buffer for in
private ByteBuffer netOut; // encrypted buffer for out
private ByteBuffer netIn; // encrypted buffer for in
private CharsetEncoder encoder = Charset.forName("UTF8").newEncoder();
private CharsetDecoder decoder = Charset.forName("UTF8").newDecoder();
public SSLNewServer() {
createServerSocket();
createSSLContext();
createSSLEngines();
createBuffers();
}
public static void main(String[] args) {
SSLNewServer sns = new SSLNewServer();
sns.selecting();
}
//create the server socket, bind it to port 1234, set unblock and register the "accept" only
private void createServerSocket() throws IOException{
selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(1234));
ssc.configureBlocking(false);
ssc.register(selector, SelectionKey.OP_ACCEPT);
}
//the ssl context initialization
private void createSSLContext() throws Exception {
KeyStore ks = KeyStore.getInstance("JKS");
KeyStore ts = KeyStore.getInstance("JKS");
char[] passphrase = "123456".toCharArray();
ks.load(new FileInputStream("ssl/kserver.keystore"), passphrase);
ts.load(new FileInputStream("ssl/tserver.keystore"), passphrase);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, passphrase);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(ts);
SSLContext sslCtx = SSLContext.getInstance("SSL");
sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
sslContext = sslCtx;
}
private void createSSLEngines() {
sslEngine = sslContext.createSSLEngine();
sslEngine.setUseClientMode(false);//work in a server mode
sslEngine.setNeedClientAuth(true);//need client authentication
}
private void createBuffers()
{
SSLSession session = sslEngine.getSession();
int appBufferMax = session.getApplicationBufferSize();
int netBufferMax = session.getPacketBufferSize();
//server only reply this sentence
appOut = ByteBuffer.wrap("This is an SSL Server".getBytes());
//appIn is bigger than the allowed max application buffer siz
appIn = ByteBuffer.allocate(appBufferMax + 10);
//direct allocate for better performance
netOut = ByteBuffer.allocateDirect(netBufferMax);
netIn = ByteBuffer.allocateDirect(netBufferMax);
}
public void selecting() {
while (true) {
try {
selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = (SelectionKey) iter.next();
iter.remove();
handle(key);
}
} catch (SSLException e) {
} catch (IOException e) {
}
}
}
private void handle(SelectionKey key) throws IOException{
if(key.isAcceptable()) {
try {
SocketChannel sc = ((ServerSocketChannel)key.channel()).accept();
doHandShake(sc); //if it is an accept event, do the handshake in a blocking mode
} catch (ClosedChannelException e) {
} catch (IOException e) {
}
}
else if(key.isReadable()) {
if (sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING) {
SocketChannel sc = (SocketChannel) key.channel();
sc.read(netIn);
netIn.flip();
SSLEngineResult engineResult = sslEngine.unwrap(netIn, appIn);
doTask();
netIn.compact();
if (engineResult.getStatus() == SSLEngineResult.Status.OK) {
System.out.println("text recieved");
appIn.flip();// ready for reading
System.out.println(decoder.decode(appIn));
appIn.compact();
}
else if(engineResult.getStatus() == SSLEngineResult.Status.CLOSED) {
doSSLClose(key);
}
}
}
else if(key.isWritable()) {
SocketChannel sc = (SocketChannel) key.channel();
//if(!sslEngine.isOutboundDone()) {
//netOut.clear();
SSLEngineResult engineResult = sslEngine.wrap(appOut, netOut);
doTask();
if (engineResult.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING)
{
System.out.println("text sent");
}
netOut.flip();
sc.write(netOut);
netOut.compact();
//}
}
}
private void doHandShake(SocketChannel sc) throws IOException {
sslEngine.beginHandshake();//explicitly begin the handshake
HandshakeStatus hsStatus = sslEngine.getHandshakeStatus();
while (!handshakeDone) {
switch(hsStatus){
case FINISHED:
//the status become FINISHED only when the ssl handshake is finished
//but we still need to send data, so do nothing here
break;
case NEED_TASK:
//do the delegate task if there is some extra work such as checking the keystore during
//the handshake
hsStatus = doTask();
break;
case NEED_UNWRAP:
//unwrap means unwrap the ssl packet to get ssl handshake information
sc.read(netIn);
netIn.flip();
hsStatus = doUnwrap();
break;
case NEED_WRAP:
//wrap means wrap the app packet into an ssl packet to add ssl handshake information
hsStatus = doWrap();
sc.write(netOut);
netOut.clear();
break;
case NOT_HANDSHAKING:
//now it is not in a handshake or say byebye status. here it means handshake is over
//and ready for ssl talk
sc.configureBlocking(false);//set the socket to unblocking mode
sc.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE);
handshakeDone = true;
break;
}
}
}
private HandshakeStatus doTask() {
Runnable runnable;
while ((runnable = sslEngine.getDelegatedTask()) != null) {
runnable.run();
}
HandshakeStatus hsStatus = sslEngine.getHandshakeStatus();
if (hsStatus == HandshakeStatus.NEED_TASK) {
//throw new Exception("handshake shouldn't need additional tasks");
}
return hsStatus;
}
private HandshakeStatus doUnwrap() throws SSLException{
HandshakeStatus hsStatus;
do{//do unwrap until the state is change to "NEED_WRAP"
SSLEngineResult engineResult = sslEngine.unwrap(netIn, appIn);
hsStatus = doTask();
}while(hsStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP
&& netIn.remaining()>0);
netIn.clear();
return hsStatus;
}
private HandshakeStatus doWrap() throws SSLException{
HandshakeStatus hsStatus;
SSLEngineResult engineResult = sslEngine.wrap(appOut, netOut);
hsStatus = doTask();
netOut.flip();
return hsStatus;
}
//close an ssl talk, similar to the handshake steps
private void doSSLClose(SelectionKey key) throws IOException {
SocketChannel sc = (SocketChannel) key.channel();
key.cancel();
sc.configureBlocking(true);
HandshakeStatus hsStatus = sslEngine.getHandshakeStatus();
while(handshakeDone) {
switch(hsStatus) {
case FINISHED:
break;
case NEED_TASK:
hsStatus = doTask();
break;
case NEED_UNWRAP:
sc.read(netIn);
netIn.flip();
hsStatus = doUnwrap();
break;
case NEED_WRAP:
hsStatus = doWrap();
sc.write(netOut);
netOut.clear();
break;
case NOT_HANDSHAKING:
handshakeDone = false;
sc.close();
break;
}
}
}
}
NIO中有socketChannel但是没有sslSocketChannel;ssl的实现也不应该依赖于是NIO还是传统的基于stream的IO
为了支持nio实现ssl通讯,从java1.5开始增加了javax.net.ssl.SSLEngine用于支持nio的ssl,这个SSLEngine的作用就像一个状态机,维护着ssl通讯中各个状态以及下一个状态。
SSLEngine正是完成了管理状态,封装应用程序数据发往网络,解析网络数据并传递给应用程序的角色。
SSL通讯过程,主要包括握手,对话,关闭对话,三个步骤。其中握手部分的主要内容有协商协议,相互验证,生成并交换对称密钥,其中相互验证和对称密钥的交换是由非对称加密来完成的。在对话过程中,实际的明文是由之前生成的对称密钥来加密的。当对话结束后,互相发送结束信号结束通讯。
SSLEngine负责入栈和出栈数据加密和解密以及加密解密算法的协商。加密解密的算法是通过握手协议协商完成的。
先看看SSLEngine有哪些状态,有哪些工作要做,假设从一个ssl server看。
首先在握手阶段,需要和client程序多次握手,进行身份验证,对称密钥生成等工作,在这段时间里,并没有实际的应用层数据交换,而只有SSL协议数据的交换。且不看实际传输的内容和意义,在握手过程中。Server的SSLEngine初始化后总是等待client的请求(等待接收数据),此时它的状态是 NEED_UNWRAP,unwrap是解包的意思,这意味着,SSLEngine等待解析一个SSL的数据包,当server收到数据包后,在nio中数据包总是放在一个buffer里而不再是读stream,我们把这个buffer交给SSLEngine,调用它的unwrap方法,SSLEngine会解析这个数据包,把其中关于SSL握手的信息提取出来,并改变自己的状态,此处它将变成NEED_WRAP状态,意味着打包,它需要把对应的SSL回复内容写到数据包中返回到客户端。以此类推,SSLEngine多数时间总是在解包和打包两个状态间切换,尤其是在实际通讯时,注意到在unwrap和wrap函数中都有一个源buffer和一个目的buffer,因为SSLEngine不仅提取 SSL协议相关的内容还要解密网络数据并把明文传递给应用程序,这其实才是这两个函数名字的来源,只不过在握手过程中,并没有实际的数据,而只有SSL协议信息,所以那个目的buffer总是没有东西。可以把SSL通讯看做交换礼物,SSLEngine把包裹拆了把礼物给你,或者他把礼物包起来送走,只是在SSL握手时,那个包裹里没有礼物,SSLEngine只是拆了个空包裹或是寄了个空包裹。那么还有没有其它状态,有一个FINISHED 状态,表示这次handshake完成了;
而当进入实际交换数据的时候,这个状态是NOT_HANDSHAKE,表示当前不在握手,一般这个时候只需要在socket可读时,调用unwrap函数解密来自网络的SSL数据包,在 socket可写的时候调用wrap函数把明文数据加密发送出去。还有一个状态NEED_TASK,首先要知道一点SSLEngine是异步的,wrap 和unwrap函数调用都会立刻返回,比如在server收到client第一次请求后,会调用unwrap,但实际上SSLEngine还会做很多工作,比如访问Keystore文件,这些操作是费时的,但是实际上函数却立刻返回了,这时候SSLEngine会进入NEED_TASK状态,而不是立刻进入NEED_WRAP状态,所以必须让SSLEngine完成手头的工作,才能进入下一步工作,这时可以调用SSLEngine的 getDelegatedTask()方法获得那个尚未完成的工作,它是一个Runnable的对象,可以调用它的run方法等待他完成,如果你是个高并发的server,也可以在这个时候做其他事情,等待这个工作完成,再接下去做wrap工作。另外还有一个非常容易出错的地方,一个 NEED_UNWRAP状态的下一个状态然有可能是NEED_UNWRAP,并且一次调用unwrap方法并不一定把buffer中的所有内容都解包出来,可能还有内容需要在一次unwrap才能把所有东西都解析完,我遇到的这种情况发生在用nio的server和老的SSLSocket通讯时,client只向server一次性发送了这些数据,而server端需要连续两次unwrap才能把client的数据完整处理掉。
除了上述4个状态描述了SSLEngine的状态,还有4个状态用于描述每次调用wrap和unwrap后的结果状态。它们分别是 BUFFER_OVERFLOW表示目标buffer没有足够的空间来存放解包的内容,这往往是因为你的目的buffer太小,或者在buffer在写入前没有clear;BUFFER_UNDERFLOW表示源buffer没足够内容让SSLEngine来解包,这往往是因为,可能还有数据尚未到达,或者在buffer读取前没有flip;CLOSED表示通讯的某一段正试图结束这个SSL通讯
SSLServerSocket和SSLSocket是阻塞的socket调用。这连个类分别继承自ServerSocket和 Socket, 封装了SSL(Secure Socket Layer)和TLS(Transport Layer Security), 提供了安全套接字。ServerSocket和Socket是阻塞的现实, 因此SSLServerSocket和SSLSocket也是阻塞的实现。
如果要实现非阻塞的安全套接字,需要将SSLEngine和SocketChannel结合使用。
首先NIO的socket基本都通过Selector来实现,把socket 的accept,read,write事件都注册到selector上,不断的循环select()就可以,只是对于一个SSL Server Socket而言,它只是个普通的ServerSocket,首先只关心accept事件,所以首先这在selector上注册一个事件。
当serversocket接收到一个SSL client的请求后,就要开始进行握手,这个过程是同步的,所以先不要吧read和write事件也注册到selector上,当完成握手后,才注册这两个事件,并把socket设置成非阻塞。当select到socket可读时先调用unwrap方法,可写时先调用wrap方法。
每个socket都有两组buffer,分别是appIn,netIn和appOut,netOut,其中netXX都代表从socket中读取或写入的东西,他们都是加了密的,而appXX代表应用程序可理解的数据内容,它们都通过SSLEngine的wrap和unwrap方法才能与netXX 相互转换。
S1: 入口是SSLEngine
S2: SSLEngine与SocketChannel的整个生命周期都有关,所以需要考虑重用在不同的SelectionKey的 registration(s)之间的同一个SSLEngine。
S3: 对应HTTP协议,你仅可能的使用SelectionKey.attach()管理这个association(SSLEngine)。
S4: 直接将SSLEngine attach 到SelectionKey,并使用SSLSession:
((SSLEngine)selectionKey.attachment()).getSession().putValue((EXPIRE_TIME,System.currentTimeMillis()); 使用SSLEngine.getSession()保存数据结构,不需要同步(synchronize)pool或同步创建自己的数据结构