NIO流在聊天室中的运用

本文详细讲解了Java NIO(Non-Blocking IO)的基本概念,如Channel和Buffer的使用,以及如何通过NIO实现一个简单的文件复制和一个基于多路选择器的非阻塞聊天室服务端。通过实例展示了如何利用ByteBuffer读写文件和高效地处理客户端连接和消息传递。

 JAVA NIO
NIO称为非阻塞IO。读写的过程中不会发生阻塞线程
我们之前所学习的流,称为BIO:阻塞是IO,就是在读写的过程中可能会发生阻塞现象。

非阻塞IO面向Channel("通道")的,不是面向Stream(流)的。

流的特点:方向单一,顺序读写。流要么是输入流用于顺序读取数据,要么是输出流用于顺序写出数据

Channel的特点:双向的,既可以读又可以写。

JAVA NIO核心API:
Channel 通道,常见的实现:
FileChannel:文件通道,可对文件进行读写操作
SocketChannel:套接字通道,可以与远端计算机进行TCP读写操作
ServerSocketChannel:服务端的套接字通道,用于监听客户端的连接

Buffer缓冲区,通道是对缓冲区中的数据进行读写操作
常见的缓冲区实现
ByteBuffer:字节缓冲区,缓冲区内部内容都是字节

缓冲区中

    //BIO的文件复制操作,使用流的方式进行复制
//        FileInputStream fis = new FileInputStream("movie.wmv");
//        FileOutputStream fos = new FileOutputStream("movie_cp.wmv");
//        byte[] buffer = new byte[1024*10];//创建一个字节数组作为缓冲区
//        int len;//记录每次实际读取的字节数
//        while((len = fis.read(buffer))!=-1){
//            fos.write(buffer,0,len);
//        }
//        System.out.println("复制完毕");
//        fis.close();
//        fos.close();


        //NIO的文件复制操作
        FileInputStream fis = new FileInputStream("movie.wmv");
        //基于文件输入流获取一个用于读取该文件的文件通道
        FileChannel inChannel = fis.getChannel();


        FileOutputStream fos = new FileOutputStream("movie_cp2.wmv");
        FileChannel outChannel = fos.getChannel();


        //创建一个字节缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024*10);//创建一个10k大小缓冲区
        int len;//记录每次实际读取的数据量

        /*
            缓冲区中重要的几个属性:
            position:当前位置,用来表示当前缓冲区已经有多少数据被操作了
            limit:缓冲区最大可以操作的位置
            capacity:容量,缓冲区的大小

            默认创建一个缓冲区时:
            position=0
            limit=capacity
            capacity=创建缓冲区时指定的大小
         */
        /*
            position=0
            limit=10240
            一次可以读取最多读取数据为:position到limit之间的数据量
            limit-position = 10240
         */
//        System.out.println("读取前buffer状态=========================");
//        System.out.println("position:"+buffer.position());
//        System.out.println("limit:"+buffer.limit());
//        len = inChannel.read(buffer);//从通道中读取数据到缓冲区中
//        System.out.println("本次读取了:"+len+"个字节");
//        System.out.println("读取后buffer状态=========================");
//        System.out.println("position:"+buffer.position());
//        System.out.println("limit:"+buffer.limit());
//
//        System.out.println("======================进行第二次读取============================");
//        System.out.println("读取前buffer状态=========================");
//        System.out.println("position:"+buffer.position());
//        System.out.println("limit:"+buffer.limit());
//         /*
//            position=10240
//            limit=10240
//            一次可以读取最多读取数据为:position到limit之间的数据量
//            limit-position = 0
//            由于position的位置与limit一直,因此表示目前缓冲区已经没有空余可操作的空间了
//         */
//        len = inChannel.read(buffer);
//        System.out.println("本次读取了:"+len+"个字节");
//        System.out.println("读取后buffer状态=========================");
//        System.out.println("position:"+buffer.position());
//        System.out.println("limit:"+buffer.limit());


        //完成一轮复制
        /*
            读取前
            position:0
            limit:10240
         */
//        len = inChannel.read(buffer);//一次读取了10K数据
        /*
            读取后
            position:10240
            limit:10240
         */
        /*
            write在写出一个缓冲区数据时,写出的也是缓冲区中position与limit之间的数据

            总结:
            Channel通道在进行读或写操作时,具体可以读取多少个字节或写出多少个字节是取决于
            我们传入的Buffer中position到limit之间的空间。
         */
//        buffer.flip();
        /*
            flip后:
            position:0
            limit:10240
         */
//        outChannel.write(buffer);//向文件movie_cp.wmv中写出10k
        /*
            position:10240
            limit:10240
         */



        //完整的复制动作
        while((len = inChannel.read(buffer))!=-1){
            buffer.flip();
            outChannel.write(buffer);
            buffer.clear();
        }

重要的几个属性:
position:当前位置,用来表示当前缓冲区已经有多少数据被操作了
limit:缓冲区最大可以操作的位置
capacity:容量,缓冲区的大小

默认创建一个缓冲区时:
position=0
limit=capacity
capacity=创建缓冲区时指定的大小

聊天室

package com;

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.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * NIO实现聊天室服务端
 *
 * netty框架
 */
public class NIOServer {
    public static void main(String[] args) {
        try {
            //存放所有客户端的SocketChannel,用于广播消息
            List<SocketChannel> allChannel = new ArrayList<>();


            //创建总机,ServerSocketChannel
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            //ServerSocketChannel模式为阻塞模式,可以将其设置为非阻塞模式
            serverSocketChannel.configureBlocking(false);//传入false设置为非阻塞模式
            //为ServerSocketChannel绑定服务端口,客户端可以通过该端口与我们建立连接
            serverSocketChannel.bind(new InetSocketAddress(8088));
            //以上创建为固定模式,以后都可以用这样的形式创建ServerSocketChannel的非阻塞模式

            /*
                多路选择器的应用
                这个是NIO解决非阻塞的关键API,用于监听所有通道对应的事件,并做出对应的操作。
                我们的线程只要轮询处理多路选择器中待处理的通道事件即可完成所有通道的工作,避免使用大量线程
                处于阻塞来减少不必要的系统开销。
             */
            Selector selector = Selector.open();//使用其静态方法open获取一个多路选择器实例

            /**
             * 让"总机"ServerSocketChannel向多路选择器上注册一个事件,即:accept事件。
             * 原因:原来使用ServerSocket时,一旦主线程调用accept方法就会进入阻塞状态,直到一个客户端连接
             * 否则将无法继续执行其他工作。而现在的操作是让多路选择器关心该事件,避免让线程处于阻塞。
             */
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);


           while(true) {
                /*
                    多路选择器的select方法
                    当注册在该选择器上的channel有事件待处理时,此方法会立即返回一个int值,表示有多少个事件待处理
                    若没有任何事件待处理时,此方法会形成阻塞。
                 */
               System.out.println("等待选择器告知是否有事件等待处理...");
               selector.select();
               System.out.println("选择器有事件待处理!!!");
               //通过选择器获取所有待处理的事件
               Set<SelectionKey> keySet = selector.selectedKeys();
               //遍历集合,将所有待处理的事件处理完毕
               for (SelectionKey key : keySet) {
                   //判断该事件是否为可以接受一个客户端连接了(对应的是向多路选择器注册的事件SelectionKey.OP_ACCEPT)
                   if (key.isAcceptable()) {
                       //处理接收客户端连接的操作
                        /*
                            通过SelectionKey的方法channel()获取该事件触发的通道

                            因为只有ServerSocketChannel向多路选择器注册了OP_ACCEPT事件,因此当该事件
                            isAcceptable()返回值为true,则说明该事件一定是由ServerSocketChannel触发的
                            所以我们通过该事件获取触发该事件的通道时,一定获取的是ServerSocketChannel
                         */
                       ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                        /*
                            获取的SocketChannel与原来ServerSocket.accept返回的Socket道理一致
                            通过该SocketChannel就可以与连接的客户端进行双向交互数据了
                         */
                       SocketChannel socket = channel.accept();//接受客户端的连接
                        /*
                            非阻塞的ServerSocketChannel就算多路选择器提示有客户端请求可接受了,accept返回时
                            得到的SocketChanel有可能为null
                         */
                       if (socket == null) {
                           continue;//忽略本次事件的后续处理
                       }
                        /*
                            当我们接受了客户端连接后,原来的聊天室服务端我们通过Socket获取输入流读取客户端
                            发送过来消息的操作时如果客户端不发送内容,那么读取操作就会阻塞!
                            对此,我们将当前SocketChannel它的读取消息操作也注册到多路选择器中,这样一来只有
                            当客户端发送过来消息时,多路选择器才会通知我们进行处理。
                         */
                       //将当前SocketChannel设置为非阻塞模式
                       socket.configureBlocking(false);
                       //向多路选择器中注册读取客户端消息的事件
                       socket.register(selector, SelectionKey.OP_READ);
                       //将该SocketChannel存入共享集合,用于将消息广播
                       allChannel.add(socket);
                       System.out.println("一个客户端连接了!当前在线人数:"+allChannel.size() );


                   //该事件是否为某个SocketChannel有消息可以读取了(某个客户端发送过来了消息)
                   }else if(key.isReadable()){
                        try {
                            //通过事件获取触发了该事件的channel
                            SocketChannel socketChannel = (SocketChannel) key.channel();
                            //通过SocketChannel读取该客户端发送过来的消息
                            ByteBuffer buffer = ByteBuffer.allocate(1024);

                            //非阻塞状态下,有可能读取数据时未读取到任何字节
                            int len = socketChannel.read(buffer);
                            if (len == 0) {
                                continue;
                            } else if (len == -1) {//若本次读取的长度返回值为-1则表示客户端断开连接了
                                socketChannel.close();//关闭SocketChannel与客户端也断开
                                allChannel.remove(socketChannel);
                                continue;
                            }

                            buffer.flip();//反转后position到limit之间就是本次读取到的数据了
                            byte[] data = new byte[buffer.limit()];
                           /*
                                Buffer的get方法要求我们传入一个字节数组,作用是将当前Buffer中从下标
                                position开始的连续data数组长度的字节量装入该data数组。
                            */
                            buffer.get(data);//调用完毕后,data中保存的就是buffer中本次读取到的所有字节了
                            //将读取的消息转换为字符串(客户端发送过来的消息)
                            String line = new String(data, StandardCharsets.UTF_8);
                            //通过SocketChannel获取客户端地址信息
                            String host = socketChannel.socket().getInetAddress().getHostAddress();
                            System.out.print(host+"说:" + line);

                            //遍历所有的SocketChannel,将该消息发送给所有客户端
                            for(SocketChannel sc : allChannel){
                                buffer.flip();//position:0   limit:buffer中所有之前读取到的字节
                                sc.write(buffer);//position=limit=buffer中所有之前读取到的字节
                            }




                        }catch(IOException e){
                            //读取客户端消息这里若抛出异常,则通常是客户端强行断开连接造成的。
                            key.channel().close();//断开该SocketChannel与客户端断开连接即可
                            allChannel.remove(key.channel());
                        }
                   }
               }

           }


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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Liamlf

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

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

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

打赏作者

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

抵扣说明:

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

余额充值