一.Java支持的三种IO模型

IO 模型

1.I/O 模型基本说明

  • I/O 模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能
  • Java共支持3种网络编程模型/IO模式:BIO、NIO、AIO

2.I/O模型使用场景分析

  • BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。
  • NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4开始支持。
  • AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

3.五种IO模型

Unix中定义了五种I/O模型:

  • 阻塞I/O
  • 非阻塞I/O
  • I/O复用(select、poll、linux 2.6种改进的epoll)
  • 信号驱动IO(SIGIO)
  • 异步I/O(POSIX的aio_系列函数)

举个例子:

如果你想吃一份宫保鸡丁盖饭:

 - 同步阻塞:你到饭馆点餐,然后在那等着,还要一边喊:好了没啊!
 - 同步非阻塞:在饭馆点完餐,就去遛狗了。不过溜一会儿,就回饭馆喊一声:好了没啊!
 - 异步阻塞:遛狗的时候,接到饭馆电话,说饭做好了,让您亲自去拿。
 - 异步非阻塞:饭馆打电话说,我们知道您的位置,一会给你送过来,安心遛狗就可以了。

明确问题

1.首先得明白一件最重要的事情,在网络通信中,数据一定是先到达双发的内核缓冲区中,比如大部分网络通信需要靠TCP建立连接,那么通信双方的数据一定是先到达TCP缓冲区,应用进程再去从TCP缓冲区把数据拿到其自己的应用进程缓冲区,当然这其中就会牵扯用户态与内核态的切换,也同样影响着数据传输的效率,参考NIO零拷贝
2.而阻塞与非阻塞模型的区别点就在于当通信双方在对内核缓冲区进行读写操作时,比如发送方要面临内核的空间写满了没空间了,接收方要面临内核中的数据还没有到达,这个时候你是让我在这等着,发送方就一直等有空就再发,接收方就硬等着所有数据到达再收?

3.1同步阻塞式IO

阻塞IO就是当应用发起读取数据申请时,在内核数据没有准备好之前,应用B会一直处于等待数据状态,直到内核把数据准备好了交给应用B才结束。
在Linux中,对于一次读取IO的操作,数据并不会直接拷贝到程序的程序缓冲区。通常包括两个不同阶段:
1.应用进程向内核发起recfrom读取数据,等待数据准备好,到达内核缓冲区(此时应用进程处于阻塞状态);
2.从内核向进程复制数据。 对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所有等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用程序缓冲区。
在这里插入图片描述

3.2同步非阻塞式IO

非阻塞IO就是当应用B发起读取数据申请时,如果内核数据没有准备好会即刻告诉应用B,不会让B在这里等待。
与阻塞式I/O不同的是,非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error(EAGAIN 或 EWOULDBLOCK)。进程在返回之后,可以处理其他的业务逻辑,过会儿再发起recvform系统调用。采用轮询的方式检查内核数据,直到数据准备好。再拷贝数据到进程,进行数据处理。 在linux下,可以通过设置socket套接字选项使其变为非阻塞。
在这里插入图片描述

明确问题

阻塞与非阻塞IO我们只是在讨论单个应用线程去接收发送数据的灵活性,实际上就是非阻塞的效率更高更灵活,倘若考虑在并发量非常大的情况下,阻塞与非阻塞其实都无所谓了,像这样:
在这里插入图片描述
实际中B是一台服务器,服务器会有大量的请求响应,每个请求都是一个来自用户的线程,它们不断的向内核发送recvfrom请求读取数据,B也要产生大量的线程去响应这些请求,倘若B创建大量线程只是去读取数据,操作系统中的线程是宝贵的资源,那么能做其他事情的线程就少了,因而B的性能一定会变差

3.3IO复用模型

倘若可以由一个线程监控多个网络请求(我们后面将称为fd文件描述符,linux系统把所有网络请求以一个fd来标识),这样就可以只需要一个或几个线程就可以完成数据状态询问的操作,当有数据准备就绪之后再分配对应的线程去读取数据,这么做就可以节省出大量的线程资源出来,这个就是IO复用模型的思路。
在这里插入图片描述
IO复用模型的思路就是系统提供了一种函数可以同时监控多个fd的操作,这个函数就是我们常说到的select、poll、epoll函数,有了这个函数后,应用线程通过调用select函数就可以同时监控多个fd,select函数监控的fd中只要有任何一个数据状态准备就绪了,select函数就会返回可读状态,这时询问线程再去通知处理数据的线程,对应线程此时再发起recvfrom请求去读取数据。
以select为例,当用户进程调用了select,那么整个进程会被阻塞,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从内核拷贝到用户进程
在这里插入图片描述

明确问题

IO复用模型会让你眼前一亮,select采用轮询的方式来监控多个fd的,通过不断的轮询fd的可读状态来知道是否就可读的数据,但其实大部分情况下的轮询都是无效的,只是纯粹的无脑轮询,所以能不能不要我总是去问你是否数据准备就绪,能不能我发出请求后等你数据准备好了就通知我,所以就衍生了信号驱动IO模型。

3.4信号驱动IO

信号驱动IO不是用循环请求询问的方式去监控数据就绪状态,而是在调用sigaction时候建立一个SIGIO的信号联系,当内核数据准备好之后再通过SIGIO信号通知线程数据准备好后的可读状态,当线程收到可读状态的信号后,此时再向内核发起recvfrom读取数据的请求,因为信号驱动IO的模型下应用线程在发出信号监控后即可返回,不会阻塞,所以这样的方式下,一个应用线程也可以同时监控多个fd。
在这里插入图片描述
在这里插入图片描述
信号驱动IO意在通过这种建立信号关联的方式,实现了发出请求后只需要等待数据就绪的通知即可,这样就可以避免大量无效的数据状态轮询操作。

明确问题

信号驱动IO又让你眼前一亮,你会觉得在IO复用模型优化后这样的方案是很不错的,阻塞与非阻塞有一个完美的单个线程数据读取方案,IO复用与信号驱动有一个完美的监听多线程的方案,这让我想起了自己,无论什么事情都那么努力只怕自己会做不好,但是反过来想,我只是想读个数据为什么我自己非要先发起一个select询问数据状态的请求,然后再发起真正的读取数据请求,什么事情都是我自己做,你这个SB内核FW做了什啊?所以我什么都不管了,我只管发起一个请求,剩下的内核你帮我做

3.5异步IO

应用只需要向内核发送一个read 请求,告诉内核它要读取数据后即刻返回;内核收到请求后会建立一个信号联系,当数据准备就绪,内核会主动把数据从内核复制到用户空间,等所有操作都完成之后,内核会发起一个通知告诉应用,我们称这种一劳永逸的模式为异步IO模型。
在这里插入图片描述
用户进程进行aio_read系统调用之后,就可以去处理其他的逻辑了,无论内核数据是否准备好,都会直接返回给用户进程,不会对进程造成阻塞。等到数据准备好了,内核直接复制数据到进程空间,然后从内核向进程发送通知,此时数据已经在用户空间了,可以对数据进行处理了。
在 Linux 中,通知的方式是 “信号”,分为三种情况:
a.如果这个进程正在用户态处理其他逻辑,那就强行打断,调用事先注册的信号处理函数,这个函数可以决定何时以及如何处理这个异步任务。由于信号处理函数是突然闯进来的,因此跟中断处理程序一样,有很多事情是不能做的,因此保险起见,一般是把事件 “登记” 一下放进队列,然后返回该进程原来在做的事。
b.如果这个进程正在内核态处理,例如以同步阻塞方式读写磁盘,那就把这个通知挂起来了,等到内核态的事情忙完了,快要回到用户态的时候,再触发信号通知。
c.如果这个进程现在被挂起了,例如陷入睡眠,那就把这个进程唤醒,等待CPU调度,触发信号通知。
在这里插入图片描述

再谈IO中的同步与异步

1.阻塞与非阻塞
所谓阻塞就是发起读取数据请求的时,当数据还没准备就绪的时候,这时请求是即刻返回,还是在这里等待数据的就绪,如果需要等待的话就是阻塞,反之如果即刻返回就是非阻塞。
1.同步与异步
在IO模型里如果请求方从发起请求到数据最后完成的这一段过程中都需要自己参与,那么这种我们称为同步请求;反之,如果应用发送完指令后就不再参与过程了,只需要等待最终完成结果的通知,那么这就属于异步。

我们再看同步阻塞、同步非阻塞,他们不同的只是发起读取请求的时候一个请求阻塞,一个请求不阻塞,但是相同的是,他们都需要应用自己监控整个数据完成的过程。而为什么之后异步非阻塞 而没有异步阻塞呢,因为异步模型下请求指定发送完后就即刻返回了,没有任何后续流程了,所以它注定不会阻塞,所以也就只会有异步非阻塞模型了。

三种IO详解

1.BIO

1.1 基本介绍

1.Java BIO(blocking I/O) 是传统的java io 编程,其相关的类和接口在 java.io 其为同步阻塞,服务器每个连接都是单独的线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。
2.BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高, 并发局限于应用中,JDK1.4以前的唯一选择,程序简单易理解
在这里插入图片描述

1.2 工作机制

  • BIO编程简单流程

1.服务器端启动一个 ServerSocket
2.客户端启动 Socket 对服务器进行通信,默认情况下服务器端需要对每个客3.户建立一个线程与之通讯
4.客户端发出请求后, 先咨询服务器 是否有线程响应,如果没有则会等待,或者被拒绝
5.如果有响应,客户端线程会等待请 求结束后,在继续执行

1.3代码实例

  • 要求:
1.使用BIO模型编写一个服务器端,监听6666端口,当有客户端连接时,就启动一个线程与之通讯。
2.要求使用线程池机制改善,可以连接多个客户端.
3.服务器端可以接收客户端发送的数据(telnet 方式即可)
  • 编写代码
public class BIOServer {
   
    public static void main(String[] args) throws IOException {
   
    // 1. 创建一个线程池
        ExecutorService threadPool = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                                                60L, TimeUnit.SECONDS,
                                                                new SynchronousQueue<Runnable>());
        // 监听“6666” 端口,接收客户连接请求,并生成与客户端连接的Socket
        ServerSocket serverSocket = new ServerSocket(6666);

        System.out.println("服务器启动了");

        while (true){
   
            // 监听,等待客户端连接
            final Socket socket = serverSocket.accept();
            System.out.println("连接到一个客户端");

    // 2. 如果有客户端连接,就创建一个线程,与之通信
            threadPool.execute(()->{
   
                handler(socket);
            });
        }
    }

    /**
     * 和客户端通信的方法
     * 循环的读取客户端的数据,然后输出
     */
    public static void handler(Socket socket){
   
        // 打印线程信息
        System.out.println("线程信息:{id:"+Thread.currentThread().getId()+", " +
                         "name: "+Thread.currentThread().getName());
        // 用于接收数据
        byte[] bytes = new byte[1024];
        // 通过 socket 获取输入流
        try {
   
            InputStream inputStream = socket.getInputStream();

            // 循环的读取客户端发送的数据
            while (true){
   
                System.out.println("进行通信线程信息:{id:"+Thread.currentThread().getId()+", " +
                        "name: "+Thread.currentThread().getName());
                int read = inputStream.read(bytes);
                if (read != -1){
   
                    // 说明还可以读
                    // 输出客户端发送的数据
                    System.out.println(new String(bytes,0, read));
                }else {
   
                    // 读取完毕
                    break;
                }
            }
        } catch (IOException e) {
   
            e.printStackTrace();
        }finally {
   
            System.out.println("关闭连接");
            try {
   
                socket.close();
            } catch (IOException e) {
   
                e.printStackTrace();
            }
        }
    }
}
  • 连接服务,测试

打开 CMD,连接 6666 端口
在这里插入图片描述
输入 Ctrl + ],传递数据
在这里插入图片描述
查看控制台
在这里插入图片描述

1.4 问题分析

从上面的结果可以发现,处理请求的线程 和 服务端客户端之间连接的线程是同一个
通过 Debug 方式运行可以发现,当连接上服务端之后,不进行任何操作,改线程只会阻塞在 int read = inputStream.read(bytes);

1.每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write 。
2.当并发数较大时,需要创建大量线程来处理连接,系统资源占 用较大。
3.连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞 在 Read 操作上,造成线程资源浪费

2.AIO

2.1基本介绍

jdk7中新增了一些与文件(网络)I/O相关的一些api。这些API被称为NIO.2,即AIO(Asynchronous I/O)。AIO最大的一个特性就是异步能力,这种能力对socket与文件I/O都起作用。AIO其实是一种在读写操作结束之前允许进行其他操作的I/O处理。AIO是对JDK1.4中提出的同步非阻塞I/O(NIO)的进一步增强。
jdk7主要增加了三个新的异步通道:

  • AsynchronousFileChannel: 用于文件异步读写;
  • AsynchronousSocketChannel: 客户端异步socket;
  • AsynchronousServerSocketChannel: 服务器异步socket。

因为AIO的实施需充分调用OS参与,IO需要操作系统支持、并发也同样需要操作系统的支持,所以性能方面不同操作系统差异会比较明显。

客户端:

public class ClientAio {
   
    private static String DEFAULT_HOST = "127.0.0.1";
    private static int DEFAULT_PORT = 8787;
    private static AsyncClientHandler clientHandle;

    public static void start() {
   
        start(DEFAULT_HOST, DEFAULT_PORT);
    }

    public static synchronized void start(String ip, int port) {
   
        if (clientHandle != null)
            return;
        clientHandle = new AsyncClientHandler(ip, port);
        new Thread(clientHandle, "Client").start();
    }

    //向服务器发送消息
    public static boolean sendMsg(String msg) {
   
        clientHandle.sendMsg(msg);
        return true;
    }

    public static void main(String[] args) {
   
        ClientAio.start();
        System.out.println("请输入请求消息:");
        Scanner scanner = new Scanner(System.in);
        while (ClientAio.sendMsg(scanner.nextLine())) ;
    }
}
public class AsyncClientHandler implements CompletionHandler<Void, AsyncClientHandler> ,Runnable{
   
    ExecutorService executor = Executors.newFixedThreadPool(20);
    //以指定线程池来创建一个AsynchronousChannelGroup
//AsynchronousChannelGroup是异步channel的分组管理器
    AsynchronousChannelGroup channelGroup;
    //以指定线程池来创建一个AsynchronousSocketChannel
    AsynchronousSocketChannel clientChannel;
    private String host;
    private int port;
    private CountDownLatch latch;


    public AsyncClientHandler(String host, int port) {
   
        this.host = host;
        this.port = port;

        //创建异步的客户端通道
        try {
   
            channelGroup = AsynchronousChannelGroup
                    .withThreadPool(executor);
            clientChannel
                    = AsynchronousSocketChannel.open(channelGroup);
        } catch (IOException e) {
   
            e.printStackTrace();
        }

    }

    public void run() {
   
        //创建CountDownLatch等待
        latch = new CountDownLatch(1);
        //发起异步连接操作,回调参数就是这个类本身,如果连接成功会回调completed方法
        System.out.println("开始建立连接");
        clientChannel.connect(new InetSocketAddress(host, port), this, this);
        try {
   
            latch.await();
        } catch (InterruptedException e1) {
   
            e1.printStackTrace();
        }
        try {
   
            clientChannel.close();
        } catch (IOException e) {
   
            e.printStackTrace();
        }
    }

    //连接服务器成功
    //意味着TCP三次握手完成
    @Override
    public void completed(Void result, AsyncClientHandler attachment) {
   
        System.out.println("客户端成功连接到服务器...");
    }

    //连接服务器失败  会调用这个方法
    @Override
    public void failed(Throwable exc, AsyncClientHandler attachment) {
   
        System.err.println("连接服务器失败...");
        exc.printStackTrace();
        try {
   
            clientChannel.close();
            latch.countDown(
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,根据您的要求,我来为您创建一个数据模型类CityList.java,并且这个类是根据CSV文件来创建的。 首先,我们需要导入以下的包: ```java import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.List; ``` 然后,我们定义CityList类: ```java public class CityList { private List<City> cities; public CityList(String csvFile) { cities = new ArrayList<>(); BufferedReader br = null; String line = ""; String cvsSplitBy = ","; try { br = new BufferedReader(new FileReader(csvFile)); while ((line = br.readLine()) != null) { String[] cityData = line.split(cvsSplitBy); City city = new City(cityData[0], cityData[1], cityData[2]); cities.add(city); } } catch (IOException e) { e.printStackTrace(); } finally { if (br != null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } } public List<City> getCities() { return cities; } } ``` 在这个类中,我们定义了一个List类型的变量cities,它包含了所有的城市信息。我们还定义了一个构造函数,这个构造函数需要传入一个csv文件的路径,然后根据csv文件中的数据,创建City对象,并将这些对象添加到cities列表中。getCities()方法用于获取cities列表。 最后,我们需要定义City类,City类用于表示城市信息: ```java public class City { private String name; private String countryCode; private String district; public City(String name, String countryCode, String district) { this.name = name; this.countryCode = countryCode; this.district = district; } public String getName() { return name; } public String getCountryCode() { return countryCode; } public String getDistrict() { return district; } } ``` City类包含了三个属性:name、countryCode和district,它们分别表示城市的名称、国家代码和所属区域。我们还定义了一个构造函数,用于创建City对象,并定义了三个getter方法,用于获取City对象的属性值。 这就是根据CSV文件创建数据模型类CityList.java的实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值