NIO原理及实战

基本介绍

在这里插入图片描述

细节描述

1)Java NIO全称java non-blocking IO ,是指JDK提供的新的API,从JDK1.4开始,Java提供了一系列改进输入/输出的新特性,被统称为NIO,同步非阻塞
2)NIO相关的类都放在java.nio包及子包下,并且对原java.io包中很多类进行改写

在这里插入图片描述
3)NIO有三大核心部分: Channel(通道),Buffer(缓冲区),Selector(选择器)
4)NIO是面向缓冲区,或者面向块编程。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程的灵活性。
5)Java NIO的非阻塞模式,使一个线程从某通道中发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有可用数据,就什么也不会获取,而不是保持线程阻塞在那里,所以直至数据变得可以读取之前,该线程可以继续做其他得事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但是不需要等待它完全写入,这个线程同时可以做别的事情。
6)NIO可以做到一个线程来处理多个操作,假设有100个请求过来,根据实际情况,可以分配5或者10个线程来处理。如果是BIO的话,那么就需要对每个请求建立一个线程。
7)HTTP2.0使用lO多路复用技术,做到同一个连接并发处理多个请求,并且并发请求的数量比HTTP1.1大了好几个数量级。

BIO和NIO比较

1)BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O效率比流IO高很多
2)BIO是阻塞的,NIO是非阻塞的
3)BIO是基于字节流和字符流进行操作,而NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区,或者从缓冲区读取到通道。Selector用于监听多个通道的事件(比如连接请求,数据到达等),因此可以用单个线程就能监听多个客户端通道。

NIO核心原理

在这里插入图片描述

Selector ,Channel和Buffer的关系图
1)每个Channel都会对应一个Buffer
2)Selector对应一个线程,一个线程对应多个Channel(连接)
3)该图反应you三个Channel注册到Selector
4)程序切换到那个Channel是由事件决定的
5)Selector会根据不同的事件在各个通道进行切换
6)Buffer就是一个内存块,底层是一个数组
7)数据的读取和写入都是通过Buffer,可以读也可以写,需要flip方法切换,BIO要么是输入流要么是输出流
8)Channel是双向的,可以返回底层操作系统的情况,比如linux

缓冲区

基本介绍
缓冲区(Buffer): 缓冲区本质就是一个可以读写数据的内存块,可以简单的理解为一个容器对象,该对象提供了一组方法,可以更轻松的使用内存块,缓冲区对象内置一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供了从文件,网络读取数据通道,但是读取和写入数据必须经过Buffer

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

常用的buffer子类
1)ByteBuffer 存储字节数据到缓冲区
2)ShortBuffer 存储短整型数据数据到缓冲区
3)CharBuffer 存储字符数据到缓冲区
4)IntBuffer 存储整数数据到缓冲区
5)LongBuffer 存储长整型数据到缓冲区
6)DoubleBuffer 存储小数数据到缓冲区
7)FloatBuffer 存储小数数据到缓冲区
在这里插入图片描述

通道

基本介绍
1)NIO通道类似流,但是区别如下

  • 通道可以同时进行读写,而流只能读或者写
  • 通道可以实现异步读写数据
  • 通道可以从缓冲区读数据,也可以写数据到缓冲区

2)Channel在NIO是一个接口

public interface Channel extends Closeable {
3)常见的channel : FileChannel(用于文件读写),DatagramChannel(用于UDP数据读写),SeverSocketChannel和SocketChannel(用于TCP的数据读写)

在这里插入图片描述
关于buffer和channel注意细节
1)ByteBuffer支持类型化的put和get,put放入什么类型,get就获取什么类型,否则就可能有BufferUnderflowException
2)可以将普通的Buffer转换为只读Buffer
3)NIO还支持通过多个Buffer(Buffer数组来完成)即Scattering和Gathering
4)NIO提供了MappedByteBuffer可以让文件直接在内存中进行修改

Selector

基本介绍
1)java的NIO,用非阻塞IO的方式 .可以用一个线程,处理多个客户端连接,就会使用到Selector(选择器)
2)Selector能够检测多个注册通道是否有事件发送,如果有事件发生,便获取事件,然后对每个事件进行相应的处理,这样一个线程就可以去管理多个通道,也就是管理多个连接和请求
3)避免了多线程的上下问切换导致的开销

在这里插入图片描述
特点说明
1)Netty的IO线程NioEventLoop聚合了Selector(选择器也叫多路复用器),可以同时并发处理成百上千个客户端连接。
2)当线程从客户端Socket通道进行读写数据的时,若没有数据可以用,该线程可以进行其他的任务。
3)线程通常以非阻塞IO的空闲时间用于在其他通道的IO操作,所以单独线程可以管理多个输入和输出通道
4)由于读写操作是非阻塞的,充分提高了IO线程的运行效率,避免由于IO阻塞导致线程挂起

在这里插入图片描述
注意事项
1)NIO中的ServerSocketChannel功能类似ServerSocket,SocketChannel类似Socket
在这里插入图片描述
说明:
1,当客户端进行连接的时候,会通过ServerSocketChannel得到SocketChannel
2,Selector进行监听selector方法,返回有事件发生的通道个数
3,将socketChannel注册到Selector上,register(Selector sel,int ops),一个selector上可以注册多个SocketChannel.
4,注册返回一个SelectionKey,会和该Selector关联
5,进一步得到各个SelectionKey(有事件发生)
6,通过SelectionKey反向获取SocketChannel,方法Channel
7,可以通过得到的channel进行业务处理
8,业务处理

SelectionKey 在这里插入图片描述

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

应用实例

应用实例1-本地文件写数据

实例要求:
1)使用前面学习后的ByteBuffer(缓冲) 和 FileChannel(通道), 将 “hello,world!” 写入到file.txt 中
2)文件不存在就创建

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class NIOFileOper01 {

    public static void main(String[] args) throws IOException {

        String str = "hello,world!";
        //创建输出流
        FileOutputStream outputStream = new FileOutputStream("file.text");
        //获取通道
        FileChannel channel = outputStream.getChannel();

        //获取缓冲区对象
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //加载数据到缓冲区内存中
        byteBuffer.put(str.getBytes("UTF-8"));

        //缓冲区翻转
        byteBuffer.flip();
        //把缓冲区的数据加载到通道中
        channel.write(byteBuffer);

        //关闭通道和流
        channel.close();
        outputStream.close();
    }
}
应用实例2-本地文件读数据

实例要求:

  1. 使用前面学习后的ByteBuffer(缓冲) 和 FileChannel(通道), 将 file.txt 中的数据读入到程序,并显示在控制台屏幕
  2. 假定文件已经存在
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class NIOFileOper02 {

    public static void main(String[] args) throws IOException {

        //创建输出流
        FileInputStream fileInputStream = new FileInputStream("file.text");
        //获取通道
        FileChannel channel = fileInputStream.getChannel();

        //获取缓冲区对象
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        int read = channel.read(byteBuffer);
        System.out.println("读取的字节数" + read);
        System.out.println(new String(byteBuffer.array()));

        //关闭通道和流
        channel.close();
        fileInputStream.close();
    }
}
应用实例3-使用一个Buffer完成文件读取

实例要求:

  1. 使用 FileChannel(通道) 和 方法 read , write,完成文件的拷贝
  2. 拷贝一个文本文件 1.txt , 放在项目下即可
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * 实例要求:
 * 使用 FileChannel(通道) 和 方法  read , write,完成文件的拷贝
 *
 * 拷贝一个文本文件 1.txt  , 放在项目下即可
 * 代码演示
 */
public class NIOFileOper03 {

    public static void main(String[] args) throws IOException {

        //创建输出流
        FileInputStream fileInputStream = new FileInputStream("file.text");
        //获取通道
        FileChannel channel = fileInputStream.getChannel();

        //获取缓冲区对象
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        channel.read(byteBuffer);
        System.out.println(new String(byteBuffer.array()));
        //缓冲区翻转
        byteBuffer.flip();


        FileOutputStream fileOutputStream = new FileOutputStream("fileCopy.text");
        FileChannel outputChanel = fileOutputStream.getChannel();
        outputChanel.write(byteBuffer);

        //关闭通道和流
        channel.close();
        outputChanel.close();
        fileInputStream.close();
        fileOutputStream.close();
    }
}

应用实例4-拷贝文件transferFrom 方法

实例要求:
1)使用 FileChannel(通道) 和 方法 transferFrom ,完成文件的拷贝
2)拷贝一张图片

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;


public class NIOFileOper04 {

    public static void main(String[] args) throws IOException {

        FileInputStream fis = new FileInputStream("01.jpg");
        FileOutputStream fos = new FileOutputStream("01cp.jpg");

        FileChannel fosChannel = fos.getChannel();
        FileChannel fisChannel = fis.getChannel();

        System.out.println("此通道文件的大小"+fisChannel.size());
        //从目标通道拷贝数据到当前通道
        fosChannel.transferFrom(fisChannel, 0, fisChannel.size());
        fos.close();
        fis.close();
        fisChannel.close();
        fosChannel.close();
        System.out.println("图片拷贝完成。。。");
    }
}
应用实例5-NIO 非阻塞 网络编程快速入门

案例要求:
编写一个 NIO 入门案例,实现服务器端和客户端之间的数据简单通讯(非阻塞)
目的:理解NIO非阻塞网络编程机制
在这里插入图片描述
服务端

import java.io.IOException;
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;

public class NIOServer1 {

    public static void main(String[] args) throws IOException {

        //1 得到一个serverSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //2 绑定端口
        serverSocketChannel.socket().bind(new InetSocketAddress(8888));
        //3 配置非阻塞
        serverSocketChannel.configureBlocking(false);
        //4 得到一个Selector对象
        Selector selector = Selector.open();

        //5 向Selector注册ServerSocketChannel
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //6 开始工作
        while (true) {
            //6.1 监听客户端
            if (selector.select(1000) == 0) {
                System.out.println("等待1s,无客户端连接");
                continue;
            }

            //6.2 判断通道事件
            Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
            //遍历通道
            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                if (key.isAcceptable()) {
                    //客户端连接请求事件
                    //接受通道
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    //配置非阻塞
                    socketChannel.configureBlocking(false);
                    //注册到selector
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                    System.out.println("有client连接请求过来");

                }
                if (key.isReadable()) {
                    //读取客户端事件
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    socketChannel.read(buffer);
                    System.out.println("接受客户端数据" + new String(buffer.array()));
                }
                // 6.3 手动从集合中移除当前key,防止重复处理
                keyIterator.remove();
            }

        }
    }
}

客户端

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NIOClient1 {

    public static void main(String[] args) throws IOException {
        //1 得到一个Socker连接
        SocketChannel socketChannel = SocketChannel.open();
        //2 配置非阻塞
        socketChannel.configureBlocking(false);
        //3 绑定端口ip
        InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 8888);
        //4连接
        if (!socketChannel.connect(socketAddress)) {
            while (!socketChannel.finishConnect()) {
                //nio非阻塞式
                System.out.println("客户端: 因为连接需要时间,客户端不会阻塞,可以做个计算工作...");
            }
        }
        //写入数据

        ByteBuffer byteBuffer = ByteBuffer.wrap("Hello Netty!!!".getBytes());
        socketChannel.write(byteBuffer);

        System.in.read();
        System.out.println("客户端完成工作");
    }
}
应用实例6 NIO 网络编程应用实例-群聊系统

实例要求:
1)编写一个 NIO 群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
2)实现多人群聊
3)服务器端:可以监测用户上线,离线,并实现消息转发功能
4)客户端:通过channel 可以无阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息(有服务器转发得到)

import com.atguigu.netty.qq.ChatClient;

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

public class GroupChatClient {
    private final String HOST = "127.0.0.1"; //服务器地址
    private int PORT = 6667; //服务器端口
    private Selector selector;
    private SocketChannel socketChannel;
    private String userName;

    public GroupChatClient() throws IOException {
        //得到选择器
        selector = Selector.open();
        //连接远程服务器
        socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", PORT));
        //设置非阻塞
        socketChannel.configureBlocking(false);
        //注册选择器并设置为 read
        socketChannel.register(selector, SelectionKey.OP_READ);
        //得到客户端 IP 地址和端口信息,作为聊天用户名使用
        userName = socketChannel.getLocalAddress().toString().substring(1);
        System.out.println(userName + " is ok ~");
    }

    //向服务器端发送数据
    public void sendInfo(String info) throws Exception {
        //如果控制台输入 exit 就关闭通道,结束聊天
        if (info.equalsIgnoreCase("exit")) {
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
            socketChannel.close();
            socketChannel = null;
            return;
        }
        info = userName + " 说: " + info;
        try {
            //往通道中写数据
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //从服务器端接收数据
    public void readInfo() {
        try {
            int readyChannels = selector.select();
            if (readyChannels > 0) { //有可用通道
                Set selectedKeys = selector.selectedKeys();
                Iterator keyIterator = selectedKeys.iterator();
                while (keyIterator.hasNext()) {
                    SelectionKey sk = (SelectionKey) keyIterator.next();
                    if (sk.isReadable()) {
                        //得到关联的通道
                        SocketChannel sc = (SocketChannel) sk.channel();
                        //得到一个缓冲区
                        ByteBuffer buff = ByteBuffer.allocate(1024);
                        //读取数据并存储到缓冲区
                        sc.read(buff);
                        //把缓冲区数据转换成字符串
                        String msg = new String(buff.array());
                        System.out.println(msg.trim());
                    }
                    keyIterator.remove(); //删除当前 SelectionKey,防止重复处理
                }
            } else {
                //会检测到没有可用的channel ,可以退出
                System.out.println("没有可用channel ...");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception  {
        //创建一个聊天客户端对象
        GroupChatClient chatClient = new GroupChatClient();
        new Thread() { //单独开一个线程不断的接收服务器端广播的数据
            public void run() {
                while (true) {
                    chatClient.readInfo();
                    try { //间隔 3 秒
                        Thread.currentThread().sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        Scanner scanner = new Scanner(System.in);
        //在控制台输入数据并发送到服务器端
        while (scanner.hasNextLine()) {
            String msg = scanner.nextLine();
            chatClient.sendInfo(msg.trim());
        }
    }
}


import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.text.SimpleDateFormat;
import java.util.Iterator;


public class GroupChatServer {
    private Selector selector;
    private ServerSocketChannel listenerChannel;
    private static final int PORT = 6667; //服务器端口

    public GroupChatServer() {
        try {
            // 得到选择器
            selector = Selector.open();
            // 打开监听通道
            listenerChannel = ServerSocketChannel.open();
            // 绑定端口
            listenerChannel.socket().bind(new InetSocketAddress(PORT));
            // 设置为非阻塞模式
            listenerChannel.configureBlocking(false);
            // 将选择器绑定到监听通道并监听 accept 事件
            listenerChannel.register(selector, SelectionKey.OP_ACCEPT);
            printInfo("服务器 ok.......");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void listen() {
        try {
            while (true) { //不停轮询
                int count = selector.select();//获取就绪 channel
                if (count > 0) {
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        SelectionKey key = iterator.next();
                        // 监听到 accept
                        if (key.isAcceptable()) {
                            SocketChannel sc = listenerChannel.accept();
                        //非阻塞模式
                            sc.configureBlocking(false);
                        //注册到选择器上并监听 read
                            sc.register(selector, SelectionKey.OP_READ);

                            //System.out.println(sc.getRemoteAddress().toString().substring(1) + "online ...");
                            System.out.println(sc.socket().getRemoteSocketAddress().toString().substring(1) + " 上线 ...");
                        //将此对应的 channel 设置为 accept,接着准备接受其他客户端请求
                            key.interestOps(SelectionKey.OP_ACCEPT);
                        }
                        //监听到 read
                        if (key.isReadable()) {
                            readData(key); //读取客户端发来的数据
                        }
                        //一定要把当前 key 删掉,防止重复处理
                        iterator.remove();
                    }
                } else {
                    System.out.println("waitting ...");
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void readData(SelectionKey key) {
        SocketChannel channel = null;
        try {
            // 得到关联的通道
            channel = (SocketChannel) key.channel();
            //设置 buffer 缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            //从通道中读取数据并存储到缓冲区中
            int count = channel.read(buffer);
            //如果读取到了数据
            if (count > 0) {
                //把缓冲区数据转换为字符串
                String msg = new String(buffer.array());

                printInfo(msg);
                //将关联的 channel 设置为 read,继续准备接受数据
                key.interestOps(SelectionKey.OP_READ);
                sendInfoToOtherClients(channel, msg); //向所有客户端广播数据
            }
            buffer.clear();
        } catch (IOException e) {
            try {
            //当客户端关闭 channel 时,进行异常如理
                //printInfo(channel.getRemoteAddress().toString().substring(1) + "offline...");
                printInfo(channel.socket().getRemoteSocketAddress().toString().substring(1) + " 离线了 ...");
                key.cancel(); //取消注册
                channel.close(); //关闭通道
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }

    public void sendInfoToOtherClients(SocketChannel except, String msg) throws IOException {
        System.out.println("服务器进行消息转发 ...");
        //转发数据到所有的 SocketChannel 中
        for (SelectionKey key : selector.keys()) {
            Channel targetchannel = key.channel();
            //排除自身
            if (targetchannel instanceof SocketChannel && targetchannel != except) {
                SocketChannel dest = (SocketChannel) targetchannel;
            //把数据存储到缓冲区中
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
            //往通道中写数据
                dest.write(buffer);
            }
        }
    }

    private void printInfo(String str) { //显示消息

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("服务器接收到消息 时间: [" + sdf.format(new java.util.Date()) + "] -> " + str);
    }

    public static void main(String[] args) {
        GroupChatServer server = new GroupChatServer();
        server.listen();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大道至简@EveryDay

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值