学习使用nio编程

学习使用nio编程

文章参考1:https://blog.csdn.net/haoyuyang/article/details/53231585

bio与nio的区别

socket编程就是使用的bio编程的方式.
socket使用:https://blog.csdn.net/sqlgao22/article/details/102858119
bio模型:
在这里插入图片描述
nio模型:
在这里插入图片描述
其本质就是阻塞和非阻塞的区别。
阻塞概念:应用程序在获取网络数据的时候,如果网络传输数据很慢,那么程序就一直等着,直到传输完毕为止。

非阻塞概念:应用程序直接可以获取已经准备就绪的数据,无需等待。

IO为同步阻塞形式,NIO为同步非阻塞形式。NIO没有实现异步,在JDK1.7之后,升级了NIO库包,支持异步非阻塞通信模型,即NIO2.0(AIO)。

同步和异步:同步和异步一般是面向操作系统与应用程序对IO操作的层面上来区别的。①同步时,应用程序会直接参与IO读写操作,并且应用程序会直接阻塞到某一个方法上,直到数据准备就绪(BIO);或者采用轮询的策略实时检查数据的就绪状态,如果就绪则获取数据(NIO)。②异步时,则所有的IO读写操作都交给操作系统处理,与应用程序没有直接关系,应用程序并不关心IO读写,当操作系统完成IO读写操作时,会向应用程序发出通知,应用程序直接获取数据即可。

同步说的是Server服务端的执行方式,阻塞说的是具体的技术,接收数据的方式、状态(io、nio)。

3.NIO编程介绍

学习NIO编程,首先需要了解几个概念:

(1)Buffer(缓冲区)

Buffer是一个对象,它包含一些需要写入或者读取的数据。在NIO类库中加入Buffer对象,体现了新类库与原IO的一个重要区别。在面向流的IO中,可以直接将数据写入或读取到Stream对象中。在NIO类库中,所有的数据都是用缓冲区处理的(读写)。 缓冲区实质上是一个数组,通常它是一个字节数组(ByteBuffer),也可以使用其他类型的数组。这个数组为缓冲区提供了访问数据的读写等操作属性,如位置、容量、上限等概念,具体的可以参考API文档。

Buffer类型:最常使用的是ByteBuffer,实际上每一种java基本类型都对应了一种缓存区(除了Boolean类型)。

①ByteBuffer②CharBuffer③ShortBuffer④IntBuffer⑤LongBuffer⑥FloatBuffer⑦DoubleBuffer

(2)Channel(管道、通道)
在Java中(Socket类),基本的 I/O 操作(bind()、 connect()、 read()和 write())依赖于底层网络传输所提供的功能。
Channel就像自来水管道一样,网络数据通过Channel读取和写入,通道与流的不同之处在于通道是双向的,而流只能在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读、写或者二者同时进行,最关键的是可以和多路复用器集合起来,有多种的状态位,方便多路复用器去识别。通道分为两大类:一类是用于网络读写的SelectableChannel,另一类是用于文件操作的FileChannel,我们使用的SocketChannel和ServerSocketChannel都是SelectableChannel的子类。

(3)Selector(选择器、多路复用器)

是NIO编程的基础,非常重要。多路复用器提供选择已经就绪的任务的能力。简单说,就是Selector会不断的轮询注册在其上的通道(Channel),如果某个通道发生了读写操作,这个通道就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的Channel集合,从而进行后续的IO操作。一个多路复用器(Selector)可以负责成千上万的通道(Channel),没有上限。这也是JDK使用了epoll代替传统的select实现,获得连接句柄(客户端)没有限制。那也就意味着我们只要一个线程负责Selector的轮询,就可以接入成千上万个客户端,这是JDK NIO库的巨大进步。

Selector线程类似一个管理者(Master),管理了成千上万个管道,然后轮询哪个管道的数据已经准备好了,通知CPU执行IO的读取或写入操作。

Selector模式:当IO事件(管道)注册到选择器以后,Selector会分配给每个管道一个key值,相当于标签。Selector选择器是以轮询的方式进行查找注册的所有IO事件(管道),当IO事件(管道)准备就绪后,Selector就会识别,会通过key值来找到相应的管道,进行相关的数据处理操作(从管道中读取或写入数据,写到缓冲区中)。每个管道都会对选择器进行注册不同的事件状态,以便选择器查找。

事件状态:

SelectionKey.OP_CONNECT

SelectionKey.OP_ACCEPT

SelectionKey.OP_READ

SelectionKey.OP_WRITE

NIO通信模型图解:
在这里插入图片描述
数据的中转状态
在这里插入图片描述

nio中的基本api

主要用的方法:

//创建一个缓冲区,int/string/byte等类型都有
ByteBuffer buffer = ByteBuffer.allocate(1024);
//清空缓冲区
buffer.clear();
//复位缓冲区
buffer.flip();
//转为字节数组
buffer.array()
//放入数据
byteBuffer.put("第一个nio网络程序".getBytes("utf8"));

api演示:

@Test
    public void test01() {
        //基本操作
        //1.创建指定长度的缓冲区(创建一个可以读写的HeapIntBuffer)[allocate分配]
        IntBuffer intBuffer = IntBuffer.allocate(10);
        //每put一个intBuffer中的pos(position)就增加一个
        intBuffer.put(11);
        intBuffer.put(22);
        intBuffer.put(33);
        //java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]
        //[位置,限制,容量]
        System.out.println(intBuffer);

        //2.将pos复位,由2变为0 ,,这个方法很重要
        intBuffer.flip();
        //flip()方法实际就是   limit = position;  position = 0;  mark = -1;
        //java.nio.HeapIntBuffer[pos=0 lim=3 cap=10]
        System.out.println(intBuffer);

        //3.获取元素,  复位之后原来的的put的依然在   并且get(index)不会改变intbuffer
        System.out.println(intBuffer.get(1));
        //java.nio.HeapIntBuffer[pos=0 lim=3 cap=10]
        System.out.println(intBuffer);

        //4.替换元素
        //将位置为1的元素替换为2222
        intBuffer.put(1, 2222);
        //获取位置为1的元素是2222
        System.out.println(intBuffer.get(1));
        //java.nio.HeapIntBuffer[pos=0 lim=3 cap=10]
        System.out.println(intBuffer);


        //5.遍历
        for (int i = 0; i < intBuffer.limit(); i++) {
            System.out.println(intBuffer.get()+"\n");
        }
        //使用get()方法,会使pos每次加一 (position++)
        //java.nio.HeapIntBuffer[pos=3 lim=3 cap=10]
        System.out.println(intBuffer);
    }

    @Test
    public void test02() {
        //wrap的使用
        IntBuffer intbuffer = IntBuffer.wrap(new int[]{11, 22, 33});
        //java.nio.HeapIntBuffer[pos=0 lim=3 cap=3]  数组的长度就是容量
        System.out.println(intbuffer);

    }

    @Test
    public void test03() {
        //其他的基本操作
        IntBuffer intBuffer = IntBuffer.allocate(10);
        intBuffer.put(new int[]{11, 22, 33});
        //java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]
        System.out.println(intBuffer);
        //1.复制一个一模一样的intBuffer
        IntBuffer intBuffer1 = intBuffer.duplicate();
        //java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]
        System.out.println(intBuffer1);

        //2.将intbuffer中的数据放在一个int数组中
        int remaining = intBuffer.remaining();
        //remaining = limit - position,还剩多少地方
        System.out.println(remaining);
        int[] arr1 = new int[intBuffer.remaining()];
        //将缓冲区的数据放入arr1中
        intBuffer.get(arr1);
        for(Integer i : arr1) {
            System.out.print(Integer.toString(i) + ",");
        }
    }

实现一个简单的server和client发消息功能

注释很详细

/**
 * 实现简单的单向通信
 */
public class NioOne {

    /*=======================这是服务端start===============================*/
    static class Server {
        //创建一个缓冲区,大家共用
        static ByteBuffer buffer = ByteBuffer.allocate(1024);
        public static void main(String[] args) throws IOException, InterruptedException {
            /*--------------------初始化一个服务端-----------------------------|*/
            //创建一个选择器(多路复用器)
            Selector selector = Selector.open();
            //打开服务通道
            ServerSocketChannel ssc = ServerSocketChannel.open();
            //设置服务通道为非阻塞的方式
            ssc.configureBlocking(false);
            //绑定服务端的监听的端口
            ssc.bind(new InetSocketAddress(8888));
            //将服务注册到多路复用器上,并设置状态是"接收"状态
            ssc.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("=====监听端口=8888");
            /*--------------------初始化一个服务端-----------------------------|*/

            //这里必须不断的轮询状态的改变,采用轮询的方式,查询获取“准备就绪”的注册过的操作
            while (selector.select() > 0) {
                /获取当前选择器中所有注册的选择键(“已经准备就绪的操作”)
                Set<SelectionKey> selectionKeySet = selector.selectedKeys();
                //迭代
                Iterator<SelectionKey> iterator = selectionKeySet.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    //当前的状态是接收状态,需要接收客户端的请求同时注册为可读取标志
                    if (key.isAcceptable()) {
                        //获取与客户端交互的通道SocketChannel,服务端的是ServerSocketChannel.注意区分
                        SocketChannel sc = ssc.accept();
                        //设置模式为非阻塞模式
                        sc.configureBlocking(false);
                        //注册到选择器上,并设置为读取标记
                        sc.register(selector, SelectionKey.OP_READ);
                    }

                    if (key.isReadable()) {
                        //清空缓冲区
                        buffer.clear();
                        //获取与客户端连接的通道
                        SocketChannel sc = (SocketChannel) key.channel();

                        //将通道中的数据转到缓冲区中
                        int read = sc.read(buffer);
                        if (read != -1) {
                            //收完消息要复位
                            buffer.flip();

                            //buffer.array()直接返回byte[]
                            String msg = new String(buffer.array(), "utf8");
                            System.out.println("=====收到的消息===" + msg);
                        }

                    }
                }
                //使用过后就移除该key,因为已经没有SocketChannel了
                //iterator.remove();
                //清理使用过的key,目的同上
                selectionKeySet.clear();
                //为了观察输出
                TimeUnit.SECONDS.sleep(1);
            }
        }
    }
    /*=======================这是服务端stop===============================*/

    /*=======================这是客户端start===============================*/
    static class Client {
        //创建一个缓冲区
        static ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        public static void main(String[] args) throws Exception {
            //创建一个端口和ip
            InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 8888);
            //创建一个客户端通道,并且连接网络
            SocketChannel sc = SocketChannel.open(socketAddress);
            //将数据放入缓冲区
            byteBuffer.put("第一个nio网络程序".getBytes("utf8"));

            //缓冲区接收数据之后要复位
            byteBuffer.flip();

            //将缓冲区数据写入通道
            sc.write(byteBuffer);
            System.out.println("==client消息发送成功===");
            //清空数据,下一次使用
            byteBuffer.clear();
            //关闭
            sc.close();
        }
    }
    /*=======================这是客户端stop===============================*/
}

结果

服务端
在这里插入图片描述
客户端
在这里插入图片描述

实现一个双向通信,一问一答

与上边的区别
在服务端收到消息后,转入writeable状态.将数据返回给client.
在客户端,发出消息后,转入readable状态,接收server的返回数据

/**
 * 实现实现双向通信
 */
public class NioTwo {

    /*=========================服务端开始============================*/
    static class Server {
        public static void main(String[] args) throws IOException, InterruptedException {
            /*------------------------初始化服务端------------------------*/
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            Selector selector = Selector.open();
            ServerSocketChannel ssc = ServerSocketChannel.open();
            ssc.configureBlocking(false);
            ssc.bind(new InetSocketAddress(8888));
            //服务端初始要注册为阻塞状态
            ssc.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("===========开始监听8888端口===");
            /*------------------------初始化服务端------------------------*/

            while (selector.select() > 0) {
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();

                    if (key.isAcceptable()) {
                        SocketChannel sc = ssc.accept();
                        sc.configureBlocking(false);
                        //有了请求就应该读了,注册为可读
                        sc.register(selector, SelectionKey.OP_READ);
                    }

                    if (key.isReadable()) {
                        SocketChannel sc = (SocketChannel) key.channel();
                        buffer.clear();
                        if (sc.read(buffer) != -1) {
                            buffer.flip();
                            System.out.println("--客户端消息是==" + new String(buffer.array()));

                            //注册为可写状态
                            sc.register(selector, SelectionKey.OP_WRITE);
                        }
                    }

                    if (key.isWritable()) {
                        //给客户端回消息
                        SocketChannel sc = (SocketChannel) key.channel();
                        buffer.clear();
                        buffer.put("服务端收到消息了".getBytes("utf8"));
                        buffer.flip();
                        sc.write(buffer);
                        sc.close();
                    }
                }
                selectionKeys.clear();

                Thread.sleep(500);
            }
        }
    }
    /*======================服务端结束==============================*/

    /*======================客户端开始==============================*/
    static class Client {
        public static void main(String[] args) throws IOException, InterruptedException {
            /*--------------------客户端初始化-----------------------*/
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            Selector selector = Selector.open();
            InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
            //创建一个客户端通道,并连接
            SocketChannel socketChannel = SocketChannel.open(address);
            socketChannel.configureBlocking(false);
            //注册为可写
            socketChannel.register(selector, SelectionKey.OP_WRITE);
            /*--------------------客户端初始化-----------------------*/

            while (selector.select()>0) {
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();

                    if (key.isWritable()) {
                        SocketChannel sc = (SocketChannel) key.channel();
                        buffer.clear();
                        buffer.put("发给服务端的消息".getBytes("utf8"));
                        buffer.flip();
                        sc.write(buffer);
                        System.out.println("=----消息发送成功---");
                        //改为读状态
                        sc.register(selector, SelectionKey.OP_READ);
                    }

                    if (key.isReadable()) {
                        //接收服务端的回信
                        SocketChannel sc = (SocketChannel) key.channel();
                        buffer.clear();
                        if (sc.read(buffer) != -1) {
                            buffer.flip();
                            System.out.println("--服务端回信==" + new String(buffer.array()));
                        }
                        //关闭连接
                        sc.close();
                    }
                }
                selectionKeys.clear();
                TimeUnit.MILLISECONDS.sleep(500);
            }
        }
    }
    /*=========================客户端结束==============================*/
}

结果

服务端
在这里插入图片描述
客户端
在这里插入图片描述

总结

①创建ServerSocketChannel,为其配置非阻塞模式。

②绑定监听,配置TCP参数,录入backlog大小等。

③创建一个独立的IO线程,用于轮询多路复用器Selector。

④创建Selector,将之前创建的ServerSocketChannel注册到Selector上,并设置监听标识位SelectionKey.OP_ACCEPT。

⑤启动IO线程,在循环体中执行Selector.select()方法,轮询就绪的通道。

⑥当轮询到处于就绪状态的通道时,需要进行操作位判断,如果是ACCEPT状态,说明是新的客户端接入,则调用accept方法接收新的客户端。

⑦设置新接入客户端的一些参数,如非阻塞,并将其继续注册到Selector上,设置监听标识位等。

⑧如果轮询的通道标识位是READ,则进行读取,构造Buffer对象等。

使用nio很麻烦,so,大家经常使用netty框架.

Reactor(一种经典的多路复用I/O模型)模式学习:
https://blog.csdn.net/eric_sunah/article/details/80437025
以及https://www.cnblogs.com/crazymakercircle/p/9833847.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值