JAVA网络编程-非阻塞IO

缓冲区

创建缓冲区
填充与获取
绝对定位
批量操作
数据转换
视图缓冲区
复制缓冲区
分片缓冲区
SocketChannel
连接
非阻塞连接
读取数据
写入数据
ServerSocketChannel
Channels
Socket选项
拆解Selector和SelectionKey
完整的客户端与服务端

缓冲区

在新的IO模型中,不在向输,出流写入数据和从输入流读取数据,而是要从缓冲区中读写数据。像在缓冲流中一样,缓冲区可能就是子节数组。不过,原始实现可以将缓冲区直接与硬件或内存连接,或者使用其他非常高效的实现。

流和缓冲区之间的关键区别在于流是基于子节的,而缓冲区是基于块的流设计为按顺序一个子节接着一个子节的传送数据。出于性能的考虑,也可以传送字节数组。而缓冲区会传送缓冲区中的数据块。可以读写缓冲区的子节之前,这些子节必须已经存储在缓冲区中,而且一次会读/写一个缓冲区的数据。

流和缓冲区之间的第二个区别是,缓冲区支持同一对象的读/写。

除了boolean外,Java的所有基本数据类型都有特定的Buffer子类:ByteBuffer,CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer和DoubleBuffer.每个子类中的方法都有相应类型的返回值和参数列表。例如,DoubleBuffer类有设置和获取double的方法。IntBuffer类有设置和获取int的方法。公共的Buffer基类只提供一些通用方法,这些方法不需要知道缓冲区所包含数据是什么类型。网络程序几乎只会使用BufferBuffer,但程序偶尔也会使用其他类型来取代ByteBuffer.

除了数据列表外,每个缓冲区都记录了4个关键信息。无论缓冲区是何种类型,都有相同的方法来获取和设置这些值

位置(position)缓冲区中将读取或写入的下一个位置。这个位置从0开始,最大值等于缓冲区的大小。可以使用以下两个方法设置和获取:

public final int position();public final Buffer position(int newPosition);

容量(capacity)缓冲区可以保存多少个元素的值。容量值在缓冲区创建时设置,此后不能改变。可以使用以下方法获取。

public final int capacity();jishi

限度(limit)缓冲区中可以访问数据的末尾位置。只要不改变限度,就无法读/写超过这个位置的数据,即使缓冲区有更大的容量也没有用。使用以下方法获取和设置限度。

public final int limit();public final Buffer limit(int newLimit);

标记(mark)缓冲区中客户端指定的索引。通过调用mark()可以标记当前位置,调用reset()可以将position设置到mark()位置。

public final Buffer mark(); public final Buffer reset();

与读取InputStream不同,读取缓冲区实际上不会以任何方式改变缓冲区的数据。只是可能向前或者向后设置位置,从而可以从缓冲区中某个特定位置开始读取。类似的,程序可以调整限度,从而控制要读取的数据的末尾。

公共的Buffer基类还提供了另外几个方法,可以通过这些公共属性的引用来进行操作。

clear()方法将位置position设置为0,并将limit设置为capacity。这么做意味着缓冲区内的数据不会有任何改变,改变的只是两个指针。

public final Buffer clear();

rewind()将位置position设置为0,但不改变limit.

public final Buffer rewind();

flip()方法将limit设置到position,然后将position设置为0.

public final Buffer flip();

remaining()返回当前缓冲区中position和limit之间的元素数,缓冲区可操作剩余数。

public final int remaining();

hasRemaining()如果remaining()返回大于0,则hasRemaining()返回true

public final bookean hasRemaining();

创建缓冲区

public static void main(String[] args) {
        // 创建一个空的缓冲区position位于0,用allocate()创建的 缓冲区基于Java数组实现
        CharBuffer buffer1 = CharBuffer.allocate(1);
        buffer1.put('a');

        // 通过array()方法获取buffer中的数据。
        char[] cs = buffer1.array();
        System.out.println(cs[0]);// 输出为a

        cs[0] = 'b';// 修改数组

        char[] cs2 = buffer1.array();// 再次获取缓冲区内数据
        // 缓冲区内的数据会受数组的修改的影响。
        System.out.println(cs2[0]);// 输出为b
    }
public static void main(String[] args) {
        // 创建一个空的缓冲区position位于0,用allocate()创建的 缓冲区基于Java数组实现
        CharBuffer buffer1 = CharBuffer.allocate(1);
        buffer1.put('a');

        // 通过array()方法获取buffer中的数据。
        char[] cs = buffer1.array();
        System.out.println(cs[0]);//输出为a
        
        //将position设置为0,从而可以继续添加数据,否则会下标越界。其实这是一个覆盖操作。
        buffer1.flip();
        buffer1.put('b');
        
        //更改缓冲区内的数据也是影响到数组。
        System.out.println(cs[0]);//输出为b
    }
public static void main(String[] args) {
        //只有ByteBuffer才支持这个方法。这个方法会直接在以太网卡,核心内存或者其他位置
        //上的缓冲区使用直接内存访问。直接缓冲区比简介缓冲区的效率更高,但是代价
        //也更高。从API的角度看它与allocate()无异。
        Buffer buffer1 = ByteBuffer.allocateDirect(1);
        System.out.println(buffer1.isDirect());//true
        
        Buffer buffer2 = CharBuffer.allocate(1);
        System.out.println(buffer2.isDirect());//false
        
        //使用直接内存创建缓冲区无法获取数组,实际上也并没有数组。
        buffer1.array();
        
    }
public static void main(String[] args) {
        char[] cs = new char[] {'a'};
        //创建已有数据的Buffer
        CharBuffer buffer1 = CharBuffer.wrap(cs);
        //可以添加一个数据,不会报错表示使用wrap创建的Buffer的position是0
        buffer1.append('b');
        //buffer1.append('c');//这将会报错
        
        //修改Buffer会影响到原有的cs
        System.out.println(cs[0]);//输出b
        
        //获取的数组还是构造Buffer的数组
        char[] c = buffer1.array();
        System.out.println(c[0]);//输出同样也是b
        
        //修改Buffer返回的数组
        c[0] = 'c';
        
        //同样会影响构造Buffer的数组
        System.out.println(cs[0]);    
    }

填充与获取

put()和get()可以操作缓冲区的最基本的填充数据和获取数据.两个操作都会移动position,意味着都不能超过limit.否则会抛出异常.

public static void main(String[] args) {
        //创建一个capacity为3的缓冲区
        CharBuffer buffer1 = CharBuffer.allocate(3);
        //每次调用put都会在buffer中的position位置存入元素,position+1
        buffer1.put('0');
        buffer1.put('1');
        buffer1.put('2');
        
        //若position已经超过capacity或limit则抛出异常
        //buffer1.put('3');//java.nio.BufferOverflowException
    }
public static void main(String[] args) {
        //创建一个capacity为3的缓冲区
        CharBuffer buffer1 = CharBuffer.allocate(3);
        buffer1.put('0');
        buffer1.put('1');
        
        //每次调用get会获取buffer里position位置的元素,如果
        //该位置没有元素则会返回一个空字符(不是null)
        //并且position+1
        System.out.println("---"+buffer1.get());//---
        
        //若position已经超过capacity或limit则抛出异常
        //System.out.println("---"+buffer1.get());//java.nio.BufferUnderflowException
    }
public static void main(String[] args) {
        //创建一个capacity为3的缓冲区
        CharBuffer buffer1 = CharBuffer.allocate(3);
        
        buffer1.put('0');
        buffer1.put('1');
        
        //使用flip将缓冲区的position重置
        buffer1.flip();
        
        //使用hasRemaining判断position位置是否受限制。
        while(buffer1.hasRemaining()) {
            System.out.println(buffer1.get());
        }
    }

绝对定位

绝对定位的put()和get()需要指定正确的下标.否则会抛出异常.这两个操作不会更改position

public static void main(String[] args) {
        //创建一个capacity为3的缓冲区
        CharBuffer buffer1 = CharBuffer.allocate(3);
        
        //可以使用绝对定位的方法填充缓冲区,这将不会改变position
        buffer1.put(0, '1');
        buffer1.put(1, '2');
        buffer1.put(2, '3');
        //但如果超过capacity依然会抛出异常
        //buffer1.put(3, '4');//java.lang.IndexOutOfBoundsException
    
    
        //使用绝对定位方法获取元素也不会改变position
        System.out.println(buffer1.get(0));//1
        System.out.println(buffer1.get(1));//2
        System.out.println(buffer1.get(2));//3
        //System.out.println(buffer1.get(3));//java.lang.IndexOutOfBoundsException
        
        //绝对定位添加元素和获取元素后position的位置还是0.
        System.out.println(buffer1.position());//0
    }

批量操作

put()的批量操作一次接收一个数组.如果buffer存不下数组则抛出异常.get()接收一个数组,缓冲区元素不足以填充数组则抛出异常.这两个操作会移动position.

public static void main(String[] args) {
        // 创建一个capacity为3的缓冲区
        CharBuffer buffer1 = CharBuffer.allocate(3);

        // 可以一次填充一个数组的数据
        // buffer1.put(new char[] { 'a', 'b', 'c' });

        // 如果缓冲区容纳不下数组则抛出异常
        // buffer1.put(new char[] {'a','b','c','d'});//java.nio.BufferOverflowException

        // 给定一个数组,从缓冲区的start位置填充length个元素到缓冲区
        buffer1.put(new char[] { 'a', 'b', 'c' }, 0, 2);
        // 当前position的位置是 2 ,表示批量添加也会修改position
        System.out.println(buffer1.position());// 2

    }
public static void main(String[] args) {
        // 创建一个capacity为3的缓冲区
        CharBuffer buffer1 = CharBuffer.allocate(3);
    
            // 可以一次填充一个数组的数据
            buffer1.put(new char[] { 'a', 'b', 'c' });
    
            buffer1.flip();
            
            //将缓冲区元素复制到
//            char b[] = new char[3];
//            buffer1.get(b);
//            System.out.println(Arrays.toString(b));//[a, b, c]
            
            //缓冲区元素比数组多不会异常
//            char b[] = new char[2];
//            buffer1.get(b);
//            System.out.println(Arrays.toString(b));//[a, b]
            
            //缓冲区元素不足以填充数组则抛出异常
//            char b[] = new char[4];
//            buffer1.get(b);//java.nio.BufferUnderflowException
    }
public static void main(String[] args) {
        // 创建一个capacity为3的缓冲区
        CharBuffer buffer1 = CharBuffer.allocate(3);

        // 可以一次填充一个数组的数据
        buffer1.put(new char[] { 'a', 'b', 'c' });

        buffer1.flip();
        
        //char[] c = new char[10];
        //将缓冲区元素填充到数组,从数组的start位置开始填充length个
        //buffer1.get(c, 1, 2);
        //[ , a, b, , , , , , , ]
        //System.out.println(Arrays.toString(c));
        
        //若缓冲区内元素不够填充数组则抛出异常
        char[] c = new char[10];
        buffer1.get(c, 1, 4);//java.nio.BufferUnderflowException
    }

数据转换 

ByteBuffer允许添加除了Boolean类型外的其他7中基本数据类型。添加byte会占用1个capacity,short占用2个子节,int占用4个子节,long占用8个子节,float占用4个子节,double占用8个子节。char占用2个子节。

public static void main(String[] args) {
        ByteBuffer b = ByteBuffer.allocate(100);

        b.put((byte) 1);
        System.out.println(b.position());//1

        b.putShort((short) 1);
        System.out.println(b.position());//3

        b.putInt(1);
        System.out.println(b.position());//7

        b.putLong(1L);
        System.out.println(b.position());//15

        b.putFloat(1F);
        System.out.println(b.position());//19

        b.putDouble(1D);
        System.out.println(b.position());//27

        b.putChar('1');
        System.out.println(b.position());//29
    }

视图缓冲区

SocketChannel获取的只能是ByteBuffer,如果可以确定输出的是某种基本数据类型则可以使用视图缓冲区,ByteBuffer有6中方法可以将ByteBuffer转换为其他的6中数据类型。以下代码为转换为IntBuffer视图。

public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(4);
        IntBuffer intBuffer = byteBuffer.asIntBuffer();
        
        byteBuffer.put((byte)0);
        byteBuffer.put((byte)0);
        byteBuffer.put((byte)0);
        byteBuffer.put((byte)1);
        
        System.out.println(intBuffer.capacity());//1
        System.out.println(intBuffer.get());//1
        System.out.println(intBuffer.position());//1
    }

复制缓冲区

可以从现有的缓冲区上复制一个新的缓冲区,两个缓冲区有相同的capacity但是position和limit是独立的。两个缓冲区会互相影响。

public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(4);
        //复制缓冲区
        ByteBuffer byteBuffer2 = byteBuffer.duplicate();
        
        //原缓冲区添加
        byteBuffer.put((byte)1);
        System.out.println(byteBuffer.position());//position 1
        
        System.out.println(byteBuffer2.position());//copy缓冲区position 0
        System.out.println(byteBuffer2.get());//可以获取 1
        
        byteBuffer2.put(0, (byte)2);//修改copy缓冲区
        
        System.out.println(byteBuffer.get(0));//获取原缓冲区 2
    }

分片缓冲区

分片缓冲区大致和复制缓冲区类似,区别在于分片缓冲区的大小只能是原缓冲区position到limit的长度。

public static void main(String[] args) {
        //原缓冲区
        IntBuffer buffer = IntBuffer.allocate(10);
        //添加5个元素
        buffer.put(1);
        buffer.put(2);
        buffer.put(3);
        buffer.put(4);
        buffer.put(5);
        
        //从原缓冲区上创建分片缓冲区
        IntBuffer buffer2 = buffer.slice();
        //分片缓冲区的capacity为原缓冲区的剩余空间
        System.out.println(buffer2.capacity());//5
        //分片缓冲区的position从0开始
        System.out.println(buffer2.position());//0
        //分片缓冲区添加元素
        buffer2.put(6);
        buffer2.put(7);
        buffer2.put(8);
        buffer2.put(9);
        buffer2.put(10);
        //缓冲区位置不变
        System.out.println(buffer.position());
        //原缓冲区数据被同步
        System.out.println(buffer.get(5));
    }

SocketChannel

SocketChannel类可以读写TCP Socket.数据必须编码到ByteBuffer对象中来完成读/写。每个SocketChannel都和一个对等端Socket对象相关联。

连接

public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(1);
        while(true) {
            Socket socket = server.accept();
            OutputStream out = socket.getOutputStream();
            out.write(new String("abc").getBytes());
            socket.close();
        }
    }//BIO服务端

public static SocketChannel open(SocketAddress remote)throws IOException;这个方法将会立刻打开连接。在建立连接之前或者建立失败之前不会返回,而是一直阻塞。

public static void main(String[] args) throws Exception {
        //使用通道连接
        SocketChannel client = SocketChannel.open(new InetSocketAddress("127.0.0.1", 1));
    }//NIO客户端

非阻塞连接

要设置非阻塞通道则需要使用迂回的方法创建连接,先设置然后调用connect()。非阻塞通道在连接建立之前就会返回,所以在使用连接之前必须调用finishConnect()判断连接是否可用。

public static void main(String[] args) throws Exception {
        //使用通道连接
        SocketChannel channel = SocketChannel.open();
        //设置非阻塞连接
        channel.configureBlocking(false);
        channel.connect(new InetSocketAddress("127.0.0.1", 1));
        
        //如果连接打开返回true
        System.out.println(channel.isConnected());
        //如果连接仍在建立,但没有打开则返回true
        System.out.println(channel.isConnectionPending());
        //如何连接可用返回true
        System.out.println(channel.finishConnect());    
    }//NIO客户端

读取数据

从channel中读取数据基本的方法是read()这个方法接收buffer,它与BIO的read()方法类似,如果可以读取数据则一次读取buffer个字节(理想状态下,也可能一次读取的字节不满buffer).当读取到末尾则返回-1.这里关键点在于如何操纵buffer,flip()和clear()两个方法前者会将limit置为当前position,然后把position置为0.后者会把position置为0,limit置为capacity.

read(Buffer[])和read(Buffer,off,len)两个方法则应用于一次读取存入多个缓冲区中.read(Buffer[])会存入缓冲区数组中的每一个buffer中,read(Buffer,off,len)则只会存入off开始len个长度的buffer.(有点咬嘴,看下边demo即可)

public static void main(String[] args) throws Exception {
        //使用通道连接
        SocketChannel channel = SocketChannel.open();
        //设置非阻塞连接
        channel.configureBlocking(false);
        channel.connect(new InetSocketAddress("127.0.0.1", 1));
        //保证连接可用
        while (!channel.finishConnect()) {
        }
        //创建缓冲区,所有基于NIO的输入和输出都基于缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(2);
        //本地的输出流
        ByteOutputStream out = new ByteOutputStream();
        //循环读取,将读取的数据存入buffer,如果返回-1则表示流结束.
        //每次read到的最大字节数是buffer的capacity.或者不满capacity
        while (channel.read(buffer)!=-1){
            //每次read到buffer后buffer的position会发生变化.如果要读则先要重置position为
            //0,总共读取limit个字节.flip会将limit置为position,position置为0
            buffer.flip();
            while (buffer.hasRemaining()){//判断是否到达limit
                //如果没有到达limit则不断的从缓冲区get,然后将字节存入本地的输出流
                out.write(buffer.get());
            }
            //buffer读取完毕后,必须要在次重置position为0,limit置为capacity.clear方法可以做到.
          buffer.clear();
        }
        System.out.println(out.toString());
    }//NIO客户端
public static void main(String[] args) throws Exception {
        //使用通道连接
        SocketChannel channel = SocketChannel.open();
        //设置非阻塞连接
        channel.configureBlocking(false);
        channel.connect(new InetSocketAddress("127.0.0.1", 1));
        //保证连接可用
        while (!channel.finishConnect()) {
        }
        //创建多个缓冲区
        ByteBuffer buffer1 = ByteBuffer.allocate(3);
        ByteBuffer buffer2 = ByteBuffer.allocate(3);
        ByteBuffer buffer3 = ByteBuffer.allocate(3);
        //将多个缓存区存入数组
        ByteBuffer[] bufArray = new ByteBuffer[3];
        bufArray[0] = buffer1;
        bufArray[1] = buffer2;
        bufArray[2] = buffer3;
        ByteOutputStream out = new ByteOutputStream();
        //read(arr[])会一个一个的向每个缓冲区写入数据.
        while (channel.read(bufArray) != -1) {
            //循环每一个缓冲区,依然要注意flip,clear
            for (int i = 0; i < bufArray.length; i++) {
                bufArray[i].flip();
                while (bufArray[i].hasRemaining()) {
                    out.write(bufArray[i].get());
                }
                bufArray[i].clear();
            }
        }
        System.out.println(out.toString());
    }//NIO客户端
public static void main(String[] args) throws Exception {
        //使用通道连接
        SocketChannel channel = SocketChannel.open();
        //设置非阻塞连接
        channel.configureBlocking(false);
        channel.connect(new InetSocketAddress("127.0.0.1", 1));
        //保证连接可用
        while (!channel.finishConnect()) {
        }
        //创建多个缓冲区
        ByteBuffer buffer1 = ByteBuffer.allocate(3);
        ByteBuffer buffer2 = ByteBuffer.allocate(3);
        ByteBuffer buffer3 = ByteBuffer.allocate(3);
        //将多个缓存区存入数组
        ByteBuffer[] bufArray = new ByteBuffer[3];
        bufArray[0] = buffer1;
        bufArray[1] = buffer2;
        bufArray[2] = buffer3;
        ByteOutputStream out = new ByteOutputStream();
        //read(arr[])会一个一个的向每个缓冲区写入数据.
        while (channel.read(bufArray,0,1) != -1) {
            System.out.println(buffer1.position());
            System.out.println(buffer2.position());
            System.out.println(buffer3.position());
            //循环每一个缓冲区,依然要注意flip,clear
            for (int i = 0; i < bufArray.length; i++) {
                bufArray[i].flip();
                while (bufArray[i].hasRemaining()) {
                    out.write(bufArray[i].get());
                }
                bufArray[i].clear();
            }
        }
        System.out.println(out.toString());
    }//NIO客户端

写入数据

向channel中写入数据的方法是write(buffer);它将把从缓冲区的position开始读取数据到limit结束将这些数据写入到channel中.写入完毕后通道要关闭输出流.对于服务端亦是如此.同时也有一次写入多个buffer的方法read(buffer[]).和read(buffer[],off,leng).

public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(1);
        while(true) {
            Socket socket = server.accept();
            OutputStream out = socket.getOutputStream();
            out.write(new String("abcdefg").getBytes());
            //写入数据完毕后一定要手动关闭输出流.否则你的接收方不知道你是否写入完毕,会一直等待.
            socket.shutdownOutput();
            //获取客户端数据
            InputStream in = socket.getInputStream();
            byte[]b= new byte[1024];
            int len = 0;
            //将客户端数据存储到容器中
            ByteOutputStream out2 = new ByteOutputStream();
            while ((len=in.read(b))!=-1){
                out2.write(b,0,len);
            }
            System.out.println(out2.toString());
            socket.close();
        }
    }//BIO服务端
public static void main(String[] args) throws Exception {
        //使用通道连接
        SocketChannel channel = SocketChannel.open();
        //设置非阻塞连接
        channel.configureBlocking(false);
        channel.connect(new InetSocketAddress("127.0.0.1", 1));
        //保证连接可用
        while (!channel.finishConnect()) {
        }
        //创建缓冲区,所有基于NIO的输入和输出都基于缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(2);
        //本地的输出流
        ByteOutputStream out = new ByteOutputStream();
        //循环读取,将读取的数据存入buffer,如果返回-1则表示流结束.
        //每次read到的最大字节数是buffer的capacity.或者不满capacity
        while (channel.read(buffer)!=-1){
            //每次read到buffer后buffer的position会发生变化.如果要读则先要重置position为
            //0,总共读取limit个字节.flip会将limit置为position,position置为0
            buffer.flip();
            while (buffer.hasRemaining()){//判断是否到达limit
                //如果没有到达limit则不断的从缓冲区get,然后将字节存入本地的输出流
                out.write(buffer.get());
            }
            //buffer读取完毕后,必须要在次重置position为0,limit置为capacity.clear方法可以做到.
            buffer.clear();
        }
        System.out.println(out.toString());

        //将要发送的数据写入到buffer中
        ByteBuffer buffer1 = ByteBuffer.allocate(10);
        buffer1.put("This NIO".getBytes());
        //数据存入完毕后一定要flip,它会充值limit和position
        buffer1.flip();
        //将buffer数据写入到channel中,写入的从position开始到limit结束
        channel.write(buffer1);
        channel.shutdownOutput();

        channel.close();
    }//NIO客户端
public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(1);
        while(true) {
            Socket socket = server.accept();
            OutputStream out = socket.getOutputStream();
            out.write(new String("abcdefg").getBytes());
            //写入数据完毕后一定要手动关闭输出流.否则你的接收方不知道你是否写入完毕,会一直等待.
            socket.shutdownOutput();
            //获取客户端数据
            InputStream in = socket.getInputStream();
            byte[]b= new byte[1024];
            int len = 0;
            //将客户端数据存储到容器中
            ByteOutputStream out2 = new ByteOutputStream();
            while ((len=in.read(b))!=-1){
                out2.write(b,0,len);
            }
            System.out.println(out2.toString());
            socket.close();
        }
    }//BIO服务端
public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(1);
        while(true) {
            Socket socket = server.accept();
            OutputStream out = socket.getOutputStream();
            out.write(new String("abcdefg").getBytes());
            //写入数据完毕后一定要手动关闭输出流.否则你的接收方不知道你是否写入完毕,会一直等待.
            socket.shutdownOutput();
            //获取客户端数据
            InputStream in = socket.getInputStream();
            byte[]b= new byte[1024];
            int len = 0;
            //将客户端数据存储到容器中
            ByteOutputStream out2 = new ByteOutputStream();
            while ((len=in.read(b))!=-1){
                out2.write(b,0,len);
            }
            System.out.println(out2.toString());
            socket.close();
        }
    }//BIO服务端

ServerSocketChannel

ServerSocketChannel类只有一个目的:接受入站连接.你无法读取,写入或连接ServerSocketChannel.它支持的唯一操作就是接受一个新的入站连接.这个类的accept()方法最重要,ServerSocketChannel的非阻塞模式需要向Selector注册得到入站连接.在阻塞模式下,它的作用仅仅是无限循环的调用accept()接受新的入站信息.accept接受到入站连接后返回一个SocketChannel它的操作与客户端的操作无异.

public static void main(String[] args) throws Exception{
        ServerSocketChannel server = ServerSocketChannel.open();
        server.bind(new InetSocketAddress(1));
        while (true){
            SocketChannel client = server.accept();
            ByteBuffer buffer = ByteBuffer.allocate(10);
            buffer.put("abcdef".getBytes());
            buffer.flip();
            client.write(buffer);
            client.shutdownOutput();

            ByteBuffer buffer1 = ByteBuffer.allocate(10);
            ByteOutputStream out = new ByteOutputStream();
            while (client.read(buffer1)!=-1){
                buffer1.flip();
                while (buffer1.hasRemaining()){
                    out.write(buffer1.get());
                }
                buffer1.clear();
            }
            System.out.println(out.toString());
            client.close();
        }
    }//NIO服务器
public static void main(String[] args) throws Exception {
        //使用通道连接
        SocketChannel channel = SocketChannel.open();
        //设置非阻塞连接
        channel.configureBlocking(false);
        channel.connect(new InetSocketAddress("127.0.0.1", 1));
        //保证连接可用
        while (!channel.finishConnect()) {
        }
        //创建缓冲区,所有基于NIO的输入和输出都基于缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(2);
        //本地的输出流
        ByteOutputStream out = new ByteOutputStream();
        //循环读取,将读取的数据存入buffer,如果返回-1则表示流结束.
        //每次read到的最大字节数是buffer的capacity.或者不满capacity
        while (channel.read(buffer)!=-1){
            //每次read到buffer后buffer的position会发生变化.如果要读则先要重置position为
            //0,总共读取limit个字节.flip会将limit置为position,position置为0
            buffer.flip();
            while (buffer.hasRemaining()){//判断是否到达limit
                //如果没有到达limit则不断的从缓冲区get,然后将字节存入本地的输出流
                out.write(buffer.get());
            }
            //buffer读取完毕后,必须要在次重置position为0,limit置为capacity.clear方法可以做到.
            buffer.clear();
        }
        System.out.println(out.toString());

        //将要发送的数据写入到buffer中
        ByteBuffer buffer1 = ByteBuffer.allocate(10);
        buffer1.put("This NIO".getBytes());
        ByteBuffer buffer2 = ByteBuffer.allocate(10);
        buffer2.put("This NIO".getBytes());
        ByteBuffer buffer3 = ByteBuffer.allocate(10);
        buffer3.put("This NIO".getBytes());

        //数据存入完毕后一定要flip,它会充值limit和position
        buffer1.flip();
        buffer2.flip();
        buffer3.flip();

        ByteBuffer[] bs = new ByteBuffer[3];
        bs[0] = buffer1;
        bs[1] = buffer2;
        bs[2] = buffer3;

        //将buffer数据写入到channel中,写入的从position开始到limit结束
        channel.write(bs,0,2);
        channel.shutdownOutput();

        System.out.println(channel.isOpen());
        channel.close();
        System.out.println(channel.isOpen());
    }//NIO客户端

Channels

Channels是一个简单的工具类,可以将传统的基于IO的流,阅读器,书写器包装在通道中,也可以从通道转换为基于IO的流,书写器,阅读器.总之Channels有一些将NIOAPI转换为BIOAPI的方法,和BIOAPI转换为NIOAPI的方法.

public static void main(String[] args) throws Exception {
        FileInputStream in = new FileInputStream("C:\\Users\\bykj\\Desktop\\周报.txt");
        //通过BIO输入流获取Channel
        ReadableByteChannel inChannel = Channels.newChannel(in);
        FileOutputStream out = new FileOutputStream("C:\\Users\\bykj\\Desktop\\111.txt");
        //通过BIO输出流获取Channel
        WritableByteChannel outChannel = Channels.newChannel(out);
        //操纵通道复制文件.
        ByteBuffer buffer = ByteBuffer.allocate(10);
        while (inChannel.read(buffer) != -1) {
            buffer.flip();
            outChannel.write(buffer);
            buffer.clear();
        }
        inChannel.close();
        outChannel.close();
        in.close();
        out.close();
    }
public static void main(String[] args) throws Exception {
        FileInputStream inFile = new FileInputStream("C:\\Users\\bykj\\Desktop\\周报.txt");
        FileChannel channel = inFile.getChannel();

        FileOutputStream outFile = new FileOutputStream("C:\\Users\\bykj\\Desktop\\111.txt");
        FileChannel channel1 = outFile.getChannel();

        ByteBuffer buffer = ByteBuffer.allocate(20);
        while (channel.read(buffer) != -1) {
            buffer.flip();
            channel1.write(buffer);
            buffer.clear();
        }

        channel.close();
        channel1.close();
        inFile.close();
        outFile.close();
    }
public static void main(String[] args) throws Exception {
        FileInputStream inFile = new FileInputStream("C:\\Users\\bykj\\Desktop\\周报.txt");
        FileChannel channel = inFile.getChannel();

        FileOutputStream outFile = new FileOutputStream("C:\\Users\\bykj\\Desktop\\111.txt");
        FileChannel channel1 = outFile.getChannel();

        InputStream inputStream = Channels.newInputStream(channel);
        OutputStream outputStream = Channels.newOutputStream(channel1);

        byte[] b = new byte[20];
        int len = 0;
        while ((len=inputStream.read(b))!=-1){
            outputStream.write(b,0,len);
        }

        inputStream.close();
        outputStream.close();
        inFile.close();
        outFile.close();
    }
public static void main(String[] args) throws Exception {
        FileInputStream inFile = new FileInputStream("C:\\Users\\bykj\\Desktop\\周报.txt");
        FileChannel channel = inFile.getChannel();

        FileOutputStream outFile = new FileOutputStream("C:\\Users\\bykj\\Desktop\\111.txt");
        FileChannel channel1 = outFile.getChannel();

        Reader reader = Channels.newReader(channel, "UTF-8");
        Writer writer = Channels.newWriter(channel1, "UTF-8");

        char[] c = new char[20];
        int len = 0;
        while ((len = reader.read(c))!=-1){
            writer.write(c,0,len);
        }
        reader.close();
        writer.close();
        channel.close();
        channel1.close();
    }

Socket选项

在Socket和ServerSocket中可以设置TCP_NODELAY,SO_TIMEOUT等选项.为Socket或ServerSocket设置更多的属性.Channel也支持设置类似的选项.但是NIO的Channel有多个.每种可设置的选项是不同的.通过以下方法可以查询特定的Channel可以设置的选项.

public static void main(String[] args) throws Exception {
        printOptions(SocketChannel.open());
        printOptions(ServerSocketChannel.open());
        printOptions(AsynchronousSocketChannel.open());
        printOptions(AsynchronousServerSocketChannel.open());
        printOptions(DatagramChannel.open());
    }

    private static void printOptions(NetworkChannel channel) throws Exception {
        System.out.println(channel.getClass().getSimpleName());
        Set<SocketOption<?>> socketOptions = channel.supportedOptions();
        for (SocketOption<?> option : socketOptions) {
            System.out.println(option.name());
        }
        System.out.println();
        channel.close();
    }

拆解Selector和SelectionKey

只有设置Channel为非阻塞才可以将Channel注册到Selector里。否则会抛出java.nio.channels.IllegalBlockingModeException异常。

public static void main(String[] args) throws Exception {
        // 创建服务端
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        // 设置服务端为非阻塞
        //serverChannel.configureBlocking(false);
        serverChannel.bind(new InetSocketAddress(1));

        // 唯一一个获取Selector的方法
        Selector selector = Selector.open();
        // 向选择器增加通道,不是所有的通道都支持注册到Selector的,但是所有的网络通道都是可以的。
        // 使用 | 定义要注册到通道的操作类型
        serverChannel.register(selector, SelectionKey.OP_ACCEPT, "附件");
    }// NIO服务端

ServerSocketChannel和SocketChannel注册到Selector里的事件不是随便的。ServerSocketChannel只能注册OP_ACCEPT(客户端连接)SocketChannel可以注册三种事件OP_CONNECT(客户端连接服务器,其实在服务端完全用不上这个,只有使用NIO客户端才用的上。)OP_WRITE(写准备就绪)OP_READ(读准备就绪)

public static void main(String[] args) throws Exception {
        // 创建服务端
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        // 设置服务端为非阻塞
        serverChannel.configureBlocking(false);
        serverChannel.bind(new InetSocketAddress(1));

        // 唯一一个获取Selector的方法
        Selector selector = Selector.open();
        // 向选择器增加通道,不是所有的通道都支持注册到Selector的,但是所有的网络通道都是可以的。
        // 使用 | 定义要注册到通道的操作类型
        serverChannel.register(selector, SelectionKey.OP_ACCEPT, "附件");

        while (true) {
            System.out.println("~~~~~~~~~~~~~~~~~~~");
            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel client = server.accept();
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_READ | SelectionKey.OP_CONNECT | SelectionKey.OP_WRITE,
                            "附件");
                    System.out.println("注册客户端连接到selector");
                }
            }
        }
    }// NIO服务端

register()的第三个参数用于给准备就绪的事件传递附件参数。在事件就绪后可以通过SelectionKey的attachment()获取该附件。

public static void main(String[] args) throws Exception {
        // 创建服务端
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        // 设置服务端为非阻塞
        serverChannel.configureBlocking(false);
        serverChannel.bind(new InetSocketAddress(1));

        // 唯一一个获取Selector的方法
        Selector selector = Selector.open();
        // 向选择器增加通道,不是所有的通道都支持注册到Selector的,但是所有的网络通道都是可以的。
        // 使用 | 定义要注册到通道的操作类型
        serverChannel.register(selector, SelectionKey.OP_ACCEPT, "附件");

        while (true) {
            System.out.println("~~~~~~~~~~~~~~~~~~~");
            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                String s = (String)key.attachment();
                System.out.println(s);
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel client = server.accept();
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_READ | SelectionKey.OP_CONNECT | SelectionKey.OP_WRITE,
                            "附件");
                    System.out.println("注册客户端连接到selector");
                }
            }
        }
    }// NIO服务端

为什么一定要remove()掉key。selectedKeys这个集合是Selector这个选择器的公用集合,也就是说所有的通道注册的事件,只要准备就绪都会在这里出现,如果不删除也不做其他的操作,下次从select()返回后,会再向selectedKeys中添加一个key,在用迭代器遍历就会出现一个通道的同一个事件被注册多次,并且第一次添加的key依然是准备就绪的。在进行key.channel()就会抛出异常。

public static void main(String[] args) throws Exception {
        // 创建服务端
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        // 设置服务端为非阻塞
        serverChannel.configureBlocking(false);
        serverChannel.bind(new InetSocketAddress(1));

        // 唯一一个获取Selector的方法
        Selector selector = Selector.open();
        // 向选择器增加通道,不是所有的通道都支持注册到Selector的,但是所有的网络通道都是可以的。
        // 使用 | 定义要注册到通道的操作类型
        serverChannel.register(selector, SelectionKey.OP_ACCEPT, "附件");

        while (true) {
            System.out.println("~~~~~~~~~~~~~~~~~~~");
            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            System.out.println(keys.size());
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                //iterator.remove();
                String s = (String)key.attachment();
                System.out.println(s);
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel client = server.accept();
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_READ | SelectionKey.OP_CONNECT | SelectionKey.OP_WRITE,
                            "附件");
                    System.out.println("注册客户端连接到selector");
                }
            }
        }
    }// NIO服务端

cancel()取消key的注册。不使用remove()的方式是key.cancel()这将取消其Selection对象的注册,这样选择器就不会浪费资源再去查询它是否准备就绪。

public static void main(String[] args) throws Exception {
        // 创建服务端
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        // 设置服务端为非阻塞
        serverChannel.configureBlocking(false);
        serverChannel.bind(new InetSocketAddress(1));

        // 唯一一个获取Selector的方法
        Selector selector = Selector.open();
        // 向选择器增加通道,不是所有的通道都支持注册到Selector的,但是所有的网络通道都是可以的。
        // 使用 | 定义要注册到通道的操作类型
        serverChannel.register(selector, SelectionKey.OP_ACCEPT, "附件");

        while (true) {
            System.out.println("~~~~~~~~~~~~~~~~~~~");
            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            System.out.println(keys.size());
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                //iterator.remove();
                String s = (String) key.attachment();
                System.out.println(s);
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel client = server.accept();
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_READ | SelectionKey.OP_CONNECT | SelectionKey.OP_WRITE,
                            "附件");
                    System.out.println("注册客户端连接到selector");
                    key.cancel();
                }
            }
        }
    }// NIO服务端

但是如果cancel()后再次使用这个key就会抛出java.nio.channels.CancelledKeyException以下情况就是取消key后,程序继续执行到可读,就报错了。

public static void main(String[] args) throws Exception {
        // 创建服务端
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        // 设置服务端为非阻塞
        serverChannel.configureBlocking(false);
        serverChannel.bind(new InetSocketAddress(1));

        // 唯一一个获取Selector的方法
        Selector selector = Selector.open();
        // 向选择器增加通道,不是所有的通道都支持注册到Selector的,但是所有的网络通道都是可以的。
        // 使用 | 定义要注册到通道的操作类型
        serverChannel.register(selector, SelectionKey.OP_ACCEPT, "附件");

        while (true) {
            System.out.println("~~~~~~~~~~~~~~~~~~~");
            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            System.out.println(keys.size());
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                String s = (String) key.attachment();
                System.out.println(s);
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel client = server.accept();
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_READ | SelectionKey.OP_CONNECT | SelectionKey.OP_WRITE,
                            "附件");
                    System.out.println("注册客户端连接到selector");
                    key.cancel();
                }
                if (key.isReadable()) {
                    SocketChannel client = (SocketChannel) key.channel();
                    client.write(ByteBuffer.wrap("你好".getBytes()));
                    client.shutdownOutput();
                }

            }
        }
    }// NIO服务端

完整的客户端与服务端

这里特别需要注意的是对于NIO的服务端如果同时有读和写的操作一定要在写之后key.cancel。对于NIO的客户端一定要在读之后key.cancel。因为客户端肯定是先写后读的,服务端是先读后写的。

package com.datang.bingxiang.demo;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringBufferInputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.Iterator;
import java.util.Set;

public class NioClient {
    public static void main(String[] args) throws Exception {
        // 使用通道连接
        SocketChannel channel = SocketChannel.open();
        // 设置非阻塞连接
        channel.configureBlocking(false);
        
        Selector selector = Selector.open();

        channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
        
        channel.connect(new InetSocketAddress("127.0.0.1", 1));

         while (!channel.finishConnect()){

         }

        while (true) {
            System.out.println("!!!!!!!!!!!!!!");
            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            System.out.println(keys.size());
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                
                if (key.isWritable()) {// 服务端可写
                    System.out.println("服务端可写");
                    SocketChannel client = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.wrap("你好".getBytes());
                    client.write(buffer);
                    client.shutdownOutput();
                }

                if (key.isReadable()) {// 服务端可读
                    System.out.println("服务端可读");
                    SocketChannel client = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(2);
                    ByteArrayOutputStream bos = new ByteArrayOutputStream();
                    while (client.read(buffer) != -1) {
                        buffer.flip();
                        while (buffer.hasRemaining()) {
                            bos.write(buffer.get());
                        }
                        buffer.clear();
                    }
                    System.out.println(bos.toString());
                    client.shutdownInput();
                    key.cancel();
                }
            }
        }

    }// NIO客户端
}
package com.datang.bingxiang.demo;

import java.io.ByteArrayOutputStream;
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.util.Iterator;
import java.util.Set;

import org.apache.tomcat.util.buf.ByteChunk.ByteOutputChannel;

public class NioServer {
    public static void main(String[] args) throws Exception {
        // 创建服务端
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        // 设置服务端为非阻塞
        serverChannel.configureBlocking(false);
        serverChannel.bind(new InetSocketAddress(1));

        // 唯一一个获取Selector的方法
        Selector selector = Selector.open();
        // 向选择器增加通道,不是所有的通道都支持注册到Selector的,但是所有的网络通道都是可以的。
        // 使用 | 定义要注册到通道的操作类型
        serverChannel.register(selector, SelectionKey.OP_ACCEPT, "附件");

        while (true) {
            System.out.println("~~~~~~~~~~~~~~~~~~~");
            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            System.out.println(keys.size());
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                String s = (String) key.attachment();
                System.out.println(s);
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel client = server.accept();
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, "附件");
                    System.out.println("注册客户端连接到selector");
                }

                if (key.isReadable()) {// 客户端可读
                    System.out.println("客户端可读");
                    SocketChannel client = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(2);
                    ByteArrayOutputStream bos = new ByteArrayOutputStream();
                    while (client.read(buffer) != -1) {
                        buffer.flip();
                        while (buffer.hasRemaining()) {
                            bos.write(buffer.get());
                        }
                        buffer.clear();
                    }
                    client.shutdownInput();
                    System.out.println(bos.toString());
                }

                if (key.isWritable()) {// 客户端可写
                    System.out.println("客户端可写");
                    SocketChannel client = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.wrap("你好".getBytes());
                    client.write(buffer);
                    client.shutdownOutput();
                    System.out.println("写入完毕");
                    key.cancel();
                }

            }
        }
    }// NIO服务端
}

 

posted @ 2021-08-14 23:45  顶风少年  阅读(319)  评论(0编辑  收藏  举报
返回顶部