当前位置: 代码迷 >> Web前端 >> 您还在用IO吗
  详细解决方案

您还在用IO吗

热度:161   发布时间:2012-11-23 22:54:33.0
你还在用IO吗?

回顾传统
有必要了解传统的I/O操作的方式。以网络应用为例,传统方式需要监听一个ServerSocket,接受请求的连接为其提供服务(服务通常包括了处理请求并发送响应),下图是服务器的生命周期图,其中标有粗黑线条的部分表明会发生I/O阻塞。

?

?

ServerSocket server=new ServerSocket(10000);
//接受新的连接请求 
Socket newConnection=server.accept();
//对于accept方法的调用将造成阻塞,直到ServerSocket接受到一个连接请求为止。一旦连接请求被接受,服务器可以读客户socket中的请求。
InputStream in = newConnection.getInputStream();
InputStreamReader reader = new InputStreamReader(in);
BufferedReader buffer = new BufferedReader(reader);
Request request = new Request();
while(!request.isComplete()) {
String line = buffer.readLine();
request.addLine(line);
}
//这样的操作有两个问题,首先BufferedReader类的readLine()方法在其缓冲区未满时会造成线程阻塞,只有一定数据填满了缓冲区或者客户关闭了套接字,方法才会返回。其次,它回产生大量的垃圾,BufferedReader创建了缓冲区来从客户套接字读入数据,但是同样创建了一些字符串存储这些数据。虽然BufferedReader内部提供了StringBuffer处理这一问题,但是所有的String很快变成了垃圾需要回收。
//同样的问题在发送响应代码中也存在
Response response = request.generateResponse();
OutputStream out = newConnection.getOutputStream();
InputStream in = response.getInputStream();
int ch;
while(-1 != (ch = in.read())) {
out.write(ch);
}
newConnection.close();
//类似的,读写操作被阻塞而且向流中一次写入一个字符会造成效率低下,所以应该使用缓冲区,但是一旦使用缓冲,流又会产生更多的垃圾。

??

?? 传统的解决方法
? 通常在Java中处理阻塞I/O要用到线程(大量的线程)。一般是实现一个线程池用来处理请求,如下图:

?

??

? 线程使得服务器可以处理多个连接,但是它们也同样引发了许多问题。每个线程拥有自己的栈空间并且占用一些CPU时间,耗费很大,而且很多时间是浪费在阻塞的I/O操作上,没有有效的利用CPU。


? 选择NIO

? NIO包(java.nio.*)引入了四个关键的抽象数据类型,它们共同解决传统的I/O类中的一些问题。
1. Buffer:它是包含数据且用于读写的线形表结构。其中还提供了一个特殊类用于内存映射文件的I/O操作。
2. Charset:它提供Unicode字符串影射到字节序列以及逆影射的操作。
3. Channels:包含socket,file和pipe三种管道,它实际上是双向交流的通道,你可能注意到现有的java.io类中没有一个能够读写Buffer类型,所以NIO中提供了Channel类来读写Buffer。channel就是一个读写的管道,通过管道的读写来完成IO操作。channel分为ServerSocketChannel和SocketChannel,前者用于监听,获得客户端的连接,后者直接用于操作IO,来看看Channel如何进行Socket操作
4. Selector:它将多元异步I/O操作集中到一个或多个线程中,在过去的阻塞I/O中,我们一般知道什么时候可以向stream中读或写,因为方法调用直到stream准备好时返回。但是使用非阻塞通道,我们需要一些方法来知道什么时候通道准备好了。在NIO包中,设计Selector就是为了这个目的。SelectableChannel可以注册特定的事件,而不是在事件发生时通知应用,通道跟踪事件。然后,当应用调用Selector上的任意一个selection方法时,它查看注册了的通道看是否有任何感兴趣的事件发生

?

String host = 127.0.0.1;
InetSocketAddress socketAddress = new InetSocketAddress(host, 80);
//默认情况下,所有channel(包括ServerSocketChannel, SocketChannel)的工作模式是阻塞
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.connect(socketAddress);
//阻塞监听客户端,获得客户端的连接channel
SocketChannel sc = ssc.accept();
Charset charset = Charset.forName("ISO-8859-1");
CharsetEncoder encoder = charset.newEncoder();
String request = "GET / \r\n\r\n";
//阻塞写
sc.write(encoder.encode(CharBuffer.wrap(request)));
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
//阻塞读
while(sc.read(buffer) != -1){
    //do something
}

??

NIO通过channel来操作IO,概念上更加清晰。但发现:它仍然是阻塞的操作模式,本质上于传统Socket IO来比,工作模式并没有本质上的变化呀,还不是要分配线程。的确,如果NIO只带来了这些,那么NIO也没有什么优势。但是NIO带来的不只是这些,这些channel可以配置成no-blocking模式,借助于selector,NIO带来了一种新的socket编写模式:

?

String host = 127.0.0.1;
InetSocketAddress socketAddress = new InetSocketAddress(host, 80);
ServerSocketChannel  ssc = ServerSocketChannel.open();
//配置channel的阻塞模式
ssc.configureBlocking(false);
ssc.connect(socketAddress);
Selector selector = Selector.open(); 
//将ServerSocketChannel注册到selector上,selector可以检测多路channel
ssc.register(selctor, SelectionKey.OP_ACCEPT);

while(true){
    //阻塞等待事件响应
    selector.select();
    Set<SelectionKey> selectionKeys = selector.selectedKeys();
    Iterator<SelectionKey> it = selectionKeys.iterator();
    //获得多路的channel,这些channel此时都已准备就绪,工作在非阻塞模式,可以非阻塞读写
    while (it.hasNext()) { 
            SelectionKey key = it.next(); 
            it.remove(); 
            if (key.isAcceptable()) { 
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); 
        //此时的accept是非阻塞的,立马返回
            SocketChannel channel = serverSocketChannel.accept(); 
            channel.configureBlocking(false); 
        //可以不断得将这些channel注册成不同的类型,使之即可读,又可写
            channel.register(selector, SelectionKey.OP_READ); 
            } else if (key.isReadable()) { 
            SocketChannel channel = (SocketChannel) key.channel();
        //非阻塞读,立马返回数据
        channel.read(buffer);
        //...
        SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_WRITE); 
            selectionKey.attach(new HandleClient(clientName));
              } else if (key.isWritable()) { 
            SocketChannel channel = (SocketChannel) key.channel(); 
            HandleClient handleClient = (HandleClient) key.attachment(); 
            ByteBuffer buffer = handleClient.readBlock(); 
       
            } 
        } 
}

?

在这种模式下,原来负责端口监听的accept()方法换成了select()方法,两者都是阻塞的,本质上没有分别.区别在于select()之后返回的所有channel都是非阻塞的,都是可以马上读写的而accept()之后的channel则是阻塞的,不能保证此时返回的channel的读写能够马上返回。因此,NIO的非阻塞方式就可以设置比较少的线程,因为这些线程拿到的channel都是立马可以读写的,这些线程的工作都是满负荷的,效率高。反之,阻塞方式需要创建同样较多的线程,因为这些线程很多都处于阻塞休眠状态,大家都不是满负荷在工作。这样NIO的优点就很明显了。

  相关解决方案