基本概念
- 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。我们先用简单的几行代码实现一个本地的測试server:
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){
//server监听请求的端口
int port = 9999;
ServerSocket server=null;
try {
server = new ServerSocket(port);
}catch( IOException e ){
System.out.println("server创建失败");
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地址那么没办法找到測试server啊
System.out.println( "请输入server的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 );//输出通道
//从server读取数据
while ( client.read(buffer) != -1 ){
//将缓存的position置为buffer内置数组的初始位置,当前position位置设置为limit
//position,limit,capacity的概念会在专门介绍buffer的博客中一齐解说
//简单地讲,就是告诉buffer。我们要開始从头读取/改动你了
buffer.flip();
out.write( buffer );
//为了方便查看调试信息,输出一个换行
System.out.write( "
".getBytes());
//仅仅是改动position为buffer内置数组初始位置。limit设置为capacity
//眼下能够简单地理解为清空缓存(详情我会在专门介绍buffer的博客中一齐解说)
buffer.clear();
}
}catch ( Exception e ){
e.printStackTrace();
}
}
}
主要就是利用Channel连接到server而且通过buffer进行读写。输出到控制台的一个简单的样例,运行效果例如以下(測试的时候一定要先打开測试server):
用一个NIO的HTTPserver解说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地址那么没办法找到測试server啊
System.out.println( "请输入server的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 );//输出通道
//从server读取数据
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( "
".getBytes());
//仅仅是改动position为buffer内置数组初始位置。limit设置为capacity
//眼下能够简单地理解为清空缓存(详情我会在专门介绍buffer的博客中一齐解说)
buffer.clear();
}
}catch ( Exception e ){
e.printStackTrace();
}
}
}
以下是由selector调度的HTTPserver,解说写在了凝视中,假设还有不明确的能够在评论中问
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地址那么没办法找到測试server啊
System.out.println( "请输入server的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 );//输出通道
//从server读取数据
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( "
".getBytes());
//仅仅是改动position为buffer内置数组初始位置。limit设置为capacity
//眼下能够简单地理解为清空缓存(详情我会在专门介绍buffer的博客中一齐解说)
buffer.clear();
}
}catch ( Exception e ){
e.printStackTrace();
}
}
}
測试结果例如以下:
详细的细节,我还会在新的博客里详谈,这篇篇幅太长了
參考书目
Java网络编程(第三版)
深入分析Java Web技术内幕