基本概念
- BIO:是阻塞I/O,不管是磁盘I/O,还是网络I/O,数据在写入OutputStream和InputStream都可能发生阻塞,一旦有阻塞,线程会失去CPU的使用权(阻塞)。
- NIO:简单的说就是非阻塞式I/O(单个I/O阻塞时不阻塞线程),一个线程可以负责多个I/O连接(利用serverSocketChannels来接收),取一个准备好接收数据的连接(注册到Selector轮询调度),尽快地用连接向缓存区(利用buffer优化I/O)填充数据,然后转向下一个准备好的连接。
- 缓存区(buffer):通信通道(channel)用来传输数据的介质,在NOI模型中,不再通向输出流写入数据或从输入流读取数据,而是在缓存区中读写数据,能够有效减少I/O中断次数,调优I/O。(我会写博客专门讲缓存区的…链接留个位置)
- 通道(channel):负责将缓冲区的数据块移入或移出到各种I/O源(我会写博客专门讲通道的…链接留个位置)
- 就绪选择(selector):为完成就绪选择,要将不同的通道注册到一个Selector对象。每个通道分配有一个SeletionKey。然后程序可以通过询问Selector对象,得知哪些通道已经准备就绪,可以无阻塞地完成I/O操作,可以向Selector对象查询相应的键集合。
通道和I/O流的区别
- 流和通道间的关键区别是流是基于字节的,而通道是基于块的,块的单次中断传输的数据量远大于字节,所以性能是有优势,当然,出于性能考虑,流也可以传输字节数组。
- 通常情况下(如网络通道),通道的缓冲区支持单个通道的读写,而流是只读或者只写的。CDROM是只读通道,但这只是特例
理解I/O:性能的瓶颈
- 现代计算机基于冯诺依曼的存储执行模型,所以数据在计算机部件间的传输速率决定了计算机的执行效率,但是各级存储(CPU寄存器、缓存、内存、硬盘)的传输速度差异巨大(数量级上的差距),在传统的BIO模式下,这导致高速计算部件的大量时间浪费在等待数据传输上。然而计算机作为节点的计算机网络中,问题依旧存在。
- 传输速度上:
CPU>内存>磁盘>网络(一般情况下)
- 传统的做法是通过缓存和多线程解决这一问题:
- 高速缓存能够减少中断次数(单次传输的数据量增大,数据总量不变)和中断的时间(缓存一般比原始存储位置传输速率快)
- 多线程可以让单个线程处理一个I/O,不会影响线程获得CPU资源,但是当I/O连接数量增多时,线程的数量随之增加,生成多个线程以及线程之间切换的开销是不容忽略的,线程管理的开销(时间+资源)极大地降低了系统性能
- 借鉴多线程对于I/O的解决方案,我们进一步解决的就是优化掉创建线程和线程间切换带来的巨大的开销,那么也就是采用单个线程非阻塞地管理多个I/O连接,也就是我们要讲的NIO。
例讲Channel和buffer的简单使用
枯燥的讲解API没有意义,也太没意思了,所以我采取更加直观的例子来说明API,不严谨但更简单直观,首先为了方便测试我们利用Channel写的Client,我们先用简单的几行代码实现一个本地的测试服务器:
package com.company;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class TestServer {
public static void main(String[] args){
//服务器监听请求的端口
int port = 9999;
ServerSocket server=null;
try {
server = new ServerSocket(port);
}catch( IOException e ){
System.out.println("服务器创建失败");
e.printStackTrace();
}
Socket temp=null;
try{
temp = server.accept();
}catch( IOException e ){
System.out.println("获取连接失败");
e.printStackTrace();
}
OutputStream output = null;
try{
output = temp.getOutputStream();
}catch ( Exception e ){
e.printStackTrace();
}
byte [] buffer = "llin of Tianjin University love JAVA and Alibaba!".getBytes();
while ( true ) {
try {
output.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
try {
//防止传输信息过于快,不方便我们测试
Thread.sleep(1000);
}catch ( InterruptedException e ){
e.printStackTrace();
}
}
}
}
主要作用是间隔一段时间向客户端发送一段信息,用来测试客户端的channel是否实际发挥了作用
下面是一个例子,可以用来简单熟悉一下JAVA中的Buffer和Channel接口的使用,虽然实际中这样使用的意义不大
package com.company;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
/**
* Created by liulin on 16-4-12.
*/
public class TestClient {
//默认的IP
public static final String ipAddress = "127.0.0.1";
//默认端口
public static final int DEFAULT_PORT = 9999;
//缓存的大小
public static final int BUFFER_SIZE = 200;
public static void main ( String [] args ){
if ( args.length == 0 ){//如果没有给出IP地址那么没办法找到测试服务器啊
System.out.println( "请输入服务器的IP地址或域名");
}
//给定端口那么使用指定端口,否则使用默认端口
int port=0;
try{
port = Integer.parseInt(args[1]);
}catch ( Exception e ){
port = DEFAULT_PORT;
e.printStackTrace();
}
try{//本例只是让大家熟悉JAVA提供的Buffer,Channel的API,让不熟悉Socket的同学学习下Socket机制
//所以不采用非阻塞机制,也不使用selector
SocketAddress address = new InetSocketAddress( ipAddress , port );
//通过静态工厂获得指定address的通道实例
SocketChannel client = SocketChannel.open(address);
//通过静态工厂获得指定大小的缓存
ByteBuffer buffer = ByteBuffer.allocate( BUFFER_SIZE );
WritableByteChannel out = Channels.newChannel( System.out );//输出通道
//从服务器读取数据
while ( client.read(buffer) != -1 ){
//将缓存的position置为buffer内置数组的初始位置,当前position位置设置为limit
//position,limit,capacity的概念会在专门介绍buffer的博客中一齐讲解
//简单地讲,就是告诉buffer,我们要开始从头读取/修改你了
buffer.flip();
out.write( buffer );
//为了方便查看调试信息,输出一个换行
System.out.write( "\n".getBytes());
//只是修改position为buffer内置数组初始位置,limit设置为capacity
//目前可以简单地理解为清空缓存(详情我会在专门介绍buffer的博客中一齐讲解)
buffer.clear();
}
}catch ( Exception e ){
e.printStackTrace();
}
}
}
主要就是利用Channel连接到服务器并且通过buffer进行读写,输出到控制台的一个简单的例子,执行效果如下(测试的时候一定要先打开测试服务器):
用一个NIO的HTTP服务器讲解NIO
作为一个有着TDD思维的程序猴子,怎么能不先写测试用的客户端呢….其实只是无耻地在之前客户端添了一行代码
package com.company;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
/**
* Created by liulin on 16-4-12.
*/
public class TestClient {
//默认的IP
public static final String ipAddress = "127.0.0.1";
//默认端口
public static final int DEFAULT_PORT = 9999;
//缓存的大小
public static final int BUFFER_SIZE = 200;
public static void main ( String [] args ){
if ( args.length == 0 ){//如果没有给出IP地址那么没办法找到测试服务器啊
System.out.println( "请输入服务器的IP地址或域名");
}
//给定端口那么使用指定端口,否则使用默认端口
int port=0;
try{
port = Integer.parseInt(args[1]);
}catch ( Exception e ){
port = DEFAULT_PORT;
e.printStackTrace();
}
try{//本例只是让大家熟悉JAVA提供的Buffer,Channel的API,让不熟悉Socket的同学学习下Socket机制
//所以不采用非阻塞机制,也不使用selector
SocketAddress address = new InetSocketAddress( ipAddress , port );
//通过静态工厂获得指定address的通道实例
SocketChannel client = SocketChannel.open(address);
//通过静态工厂获得指定大小的缓存
ByteBuffer buffer = ByteBuffer.allocate( BUFFER_SIZE );
WritableByteChannel out = Channels.newChannel( System.out );//输出通道
//从服务器读取数据
client.write( ByteBuffer.wrap("testing...".getBytes()));
while ( client.read(buffer) != -1 ){
//将缓存的position置为buffer内置数组的初始位置,当前position位置设置为limit
//position,limit,capacity的概念会在专门介绍buffer的博客中一齐讲解
//简单地讲,就是告诉buffer,我们要开始从头读取/修改你了
buffer.flip();
out.write( buffer );
//为了方便查看调试信息,输出一个换行
System.out.write( "\n".getBytes());
//只是修改position为buffer内置数组初始位置,limit设置为capacity
//目前可以简单地理解为清空缓存(详情我会在专门介绍buffer的博客中一齐讲解)
buffer.clear();
}
}catch ( Exception e ){
e.printStackTrace();
}
}
}
下面是由selector调度的HTTP服务器,讲解写在了注释中,如果还有不明白的可以在评论中问
package com.company;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
/**
* Created by liulin on 16-4-12.
*/
public class TestClient {
//默认的IP
public static final String ipAddress = "127.0.0.1";
//默认端口
public static final int DEFAULT_PORT = 9999;
//缓存的大小
public static final int BUFFER_SIZE = 200;
public static void main ( String [] args ){
if ( args.length == 0 ){//如果没有给出IP地址那么没办法找到测试服务器啊
System.out.println( "请输入服务器的IP地址或域名");
}
//给定端口那么使用指定端口,否则使用默认端口
int port=0;
try{
port = Integer.parseInt(args[1]);
}catch ( Exception e ){
port = DEFAULT_PORT;
e.printStackTrace();
}
try{//本例只是让大家熟悉JAVA提供的Buffer,Channel的API,让不熟悉Socket的同学学习下Socket机制
//所以不采用非阻塞机制,也不使用selector
SocketAddress address = new InetSocketAddress( ipAddress , port );
//通过静态工厂获得指定address的通道实例
SocketChannel client = SocketChannel.open(address);
//通过静态工厂获得指定大小的缓存
ByteBuffer buffer = ByteBuffer.allocate( BUFFER_SIZE );
WritableByteChannel out = Channels.newChannel( System.out );//输出通道
//从服务器读取数据
client.write( ByteBuffer.wrap("testing...".getBytes()));
while ( client.read(buffer) != -1 ){
//将缓存的position置为buffer内置数组的初始位置,当前position位置设置为limit
//position,limit,capacity的概念会在专门介绍buffer的博客中一齐讲解
//简单地讲,就是告诉buffer,我们要开始从头读取/修改你了
buffer.flip();
out.write( buffer );
//为了方便查看调试信息,输出一个换行
System.out.write( "\n".getBytes());
//只是修改position为buffer内置数组初始位置,limit设置为capacity
//目前可以简单地理解为清空缓存(详情我会在专门介绍buffer的博客中一齐讲解)
buffer.clear();
}
}catch ( Exception e ){
e.printStackTrace();
}
}
}
测试结果如下:
具体的细节,我还会在新的博客里详谈,这篇篇幅太长了
参考书目
Java网络编程(第三版)
深入分析Java Web技术内幕