IO模型

IO模型

BIO模型

在BIO模式下,数据的写入和读取都必须阻塞在一个线程中执行,在写入完成或读取完成前,线程阻塞。

在传统的BIO中,一个客户端请求服务器后,服务器会经过Sokcet启动一条链路将其连接并且处理,该链路的IO操作的同步阻塞的,所以该客户端和服务器的连接不可被其他客户端所使用,只能够等待当前的客户端操作完成后释放掉当前连接。

image-20230915222948781

简单实列

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
//服务器端
public class Server {
    public static void main(String[] args) throws IOException {
        InputStream inputStream=null;
        BufferedReader Reader=null;
        try {
            System.out.println("===服务端开启===");
            //注册服务端端口
            ServerSocket  server=new ServerSocket(9999);
            //监听端口连接信息
            Socket accept = server.accept();
            //服务器端需要拥有输入流
             inputStream = accept.getInputStream();
            //将字节输入流转换称为缓冲字符输入流
            Reader = new BufferedReader(new InputStreamReader(inputStream));
            //读取一行数据
            String msg=null;
            //循环等待接收消息(体现了BIO的一个同步堵塞的特点)
            while((msg=Reader.readLine())!=null){
                System.out.println("服务端接收到的消息="+msg);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally { //
            if(Reader!=null){
                Reader.close();
            }
            //关闭掉字节输入流
           if(inputStream!=null){
               inputStream.close();
           }

        }
    }
}

客户端

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
//客户端
public class Client {
    public static void main(String[] args) throws IOException {
        OutputStream out=null;
        PrintStream print=null;
        try {
            //指定好目的地址和接口
            Socket client=new Socket("127.0.0.1",9999);
            //获得字节输出流
            out = client.getOutputStream();
            //将字节输入流转换称为打印流
            print=new PrintStream(out);
            print.println("服务器你好");
            print.flush();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            if(print!=null){
                print.close();
            }
            if(out!=null){
                out.close();
            }
        }
    }
}

接收多个客户端

当我们直接开启多个客户端时,当使用多个客户端给服务器发送消息时,我们可以发现服务器智能接收到第一个客户端的消息。这是由于BIO一个客户端与服务器端之间是独占一个Thread(线程的),所以我们向接收多个客户端的消息的话,需要创建多个Thread。

image-20230915230415639

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {
        try {
            System.out.println("===服务端开启===");
            //注册服务端端口
            ServerSocket server = new ServerSocket(9999);
            //不断的去接收
            while(true) {
                Socket accept = server.accept();
                new Thread(() -> {
                    InputStream inputStream = null;
                    BufferedReader Reader = null;
                    try {
                        //监听端口连接信息
                        //服务器端需要拥有输入流
                        inputStream = accept.getInputStream();
                        //将字节输入流转换称为字符输入流
                        Reader = new BufferedReader(new InputStreamReader(inputStream));
                        //读取一行数据
                        String msg = null;
                        //循环等待接收消息(体现了BIO的一个同步堵塞的特点)
                        while ((msg = Reader.readLine()) != null) {
                            System.out.println("服务端接收到的消息=" + msg);
                        }
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    } finally {
                        try {
                            if (Reader != null) {
                                Reader.close();
                            }
                            if (inputStream != null) {
                                inputStream.close();
                            }
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }).start();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

小结:

1.每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能;·

2.每个线程都会占用栈空间和CPU资源;

3.并不是每个socket都进行lO操作,无意义的线程处理;

4.客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。

伪异步I/O操作

我们采用一个伪异步l/O的通信框架,采用线程池和任务队列实现(线程池的队列可以存储客户端发送的消息,所以可以认为是伪异步IO),当客户端接入时,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable线程任务接口)交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

将上述优化(使用线程池)

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
 * @author zl
 * @date 2023/09/15
 */
public class Server {
    public static void main(String[] args) throws IOException {
        try {
            System.out.println("===服务端开启===");
            //注册服务端端口
            ServerSocket server = new ServerSocket(9999);
            //初始化线程池
            ServerThreadPool threadPool=new ServerThreadPool(6,3);
            //不断的去接收
            while(true) {
                Socket accept = server.accept();
                //创建Runnable
                Runnable target=(() -> {
                    InputStream inputStream = null;
                    BufferedReader Reader = null;
                    try {
                        //监听端口连接信息
                        //服务器端需要拥有输入流
                        inputStream = accept.getInputStream();
                        //将字节输入流转换称为字符输入流
                        Reader = new BufferedReader(new InputStreamReader(inputStream));
                        //读取一行数据
                        String msg = null;
                        //循环等待接收消息(体现了BIO的一个同步堵塞的特点)
                        while ((msg = Reader.readLine()) != null) {
                            System.out.println("服务端接收到的消息=" + msg);
                        }
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    } finally {
                        try {
                            if (Reader != null) {
                                Reader.close();
                            }
                            if (inputStream != null) {
                                inputStream.close();
                            }
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                });
                //执行任务
                threadPool.setExecutorTarget(target);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

创建线程池

import java.util.Queue;
import java.util.concurrent.*;

/**
 * @author zl
 * @date 2023/09/16
 */
public class ServerThreadPool {
    private ExecutorService executorService;

    /**
     * 使用构造方法创建好线程池
     * @param maxnum 最大线程数量
     * @param queuesize 工作队列(线程等待队列)
     */
    public ServerThreadPool(int maxnum,int queuesize){
/**
 * public ThreadPoolExecutor(int corePoolSize,   //核心线程数(就是会存在的线程最少数量)
 *                           int maximumPoolSize, //最大线程数
 *                           long keepAliveTime,  //超时时间,当这个时间没有线程了之后,就会释放一些线程
 *                           TimeUnit unit,   //时间单位
 *                           BlockingQueue<Runnable> workQueue, //堵塞队列,相当于一个缓冲区
 *                           ThreadFactory threadFactory, //线程工厂
 *                           RejectedExecutionHandler handler   //拒绝处理的措施,未执行的线程数大于了缓冲区和最大线程数量之和之后会穿法的机制)
 * }
 */
        executorService= new ThreadPoolExecutor(
               3,
               maxnum,
                120,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(queuesize),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy() 
        );
    }
    public void setExecutorTarget(Runnable target){
        executorService.execute(target);
    }
}

小结:

  • ·伪异步io采用了线程池实现,因此避免了为每个请求创建一个独立线程造成线程资源耗尽的问题但由于底层依然是采用的同步阻塞模型,因此无法从根本上解决问题。
  • 如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续socket的ilo消息都将在队列中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时。

实列

实现服务器端接收客户端任意后缀的文件
package File;

/*
* 任务:客服端传输任何后缀的文件给服务器端
* */
public class Client {
    public static void main(String[] args) throws IOException {
        String path="C:\\Users\\pc\\Desktop\\logo.png";
        File file =new File(path);
        try(FileInputStream fileInputStream=new FileInputStream(file);){
            Socket socket=new Socket("127.0.0.1",8888);
            //使用DataOutStream将数据传输到服务器端
            DataOutputStream data=new DataOutputStream(socket.getOutputStream());
            int i = path.lastIndexOf(".");
            //先告诉服务器端传输文件的后缀为什么
            data.writeUTF(path.substring(i,path.length()));
            byte[] by=new byte[1024];
            int len;
            while((len=fileInputStream.read(by))>0){
                //将数据传输到服务器端
                 data.write(by,0,len);
            }
               //通知服务器端传输完成的两种方式,如果不告诉服务器端客户端传输完毕的话,服务器端就会一直的等待客户端数据,造成了连接中断的错误
            data.close();
            socket.shutdownOutput();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

package File;

public class Server {
    public static void main(String[] args) {
        try {
            ServerSocket server=new ServerSocket(8888);
            while(true){
                Socket accept = server.accept();
                Runnable target=()->{
                    //1.创建dataInputStream对象,读取数据
                    try {
                        DataInputStream  data=new DataInputStream(accept.getInputStream());
                        //读取文件后缀,好创建文件类型
                        String s = data.readUTF();
                        //创建一个输出流,将接收到文件写入到我们自己的文件夹中
                        String path="D:\\image\\"+ UUID.randomUUID().toString()+s;
                        System.out.println("服务端保存的位置为"+path);
                        OutputStream out=new FileOutputStream(path);
                        byte[] by=new byte[1024];
                        int len;
                        while((len=data.read(by))>0){
                            out.write(by,0,len);
                        }
                        out.close();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                };
                new Thread(target).start();
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

实现端口转换

实现一个client发送消息,服务器将消息转发到其他client上

package PortTransaf;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

/**
 实现一个端口转发的机制,当一个client发送消息给服务器端之后,服务器将消息转发给其他在线的Client
 1、注册服务器端口
 2、与客户端间里socket连接,将所有的socket连接保存在一个socket集合里面
 3、接收到消息之后,将消息转发给所有的socket
 */
public class Server {
    public static List<Socket> AllSocketList=new ArrayList<>();
    public static void main(String[] args) {
        try {
            ServerSocket ss=new ServerSocket(9999);
            while (true){
                Socket accept = ss.accept();
                //将socket加入到Socket集合里面
                AllSocketList.add(accept);
                Runnable target=()->{
                    //将消息接收下来并且将其转发
                    try {
                        BufferedReader bf=
                                new BufferedReader(new InputStreamReader(accept.getInputStream()));
                        String msg=null;
                        while((msg=bf.readLine())!=null){
                             //将消息转发给其他的socket
                            sendMsg(msg);
                        }
                    } catch (IOException e) {
                       //将下线的socket从socket集合中删除
                        AllSocketList.remove(accept);
                    }
                };
                new Thread(target).start();
            }

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

    /**
     * @param msg 消息
     */
    public static void sendMsg(String msg) throws IOException {
        //遍历socket集合
        for (Socket socket : AllSocketList) {
             //将消息发送出去需要是用输出流(打印流)
            PrintStream pr=new PrintStream(socket.getOutputStream());
            pr.println(msg);
            pr.flush();
        }
    }
}

package PortTransaf;

import java.io.*;
import java.net.Socket;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) throws IOException {
        try {
            //指定好目的地址和接口
            Socket client = new Socket("127.0.0.1", 9999);
            //消息发送的线程
            new Thread(() -> {
                try(
                        OutputStream out = client.getOutputStream();
                        PrintStream print= new PrintStream(out)
                ) {
                    Scanner sc = new Scanner(System.in);
                    while (true) {
                        System.out.print("请输入:");
                        String msg = sc.nextLine();
                        print.println(msg);
                        print.flush();
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            },"send").start();
            //接收消息的线程
            new Thread(()->{
                try(BufferedReader bu=
                            new BufferedReader(new InputStreamReader(client.getInputStream()));){
                    String msg;
                    while((msg=bu.readLine())!=null){
                        System.out.println("接收到的消息为"+msg);
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            },"receive").start();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

NIO模型

NIO从入门到踹门 (qq.com)

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

NIO有三大核心部分:channel(通道),Buffer(缓冲区), Selector(选择器)

Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。一个线程处理多个操作

image-20230916174803522

NIO三大核心

NIO的核心 对应的类或接口 应用 作用
缓冲区 Buffer 文件IO/网络IO 存储数据
通道 Channel 文件IO/网络IO 运输
选择器 Selector 网络IO 控制器

img

Buffer

Buffer - Java17中文文档 - API参考文档 - 全栈行动派 (qzxdp.cn)

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

Channel

image-20230916220932560

常用的Channel有这四种:

FileChannel,读写文件中的数据。

SocketChannel,通过TCP读写网络中的数据.

ServerSockectChannel,监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel

DatagramChannel,通过UDP读写网络中的数据。

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

package NIO;

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

public class FileChannelTest {
    public static void main(String[] args) throws IOException {
        FileOutputStream file=new FileOutputStream("./data01.txt");
        //获取FileChannel管道
        FileChannel channel = file.getChannel();
        //创建缓冲区
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        String str="hello world";
        //将消息存入到缓冲区中
        buffer.put(str.getBytes());
        buffer.flip();
        channel.write(buffer);
        file.close();
        channel.close();
        System.out.println("文件写入成功");
    }
}

文件复制到data2.txt

小数据文件

package NIO;
public class FileChannelTest {
    public static void main(String[] args) throws IOException {
        //获得文件输入流,读取文件信息
        FileInputStream file1=new FileInputStream("./data01.txt");
        //获取FileChannel管道
        FileChannel channel = file1.getChannel();
        //获取文件输出流,将信息写入到文件中
        FileOutputStream file2=new FileOutputStream("./data02.txt");
        //获得FileChannel管道
        FileChannel channel1 = file2.getChannel();
        //创建缓冲区
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        //先将管道的数据读入到缓冲区
        channel.read(buffer);
        //将缓冲区切换到position为0的位置
        buffer.flip();
        //将缓冲区的数据写入到管道中
        channel1.write(buffer);
        //大文件需要循环写入因为可能没有办法一次读完
         while(true){
            //将管道清空
            buffer.clear();
            //先将管道的数据读入到缓冲区
            int flag=channel.read(buffer);
            if(flag==-1){
                break;
            }
            buffer.flip();
            channel1.write(buffer);
        }
        channel.close();
        channel1.close();
        System.out.println("文件写入成功");
    }
}

实现服务端与客户端之间的通信

package NIO.SelectoeTest;

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

public class ServerSocketChannelTest {
    public static void main(String[] args) throws IOException {
        //获取管道
        ServerSocketChannel seChannel = ServerSocketChannel.open();
        //将管道设置为非堵塞的管道
        seChannel.configureBlocking(false);
        //绑定端口
        seChannel.bind(new InetSocketAddress(9999));
        //获取选择器
        Selector selector = Selector.open();
        //将管道注册到选择器里面,并且监听端口的接入事件
        seChannel.register(selector, SelectionKey.OP_ACCEPT);
        //选择器轮询,判断是否有事件发生
        while (selector.select() > 0) {
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                if (selectionKey.isAcceptable()) {
                    //处理接入事件
                    SocketChannel newChannel = seChannel.accept();
                    newChannel.configureBlocking(false);
                    newChannel.register(selector, SelectionKey.OP_READ);
                } else if (selectionKey.isReadable()) {
                    //处理读事件
                    //直接获取发生读事件的管道
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    int len = 0;
                    //读取到的内容,写入到缓冲区里面。
                    while ((len = channel.read(byteBuffer)) > 0) {
                        byteBuffer.flip();
                        System.out.println(new String(byteBuffer.array(), 0, len));
                        //将管道清空,准备下一次读取。
                        byteBuffer.clear();
                    }
                }
                iterator.remove();
            }
        }
    }
}

package NIO.SelectoeTest;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.sql.SQLOutput;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) throws IOException {
        //获取SocketChannel,并且连接到服务器
        SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",9999));
        socketChannel.configureBlocking(false);
        Scanner sc=new Scanner(System.in);
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        while(true){
            System.out.println("请说:");
            String str=sc.nextLine();
            buffer.put(str.getBytes());
            buffer.flip();
            socketChannel.write(buffer);
            buffer.clear();
        }
    }
}
基于NIO的多人聊天室
package NIO.Chat;

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;

public class Client {
    public Selector selector;
    public SocketChannel schannel;
    public Client(){
        //之前我们的客户端只需要处理写事件(所以不需要选择器),现在需要选择器,判断事件是什么事件好进行相应的处理
        try{
            schannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",Server.PORT));
            selector=Selector.open();
            //设置为非阻塞
            schannel.configureBlocking(false);
            //监听读事件
            schannel.register(selector, SelectionKey.OP_READ);
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
         Client client=new Client();
         //需要开启两个线程一个线程处理读事件,一个线程处理写事件。
        new Thread(()->{
            //需要使用选择器监听着事件
            try {
                while (client.selector.select() > 0) {
                    Iterator<SelectionKey> iterator
                            = client.selector.selectedKeys().iterator();
                    while(iterator.hasNext()){
                        SelectionKey key = iterator.next();
                        if(key.isReadable()){
                            ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
                            //将管道里面的消息写入到缓冲区里面
                            client.schannel.read(byteBuffer);
                            byteBuffer.flip();
                            System.out.println(client.schannel.getRemoteAddress()+"读取到的消息"+new String(byteBuffer.array(),0,byteBuffer.remaining()));
                            byteBuffer.clear();
                        }
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        },"ReadThing").start();
        //main线程用来处理写线程
        Scanner sc=new Scanner(System.in);
        while(sc.hasNext()){
            String s=sc.next();
            ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
            byteBuffer.put(s.getBytes());
            byteBuffer.flip();
            client.schannel.write(byteBuffer);
            byteBuffer.clear();
        }
    }
}

package NIO.Chat;

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;

public class Client {
    public Selector selector;
    public SocketChannel schannel;
    public Client(){
        //之前我们的客户端只需要处理写事件(所以不需要选择器),现在需要选择器,判断事件是什么事件好进行相应的处理
        try{
            schannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",Server.PORT));
            selector=Selector.open();
            //设置为非阻塞
            schannel.configureBlocking(false);
            //监听读事件
            schannel.register(selector, SelectionKey.OP_READ);
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
         Client client=new Client();
         //需要开启两个线程一个线程处理读事件,一个线程处理写事件。
        new Thread(()->{
            //需要使用选择器监听着事件
            try {
                while (client.selector.select() > 0) {
                    Iterator<SelectionKey> iterator
                            = client.selector.selectedKeys().iterator();
                    while(iterator.hasNext()){
                        SelectionKey key = iterator.next();
                        if(key.isReadable()){
                            ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
                            //将管道里面的消息写入到缓冲区里面
                            client.schannel.read(byteBuffer);
                            byteBuffer.flip();
                            System.out.println(client.schannel.getRemoteAddress()+"读取到的消息"+new String(byteBuffer.array(),0,byteBuffer.remaining()));
                            byteBuffer.clear();
                        }
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        },"ReadThing").start();
        //main线程用来处理写线程
        Scanner sc=new Scanner(System.in);
        while(sc.hasNext()){
            String s=sc.next();
            ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
            byteBuffer.put(s.getBytes());
            byteBuffer.flip();
            client.schannel.write(byteBuffer);
            byteBuffer.clear();
        }
    }
}

posted @ 2023-12-17 23:55  zL66  阅读(1)  评论(0编辑  收藏  举报