NIO网络编程

Java支持的网络编程模型的I/O模式: BIO、 NIO、AIO。

1、BIO

关于BIO有同学可能会说不知道,但是知道IO。其实你之前学过那些java.io包下面的输入流和输出流就是BIO中的文件IO部分。例如InputStream、OutputStream。还有一部分就是网络IO,大家学过的在java.net包下面的提供了部分网络API,例如Socket, ServerSocket。

1.1 概念

1.1.1 BIO概念

Java BIO就是传统的java io编程,在java.io包下有相关的类和接口。

BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理。如果这个连接不做任何事情就会造成不必要的线程开销(当然可以通过线程池机制改善)。

同步和异步是针对应用程序和内核的交互而言的。
同步:指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪。
生活案例:自己拿上银行卡去银行取钱(使用同步IO时,Java自己处理IO读写)。
异步:指的是用户进程触发IO操作以后便开始做其他的事情,而当IO操作已经完成的时候会得到IO
完成的通知。
生活案例:托朋友拿着自己的银行卡去银行取钱,告诉朋友自己的银行卡密码,自己去办理其他事
情。同时,你还要告诉朋友取完钱给你送到哪里。( 使用异步I/O时, Java将IO读写委托给OS处理,
需要将数据缓冲区地址和大小传给OS )。

阻塞和非阻塞是针对进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式。
阻塞:指的是准备对文件进行读写时,如果当时没有东西可读,或暂时不可写,程序就进入等待状
态,直到有东西可读或可写为止。
生活案例:你在ATM取款,发现前面有人,你只能等待,等其他人办理完你才能取钱(使用阻塞
IO时,Java调用会一直阻塞到读写完成才返回)。
非阻塞:指的是如果没有东西可读,或不可写,读写函数马上返回,而不会等待。
生活案例:在银行办业务时,人多要先取个号,然后我们可以跟朋友开黑,等轮到我们,银行就会通
知,这时候我们就可以去办业务了(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事
件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)。

1.1.2 工作原理

在这里插入图片描述

  1. 启动一个服务器(serverSocket),等待客户端的连接
  2. 启动一个客户端(Socket),然后与服务器进行通信(默认情况下服务器端需要给每个客户端建立一个线程与其通信)。
  3. 客户端发出请求,询问服务器是否有线程响应,如果没有就会等待或被拒绝。
  4. 如果有响应,客户端线程会等待请求结束后再继续执行。

1.2 案例

使用BIO模型编写一个服务器(服务器端口为10086),客户端可以发消息给服务器。

1.2.1 服务器端

/**
 * BIO模式下的服务器
 */
public class BIOServer {
    public static void main(String[] args) {
        //创建线程池
        ExecutorService executorService= Executors.newCachedThreadPool();
        try {
            //服务器
            ServerSocket serverSocket=new ServerSocket(10086);
            System.out.println("服务器启动--端口号是10086");

            while(true){
                //连接到服务器的客户端--堵塞方法
                Socket client = serverSocket.accept();
                System.out.println("有客户端连接成功!");
                //为每个客户端连接都创建一个新的线程与之通信
                new Thread(()->{
                    System.out.print("线程id:"+Thread.currentThread().getId());
                    System.out.println("线程名称:"+Thread.currentThread().getName());
                    try {
                        int len=0;
                        byte[] byteArr=new byte[1024];
                        //读取来自客户端的数据
                        InputStream inputStream = client.getInputStream();
                        while((len=inputStream.read(byteArr))!=-1){
                            String msg=new String(byteArr);
                            System.out.println("来自客户端的消息:"+msg);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }).start();
            }

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

1.2.2 客户端

/**
 * BIO模式的客户端
 */
public class BIOClient {
    public static void main(String[] args) {
        try {
            //创建客户端
            Socket client=new Socket("127.0.0.1", 10086);
            String msg="hi,dude";
            OutputStream outputStream = client.getOutputStream();
            outputStream.write(msg.getBytes(), 0, msg.length());
            outputStream.close();
            System.in.read();//目的是让客户端保持与服务器的连接
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

1.2.3 测试

方式1:
在这里插入图片描述
方式2:使用 telnet方式测试,该种方式不需编写客户端代码
如果大家的telnet显示为不是内部或者外部命令,就需要先百度解决怎么开启telnet。
在这里插入图片描述
在这里插入图片描述

1.3 总结

1.3.1 BIO缺点:

客户端越来越来越多,服务器就要开启越来越多的线程,对服务器的压力就会越大;而且客户端发起一个连接之后不一定都在做事情,这个时候服务器也要维护,造成不必要的压力。

1.3.2 使用场景:

BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,该方式是JDK1.4以前的唯一选择,但程序直观简单易理解。

2、NIO

2.1 NIO概念

Java NIO(全称java non-blockingIO): 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。

NIO和BIO的作用和目的相同,但是实现方式不同。BIO以流的方式处理数据,而NIO以块的方式处理数据,因此效率要高很多。

NIO是在Java 1.4开始引入了NIO框架(java.nio包) ,java提供了一系列改进的输入输出的新特性,这些统称NIO,也有人成为New IO。

NIO提供了Channel、Selector、 Buffer等新的抽象 ,可以构建多路复用IO程序,同时提供更接近操作系统底层的高性能数据操作方式。传统BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区,或者从缓冲区写入到通道中。Selector用于监听多个通道的事件,因此使用单个线程就可以监听多个数据通道。

2.2 工作原理

在这里插入图片描述

  1. 一个线程一个selector,一个线程对应多个channel(连接),每个Channel对应一个Buffer。
  2. 多个channel可以注册到一个selector,事件决定selector切换到哪一个channel。
  3. 数据的读写通过Buffer,BIO中的流是单向的,要么输入流要么输出流,NIO的Buffer是可双向读写,通过flip方法切换即可。
  4. channel也是双向的,可以返回底层操作系统的情况,例如Linux,底层的操作系统通道就是双向的。

2.3 使用场景

NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器。

2.3 NIO核心— 缓冲区 Buffer

2.3.1 概念

缓冲区(Buffer) :缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存,这块内存被包装成NIO Buffer对象,可以理解成是一个容器,是一个特殊的数组,该对象提供了一组方法,用来方便的访问该块内存。

在这里插入图片描述
Channel提供从文件或网络读取数据的渠道,但是读取或者写入的数据都是经过Buffer。

2.3.2 Buffer类及其子类

在NIO中,Buffer是一个顶级父类,也是一个抽象类,有很多的子类。

在这里插入图片描述

2.3.3 Buffer中的属性

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

2.3.4 Buffer中的方法

在这里插入图片描述

2.3.5 Buffer的基本用法

使用Buffer读写数据一般遵循以下四个步骤:

  1. 创建缓冲区,写入数据到Buffer
  2. 调flip()方法将缓冲区改成读取模式
  3. 从Buffer中读取数据
  4. 调用clear()方法或者compact()方法

虽然java中的基本数据类型都有对应的Buffer类型与之对应(Boolean除外),但是使用频率最高的是ByteBuffer类。所以先介绍一下ByteBuffer中的常用方法。

2.3.5.1 ByteBuffer中常用方法

在这里插入图片描述

  • allocate(int):创建间接缓冲区:在堆中开辟,易于管理,垃圾回收器可以回收,空间有限,读写文件 速度较慢。
  • allocateDirect(int):创建直接缓冲区:不在堆中,物理内存中开辟空间,空间比较大,读写文件速度 快,缺点:不受垃圾回收器控制,创建和销毁耗性能。
2.3.5.2 案例

案例1:ByteBuffer的常用方法

public class BufferTest01 {
    public static void main(String[] args) {
       // 1、创建缓冲区,写入数据到Buffer
        ByteBuffer buffer=ByteBuffer.allocate(1024);//创建指定容量的间接缓冲区
       // ByteBuffer buffer1=ByteBuffer.allocateDirect(1024);//创建指定容量的直接缓冲区
        //写入数据的方式1
        //buffer.put("hi,dude".getBytes());
        //写入数据的方式2
        buffer.put((byte) 'h');
        buffer.put((byte) 'i');
        buffer.put((byte) ',');
        buffer.put((byte) 'd');
        buffer.put((byte) 'u');
        buffer.put((byte) 'd');
        buffer.put((byte) 'e');
       // 2、调flip()方法将缓冲区改成读取模式
        buffer.flip();
       // 3、从Buffer中读取数据的方式1:单个自己的读取
        /*while(buffer.hasRemaining()) {
            byte b = buffer.get();
            System.out.println((char) b);
        }*/
        //读取数据的方式2:
        byte[] data=new byte[buffer.limit()];
        buffer.get(data);
        System.out.println(new String(data));

       // 4、调用clear()方法或者compact()方法
        buffer.clear();
        buffer.compact();
    }
}
  • clear(): position将被置为0,limit被设置成capacity的值。可以理解为Buffer被清空了,但 是Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。如果Buffer中 有一些未读的数据,调用clear()方法,未读数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据 被读过,哪些还没有。
  • compact(): 将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后 面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆 盖未读的数据。

案例2:ByteBuffer的常用方法

ByteBuffer 支持类型化的put和get, put放入的是什么数据类型,get取出来依然是什么类型, 否则可能
出现BufferUnderflowException 异常。

public class BufferTest02 {
    public static void main(String[] args) {
        //1、创建buffer
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        //2、写入数据:按照类型化方式
        buffer.putChar('K');
        buffer.putLong(1024L);
        buffer.putInt(512);
        buffer.putShort((short) 0);
        //3、读写切换
        buffer.flip();
        //4、读取数据
        System.out.println(buffer.getChar());
        System.out.println(buffer.getLong());
        System.out.println(buffer.getInt());
        System.out.println(buffer.getShort());
    }
}

案例3:一个普通Buffer转成只读Buffer 。

public class readOnlyBuffer{
    public static void main(String[] args) {
        //创建一个buffer
        ByteBuffer buffer=ByteBuffer.allocate(10);
        //写入数据
        for(int i=0;i<buffer.capacity();i++){
            buffer.put((byte) i);
        }
        //读写切换
        buffer.flip();
        //获取一个只读的buffer:普通的buffer转换为了只读的buffer
        ByteBuffer readOnlyBuffer=buffer.asReadOnlyBuffer();
        System.out.println("readOnlyBuffer的类型:"+readOnlyBuffer.getClass());
        //读取数据
        while (readOnlyBuffer.hasRemaining()){
            System.out.println(readOnlyBuffer.get());
        }
        //写入数据--会抛出ReadOnlyBufferException异常,只读buffer不可以写入数据
        readOnlyBuffer.put((byte) 66);//此处会抛出异常
    }
}

2.4 NIO核心— 通道 Channel

2.4.1 Channel介绍

通道(Channel) :类似于BIO中的stream,例如FileInputStream对象,用来建立到目标(文件,网络套接字,硬件设备等)的一个连接。

但是也有区别:

  • 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
  • 通道可以异步地读写。
  • 通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。

在这里插入图片描述

2.4.2 Channel的实现

常用的Channel类有: FileChannel、DatagramChannel、ServerSocketChannel 和SocketChannel。

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

在这里插入图片描述

2.4.2 FileChannel

FileChannel主要用来对本地文件进行读写操作,但是FileChannel是一个抽象类,所以我们实际用的更多的是其子类FileChannelImpl

2.4.2.1 FileChannel常用API

在这里插入图片描述

2.4.2.1 案例
  1. 写入数据到文件
/**
 * 写出数据到本地文件中
 */
public class FileChannelTest01 {
    public static void main(String[] args) throws IOException {
        String msg="hi,dude";
        String fileName="channel01.txt";
        //创建一个输出流
        FileOutputStream fileOutputStream=new FileOutputStream(fileName);
        //获取同一个通道--channel的实际类型是FileChannelImpl
        FileChannel channel=fileOutputStream.getChannel();
        //创建一个缓冲区
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        //将信息写入缓冲区中
        buffer.put(msg.getBytes());
        //对缓冲区读写切换
        buffer.flip();
        //将缓冲区中的数据写到到通道中
        int num=channel.write(buffer);
        System.out.println("写入完毕!"+num);
        fileOutputStream.close();
    }
}

在这里插入图片描述

  1. 读取文件中的数据
/**
 * 从本地文件中读取数据
 */
public class FileChannelTest02 {
    public static void main(String[] args) throws IOException {
        File file=new File("channel01.txt");
        //创建输入流
        FileInputStream fileInputStream=new FileInputStream(file);
        //获取通道
        FileChannel channel = fileInputStream.getChannel();
        //创建缓冲区
        ByteBuffer buffer=ByteBuffer.allocate((int) file.length());
        //将通道中的数据读取buffer中
        channel.read(buffer);
        //将buffer中的字节数组转换为字符串输出
        System.out.println(new String(buffer.array()));
        fileInputStream.close();
    }
}
  1. 文件的复制

方式1:非transferFrom方式

/**
 * 实现文件的复制
 */
public class FileChannelTest03 {
    public static void main(String[] args) throws IOException {
        //准备好要复制的源文件和目标文件
        File file=new File("cat.jpg");
        File fileCopy=new File("catCopy.jpg");
        //创建输入和输出流
        FileInputStream fileInputStream=new FileInputStream(file);
        FileOutputStream fileOutputStream=new FileOutputStream(fileCopy);
        //获取两个通道
        FileChannel inChannel = fileInputStream.getChannel();
        FileChannel outChannel = fileOutputStream.getChannel();
        //创建缓冲区
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        int len=0;
        while(true){
            //将标志位重置
            buffer.clear();
            len=inChannel.read(buffer);
            System.out.println("len="+len);
            if(len==-1){
                break;
            }
            //读写切换
            buffer.flip();
            //将buffer中的数据写入到了通道中
            outChannel.write(buffer);
        }
        System.out.println("复制完毕!OK!");
        inChannel.close();
        outChannel.close();
        fileInputStream.close();
        fileOutputStream.close();
    }
}

方式2:使用transferFrom方式拷贝

/**
 * 文件的复制--方式2 transferFrom
 */
public class FileChannelTest04 {
    public static void main(String[] args) throws IOException {
        //要复制的文件和目标文件
        File file=new File("cat.jpg");
        File fileCopy=new File("catCopy1.jpg");
        //创建输入流和输出流
        FileInputStream fileInputStream=new FileInputStream(file);
        FileOutputStream fileOutputStream=new FileOutputStream(fileCopy);
        //获取两个通道
        FileChannel inChannel=fileInputStream.getChannel();
        FileChannel outChannel=fileOutputStream.getChannel();
        //使用transferFrom复制--两个方式适合大文件的复制
        outChannel.transferFrom(inChannel, 0,inChannel.size());
        //使用transferTo复制---注意方向
        //inChannel.transferTo(0, inChannel.size(),outChannel);
        System.out.println("复制完毕!OK");
        inChannel.close();
        outChannel.close();
        fileInputStream.close();
        fileOutputStream.close();
    }
}

2.5 NIO核心— 选择器Selector

2.5.1 概念

Selector 一般称 为选择器 ,当然你也可以翻译为 多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。

在这里插入图片描述
咱们刚刚讲过的进行文件IO时用到的FileChannel并不支持非阻塞操作,咱们学习NIO主要就是进行网络IO的学习, Java NIO中的网络通道是非阻塞IO的实现,基于事件驱动,非常适用于服务器需要维持大量连接但数据交换量不大的情况,例如一些即时通信的服务等等。

在之前的BIO中我们已经编写过Socket服务器:

  1. ServerSocket–BIO模式的服务器,一个客户端一个线程,虽然写起来简单,但是如果连接越多,线程就会越多,容易耗尽服务器资源而使其宕机。
  2. 使用线程池优化–让每个客户端的连接请求交给固定数量线程的连接池处理,写起来简单还能处理大量连接。但是线程的开销依然不低,如果请求过多,会出现排队现象。

如果使用java中的NIO,就可以用非堵塞的IO模式处理,这种模式下可以使用一个线程,处理大量的客户端连接请求。

只有在连接真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程, 不用去维护多个线程。

避免了多线程之间的上下文切换导致的开销。

2.5.2 选择器(Selector)

2.5.2.1 概念

Selector:选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。

Selector是一个抽象类,实际使用的时候用的是SelectorImpl
在这里插入图片描述
在这里插入图片描述
Selector类的select的三种不同形式:

  1. 无参的select():Selector类的select()方法会无限阻塞等待,直到有信道准备好了IO操作,或另一个线程唤醒了它(调用了该选择器的wakeup())返回SelectionKey。
  2. 带有超时参数的select(long time):当需要限制线程等待通道就绪的时间时使用,如果在指定的超时时间(以毫秒计算)内没有通道就绪时,它将返回0。将超时参数设为0表示将无限期等待,那么它就等价于select( )方法了。
  3. selectNow()是完全非阻塞的:该方法执行就绪检查过程,但不阻塞。如果当前没有通道就绪,它将立即返回0。
2.5.2.2 使用

通过调用Selector.open()方法创建一个Selector对象,如下:

Selector selector = Selector.open();

2.5.3 SelectionKey介绍

2.5.3.1 概念

SelectionKey:一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。这种注册的关系共有四种:

在这里插入图片描述

2.5.3.2 常用方法:

在这里插入图片描述

2.5.4 SelectableChannel介绍

2.5.4.1 类关系

在这里插入图片描述

2.5.4.2 两个重要方法
  1. configureBlocking()方法:设置阻塞或非阻塞模式
public abstract SelectableChannel configureBlocking(boolean block)

SelectableChannel抽象类的configureBlocking() 方法是由 AbstractSelectableChannel抽象类实现的,SocketChannel、ServerSocketChannel、DatagramChannel都是直接继承了AbstractSelectableChannel抽象类 (上图明确展示了这些类关系)。

  1. register()方法 注册一个选择器并设置监听事件
public abstract SelectionKey register(Selector sel, int ops)

register() 方法的第二个参数是一个interset集合 ,指通过Selector监听Channel时对什么事件感兴趣。
可以监听四种不同类型的事件:

  • Connect
  • Accept
  • Read
  • Write

通道触发了一个事件意思是该事件已经就绪。比如某个Channel成功连接到另一个服务器称为“ 连接就绪 ”。一个ServerSockeChannel准备好接收新进入的连接称为“ 接收就绪 ”。一个有数据可读的通道可以说是“ 读就绪 ”。等待写数据的通道可以说是“ 写就绪 ”。

这四种事件用SelectionKey的四个常量来表示:

SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE

如果你对不止一种事件感兴趣,使用或运算符即可,如下:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

2.5.5 ServerSocketChannel

ServerSocketChannel用来在服务器端监听新的客户端Socket连接。

常用的方法除了两个从SelectableChannel类中继承而来的两个方法configureBlocking()和register()方法外,还有以下要记住的方法:

在这里插入图片描述

2.5.6 SocketChannel

SocketChannel,网络IO通道,具体负责读写操作。NIO总是把缓冲区的数据写入通道,或者把通道里的数据读出到缓冲区(buffer) 。常用方法如下所示:

常用的方法除了两个从SelectableChannel类中继承而来的两个方法configureBlocking()和register()方法外,还有以下要记住的方法:

在这里插入图片描述

2.5.7 案例1

2.5.7.1 NIO服务器端
public class NIOServer{
    public static void main(String[] args) throws IOException {
        //创建一个服务器
        ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
        //绑定服务器端口
        serverSocketChannel.bind(new InetSocketAddress(10086));
        //将通道设置为非堵塞
        serverSocketChannel.configureBlocking(false);
        //创建Selector
        Selector selector=Selector.open();
        //将通道注册到selector中,并监听的请求连接事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //循环等待客户端的连接
        while(true){
            if(selector.select(3000)==0){
                System.out.println("Server: 等的花儿都谢了.....我也先忙一会儿");
                continue;
            }
            //获取SelectionKey的集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while(iterator.hasNext()){
                SelectionKey key = iterator.next();
                if(key.isAcceptable()){//如果是连接事件
                    //获取连接到服务器的客户端
                    SocketChannel socketChannel=serverSocketChannel.accept();
                    //设置为非堵塞
                    socketChannel.configureBlocking(false);
                    System.out.println("有客户端连接!连接的socketchannel:"+socketChannel.hashCode());
                    //注册
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                if(key.isReadable()){//如果是OP_READ事件
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer= (ByteBuffer) key.attachment();
                    socketChannel.read(buffer);
                    System.out.println("来自客户端的消息是:"+new String(buffer.array()));
                }
                iterator.remove();
            }
        }
    }
}
2.5.7.2 NIO客户端
public class NIOClient {
    public static void main(String[] args) throws IOException {
        //创建一个客户端
        SocketChannel socketChannel=SocketChannel.open();
        //通道都要设置为非堵塞
        socketChannel.configureBlocking(false);
        //准备要连接的服务器名称和端口号
        InetSocketAddress inetSocketAddress=new InetSocketAddress("127.0.0.1", 10086);
        //连接服务器
        if(!socketChannel.connect(inetSocketAddress)){
            while(!socketChannel.finishConnect()){
                System.out.println("Client:努力连接中.....可以先做点其他事情.......");
            }
        }
        //连接成功之后发消息给服务器
        String msg="hi,dude";
        ByteBuffer buffer=ByteBuffer.wrap(msg.getBytes());
        socketChannel.write(buffer);
        System.in.read();
    }
}

2.5.8 案例2-聊天室

服务器端

/**
 * 聊天室的服务器端
 */
public class ChatServer {
    private ServerSocketChannel serverSocketChannel;
    private Selector selector;
    private static final  int PORT=10086;

    public ChatServer() {
        //完成属性的初始化
        try {
            serverSocketChannel=ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(PORT));
            serverSocketChannel.configureBlocking(false);
            selector=Selector.open();
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //监听事件
    public void listening(){
            try {
                while(true){
                    int num=selector.select(3000);
                    if(num>0){
                        Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                        while(iterator.hasNext()){
                            SelectionKey key=iterator.next();
                            if(key.isAcceptable()){//处理客户端的连接请求
                                //连接到服务器的客户端
                                SocketChannel socketChannel = serverSocketChannel.accept();
                                socketChannel.configureBlocking(false);//设置为非堵塞
                                //将连接成功的客户端注册到了selector
                                socketChannel.register(selector, SelectionKey.OP_READ);
                                System.out.println(socketChannel.getRemoteAddress()+"连接成功!进入聊天室!");
                            }
                            if(key.isReadable()){//处理客户端的通信请求
                                //处理数据的读取和转发给除了自己之外的客户端
                                handleReadData(key);
                            }
                            //避免一致处理同一个通道的事件
                            iterator.remove();
                        }
                    }else{
                        System.out.println("server: waiting.......");
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
    }
    //处理数据的读取
    private void handleReadData(SelectionKey key){
        SocketChannel socketChannel=null;
        try {
            //服务器接收来自客户端的消息
            socketChannel= (SocketChannel) key.channel();
            ByteBuffer buffer=ByteBuffer.allocate(1024);
            int len=socketChannel.read(buffer);
            if(len>0){
                String msg=new String(buffer.array());
                System.out.println("来自客户端的消息是:"+msg);
                //转发消息给其他的客户端
                transferMessage(msg,socketChannel);
            }
        } catch (IOException e) {
            try {
                System.out.println(socketChannel.getRemoteAddress()+"离开了聊天室!");
                key.cancel();//取消注册
                socketChannel.close();//关闭通道
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
           // e.printStackTrace();
        }
    }
    //服务器端转发消息
    public void transferMessage(String msg,SocketChannel socketChannel) throws IOException {
        System.out.println("server转发消息ing.......");
        for (SelectionKey selectionKey : selector.keys()) {
            SelectableChannel channel = selectionKey.channel();
            if(channel instanceof SocketChannel && channel!=socketChannel){
                SocketChannel client= (SocketChannel) channel;
                ByteBuffer buffer1=ByteBuffer.wrap(msg.getBytes());
                client.write(buffer1);
            }
        }
    }

    public static void main(String[] args) {
        ChatServer chatServer=new ChatServer();
        chatServer.listening();
    }
}

客户端

public class ChatClient {
    private final String HOSTNAME="127.0.0.1";
    private final int PORT=10086;
    private SocketChannel socketChannel;
    private Selector selector;
    private String userName;

    public ChatClient() {
        try {
            //初始化
            socketChannel=SocketChannel.open(new InetSocketAddress(HOSTNAME, PORT));
            socketChannel.configureBlocking(false);
            selector=Selector.open();
            socketChannel.register(selector, SelectionKey.OP_READ);
            userName=socketChannel.getLocalAddress().toString();
            System.out.println(userName+" is ready!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void sendInfo(String info){
        try {
            info=userName+":"+info;
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void readInfo(){
        try {
            int readyChannel = selector.select();
            if(readyChannel>0){
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while(iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    if(key.isReadable()) {
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        socketChannel.read(buffer);
                        String msg = new String(buffer.array());
                        System.out.println(msg);
                    }
                    iterator.remove();
                }
            }else{
                System.out.println("没有准备就绪的通道!");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {
        ChatClient chatClient=new ChatClient();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    chatClient.readInfo();
                }
            }
        }).start();

        Scanner input=new Scanner(System.in);
        while(input.hasNextLine()){
            String msg=input.nextLine();
            chatClient.sendInfo(msg);
        }
    }
}

3、BIO VS NIO

  • BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
  • NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
    • BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的I/O 处理模型来应对更高的并发量。
    • NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Nonblocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值