前言

  • 在将NIO之前,咱们必须要理解一下Java的IO局部常识。
  • BIO(Blocking IO)
  • 阻塞IO,在Java中次要就是通过ServerSocket.accept()实现的。
  • NIO(Non-Blocking IO)
  • 非阻塞IO,在Java次要是通过NIOSocketChannel + Seletor实现的。
  • AIO(Asyc IO)
  • 异步IO,目前不做学习。

BIO

简略实现服务器和客户端

package net.io;import net.ByteUtil;import java.io.*;import java.net.ServerSocket;import java.net.Socket;//NIO(NonBlocking IO)非阻塞IO//通过一个事件监听器,吧这些客户端的连贯保存起来,如果有工夫产生再去解决,没工夫产生不解决public class Server {    public Server(int port) {        try {            //创立服务器端,监听端口port            ServerSocket serverSocket = new ServerSocket(port);            //对客户端进行一个监听操作,如果有连贯过去,就将连贯返回(socket)-----阻塞办法            while (true) {                //监听,阻塞办法                Socket socket = serverSocket.accept();                //每个服务器和客户端的通信都是针对与socket进行操作                System.out.println("客户端" + socket.getInetAddress());                InputStream inputStream = socket.getInputStream();                ObjectInputStream ois = new ObjectInputStream(inputStream);                //获取客户端发送的message                Object get = ois.readObject();                System.out.println("接管到的音讯为:"  + get);                //服务器须要给客户端进行一个回应                OutputStream outputStream = socket.getOutputStream();                ObjectOutputStream oos = new ObjectOutputStream(outputStream);                String message = "客户端你好,我是服务器端";                //我这里写了,不代表发送了,常识写到了输入流的缓冲区                oos.writeObject(message);                //发送并清空                oos.flush();            }        } catch (Exception e) {            e.printStackTrace();        }    }    public static void main(String[] args) {        new Server(7000);    }}
package net.io;import net.ByteUtil;import java.io.*;import java.net.Socket;public class Client {    public Client(int port){        try {            Socket socket = new Socket("localhost",port);            //inputStream是输出流,从里面接管信息            //outpurStream是输入流, 往外面输入信息            OutputStream outputStream = socket.getOutputStream();            ObjectOutputStream oos = new ObjectOutputStream(outputStream);            //发送的信息            String message = "服务器你好,我是客户端";            //我这里写了,不代表发送了,常识写到了输入流的缓冲区            oos.writeObject(message);            //发送并清空            oos.flush();            //接管服务器的回应            InputStream inputStream = socket.getInputStream();            ObjectInputStream ois = new ObjectInputStream(inputStream);            Object get = ois.readObject();            System.out.println("接管的信息为:" + get);        } catch (Exception e) {            e.printStackTrace();        }    }    public static void main(String[] args) {        new Client(7000);    }}

针对于BIO,为什么是阻塞IO,是因为BIO是基于Socket实现数据的读写操作,Server调用accept()办法继续监听socket连贯,所以就阻塞在accept()这里(前面的操作如果没有socket连贯则无奈执行),那这样就代表服务器只能同时解决一个socket的操作。
所以后序咱们通过为socket连贯创立一个线程执行,这样就能够增大服务器解决连贯的数量了。
然而新的问题也就进去了,如果客户端的连贯很多,那么就会导致服务器创立很多的线程
对socket进行解决,如果服务器端不断开连接的话,那么对应的线程也不会被销毁,这样大数量的线程的保护非常耗费资源。针对于这种状况设计出了Java的NIO。

NIO

首先咱们须要介绍一下NIO。如果说BIO是面向socket进行读写操作的话,那么NIO则是面向channel进行读写操作(或者说面向buffer)。
这里咱们在解说NIO之前,须要先解说一下这个buffer。家喻户晓这就是一个缓冲,并且咱们晓得socket具备输出流inputStream和输入流outputStream(读写离开的),然而咱们的channel是同时具备read和write两个办法,而且两个办法都是基于buffer进行操作(这里就能够阐明channel仅能比一般输入输出流好,相当于channel是一条双向,输入输出流是两条单向),所以咱们能够晓得buffer的重要性。

Buffer

诸如ByteBuffer,IntBuffer等都是Buffer的派生抽象类,须要调用抽象类的静态方法allocate(X capacity)办法进行一个初始化操作,该办法就是初始化buffer的大小,或者应用wrap(X x)办法,该办法相当于间接将信息存入缓冲中。至于存入buffer的put()办法和取出缓存的get()办法在上面代码中我就具体介绍(有底层常识,具备源码浏览能力的能够依据我的正文进行浏览),最要害的还有flip()办法,它是作为一个读写切换的作用,他使的缓存可又读又写,又使得读写互相隔离(须要留神的是应用buffer尽量是顺次写完而后再一次读完,最初在调用clear()办法进行复位,不然会导致buffer容量越来越小,具体解释在上面代码)。

package net.nio.buffer;import java.nio.IntBuffer;public class TestBuffer {    public static void main(String[] args) {        /*         *IntBuffer有四个重要参数         * 1.mark 标记         * 2.position 相当于以后下标、索引         * 3.limit 代表缓冲的起点,读取不能超过该下标,当然也不能超过最大容量。(在调用flip时候会将以后下标position值赋值给limit,而后position置0)         * 4.Capacity 最大容量,在初始化IntBuffer对象时候就定义好了,不能扭转(IntBuffer.allocate(int capacity) )         *         * ctrl+h 能够查看该类的子类         */        //intBuffer初始化        IntBuffer intBuffer = IntBuffer.allocate(5);        //放数据到缓冲区中        intBuffer.put(10);        intBuffer.put(11);        intBuffer.put(12);//        intBuffer.put(13);//        intBuffer.put(14);        /*         *这里的读写反转的实现机制是:         * 例如咱们缓冲区容量为5,调用办法put()将数据写入缓冲区中,如果咱们写入三个此时position为3,此时limit = capacity         * 如果咱们调用flip办法使得limit = 3 ,position = 0 ,mark咱们当初先不论(下图源码已阐明)         *  public Buffer flip() {         *      limit = position;         *      position = 0;         *      mark = -1;         *      return this;         * }         *         * 此时咱们调用get()办法时候,获得下标是position的值,即从0下标读取。直到读取到position = limit = 3时候进行(不包含3)         * 1.如果咱们这个时候不调用flip()办法间接再次put()往缓冲区写入数据(即没从读状态切换到写状态),那么就会报错超过下标overflow         * 2.如果咱们调用一次flip()(即进入写状态)写入一个数据后,那么此时position = 0,limit = 3,此时咱们最多寄存3个数据(即下标0,1,2)         * 如果咱们不再次调用flip()切换状态那么就会导致,读取到谬误数据,(即只存入了一个数据,然而却取出来了3个数据)         *         * 上述阐明了一个问题,如果咱们存取的数据越来越小,那么这个缓冲区逐步放大,导致并不能存取他的最大容量,可能会节约内存,         *(因为position是不能超过limit的,然而调用flip()办法后会使的limit = position(赋值操作),那么如果数据越来越少,         * 就会导致缓冲区能应用的局部越来越小)         *         * 总结:缓冲区的大小设置应该依据理论应用进行设置(并且要及时调用clear() ),否则可能会导致缓冲区的内存节约。         */        intBuffer.flip();        //切换读写状态        //判断缓存区是否还有残余        while (intBuffer.hasRemaining()) {            System.out.println(intBuffer.get());        }    }}

Channel

Channel是NIO实现的根底,对于NIO,Channel的位置相当于BIO的socket。
Channel具备十分多办法,其中应用最多的就是两个办法write(ByteBuffer buf)和read(ByteBuffer buf)办法。
(这里须要留神的是这个read和write是buffer作为主体的,即read()办法是channel往buffer里写数据,而write()办法是指buffer向channel写数据)

package net.nio.channel;        import java.io.FileOutputStream;        import java.nio.ByteBuffer;        import java.nio.channels.FileChannel;public class TestChannel {    public static void main(String[] args) throws Exception{        String abc = "我写入文件了";        //写入的文件地址与文件名        FileOutputStream fileOutputStream = new FileOutputStream("C:\\xxx\\xxx\\xxx\\test.txt");        //从输入流中获取channel实例        FileChannel channel = fileOutputStream.getChannel();        //创立字节缓冲区        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);        //将字符串转化成字节数组并放入缓冲区中,而后缓冲区转换读写状态,由写入状态变为读取状态        byteBuffer.put(abc.getBytes());        byteBuffer.flip();        //将缓冲区数据写入到channel中(这里write代表从缓冲区写入,read代表从channel读取到缓冲区)        channel.write(byteBuffer);        //敞开通道和输入流        channel.close();        fileOutputStream.close();    }}

简略NIO实现

下面在介绍NIO时候讲过,NIO是须要一个Selector线程去监听那些客户端有实现产生,从而在进行解决,而不是BIO的一个线程保护一个socket。

上面针对于NIO咱们先不引入Selector,就用BIO的形式实现一个客户端和服务器端。(相当于作为一个练手)

Server

package net.nio.socket;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.Selector;import java.nio.channels.ServerSocketChannel;import java.nio.channels.SocketChannel;import java.util.Arrays;public class Server {    public static void main(String[] args) throws Exception {        //开启nio的服务器端,并且绑定8000端口进行监听        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();        InetSocketAddress inetSocketAddress = new InetSocketAddress(8000);        serverSocketChannel.bind(inetSocketAddress);        //创立缓冲区数组        ByteBuffer byteBuffer = ByteBuffer.allocate(40);        //服务器端接管来自客户端的申请,创立客户端的socket的实例        SocketChannel socketChannel = serverSocketChannel.accept();        //将客户端发送的数据读取到buffer数组中        socketChannel.read(byteBuffer);        byte[] array = byteBuffer.array();        String msg = new String(array);        System.out.println("服务器收到信息 : " + msg);        //对buffer数组进行读写反转,由读状态到写状态        byteBuffer.flip();        //将数据回显到客户端去        byteBuffer.put("ok".getBytes());        //做完一套读写操作后,须要进行clear        byteBuffer.clear();        }}

Client

package net.nio.serverclient;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.SocketChannel;public class NIOClient {    public static void main(String[] args) throws Exception {        SocketChannel socketChannel = SocketChannel.open();        InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost",8000);        socketChannel.configureBlocking(false);        //如果客户端未连贯上服务器        if (!socketChannel.connect(inetSocketAddress)) {            System.out.println("客户端连贯不上服务器。。。。");            //如果客户端没有实现连贯            while (!socketChannel.finishConnect()) {                System.out.println("连贯中。。。。");            }        }        //进入到这里阐明连贯胜利        String message = "hello , Server!";        ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes());        //将buffer中的数据写入socketChannel        socketChannel.write(byteBuffer);        System.out.println("发送结束");    }}

NIO实现(基于Selector)

首先咱们须要晓得Selector是什么?
Selector是一个选择器,既然是一个选择器,那么必定是先有选项再有抉择,了解这个后就晓得channel必定有的就是rigister()办法(因为须要将本人注册到Selector中)。

既然选项有了,那么如何抉择呢?
Selector是针对已注册的channel中对有事件(例如:服务器:承受,读写,客户端:读写,服务器是在服务器开始就将本人注册,客户端是连贯胜利后由服务器将其注册)产生的channel进行解决。

Selector注册的不是简略的channel,而是将channel和其监听事件封装成一个SelectionKey保留在Selector底层的Set汇合中。

Selector的keys()和selectedKeys()两个办法须要留神:
keys()办法是返回已注册的所有selectionKey。
selectedKeys()办法是返回有事件产生的selectionKey。

下面就是Selector的简略工作流程,上面我将附上代码,因为有较具体的正文,所以除了重要知识点我不再多介绍。
Server

package net.nio.serverclient;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;public class NIOServer {    public static void main(String[] args) throws Exception {        //开启ServerSocketChannel的监听        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();        //绑定端口8000        serverSocketChannel.bind(new InetSocketAddress(8000));        //创立Selector对象        Selector selector = Selector.open();        //设置监听为非阻塞        serverSocketChannel.configureBlocking(false);        //将ServerSocketChannel注册到Selector中(注册事件为ACCEPT)        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);        //Selector监听ACCEPT工夫        while (true) {            //未有事件产生(上面是期待),返回值为int,代表事件产生个数            if (selector.select(1000) == 0) {                System.out.println("服务器期待了1S,无事件产生。。。。");                continue;            }            //有客户端申请过去,就获取到相干的selectionKeys汇合            Set<SelectionKey> selectionKeys = selector.selectedKeys();            Iterator<SelectionKey> iterator = selectionKeys.iterator();            while (iterator.hasNext()) {                //获取到事件                SelectionKey selectionKey = iterator.next();                //移出读取过的事件                iterator.remove();                //依据对应事件对应解决                if (selectionKey.isAcceptable()) {                    //有新的客户端连贯服务器                    SocketChannel socketChannel = serverSocketChannel.accept();                    //给客户端设置非阻塞                    socketChannel.configureBlocking(false);                    //设置该SocketChannel为读事件,并为它绑定一个Buffer                    socketChannel.register(selector,SelectionKey.OP_READ,ByteBuffer.allocate(1024));                }                if (selectionKey.isReadable()) {                    //通过Key反向获取到事件的channel                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();                    //获取到事件绑定的buffer                    ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();                    socketChannel.read(byteBuffer);                    //重置缓冲                    byteBuffer.clear();                    String message = new String(byteBuffer.array());                    System.out.println("接管到客户端信息为: "+ message);                }            }        }    }}

Client

package net.nio.serverclient;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.SocketChannel;import java.util.Scanner;public class NIOClient {    public static void main(String[] args) throws Exception {        SocketChannel socketChannel = SocketChannel.open();        InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost",8000);        socketChannel.configureBlocking(false);        //如果客户端未连贯上服务器        if (!socketChannel.connect(inetSocketAddress)) {            System.out.println("客户端连贯不上服务器。。。。");            //如果客户端没有实现连贯            while (!socketChannel.finishConnect()) {                System.out.println("连贯中。。。。");            }        }        //进入到这里阐明连贯胜利        while(true) {            Scanner scanner = new Scanner(System.in);            String message = scanner.nextLine();            ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes());            //将buffer中的数据写入socketChannel            socketChannel.write(byteBuffer);            System.out.println("发送结束");        }    }}

咱们这里能够发现,只须要主函数中进行一个死循环,死循环中对selector注册的channel进行监听(select()办法),有事件产生则依据channel注册的监听事件对应进行解决。

这里须要留神的是须要将ServerSocketChannel和SocketChannel编程非阻塞(调用configureBlocking(false)),不然是无奈注册到Selector中。

还有一件事须要留神:咱们每次是通过iterator(迭代器)遍历产生工夫的Set ,为了防止反复解决工夫,咱们在获取产生工夫的selctionKey当前,就将其remove()。

最初

感激你看到这里,看完有什么的不懂的能够在评论区问我,感觉文章对你有帮忙的话记得给我点个赞,每天都会分享java相干技术文章或行业资讯,欢送大家关注和转发文章!