NIO编程

一、概述

1、定义

java.nio全称java non-blocking IO,是指JDK1.4 及以上版本里提供的新api(New IO) ,为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络(来源于百度百科)。

2、为什么使用NIO

在上面的描述中提到,是在JDK1.4以上的版本才提供NIO,那在之前使用的是什么呢?答案很简单,就是BIO(阻塞式IO),也就是我们常用的IO流。

BIO的问题其实不用多说了,因为在使用BIO时,主线程会进入阻塞状态,这就非常影响程序的性能,不能充分利用机器资源。但是这样就会有人提出疑问了,那我使用多线程不就可以了吗?

但是在高并发的情况下,会创建很多线程,线程会占用内存,线程之间的切换也会浪费资源开销。

而NIO只有在连接/通道真正有读写事件发生时(事件驱动),才会进行读写,就大大地减少了系统的开销。不必为每一个连接都创建一个线程,也不必去维护多个线程。

避免了多个线程之间的上下文切换,导致资源的浪费。

二、Buffer缓冲区

请添加图片描述
Buffer是一个内存块。在NIO中,所有的数据都是用Buffer处理,有读写两种模式。所以NIO和传统的IO的区别就体现在这里。传统IO是面向Stream流,NIO而是面向缓冲区(Buffer)。

import java.nio.ByteBuffer;

public class BufferTest {
    public static void main(String[] args) {
        String msg="HelloWorld,我爱Java";
        //创建一个大小为1024的byteBuffer
        ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
        byte[] bytes=msg.getBytes();
        //将数据写入byteBuffer中
        byteBuffer.put(bytes);
        //关键一步,将byteBuffer转化为读取模式
        byteBuffer.flip();
        byte[] temp=new byte[bytes.length];
        int i=0;
        //将byteBuffer中的数据放入temp
        while (byteBuffer.hasRemaining()){
            byte b=byteBuffer.get();
            temp[i++]=b;
        }
        //打印结果
        System.out.println(new String(temp));
    }
}

这上面有一个flip()方法是很重要的。意思是切换到读模式。上面已经提到缓存区是双向的,既可以往缓冲区写入数据,也可以从缓冲区读取数据。但是不能同时进行,需要切换

三、Channel管道

请添加图片描述
常用的Channel有这四种:

  • FileChannel,读写文件中的数据。
  • SocketChannel,通过TCP读写网络中的数据。
  • ServerSockectChannel,监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
  • DatagramChannel,通过UDP读写网络中的数据。

Channel本身并不存储数据,只是负责数据的运输。必须要和Buffer一起使用。

1、FileChannel

在这里插入图片描述

首先准备一个"1.txt"放在项目的根目录下,然后编写一个main方法:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelTest {
    public static void main(String[] args) throws Exception {
        //获取文件输入流
        File file = new File("1.txt");
        FileInputStream inputStream = new FileInputStream(file);
        //从文件输入流获取通道
        FileChannel inputStreamChannel = inputStream.getChannel();
        //获取文件输出流
        FileOutputStream outputStream = new FileOutputStream(new File("2.txt"));
        //从文件输出流获取通道
        FileChannel outputStreamChannel = outputStream.getChannel();
        //创建一个byteBuffer,小文件所以就直接一次读取,不分多次循环了
        ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());
        //把输入流通道的数据读取到缓冲区
        inputStreamChannel.read(byteBuffer);
        //切换成读模式
        byteBuffer.flip();
        //把数据从缓冲区写入到输出流通道
        outputStreamChannel.write(byteBuffer);
        //关闭通道
        outputStream.close();
        inputStream.close();
        outputStreamChannel.close();
        inputStreamChannel.close();
    }
}

运行之后,根目录多了一个2.txt
在这里插入图片描述

2、SocketChannel

public static void main(String[] args) throws Exception {
        //获取ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
        //绑定地址,端口号
        serverSocketChannel.bind(address);
        //创建一个缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        while (true) {
            //获取SocketChannel
            SocketChannel socketChannel = serverSocketChannel.accept();
            while (socketChannel.read(byteBuffer) != -1){
                //打印结果
                System.out.println(new String(byteBuffer.array()));
                //清空缓冲区
                byteBuffer.clear();
            }
        }
    }

然后运行main()方法,我们可以通过telnet命令进行连接测试:
在这里插入图片描述

3、Selector选择器

Selector翻译成选择器,有些人也会翻译成多路复用器,实际上指的是同一样东西。

四、通道间的数据传输

1、transferTo()

把源通道的数据传输到目的通道中。

    public static void main(String[] args) throws Exception {
        //获取文件输入流
        File file = new File("1.txt");
        FileInputStream inputStream = new FileInputStream(file);
        //从文件输入流获取通道
        FileChannel inputStreamChannel = inputStream.getChannel();
        //获取文件输出流
        FileOutputStream outputStream = new FileOutputStream(new File("2.txt"));
        //从文件输出流获取通道
        FileChannel outputStreamChannel = outputStream.getChannel();
        //创建一个byteBuffer,小文件所以就直接一次读取,不分多次循环了
        ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
        //把输入流通道的数据读取到输出流的通道
        inputStreamChannel.transferTo(0, byteBuffer.limit(), outputStreamChannel);
        //关闭通道
        outputStream.close();
        inputStream.close();
        outputStreamChannel.close();
        inputStreamChannel.close();
    }    

2、transferFrom()

把来自源通道的数据传输到目的通道。

    public static void main(String[] args) throws Exception {
        //获取文件输入流
        File file = new File("1.txt");
        FileInputStream inputStream = new FileInputStream(file);
        //从文件输入流获取通道
        FileChannel inputStreamChannel = inputStream.getChannel();
        //获取文件输出流
        FileOutputStream outputStream = new FileOutputStream(new File("2.txt"));
        //从文件输出流获取通道
        FileChannel outputStreamChannel = outputStream.getChannel();
        //创建一个byteBuffer,小文件所以就直接一次读取,不分多次循环了
        ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
        //把输入流通道的数据读取到输出流的通道
        outputStreamChannel.transferFrom(inputStreamChannel,0,byteBuffer.limit());
        //关闭通道
        outputStream.close();
        inputStream.close();
        outputStreamChannel.close();
        inputStreamChannel.close();
    }

3、分散读取和聚合写入

1.txt内容

你好你好你好你好你好+-
JavaJava
abcdefg
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;

public class WriteAndRead {
    public static void main(String[] args) throws Exception {
        //获取文件输入流
        File file = new File("1.txt");
        FileInputStream inputStream = new FileInputStream(file);
        //从文件输入流获取通道
        FileChannel inputStreamChannel = inputStream.getChannel();
        //获取文件输出流
        FileOutputStream outputStream = new FileOutputStream(new File("2.txt"));
        //从文件输出流获取通道
        FileChannel outputStreamChannel = outputStream.getChannel();
        //创建三个缓冲区,分别都是5
        ByteBuffer byteBuffer1 = ByteBuffer.allocate(5);
        ByteBuffer byteBuffer2 = ByteBuffer.allocate(5);
        ByteBuffer byteBuffer3 = ByteBuffer.allocate(5);
        //创建一个缓冲区数组
        ByteBuffer[] buffers = new ByteBuffer[]{byteBuffer1, byteBuffer2, byteBuffer3};
        //循环写入到buffers缓冲区数组中,分散读取
        long read;
        long sumLength = 0;
        while ((read = inputStreamChannel.read(buffers)) != -1) {
            sumLength += read;
            Arrays.stream(buffers)
                    .map(buffer -> "posstion=" + buffer.position() + ",limit=" + buffer.limit())
                    .forEach(System.out::println);
            //切换模式
            Arrays.stream(buffers).forEach(Buffer::flip);
            //聚合写入到文件输出通道
            outputStreamChannel.write(buffers);
            //清空缓冲区
            Arrays.stream(buffers).forEach(Buffer::clear);
        }
        System.out.println("总长度:" + sumLength);
        //关闭通道
        outputStream.close();
        inputStream.close();
        outputStreamChannel.close();
        inputStreamChannel.close();
    }
}

打印内容:

posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5
posstion=1,limit=5
posstion=0,limit=5
总长度:51

可以看出,进行了四次循环,最后一次循环的第三个buffer没有存数据刚好读完
这就是分散读取,聚合写入的过程。

四、使用Selector的小例子

1、客户端连接服务器

客户端代码

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 address = new InetSocketAddress("127.0.0.1", 6666);
        socketChannel.configureBlocking(false);
        //连接服务器
        boolean connect = socketChannel.connect(address);
        //判断是否连接成功
        if(!connect){
            //等待连接的过程中
            while (!socketChannel.finishConnect()){
                System.out.println("连接服务器需要时间,期间可以做其他事情...");
            }
        }
        String msg = "hello 我爱Java!!!";
        ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
        //把byteBuffer数据写入到通道中
        socketChannel.write(byteBuffer);
        //让程序卡在这个位置,不关闭连接
        System.in.read();
    }
}

服务端代码

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();
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
        //绑定地址
        serverSocketChannel.bind(address);
        //设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        //打开一个选择器
        Selector selector = Selector.open();
        //serverSocketChannel注册到选择器中,监听连接事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //循环等待客户端的连接
        while (true) {
            //等待3秒,(返回0相当于没有事件)如果没有事件,则跳过
            if (selector.select(3000) == 0) {
                System.out.println("服务器等待3秒,没有连接");
                continue;
            }
            //如果有事件selector.select(3000)>0的情况,获取事件
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            //获取迭代器遍历
            Iterator<SelectionKey> it = selectionKeys.iterator();
            while (it.hasNext()) {
                //获取到事件
                SelectionKey selectionKey = it.next();
                //判断如果是连接事件
                if (selectionKey.isAcceptable()) {
                    //服务器与客户端建立连接,获取socketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    //设置成非阻塞
                    socketChannel.configureBlocking(false);
                    //把socketChannel注册到selector中,监听读事件,并绑定一个缓冲区
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                //如果是读事件
                if (selectionKey.isReadable()) {
                    //获取通道
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    //获取关联的ByteBuffer
                    ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                    //打印从客户端获取到的数据
                    socketChannel.read(buffer);
                    System.out.println("from 客户端:" + new String(buffer.array()));
                }
                //从事件集合中删除已处理的事件,防止重复处理
                it.remove();
            }
        }
    }
}

控制台打印信息
在这里插入图片描述

2、SelectionKey

在SelectionKey类中有四个常量表示四种事件,来看源码:

public abstract class SelectionKey {
    //读事件
    public static final int OP_READ = 1 << 0; //2^0=1
    //写事件
    public static final int OP_WRITE = 1 << 2; // 2^2=4
    //连接操作,Client端支持的一种操作
    public static final int OP_CONNECT = 1 << 3; // 2^3=8
    //连接可接受操作,仅ServerSocketChannel支持
    public static final int OP_ACCEPT = 1 << 4; // 2^4=16
}

3、NIO实现聊天室

服务器端

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class Server {
    private Selector selector;

    private ServerSocketChannel serverSocketChannel;

    public static final int PORT = 6666;

    //构造器初始化成员变量
    public Server(){
        try{
            //打开一个选择器
            this.selector = Selector.open();
            //打开serverSocketChannel
            this.serverSocketChannel = ServerSocketChannel.open();
            //绑定地址,端口号
            this.serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", PORT));
            //设置为非阻塞
            serverSocketChannel.configureBlocking(false);
            //把通道注册到选择器中
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (Exception e){
            e.printStackTrace();
        }
    }

    /*
       监听,接收客户端的信息,并转发到其他客户端
     */
    public void listen(){
        try{
            while(true){
                //获得监听的事件总数
                int count=selector.select(2000);    //2s
                if(count>0){
                    Set<SelectionKey> selectionKeys=selector.selectedKeys();
                    //获取SelectorKey集合
                    Iterator<SelectionKey> it=selectionKeys.iterator();
                    while(it.hasNext()){
                        SelectionKey sk=it.next();
                        //如果是获取连接事件
                        if(sk.isAcceptable()){
                            SocketChannel socketChannel=serverSocketChannel.accept();
                            //设置为非阻塞
                            socketChannel.configureBlocking(false);
                            //注册到selector中
                            socketChannel.register(selector,SelectionKey.OP_READ);
                            System.out.println(socketChannel.getRemoteAddress()+"上线了");
                        }
                        //如果是读取事件
                        if(sk.isReadable()){
                            //读取消息,并且转发到其他客户端
                            readData(sk);
                        }
                        it.remove();
                    }
                }else{
                    System.out.println("等待。。。");
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    //获取客户端来的消息
    private void readData(SelectionKey selectionKey){
        SocketChannel socketChannel=null;
        try{
            //从selectionKey中获取Channel
            socketChannel=(SocketChannel)selectionKey.channel();
            //创建一个缓冲区
            ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
            //把通道的数据写入缓冲区
            int count=socketChannel.read(byteBuffer);
            //判断返回是否大于0,大于0表示读到了数据
            if(count>0){
                //把缓冲区的byte[]转成字符串
                String msg=new String(byteBuffer.array());
                //输出到控制台
                System.out.println("From 客户端:"+msg);

                //转发到其它客户端
                notifyAllClient(msg,socketChannel);
            }
        } catch (Exception e){
            try{
                //打印离线通知
                System.out.println(socketChannel.getRemoteAddress()+"离线了~");
                //取消注册
                selectionKey.cancel();
                //关闭流
                socketChannel.close();
            }catch (Exception e1){
                e1.printStackTrace();
            }
            e.printStackTrace();
        }
    }



    /*
        转发到其他客户端
        msg 消息
        noNotifyChannel 不需要通知的Channel
     */
    private void notifyAllClient(String msg,SocketChannel noNotifyChannel) throws Exception{
        System.out.println("服务器转发~");
        for(SelectionKey selectionKey:selector.keys()){
            Channel channel=selectionKey.channel();
            //channel的类型是SocketChannel并且不是noNotifyChannel
            if(channel instanceof SocketChannel && channel!=noNotifyChannel){
                //强转成SocketChannel类型
                SocketChannel socketChannel=(SocketChannel)channel;
                //通过消息,包裹获取一个缓冲区
                ByteBuffer byteBuffer= ByteBuffer.wrap(msg.getBytes());
                socketChannel.write(byteBuffer);
            }
        }
    }
    public static void main(String[] args) throws Exception {
        Server chatServer = new Server();
        //启动服务器,监听
        chatServer.listen();
    }
}

客户端

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;

public class Client {

    private Selector selector;

    private SocketChannel socketChannel;

    private String userName;

    public Client(){
        try{
            //打开选择器
            this.selector=Selector.open();
            //连接服务器
            socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",Server.PORT));
            //设置为非阻塞
            socketChannel.configureBlocking(false);
            //注册到选择器中
            socketChannel.register(selector, SelectionKey.OP_READ);
            //获取用户名
            userName=socketChannel.getLocalAddress().toString().substring(1);
            System.out.println(userName+" is ok~");
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    //发送消息到服务端
    private void sendMsg(String msg) {
        msg = userName + "说:" + msg;
        try {
            socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //读取服务端发送过来的消息
    private void readMsg() {
        try {
            int count = selector.select();
            if (count > 0) {
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    //判断是读就绪事件
                    if (selectionKey.isReadable()) {
                        SocketChannel channel = (SocketChannel) selectionKey.channel();
                        //创建一个缓冲区
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        //从服务器的通道中读取数据到缓冲区
                        channel.read(byteBuffer);
                        //缓冲区的数据,转成字符串,并打印
                        System.out.println(new String(byteBuffer.array()));
                    }
                    iterator.remove();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        Client chatClinet = new Client();
        //启动线程,读取服务器转发过来的消息
        new Thread(() -> {
            while (true) {
                chatClinet.readMsg();
                try {
                    Thread.sleep(3000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
        //主线程发送消息到服务器
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String msg = scanner.nextLine();
            chatClinet.sendMsg(msg);
        }
    }
}

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
转载于:NIO从入门到踹门

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值