读Java编程艺术之笔记(非阻塞IO)

本篇重点是非阻塞IO,即java1.4提供的nio包,顺带记录一些其他信息。

在Socket技术中我们利连接时间的付出换来数据传输的可靠性。Java提供一些控制连接时间的技术,以增强其传输效率。例如超时和中断。

为防止无限制等待或为控制等待时间,可调用Socket.setSoTimeout(millies)设置超时时间。另外,在应用构造器Socket(address, port)和ServerSocket(port)时,JVM将先建立连接而后创建socket对象,无参构造器Socket()及ServerSocket()则无需建立连接,然后调用发表在JDK1.4中的connect()方法,可以改善因连接而造成的延误,并能指定连接时间。

try {
    Socket clientSocket = new Socket(); //无参构造函数
    //其他代码
    ...
    clientSocket.connect(address, port, timeout);
    ...
} catch (SocketException e) {
    e.printStackTree();
}

超时控制有其局限性,在数据发送以及接受过程中,Socket对象无响应,不回答或延误读写的情况,不能控制。此时我们希望中断这个读写操作,可中断的Socket技术,包括在java.nio的API类SocketChannel中。利用SocketChannel创建的对象本身就具有可中断的功能,将抛出InterruptedException,例:

...
try{
    InetSocketAddress addr = new InetSocketAddress(IPaddress, port);
    SocketChannel  channel = SocketChannel .open(addr);
    //将通道应用到Scanner
    Scanner inData = new Scanner(channel);
    while(true) {
        if(inData.hasNextLine()) {
            //得到通道中的数据
            String line = inData.nextLine();
            ...
        } else {
            Thread.sleep(500);
        }
    }
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (IOException e) {
  e.printStackTrace();
}

    具有可中断功能的输出操作如下:

...
try{
    InetSocketAddress addr = new InetSocketAddress(IPaddress, port);
    SocketChannel  channel = SocketChannel .open(addr);
    //将通道包装在输出流对象中
   OutputStream outStream = Channels.newOutputStream(channel);
    //刷新方式输出通道中的数据
    PrintWriter outData = new PrintWriter(outStream, true);
    //向服务器发送请求信息
    outData.println(requestMessage);
    ...
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (IOException e) {
  e.printStackTrace();
}
...

针对网络编程中数据IO,java.nio与java.io相比,应用java.nio包中的API类可提高数据输入、输出的执行速度;纯化Java代码,在传统java.io中,涉及数据输入、输出的底层操作,例如缓冲器的填充和刷新,JVM必须装入本机操作系统的有关代码,完成其IO操作。在java.nio中,涉及缓冲器的操作完全交给操作系统自行,从而提高了Java程序的纯度。
数据流与数据块的比较:面向数据流的IO按照一个个有序的字节处理数据,一次只处理一个字节;面向数据块的IO一次处理整个数据块。这数据块由缓冲器对象类定义,最大数据块容量科大64KB。几乎每种基本数据类型都有其对应的缓冲器类,用来包装不同类型的数据块。数据块的优点是操作速度快;数据流的优点是易于控制和过滤传输的数据,易于编写代码。

通道Channel可看做是数据块进行输入、输出操作的传输带,具有方向性。在网络编程中经常用到的是实现了Channel这个接口的SocketChannel和ServerSocketChannel,它们的一个重要特点是可以运用选择器Selector支持非阻塞输入、输出。

缓冲Buffer是运用通道技术传输数据块的容器。值得注意的是,在应用通道进行数据传输时,必须根据数据类型,来创建匹配的缓冲对象。在java.nio中,Java提供了如下类型的缓冲类:ByteBuffer、CharBufferShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。以上每个缓冲类都是Buffer类的子类。关于缓冲Buffer,缓冲是一块连续的内存区,是nio数据读写的中转地。缓冲有两种工作模式——写模式和读模式,缓冲主要由position,limit,capacity三个变量控制读写过程,在写模式和读模式下缓冲的内部结构如图:

其中capacity,limit和position的含义分别如下表:

参数

写模式

读模式

position

当前写入的单位数据数量。

当前读取的单位数据位置。

limit

代表最多能写多少单位数据和容量是一样的。

代表最多能读多少单位数据,和之前写入的单位数据量一致。

capacity

buffer容量

buffer容量

 

 

 

 

 

  

这三个属性的大小关系为capacity>=limit>=position>=0。缓冲类常见方法:flip()——写模式转换成读模式;rewind()——将position重置为0,一般用户重复读;clear()——清空buffer,准备再次被写入(position变为0,limit变成capacity);compact()——将未读取的数据拷贝到buffer头部;mark(),reset()——mark标记一个位置,reset可重置到该位置。

在利用通道进行数据块块传输中,经常利用java.nio.charset包中提供的Charset类进行数据块的编码和解码操作,以便提高数据块的传输效率和可靠性。除系统预设的Unicode字符集外,Charset还支持如下字符编码定义:US ASCII,ISO-8859-1,UTF-8,UTF-16。Charset是一个抽象类,在应用时必须调用其forName()方法,来返回一个指定字符集编码的对象。并且利用Charset类的encode()以及decode()方法尽心编码和解码操作:Charset forName(String charsetName)——按指定字符集名返回一个Charset对象,ByteBuffer encode(CharBuffer buffer)——对执行CharBuffer对象编码,并返回编码后的ByteBuffer对象,CharBuffer decode(ByteBuffer buffer)——对指定ByteBuffer对象解码,并返回解码后的CharBuffer对象。

阻塞式网络IO的特点:多个线程处理多个连接,每个线程拥有自己的栈空间并占用一些CPU时间,每个线程遇到外部未准备好的时候都会阻塞(accept等待连接到来,会阻塞;recieve/read等待数据,会阻塞)。阻塞会带来大量的线程上下文切换,且大部分的切换都以阻塞告终。

何为非阻塞?

下面有个隐喻:

一辆从A开往B的公共汽车上,路上有很多点可能会有人下车。司机不知道哪些点会有哪些人会下车,对于需要下车的人,如何处理更好?

1.司机过程中定时询问每个乘客是否到达目的地,若有人说到了,那么司机停车,乘客下车。(类似阻塞式)

2.每个人告诉售票员自己的目的地,然后睡觉,司机只和售票员交互,到了某个点由售票员通知乘客下车。 (类似非阻塞)

很显然,每个人要到达某个目的地可以认为是一个线程,司机可以认为是CPU。在阻塞式里面,每个线程需要不断的轮询,上下文切换,以达到找到目的地的结果。而在非阻塞方式里,每个乘客(线程)都在睡觉(休眠),只在真正外部环境准备好了才唤醒,这样的唤醒肯定不会阻塞。

非阻塞的原理

把整个过程切换成小的任务,通过任务间协作完成。由一个专门的线程来处理所有的IO事件,并负责分发。事件驱动机制:事件到的时候触发,而不是同步的去监视事件。线程通讯:线程之间通过wait,notify等方式通讯。保证每次上下文切换都是有意义的。减少无谓的进程切换。

以下是异步IO的结构:

一个简单的通道的例子(服务器端程序)

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

public class SelectoServer {

    /**
     * @param args
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {
        // TODO Auto-generated method stub
        /* 为第一个用户创建连接 */
        //指定端口地址
        InetSocketAddress address1 = new InetSocketAddress(10001);
        //创建ServerSocketChannel
        ServerSocketChannel channel1 = ServerSocketChannel.open();
        //设置非阻塞IO
        channel1.configureBlocking(false);
        //绑定端口
        channel1.socket().bind(address1);
        /* 为第二个用户创建连接 */
        InetSocketAddress address2 = new InetSocketAddress(10002);
        ServerSocketChannel channel2 = ServerSocketChannel.open();
        channel2.configureBlocking(false);
        channel2.socket().bind(address2);
        /* 为第三个用户创建连接 */
        InetSocketAddress address3 = new InetSocketAddress(10003);
        ServerSocketChannel channel3 = ServerSocketChannel.open();
        channel3.configureBlocking(false);
        channel3.socket().bind(address3);
        
        //创建选择器
        Selector selector = Selector.open();
        //指定连接方式
        channel1.register(selector, SelectionKey.OP_ACCEPT);
        channel2.register(selector, SelectionKey.OP_ACCEPT);
        channel3.register(selector, SelectionKey.OP_ACCEPT);
        
        //如果发生任何事件
        while (selector.select() > 0) {
            //得到事件集合
            Set keys = selector.selectedKeys();
            Iterator iterator = keys.iterator();
            while (iterator.hasNext()) {
                //得到事件源
                SelectionKey key = (SelectionKey) iterator.next();
                //得到通道
                ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                //接收通道连接请求
                SocketChannel socketChannel = channel.accept();
                //调用自定义的请求处理方法
                handleClient(socketChannel);
                //删除处理完的事件
                iterator.remove();
            }
        }
    }

    private static void handleClient(SocketChannel socketChannel) {
        // TODO Auto-generated method stub
        int port = socketChannel.socket().getLocalPort();
        System.out.println("Listen to the client address: "+socketChannel.socket().getInetAddress());
        System.out.println("Port: "+port);
        switch (port) {
        case 10001:
            writeClient(socketChannel, "服务器响应。使用端口为:"+port);
            break;
        case 10002:
            writeClient(socketChannel, "服务器响应。使用端口为:"+port);
            break;
        case 10003:
            writeClient(socketChannel, "服务器响应。使用端口为:"+port);
            break;
        default:
            writeClient(socketChannel, "服务器响应,非定义端口"+port);
            break;
        }
    }

    private static void writeClient(SocketChannel socketChannel, String string) {
        // TODO Auto-generated method stub
        //指定字符集
        Charset charset = Charset.forName("UTF-8");
        //自定义缓冲
        ByteBuffer buffer = ByteBuffer.allocate(256);
        //
        buffer = charset.encode(string);
        try {
            socketChannel.write(buffer);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

一个简单的通道的例子(客户端程序)

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;

public class SelectorClient1 {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        try {
            InetSocketAddress address = new InetSocketAddress("localhost", 10001);
            Charset charset = Charset.forName("UTF-8");
            SocketChannel channel = SocketChannel.open(address);
            System.out.println("address: " + address);
            ByteBuffer buffer = ByteBuffer.allocate(256);
            channel.read(buffer);
            buffer.flip();
            CharBuffer charBuffer = charset.decode(buffer);
            System.out.println(charBuffer);
            channel.close();
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }

    }
}

三个客户端程序基本相同,这里只列出第一个用户端代码。

其他参考资料:http://www.iteye.com/topic/834447——JAVA NIO 简介

posted on 2013-04-25 15:59  夜月升  阅读(323)  评论(0编辑  收藏  举报

导航