NIO原理,及实现群聊功能

什么是NIO

NIO是一种非阻塞,基于事件响应的IO

NIO和原生IO有什么不同

IO有三种,BIO,NIO和AIO
BIO:
	就是jdk原生的IO,是一个同步阻塞的io(等待客户端链接的时候,等待读写请求的时候都是阻塞的);
    客户端和服务端进行io时,都会生成一个线程来管理本次io,当客户端请求多的时候,生成大量线程,耗费资源,用线程池优化,可以解决并发问题,但是线程数并没有减少;
    适合小型,访问量不大的项目,代码简单好维护;
NIO:
	同步非阻塞io(也可以阻塞);
	多个客户端访问服务器端的时候,只会有一个线程进行管理,中间会有一个选择器(多路复用器),选择和某个客户端进行连接(管道);
	选择器选择的方式,就是有个while循环,自旋判断当前是否有读写操作,没有就选择和别的管道连接;
	适合访问量比较大,连接短的项目;
AIO:
	异步非阻塞io;
	和NIO的区别在于 “while循环,自旋判断” 的步骤没了,如果有读写操作你就来通知我,而不是我一直循环去问你;

NIO的三大组件核心

在这里插入图片描述

三大组件的关系

如图所示:
	1.一个channel(可以理解为一个链接)对应一个buffer(一个内存快,底层有一个数组);
	2.一个selector对应了多个channel,相当一channel注册到了selector;
	3.一个线程对应一个selector,selector通过事件决定对哪个客户端进行连接操作数据,切换channel,是非阻塞的核心;
	4.每次数据的读写都要先经过buffer,将数据先放进buffer中在进行操作,普通IO是直接输入输出流的形式;
	5.在buffer中如果先对buffer进行写入操作了,现在想读数据,需要 flip 方法切换,反之亦然;

buffer(缓冲区)

1.缓冲区为独立的一块内存,底层包含了一个数组,每次的数据存在这个数组中;
2.所有的读写操作都先经过这个缓冲区,读和写的转换需要 flip 方法切换;
3.底层有4个变量 mark,position,limit,capacity,读写数据就是根据这四个数据来读写的;
		mark:标记作用,一般用不到
		position:数组记录的数组下标变化,判断是否超过了limit,超过能不能在继续操作数据了
		limit:最大能操作的范围,设置3就是能操作数组的前三个数据,设置为capacity,则是能操作全部数据
		capacity: 相当于数组的length方法
4.Buffer是一个抽象类,它有很多的子类,IntBuffer,ByteBuffer,基本类型里除了boolen类型以外,全部有相应的实现类
5.前面说到的buffer的底层都有一个数组,这个数组就在对应的子类里,变量名叫做hb,数组存储的数据类型就是当前子类的类型
6.Buffer有很多的api方法可以调用,比如buffer的数据长度,切换读写,从某个位置开始读取 等等,就不一一列举了

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

Buffer的简单使用
import java.nio.*;

public class NioBufferTest {
    public static void main(String[] args) {
        //初始化容量为512的buffer, 基本数据类型,除了boolean 都有对应的buffer
 		ByteBuffer byteBuffer = ByteBuffer.allocate(512);
 		ShortBuffer shortBuffer = ShortBuffer.allocate(512);
 		IntBuffer intBuffer = IntBuffer.allocate(512);
 		LongBuffer longBuffer = LongBuffer.allocate(512);
 		DoubleBuffer doubleBuffer = DoubleBuffer.allocate(512);
 		FloatBuffer floatBuffer = FloatBuffer.allocate(512);
 		CharBuffer charBuffer = CharBuffer.allocate(512);

		//向buffer中存放数据
 		intBuffer.put(10);
 		intBuffer.put(100);
 		intBuffer.put(1000);
 		//上面是存放数据,下面如果要读取数据,就要调用flip方面,将4个属性值就行初始化
 		intBuffer.flip();
 		//功能和迭代器的hasNext功能类似
 		while(intBuffer.hasRemaining()){
            //打印buffer中的数据
 			System.out.println(intBuffer.get());
 		}
        //打印buffer中数据个数
 		System.out.println(intBuffer.limit());
 		//打印下标是1,也就是第二个元素
 		System.out.println(intBuffer.get(1));
 	}
}
Buffer的注意事项
1.Buffer存数据和取的数据必须一直
	例如:ByteBuffer 第一个位置放char类型,第二个放int,那取数时,类型必须和放值顺序保持一直
2.Buffer可以设置只读buffer,调用buffer的asReadOnlyBuffer方法
3.操作大文件使用MappedByteBuffer,他是在堆外内存直接进行操作,少了JVM和操作系统间的复制操作,此类继承自ByteBuffer,所以ByteBuffer的功能它都有
4.Buffer可以使用数组的形式进行读写,此形式的数组是按照数组下标以此进行:
		如写数据会先将第一个buffer写满,在写第二个buffer,以此类推,读数据也是一个道理

channel(通道)

1.channel可以理解为一个连接,一个流,它主要是和buffer进行连接,然后对buffer中的数据进行读写
2.它可以异步双向传输(又可以写数据到通道又可以读数据到通道),但是必须和buffer配合使用
3.通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入,和第二点相对应
4.常用实现类 
	FileChannel:用于操作文件
	DatagramChannel:通过UDP读写网络中的数据
	SocketChannel:通过TCP读写网络中的数据
	ServerSocketChannel:监听每一个TCP连接,对每一个新进来的连接创建一个SocketChannel。
FileChannel的简单使用
1.将"Hello word" 写入文件a.txt中
2.读取文件a.txt中的内容
注意:
	Channel.write(buffer):是将缓存区数据写入通道中
	Channel.read(buffer):是读取通道中的数据写入缓存区
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;

public class NioFileChannelTest {

    public static void main(String[] args) {
        try {
            //将 hello word 写入到文件中
 			writeToFile(" hello word");
 			//打印读取的数据
 			System.out.println(readToFile());
 
 		} catch (Exception e) {
            e.printStackTrace();
 		}
    }

     /**
 		* 将msg写入到文件中
	 */
	 public static void writeToFile(String msg) throws Exception {
        //获取文件输出流
		FileOutputStream fileOutputStream = new FileOutputStream(new File("Z:/a.txt"));
 		//获取fileChannel
 		FileChannel fileChannel = fileOutputStream.getChannel();
 		//初始化buffer
 		ByteBuffer buffer = ByteBuffer.allocate(512);
 		//将数据先写入buffer中
 		buffer.put(msg.getBytes(StandardCharsets.UTF_8));
 		//上面的buffer在写入操作,下面要读取操作,所以要变换一次状态
 		buffer.flip();
 		//将buffer中的数据写入通道中
 		//由于通道和流是绑定的(流的方法点出了通道),流又和文件绑定,所以写入了通道,文件中就有数据了
 		fileChannel.write(buffer);
 		//关闭流
 		fileOutputStream.close();
 		fileChannel.close();
 	}

    /**
 		* 打印从文件中读取的数据
 	*/
 	public static String readToFile() throws Exception {
        //获取文件输入流
 		FileInputStream fileInputStream = new FileInputStream(new File("Z:/a.txt"));
 		//获取fileChannel
 		FileChannel fileChannel = fileInputStream.getChannel();
 		//初始化buffer
 		ByteBuffer buffer = ByteBuffer.allocate(512);
 		//将通道数据写入buffer中
 		while (fileInputStream.read() != -1) {
            fileChannel.read(buffer);
 		}
        //关闭流
 		fileInputStream.close();
 		fileChannel.close();
 		//转换类型
 		buffer.flip();
 		//初始化字符串容器
 		StringBuffer sb = new StringBuffer();
 		//如果buffer中有未读取数据,就循环
 		while (buffer.hasRemaining()) {
            //添加到字符串容器中
 			sb.append((char) buffer.get());
 		}
        return sb.toString();
 	}
}

selector(选择器)

在这里插入图片描述

图片是借来的,出处 https://cloud.tencent.com/developer/article/1129674

1.selector 是基于事件驱动唤醒主线程分配资源进行处理,在没有事件时是阻塞状态的
2.事件驱动包括:读、写、连接成功、请求连接四种
3.服务器端会创建一个ServerSocketChannel,注册到 selector 上面,并在注册时告诉 selector 想要监听的事件
4.客户端会创建一个 SocketChannel,注册到 selector 上面(一个 selector  可以注册多个 SocketChannel)

使用 selector 实现简单的读写通讯

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;

/**
 * server 端代码
 */
public class NioServer {

    public static void main(String[] args) {

        try {
            //获取ServerSocketChannel
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            //绑定监听端口 ,监听8888
            serverSocketChannel.socket().bind(new InetSocketAddress("localhost",8888));
            //设置非阻塞
            serverSocketChannel.configureBlocking(false);
            //创建selector对象
            Selector selector = Selector.open();
            //将socket注册到 selector 上,并创建监听事件为 “连接成功”
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            //selector对象2秒一次轮询有没有事件发生,select执行大于0,说明有监听的事件发生,返回结果等于几,就说明有几个监听的事件发生
            while(true){
                if(selector.select(2000) == 0){
                    System.out.println("当前无事件发生,继续循环");
                    continue;
                }

                //获取所有事件key,并循环遍历
                Iterator<SelectionKey> iteratorKey = selector.selectedKeys().iterator();
                while(iteratorKey.hasNext()){
                    //当前key
                    SelectionKey key = iteratorKey.next();
                    //已经连接的事件
                    if(key.isAcceptable()){
                        System.out.println("已经连接的事件");
                        // 创建新的连接,并且把连接注册到selector上,而且,
                        // 声明这个channel对读、写操作都感兴趣。
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ);
                    }
                    //读请求的事件
                    if(key.isReadable()){
                        System.out.println("读请求的事件");
                        ByteBuffer readBuff = ByteBuffer.allocate(1024);
                        //获取出关联的通道
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        //将通道中数据读出,写入buffer
                        socketChannel.read(readBuff);
                        //转换
                        readBuff.flip();
                        System.out.println("readBuff : " + new String(readBuff.array()));
                    }
                    //写求情的事件
                    if(key.isWritable()){
                        System.out.println("写求情的事件");
                        //获取出关联的通道
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        //将通道中数据读出,写入buffer
                        socketChannel.write(ByteBuffer.wrap("收到一个写请求".getBytes()));
                    }
                    //有连接请求的事件
                    if(key.isConnectable()){
                        System.out.println("有连接请求的事件");
                    }
                    //删除当前key
                    iteratorKey.remove();
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

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

/**
 * client 端代码
 */
public class NioClient {

    public static void main(String[] args) {

        try {
            //创建SocketChannel
            SocketChannel socketChannel = SocketChannel.open();
            //设置非阻塞
            socketChannel.configureBlocking(false);
            //绑定连接的地址
            socketChannel.connect(new InetSocketAddress("localhost",8888));

            while (!socketChannel.finishConnect()){
                System.out.println("连接中,请稍后");
            }
//            //发送写数据请求
            socketChannel.write(ByteBuffer.wrap("给服务器发送写请求".getBytes()));

            //发送读数据请求
            ByteBuffer readBuffer = ByteBuffer.allocate(64);

            while(socketChannel.read(readBuffer) == 0){
                System.out.println("正在读取中,请稍后~~");
                Thread.sleep(1000);
            }
            readBuffer.flip();
            System.out.println("readBuffer : " + new String(readBuffer.array()));

            //不让当前服务停止
            System.in.read();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

NIO实现简单的群聊功能

要求:
	1.实现客户端上线,下线通知功能;
	2.实现某一个客户端发送消息,其他客户端也能接收;
代码逻辑:
	1.编写服务器端代码
		1).监听客户端上线,下线
		2).接收客户端发送的消息,并进行转发,转发时提出 当前发送消息的 客户端
	2.编写客户端代码   	
		1).连接服务器端
		2).发送消息
		3).接收消息
package com.clamc.group;

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

/**
 * server 端代码
 */
public class NioServer {

    //定义全局变量
    ServerSocketChannel serverSocketChannel;
    Selector selector;

    public NioServer(){
        try {
            //获取ServerSocketChannel
            serverSocketChannel = ServerSocketChannel.open();
            //绑定监听端口 ,监听8888
            serverSocketChannel.socket().bind(new InetSocketAddress("localhost",8888));
            //设置非阻塞
            serverSocketChannel.configureBlocking(false);
            //创建selector对象
            selector = Selector.open();
            //将socket注册到 selector 上,并创建监听事件为 “连接成功”
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("服务器启动成功!");

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 监听事件
     */
    private void listen(){
        try {
            while (true) {
                //有事件发生,selector.select() 这个方法是堵塞的
                if (selector.select() > 0) {
                    //获取所有事件key,并循环遍历
                    Iterator<SelectionKey> iteratorKey = selector.selectedKeys().iterator();
                    while (iteratorKey.hasNext()) {
                        //当前key
                        SelectionKey key = iteratorKey.next();
                        //已经连接的事件
                        if (key.isAcceptable()) {
                            //创建一个监听 读事件 的socket
                            SocketChannel accept = serverSocketChannel.accept();
                            //设置非阻塞
                            accept.configureBlocking(false);
                            //监听 读事件
                            accept.register(selector, SelectionKey.OP_READ);

                            System.out.println("用户" + accept.getRemoteAddress() + "上线啦");

                            send("用户" + accept.getRemoteAddress() + "上线啦",accept);
                        }
                        //读请求的事件
                        if (key.isReadable()) {
                            read(key);
                        }
                        //删除当前key
                        iteratorKey.remove();
                    }
                }
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    /**
     * 读事件的操作
     */
    private void read(SelectionKey key) throws IOException {
        SocketChannel channel = null;
        try {
            //获取到当前有事件发生的 SocketChannel
            channel = (SocketChannel) key.channel();
            //创建buffer
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            //将channel中的数据写入buffer
            channel.read(buffer);
            String msg = new String(buffer.array());
            //打印
            System.out.println(msg);
            //将数据发送到其他客户端
            send(msg, channel);
        }catch (IOException e){
            System.out.println("用户" + ((SocketChannel)key.channel()).getRemoteAddress() + "下线啦");
            //发送消息
            send("用户" + ((SocketChannel)key.channel()).getRemoteAddress() + "下线啦",(SocketChannel)key.channel());
            //取消注册
            key.cancel();
            channel.close();
        }
    }

    /**
     * 将数据发送到其他客户端
     * @param self  当前发送消息的客户端,转发消息时,不应包含当前客户端
     */
    private void send(String msg,SocketChannel self) throws IOException {
        //获取所有注册的 socket 的迭代器
        Iterator<SelectionKey> iterator = selector.keys().iterator();
        while(iterator.hasNext()){
            SelectionKey key = iterator.next();
            //获取 循环的 客户端SocketChannel
            Channel channel = key.channel();
            //判断循环的数据 不是当前客户端
            if(channel instanceof SocketChannel && channel != self){
                //转型
                SocketChannel target = (SocketChannel)channel;
                //创建 buffer
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                //将buffer中数据写入通道
                target.write(buffer);
            }
        }
    }


    public static void main(String[] args) {
        //初始化服务器
        NioServer nioServer = new NioServer();
        nioServer.listen();
    }
}
package com.clamc.group;

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;

/**
 * client 端代码
 */
public class NioClient {

    //定义全局变量
    SocketChannel socketChannel;
    Selector selector;
    String userName;

    public NioClient(){
        //创建SocketChannel
        try {
            //绑定连接的地址
            socketChannel = socketChannel.open(new InetSocketAddress("localhost",8888));
            //设置非阻塞
            socketChannel.configureBlocking(false);
            //创建selector对象
            selector = Selector.open();
            //注册到 selector
            socketChannel.register(selector, SelectionKey.OP_READ);
            //当前路径,方便 打印
            userName = socketChannel.getLocalAddress().toString().substring(1);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 接受服务器消息
     */
    private void read() throws IOException {
        if(selector.select() > 0){
            //读取数据
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while(iterator.hasNext()){
                SelectionKey next = iterator.next();
                if(next.isReadable()){
                    SocketChannel channel = (SocketChannel) next.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    channel.read(buffer);
                    // 把读到的缓冲区数据转成字符串
                    String msg = new String(buffer.array());
                    System.out.println(msg.trim());
                }
                iterator.remove();
            }
        }
    }

    /**
     * 发送数据
     * @param msg
     */
    private void send(String msg){
        msg = userName + " : " + msg;
        try {
            socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        //接受数据
        NioClient client = new NioClient();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    try {
                        //两秒一次读取数据
                        client.read();
                        Thread.sleep(2000);
                    } catch (IOException | InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        //发送数据
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            client.send(scanner.nextLine());
        }
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值