Java NIO基础 -我们到底能走多远系列(17)

我们到底能走多远系列(17)

扯淡:

  长期接触所谓web框架的企业级应用的开发者们,不知道你们有没有这样的感受,几年之后,发现:路越走越窄,学的东西大多是表层的,编程的技巧没有太大的改变,接触大量的所谓框架也写不出一个核心的模块,学习了框架的原理后也不会很好的设计,大量调用别人的库函数感觉看得懂业务的人都可以写。

  我觉得作为从事编码行业的我们,埋头苦干是必备的素质,但是抬头思考却是核心的竞争力,因为前者是进步的一个条件,而后者则是进步的原动力。

  这里我斗胆提出一个自己想法:假如你没有进入一个类似金饭碗的公司,那么在你工作5年内至少以1年的频率进行面试经历,我不是指一年平率的跳槽,是面试,面试可以让你知道目前其他公司使用的技术方向,让你知道你的水平,以及你期望的岗位距离有多远,总之残酷的面试可以让你认清自己,从而不断重构自己,不断更新自己,让今天的自己优于昨天的自己。

  假如还有学习的动力,这样的方式是不错的选择,还有一个前提是:你必须是一个有勇气推翻重来的人。

主题:

关于NIO的基础知识博客园的文章:直达车

1,关于缓冲区:

NIO (No-blocking I/O)从JDK 1.4起,NIO API作为一个基于缓冲区,并能提供非阻塞I/O操作的API被引入。

为什么说是基于缓冲区呢?因为NIO中使用的数据类型都是有缓冲功能的。

数据类型如下:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

不知道有没有朋友对缓冲这个有点疑惑呢?所谓的缓冲的就是:比如一百个箱子要从A搬到B只能一个个般,缓冲区就相当于来了辆三轮车,从A搬10个盒子到三轮车,运到B,在把10个盒子搬给B。就是这么个过程。如果你接触过BufferedInputStream的源码,那么就比较直观的看到所谓的Buffer,就是一个new出来的byte数组,了解详细请 猛击

上面7个数据类型其实就是对各自的基本类型数组的封装,比如ByteBuffer就是对Byte[]的封装,CharBuffer就是Char[]的封装,封装后对外提供才做他们的方法。

ByteBuffer为例,来看下它的源码:

我们通过allocate方法取得ByteBuffer的:

public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
        //调用HeapByteBuffer的构造函数
    return new HeapByteBuffer(capacity, capacity);
    }

HeapByteBuffer的构造函数:

 HeapByteBuffer(int cap, int lim) {        // package-private
        // HeapByteBuffer的super又是ByteBuffer
        // new  byte[] 在这呢,这就所谓开辟的缓存区啦
    super(-1, 0, lim, cap, new byte[cap], 0);
    }

ByteBuffer的构造函数:

    ByteBuffer(int mark, int pos, int lim, int cap,    // package-private
         byte[] hb, int offset)
    {
        // ByteBuffer的super是Buffer类
    super(mark, pos, lim, cap);
        // hb就是new出来的byte数组,从此 ByteBuffer就由维护这个数组了
    this.hb = hb;
    this.offset = offset;
    }

Buffer类的构造函数做什么呢?

这个构造函数的作用是初始化Buffer的状态变量(后面会了解到),因为上面7个基本类型的Buffer数据类型都继承Buffer类,他们的状态变量统一归Buffer类管理,继承带来的好处啊。

    Buffer(int mark, int pos, int lim, int cap) {    // package-private
    if (cap < 0)
        throw new IllegalArgumentException();
    // 设置capacity
    this.capacity = cap;
    //设置limit
    limit(lim);
    //设置position
    position(pos);
    if (mark >= 0) {
        if (mark > pos)
        throw new IllegalArgumentException();
        this.mark = mark;
    }
    }

至此,我想我们应该对所谓的缓冲区没有什么疑惑了吧。

2,阻塞IO和非阻塞IO

网上比较好解释如下:

  Java 传统的IO操作都是阻塞式的(blocking I/O),如果有socket的编程基础,你会接触过堵塞socket和非堵塞socket,堵塞socket就是在accept、read、write等IO操作的的时候,如果没有可用符合条件的资源,不马上返回,一直等待直到有资源为止。而非堵塞socket则是在执行select的时候,当没有资源的时候堵塞,当有符合资源的时候,返回一个信号,然后程序就可以执行accept、read、write等操作,一般来说,如果使用堵塞socket,通常我们通常开一个线程accept socket,当读完这次socket请求的时候,开一个单独的线程处理这个socket请求;如果使用非堵塞socket,通常是只有一个线程,一开始是select状,当有信号的时候可以通过 可以通过多路复用(Multiplexing)技术传递给一个指定的线程池来处理请求,然后原来的线程继续select状态。 最简单的多路复用技术可以通过java管道(Pipe)来实现。换句话说,如果客户端的并发请求很大的时候,我们可以使用少于客户端并发请求的线程数来处理这些请求,而这些来不及立即处理的请求会被阻塞在java管道或者队列里面,等待线程池的处理。请求 听起来很复杂,在这个架构当道的java 世界里,现在已经有很多优秀的NIO的架构方便开发者使用,比如Grizzly,Apache Mina等等,如果你对如何编写高性能的网络服务器有兴趣,你可以研读这些源代码。

3,结合tomcat使用NIO来理解

  在web服务器上阻塞IO(BIO)与NIO一个比较重要的不同是,我们使用BIO的时候往往会为每一个web请求引入多线程,每个web请求一个单独的线程,所以并发量一旦上去了,线程数就上去了,CPU就忙着线程切换,所以BIO不合适高吞吐量、高可伸缩的web服务器;而NIO则是使用单线程(单个CPU)或者只使用少量的多线程(多CPU)来接受Socket,而由线程池来处理堵塞在pipe或者队列里的请求.这样的话,只要OS可以接受TCP的连接,web服务器就可以处理该请求。大大提高了web服务器的可伸缩性。

  从Tomcat6.0以后, Java开发者很容易就可以是用NIO的技术来提升tomcat的并发处理能力。

<Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />

修改成:

<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
               connectionTimeout="20000"
               redirectPort="8443" />

然后启动服务器,你会看到org.apache.coyote.http11.Http11NioProtocol start的信息,表示NIO已经启动

从tomcat使用NIO这一点来看,如果我们从事java服务器的开发,肯定是离不开NIO的。

4,状态变量

状态变量的解释在最前面推荐的博文中已经有非常好的解释了,这里是学习下源码:

三个值指定缓冲区的状态:

  • position  它指定了下一个字节将放到数组的哪一个元素中。
  • limit  表明还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。
  • capacity  表明可以储存在缓冲区中的最大数据容量。实际上,它指定了底层数组的大小 ― 或者至少是指定了准许我们使用的层数组的容量。

Position settingflip()函数源码:

    public final Buffer flip() {
    // limit 置成position位置
    limit = position;
    // position 置0
    position = 0;
    mark = -1;
    return this;
    }

flip()执行后的图:

Position advanced to 5, limit unchanged

 

clear()源码:

  // 注意这里的妙处是我们对数组的处理知识改变状态变量,而没有真正去clear数组中的内容,
    // 因为真的去删数组中的内容,效率会很慢,而且也没有必要这样做
    public final Buffer clear() {
    // position置为0
    position = 0;
    // limit调成最大
    limit = capacity;
    mark = -1;
    return this;
    }

我们可以通过下面的方式取得或设置状态变量:

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);  
buffer.position();
buffer.limit();
buffer.capacity();
buffer.mark();
buffer.position( 3 );
buffer.limit( 7 );

5,基础例子

1,copy文件例子:

package code.stu.nio;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
// 拷贝文件例子
public class CopyFileByNio {
    public static void main(String[] args) throws IOException {
        FileInputStream in = new FileInputStream("");
        FileOutputStream out = new FileOutputStream("");
        // 得到缓冲区
        ByteBuffer bb = ByteBuffer.allocate(1024);
        // 通道
        FileChannel fi = in.getChannel();
        FileChannel fo = out.getChannel();
        while(fi.read(bb) != -1){
            //System.out.println(bb.position());
            // 写之前先调flip
            bb.flip();
            fo.write(bb); 
            // 写之后再调clear
            bb.clear();
        }
        in.close();
        out.close();
    }
}

2,网上传输文件的例子:

a,服务器

package code.stu.nio.socket;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
  
public class MyServer {  
  
      
    public static void main(String[] args) {  
        Selector selector = null;  
        ServerSocketChannel serverSocketChannel = null;  
          
        try {  
            // Selector for incoming time requests  
            selector = Selector.open();  
  
            // Create a new server socket and set to non blocking mode  
            serverSocketChannel = ServerSocketChannel.open();  
            serverSocketChannel.configureBlocking(false);  
              
            // Bind the server socket to the local host and port  
            serverSocketChannel.socket().setReuseAddress(true);  
            serverSocketChannel.socket().bind(new InetSocketAddress(10000));  
              
            // Register accepts on the server socket with the selector. This  
            // step tells the selector that the socket wants to be put on the  
            // ready list when accept operations occur, so allowing multiplexed  
            // non-blocking I/O to take place.  
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);  
      
            // Here's where everything happens. The select method will  
            // return when any operations registered above have occurred, the  
            // thread has been interrupted, etc.  
            while (selector.select() > 0) {  
                // Someone is ready for I/O, get the ready keys  
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();  
      
                // Walk through the ready keys collection and process date requests.  
                while (it.hasNext()) {  
                    SelectionKey readyKey = it.next();  
                    it.remove();  
                      
                    // The key indexes into the selector so you  
                    // can retrieve the socket that's ready for I/O  
                    doit((ServerSocketChannel) readyKey.channel());  
                }  
            }  
        } catch (ClosedChannelException ex) {  
            System.out.println(ex);
        } catch (IOException ex) {  
             System.out.println(ex);
        } finally {  
            try {  
                selector.close();  
            } catch(Exception ex) {}  
            try {  
                serverSocketChannel.close();  
            } catch(Exception ex) {}  
        }  
    }  
  
    private static void doit(final ServerSocketChannel serverSocketChannel) throws IOException {  
        SocketChannel socketChannel = null;  
        try {  
            socketChannel = serverSocketChannel.accept();  
            // 做一个接文件的操作
            receiveFile(socketChannel, new File("E:\\test\\test.txt"));  
        } finally {  
            try {  
                socketChannel.close();  
            } catch(Exception ex) {}  
        }  
          
    }  
      
    private static void receiveFile(SocketChannel socketChannel, File file) throws IOException {  
        FileOutputStream fos = null;  
        FileChannel channel = null;  
          
        try {  
            // 用于把数据从管道上拿下来变成文件
            fos = new FileOutputStream(file);  
            channel = fos.getChannel();  
            ByteBuffer buffer = ByteBuffer.allocateDirect(1024);  
            int size = 0;
            while ((size = socketChannel.read(buffer)) != -1) {  
                buffer.flip();  
                if (size > 0) {  
                    buffer.limit(size);  
                    channel.write(buffer);  
                    buffer.clear();  
                }  
            }  
        } finally {  
            try {  
                channel.close();  
            } catch(Exception ex) {}  
            try {  
                fos.close();  
            } catch(Exception ex) {}  
        }  
    }  
}  

客户端:

package code.stu.nio.socket;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
  
public class MyClient {  
  
      
    public static void main(String[] args) throws Exception {  
        new Thread(new MyRunnable()).start(); // 建立个线程来处理
    }  
      
    private static final class MyRunnable implements Runnable {  
        public void run() {  
            SocketChannel socketChannel = null;  
            try {  
                socketChannel = SocketChannel.open();  
                SocketAddress socketAddress = new InetSocketAddress("localhost", 10000);  
                socketChannel.connect(socketAddress);  
                // 发送数据
                sendFile(socketChannel, new File("D:\\我的文档\\test.txt"));  
            } catch (Exception ex) {  
                System.out.println(ex); 
            } finally {  
                try {  
                    socketChannel.close();  
                } catch(Exception ex) {}  
            }  
        }  
  
        private void sendFile(SocketChannel socketChannel, File file) throws IOException {  
            FileInputStream fis = null;  
            FileChannel channel = null;  
            try {  
                // 把文件编程流再方法通道上去
                fis = new FileInputStream(file);  
                channel = fis.getChannel();  
                ByteBuffer buffer = ByteBuffer.allocateDirect(1024);  
                int size = 0;  
                while ((size = channel.read(buffer)) != -1) {  
                    buffer.rewind();  
                    buffer.limit(size);  
                    socketChannel.write(buffer);  
                    buffer.clear();  
                }  
                socketChannel.socket().shutdownOutput();  
            } finally {  
                try {  
                    channel.close();  
                } catch(Exception ex) {}  
                try {  
                    fis.close();  
                } catch(Exception ex) {}  
            }  
        }  
    }  
}  

总结:

1,就目前来看原IO的操作已经可以被NIO代替,在java服务器端的开发中使用多线程技术结合NIO,是一个比较好的解决方案。
2,大家也看到了在处理new出来的缓存是,不移动删除内容,只需用标记的方式来实现读写操作,大大提高了效率,印象深刻

 

让我们继续前行

----------------------------------------------------------------------

努力不一定成功,但不努力肯定不会成功。
共勉。

posted on 2012-11-25 17:17  每当变幻时  阅读(2901)  评论(5编辑  收藏  举报

导航