Thinking in Java--使用NIO实现非阻塞Socket通信

Java1.4提供了一种新的IO读取方式,称为NIO。NIO中使用了通道和缓冲器的概念,并且以块的形式操作数据,这样更接近操作系统IO操作的形式,提高了JavaIO的效率。NIO的核心类有两个Channel和Buffer。但是其实除了提升了基本IO操作的性能外,NIO还提供了非阻塞IO的功能。这里先介绍下阻塞IO和非阻塞IO的概念。考虑到应用程序发送出IO请求,如果这个IO请求会阻塞线程(就是线程停在这里直到读取到了数据再继续运行下去),那么就是阻塞IO;如果这个IO请求没有阻塞线程(线程发出了IO请求,但是并停在这里等数据的到来而是先去做别的事情)就称为非阻塞IO。可以很显然的看到,非阻塞的IO可以提高程序的性能。这篇博客下面会先介绍用于Socket通信的非阻塞IO的具体类,然后再利用这些类实现一个非阻塞的Socket通信服务器。

一.用于非阻塞Socket通信的几个类
(1).Selector类
它是SelectableChannel对象的多路复用器,所有希望采用非阻塞方式进行通信的Channel都应该注册到Selector对象。但是这个类的对象是不能通过调用构造器得到的,而是通过这个类静态的open()方法得到,该方法将使用系统默认的Selector来返回新的Selector。
Selector对象可以同时监听多个SelectableChannel的IO状况,是非阻塞IO的核心。一个Selector实例有3个SelectionKey集合。
1)所有的SelectionKey集合:代表了注册在该Selector上的Channel,这个集合可以通过keys()方法返回
2)被选择的SelectionKey集合:代表了所有可以通过select()方法获取的,需要进行IO处理的Channel,这个集合可以通过selectedKeys()返回。
3)被取消的SelectionKey集合:代表了所有被取消注册关系的Channel,下一次执行select()方法时,这些Channel对应的SelectionKey就会被彻底删除,程序通常无须直接访问这个集合。
Selector类还提供了一系列和select()相关的方法,这些方法比较重要,需要了解一下:
int select():监控所有注册的Channel,当他们中有需要处理的IO操作时,该方法返回,并将对应的SelectionKey加入到被选择的SelectionKey集合中,并返回这些Channel的数量。
int select(long timeout):可以设置超时时长的select()操作
int selectNow():执行一个立即返回的select()操作,相对与无参数的select()方法而言,该方法不会阻塞线程
Selector wakeup():使一个还未返回的select()方法立刻返回。

(2)SelectableChannel类
Selectabel类是一种支持阻塞I/O和非阻塞I/O的通道。应用程序可以调用SelectabelChanel的register()方法将其注册到指定的Selector上。SelectableChannel对象支持阻塞和非阻塞两种模式,但是默认情况下是阻塞的(所有的Channel默认都是阻塞模式),必须使用非阻塞模式才能支持非阻塞IO。但是不同的SelectableChannel支持的操作是不一样的,向ServerSocketChannel代表一个ServerSocket,它只支持OP_ACCEPT操作。而SocketChannle代表一个socket,支持OP_READ操作。下面是几个SelectableChannel常用的方法:
boolean isBlocking():返回该Channel是否为阻塞模式
SelectabelChannel configureBlocking(boolean block):设置是否采用阻塞模式。
int valiOps():返回一个整数值,表示这个Channel所支持的操作。
boolean isRegistered():返回该Channel是否已经注册在一个或多个Selector上。

(3)SelectionKey类
该类对象代表SelectableChannel和Selector之间的注册关系。

(4)ServerSocketChannel类
支持非阻塞操作,对应与ServerSocket这个类,支持OP_ACCEPT操作;该类也提供了accept()方法,功能相当于ServerSocket提供的accept()方法。

(5).SocketChannel类
支持非阻塞操作,对应Socket这个类,支持OP_CONNECT,OP_READ和OP_WRITE操作。这个类还实现了ByteChannel,可以通过SocketChannel来读写ByteBuffer对象。

二.利用非阻塞IO实现一个聊天室的服务器
前面我自己写了一个仿QQ的C/S局域网聊天工具,在这个工具中,服务器使用SeverSocket进行监听,每新加入一个人就新建一个socekt与其通信并且还要单独为其开启一个服务线程。这样如果加入的用户比较多,那么就要开启很多的服务线程了,服务器的压力就会比较大。现在我们用非阻塞IO,服务器只需要一个线程就可以同时与多个客户端进行通信。
具体的思路是:原先服务器中使用ServerSocket进行监听,现在改用ServerSocketChannel对象进行监听。原先每接入一个客户端,就新建一个Socket进行通信,现在新建一个SocketChannel进行通信。最重要的是这些SelectableChannel对象,都必须注册到一个Selector对象上;然后我们只需要检测这个Selector对象就行了,我们可以调用这个Selector对象的select()方法监听,这样就可以实时监听所有客户端的行为,并可以通过selectedKeys()方法返回需要处理的SelectionKey对象,SelectionKey对象可以判定返回消息的内容(是连接请求还是具体的消息),并且这个对象的Channel方法可以返回被选中的客户端的Channel。更具体的思路见下面的代码及注释:

package IO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.Channel;
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.nio.*;


public class NServer {

    //用于检测所有Channel状态的Selector
    private Selector selector = null;
    ///默认端口
    static final int PORT = 30000;
    //定义实现编码和解码的字符集对象
    private Charset charset = Charset.forName("UTF-8");
    public void init() throws IOException
    {
        //Selector对象是不能通过构造器得到的,必须通过静态的open()方法得到
        selector=Selector.open();

        //SeverSocktChannel对象也不能通过构造器得到,所以需要通过open()方法打开
        ServerSocketChannel server = ServerSocketChannel.open();
        InetSocketAddress isa  = new InetSocketAddress("127.0.0.1",PORT);

        //将ServerSocketChannel绑定到指定的IP地址
        server.bind(isa);

        //设置ServerSocketChannel以非阻塞方式工作(默认是阻塞的)
        server.configureBlocking(false);

        //将server注册到指定的Selector对象
        server.register(selector, SelectionKey.OP_ACCEPT);

        //返回值大于0,表示有Channel中含有需要处理的数据
        while(selector.select()>0){

            //依次处理selector上的每个已选择的SelectionKey
            for(SelectionKey sk : selector.selectedKeys()){

                //从selector已选择的Key集中删除正在处理的SelectionKey
                selector.selectedKeys().remove(sk);
                //如果sk对应的Channel包含客户端的连接请求
                if(sk.isAcceptable()){
                    //调用accept方法接受连接,产生服务端的SocketChannel
                    SocketChannel sc = server.accept();
                    //设置采用非阻塞模式
                    sc.configureBlocking(false);
                    //将该SocketChannel也注册到selector上去
                    sc.register(selector, SelectionKey.OP_READ);
                    //将sk的Channel设置成准备接收其它的请求
                    sk.interestOps(SelectionKey.OP_ACCEPT);
                }
                //如果sk对应的Channel有数据需要读取
                if(sk.isReadable()){
                    //获取SelectionKey对应的Channel,该Channel中有需要读取的数据
                    SocketChannel sc =(SocketChannel)sk.channel();
                    //定义准备执行读取数据的ByteBufferer
                    ByteBuffer buff = ByteBuffer.allocate(1024);
                    String content="";
                    try{
                        while(sc.read(buff)>0){
                            buff.flip();
                            content+=charset.decode(buff);
                        }
                        //将sk对应的Channel设置成准备洗一次读取
                        sk.interestOps(SelectionKey.OP_READ);
                    }

                    //如果捕获到该sk对应的Channel出现了异常,即表明该Channel
                    //对应的Client出现了异常,所以从Selector中取消掉sk的注册
                    catch(IOException e){

                        //从Selector中删除掉指定的SelectionKey
                        sk.cancel();
                        if(sk.channel()!=null){
                            sk.channel().close();
                        }
                    }
                    //如果content的长度不为空,即该聊天信息不为空
                    if(content.length()>0){

                        //遍历该selector中注册的所有SelectionKey
                        for(SelectionKey key: selector.keys()){

                            //获取key对应的Chanel
                            Channel targetChannel = key.channel();
                            //如果该Channel是SocketChannel对象
                            if(targetChannel instanceof SocketChannel){
                                //将读到的内容写入到Channel中
                                SocketChannel dest =(SocketChannel)targetChannel;
                                dest.write(charset.encode(content));
                            }
                        }
                    }
                }
            }
        }
    }
}

你可能感兴趣的:(nio,非阻塞,java编程思想)