Socket网络通信之NIO

Socket网络通信之NIO

NIO:new io ,java1.4开始推出的可非阻塞IO。

java.nio 包,可解决BIO阻塞的不足 但比BIO学习、使用复杂。

可以以阻塞、非阻塞两种方式工作。

可以在非阻塞模式下,可以用少量(甚至一个)线程处理大量IO连接。

Java7推出了 Nio.2  (又称AIO,异步IO)。

一、NIO工作流程如下图:

流程图如上所示,要理解NioSocket的使用必须先理解三个概念:Selector,Channel和Buffer。举个例子。大学时有人卖电话卡,提供送货上门服务,只要有人打电话,他就送过去、收钱在回去,然后等下一个电话,这就相当于普通的Socket处理请求的模式。如果请求不是很多,这是没有问题的。而像现在的电商配送模式——送快递就类似于NioSocket。快递并不会一件一件的送,而是将很多件货一起拿去送,而且在中转站都有专门的分拣员负责按配送范围把货物分给不同的送货员,这样效率就提高了很多。Selector就是中转战的分拣员,Channel就是送货员(或者开往某个区域的配货车),Buffer就是所要送的货物。
NioSocket使用中首先要创建ServerSocketChannel,然后注册Selector,接下来就可以用Selector接收请求并处理了。 
ServerSocketChannel可以使用自己的静态工程方法open创建。每个ServerSocketChannel对应一个ServerSocket,可以调用其socket方法来获取,不过如果直接使用获取到ServerSocket来监听请求,那还是原来的处理模式,一般使用获取到的ServerSocket来绑定端口。ServerSocketChannel可以通过configureBlocking方法来设置是否采用阻塞模式,如果要采用非阻塞模式可以用configureBlocking(false)来设置,设置了非阻塞模式之后就可以调用register方法注册Selector来使用了(阻塞模式不可以使用Selector)。 
Selector可以使用自己的静态工程方法open创建,创建后通过Channel的register方法注册到ServerSocketChannel或者SocketChannel上,注册完之后Selector就可以通过select方法来等等请求,select方法有一个long类型的参数,代表最长等待时间,如果在这段时间里接收到了相应操作的请求则返回可以处理的请求的数量,否则在超时后返回0,程序继续往下走,如果传入的参数为0或者调用无参数的重载方法,select方法会采用阻塞模式直到有相应操作的请求出现。当接收到请求后Selector调用selectedKeys方法返回SelectedKey的集合。 
selectedKey保存了处理当前请求的Channel和Selector,并且提供了不同的操作类型。Channel在注册Selector的时候可以通过register的第二个参数选择特定的操作,这里的操作就是在selectedKey中定义的,一共有4种: 
SelectionKey.OP_ACCEPT 
SelectionKey.OP_CONNECT 
SelectionKey.OP_READ 
SelectionKey.OP_WRITE 
分别表示接收请求操作、连接操作、读操作和写操作,只有在register方法中注册了相应的操作Selector才会关心相应类型操作的请求。 
Channel和Selector并没有谁属于谁的关系,就像数据库里的多对多的关系,不过Selecor这个分拣员分拣的更细,它可以按不同类型来分拣,分拣后的结果保存在SelectionKey中,可以分别通过SelectionKey的channel方法和selector方法来获取对应的Channel和Selector,而且还可以通过isAcceptable、isConnectable、isReadable和isWritable方法来判断是什么类型的操作。 
NioSocket中服务端的处理过程可以分为5步: 
1、创建ServerSocketChannel并设置相应参数 。
2、创建Selector并注册到ServerSocketChannel上 。
3、调用Selector的select方法等待请求 。
4、Selector接收到请求后使用selectionKeys返回SelectionKey集合 。
5、使用SelectionKey获取到Channel、Selector和操作类型并进行具体操作。

下面具体说说Selector,Channel和Buffer的用法。

 二、Selector 选择器 :

Selector 选择器 :非阻塞模式下,一个选择器可检测多个SelectableChannel,获得为读写等操作准备好的通道。就不需要我们用循环去判断了。通过Selector,一个线程就可以处理多个Channel,可极大减少线程数。 用cpu核心数量的线程,充分利用cpu资源,又减少线程切换。

Selector 用法:1,创建Selector。Selector selector = Selector.open();

       2,将要交给Selector检测的SelectableChannel注册进来。

          (1)channel.configureBlocking(false);   // 注意:一定要设为非阻塞模式

          (2)SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

          channel.register方法的第二个参数指定要selector帮忙监听的就绪操作:SelectionKey.OP_CONNECT(可连接);SelectionKey.OP_ACCEPT(可接受);SelectionKey.OP_READ(可读);SelectionKey.OP_WRITE(可写)。

       3,通过Selector来选择就绪的Channel,有三个select方法。int n = selector.select();

        (1) int select() //阻塞直到有就绪的Channel。
        (2)int select(long timeout) //阻塞最长多久。
        (3)int selectNow() //不阻塞。 

          三个方法返回值:就绪的Channel数量。 
          注意:select()方法返回当前的就绪数量。
          例:第一次select返回1;第二次select,又一个channel就绪,如果第一个就绪的channel还未被处理,则此时就绪的channel是2个,会返回2。在用线程池异步处理任务时需特别小心,重复选择!

       4,获得就绪的SelectionKey集合(当有就绪的Channel时)。

        Set<SelectionKey> selectedKeys = selector.selectedKeys();

       5,处理selectedKeys set。  

        Set<SelectionKey> selectedKeys = selector.selectedKeys();
        Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
        while(keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
        } else if (key.isConnectable()) {
        // a connection was established with a remote server.
        } else if (key.isReadable()) {
        // a channel is ready for reading
        } else if (key.isWritable()) {
        // a channel is ready for writing
        }
        keyIterator.remove(); //处理了,一定要从selectedKey集中移除
        }

 三、Channel 通道:数据的来源或去向目标

Java NIO: Channels read data into Buffers, and Buffers write data into Channels。

1、Channel的实现

  FileChannel 文件通道
  DatagramChannel UDP协议的通道
  SocketChannel 通常通道
  ServerSocketChannel 服务通道

2、各Channel的API方法

  open():创建通道
  read(Buffer):从通道中读数据放入到buffer
  write(Buffer):将buffer中的数据写给通道

三、Buffer   缓冲区:数据的临时存放区

Buffer类型:ByteBuffer、MappedByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer

Buffer的基本使用步骤:

(1)调用xxxBuffer.allocate(int)创建Buffer
(2)调用put方法往Buffer中写数据
(3)调用buffer.flip()将buffer转为读模式
(4)读取buffer中的数据
(5)Call buffer.clear() or buffer.compact()

1、Buffer的操作API 

(1)调用xxxBuffer.allocate(int)创建Buffer
  ByteBuffer buf = ByteBuffer.allocate(48);
  CharBuffer buf = CharBuffer.allocate(1024);
(2)往Buffer中写数据
  int bytesRead = inChannel.read(buf); //read into buffer.
  buf.put(127);
(3)调用buffer.flip()将buffer转为读模式
  buf.flip(); // 转为读模式,position变为0
(4)读取buffer中的数据
  //read from buffer into channel.
  int bytesWritten = inChannel.write(buf);
  byte aByte = buf.get();
(5)读完后,调用clear()或compact()为下次写做好准备
  buf.clear(); //position=0 limit = capacity
  buf.compact(); //整理,将未读的数据移动到头部

如下所示为NioServer和NioClient的代码

public class NioServer {
    private static Charset charset = Charset.forName("UTF-8");
    private static CharsetDecoder decoder = charset.newDecoder();
    public static void main(String[] args) throws IOException {
        int port = 1104;
        // 极少的线程
        int threads = 3;
        ExecutorService tpool = Executors.newFixedThreadPool(threads);
        // 1、得到一个selector
        Selector selector = Selector.open();
        try (ServerSocketChannel ssc = ServerSocketChannel.open()) {
            ssc.bind(new InetSocketAddress(port));
            // 2 注册到selector
            // 要非阻塞
            ssc.configureBlocking(false);
            // ssc向selector 注册,监听连接到来。
            ssc.register(selector, SelectionKey.OP_ACCEPT);
            // 连接计数
            int connectionCount = 0;
            // 3、循环选择就绪的通道
            while (true) {
                // 阻塞等待就绪的事件
                int readyChannels = selector.select();
                // 因为select()阻塞可以被中断
                if (readyChannels == 0) {
                    continue;
                }

                // 取到就绪的key集合
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    if (key.isAcceptable()) {
                        // a connection was accepted by a ServerSocketChannel.
                        ServerSocketChannel sssc = (ServerSocketChannel) key.channel();
                        // 接受连接
                        SocketChannel cc = sssc.accept();
                        // 请selector 帮忙检测数据到了
                        // 设置非阻塞
                        cc.configureBlocking(false);
                        // 向selector 注册
                        cc.register(selector, SelectionKey.OP_READ, ++connectionCount);
                    } else if (key.isConnectable()) {
                        // a connection was established with a remote server.
                    } else if (key.isReadable()) {
                        // a channel is ready for reading
                        // 4、读取数据进行处理
                        // 交各线程池去处理
                        tpool.execute(new SocketReadProcess(key));
                        // 取消一下注册,防止线程池处理不及时,没有注销掉
                        key.cancel();
                    } else if (key.isWritable()) {
                        // a channel is ready for writing
                    }
                    keyIterator.remove(); // 处理了,一定要从selectedKey集中移除
                }
            }
        }
    }
    
    static class SocketReadProcess implements Runnable {
        SelectionKey key;
        public SocketReadProcess(SelectionKey key) {
            super();
            this.key = key;
        }
        @Override
        public void run() {
            try {
                System.out.println("连接" + key.attachment() + "发来:" + readFromChannel());
                // 如果连接不需要了,就关闭
                key.channel().close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        private String readFromChannel() throws IOException {
            SocketChannel sc = (SocketChannel) key.channel();
            int bfsize = 1024;
            ByteBuffer rbf = ByteBuffer.allocateDirect(bfsize);
            // 定义一个更大的buffer
            ByteBuffer bigBf = null;
            // 读的次数计数
            int count = 0;
            while ((sc.read(rbf)) != -1) {
                count++;
                ByteBuffer temp = ByteBuffer.allocateDirect(bfsize * (count + 1));
                if (bigBf != null) {
                    // 将buffer有写转为读模式
                    bigBf.flip();
                    temp.put(bigBf);
                }
                bigBf = temp;
                // 将这次读到的数据放入大buffer
                rbf.flip();
                bigBf.put(rbf);
                // 为下次读,清理。
                rbf.clear();
                // 读出的是字节,要转为字符串
            }
            if (bigBf != null) {
                // 转为读模式
                bigBf.flip();
                // 转成CharBuffer,再转为字符串。
                return decoder.decode(bigBf).toString();
            }
            return null;
        }
    }
}
NioServer
public class NioClient {
    static Charset charset = Charset.forName("UTF-8");
    public static void main(String[] args) {
        try (SocketChannel sc = SocketChannel.open();) {
            // 连接 会阻塞
            boolean connected = sc.connect(new InetSocketAddress("localhost", 1104));
            System.out.println("connected=" + connected);
            //
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入:");
            String mess = scanner.nextLine();
            ByteBuffer bf = ByteBuffer.wrap(mess.getBytes(charset));
            while (bf.hasRemaining()) {
                int writedCount = sc.write(bf);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
NioClient

如上代码所示,先执行NioServer再执行NioClient,用debug模式分步执行,多个客户端连接的时候,会发现不会阻塞。

 当然对于NIO通信,还可以使用non-blockin模式和更加稳定的java开源框架Netty和MINA。

posted @ 2019-01-07 15:25  怡安  阅读(1336)  评论(1编辑  收藏  举报