一文搞懂什么是BIO

BIO

BIO英文全名是 blocking IO,也叫做 阻塞IO,是最容易理解、最容易实现的IO工作方式。

1.1、什么是阻塞IO(BIO)

当我们在谈论阻塞IO(Blocking IO)时,我们指的是一种输入输出方式,其中线程正在进行IO操作时会被阻塞(即暂停运行),直到IO 操作完成。这种阻塞是同步的,也就是说线程会等待IO操作完成后再继续执行后续的任务。

在阻塞IO中,当一个线程调用IO操作(如读取或写入数据)时,如果没有数据可用或无法立即完成IO操作,线程会被挂起,直到满足操作条件。这种挂起意味着线程无法执行其他任务,因为它一直等待IO操作完成。

举个例子说明,假设一个线程负责从网络套接字读取数据,Socket就是网络套接字。当线程调用读取数据的方法时,如果没有数据可用,线程将被阻塞,直到有数据可读为止。在此期间,线程无法执行其他任务,他会一直等待直到数据到达或IO操作超时。

阻塞IO的特点是简单易懂,但也存在一些问题。其他一个问题是当有多个IO操作需要处理时,每个操作都会阻塞对应的线程,导致线程的浪费。在高并发或大规模的应用程序中,这可能会导致性能的下降,因为线程的创建和上下文切换会带来额外的开销。

为了解决阻塞IO的性能问题,Java引入了非阻塞IO(Non-blocking IO)模型,例如NIO(New IO)和NIO.2。这些模型使用了事件驱动的方式,通过单个线程处理多个IO操作。当一个IO操作无法立即完成时,线程不会被阻塞,而是继续执行其他任务,等待IO操作完成后再处理。这种方式能更有效地利用系统资源,并提高并发处理能力。

综上所述,阻塞IO是一种简单易懂的IO操作方式,但在高并发或大规模应用程序中可能存在性能问题。了解阻塞IO的概念可以帮助我们理解其他IO模型的工作原理和优势。

1.2、BIO工作原理

当一个客户端请求到达服务器时,服务器会为该请求创建一个新的线程。这个线程将负责处理该请求的所有IO操作,包括读取请求数据、处理请求、发送响应等。这种一对一的线程模型在简单的应用场景下可能是可行的,但当并发请求增加时,线程的数量也会相应增加。

大量线程的创建和管理开销较大,会消耗大量的系统资源,包括内存和CPU。每个线程都需要占用一定的内存空间,并且线程之间的切换也需要消耗CPU资源。如果并发请求非常高,线程的数量可能会过多,导致系统资源不足,甚至引发性能下降、系统崩溃等问题。

BIO的优点是通俗易懂,适用于一些处理少量应发请求的简单服务器,比如:单线程服务器、简单的客户端-服务器通信等。对于高并发、大规模的网络应该程序,BIO模型可能无法满足要求,这会使得线程创建和管理开销太大。

1.3、BIO服务器

当谈到编写一个BIO(Blocking I/O)程序时,我们可以创建一个简单的服务器,它能够接受客户端连接请求并处理这些请求。下面是一个使用java socket编写的BIO服务器简单示例:

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

public class BioServer {

    public static void main(String[] args) {
        int port = 8080; // 服务器监听的端口号

        try {
            ServerSocket serverSocket = new ServerSocket(port);
            System.out.println("服务器启动,监听端口:" + port);

            while (true) {
                // 等待客户端连接
                Socket socket = serverSocket.accept();
                System.out.println("客户端连接成功,地址:" + socket.getInetAddress() + ":" + socket.getPort());

                // 创建线程处理客户端请求
                new Thread(() -> {
                    try {
                        // 获取输入流和输出流
                        BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                        PrintWriter writer = new PrintWriter(socket.getOutputStream());

                        // 读取客户端发送的数据
                        String request = reader.readLine();
                        System.out.println("接收到客户端数据:" + request);

                        // 处理请求并返回响应
                        String response = "Hello, client!";
                        writer.println(response);
                        writer.flush();
                        System.out.println("发送响应给客户端:" + response);

                        // 关闭连接
                        socket.close();
                        System.out.println("客户端连接关闭");
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这个示例程序创建了一个ServerSocket来监听指定端口(这里使用8080)。在主循环中,通过调用accept()方法等待客户端的连接请求,没有连接的时候,程序会一直阻塞在这里,直到收到客户端连接请求。一旦客户端连接成功,程序会创建一个新的线程来处理该客户端的请求。

在处理线程中,我们获取与客户端连接的输入流和输出流,使用BufferedReader来读取客户端发送的数据,并使用PrintWriter来进行客户端发送响应。这里处理逻辑非常简单,及仅仅返回一个固定的字符作为响应。

当请求处理结束后,关闭与客户端的连接,并继续等待下一个客户端的连接。

需要注意,这个示例程序是单线程的。当有新的客户端连接时,都会创建一个新的线程来处理请求。这种方式仅适用于简单的应用场景,在高并发下,会创建大量的线程,导致性能下降。

1.4、BIO客户端

如果想发送消息到上面的BIO服务器,我们可以使用一个简单的Socket客户端来连接服务器并发送请求。以下是一个示例代码:

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

public class BioClient {

    public static void main(String[] args) {
        String serverAddress = "localhost"; // 服务器地址
        int serverPort = 8080; // 服务器端口号

        try {
            // 连接服务器
            Socket socket = new Socket(serverAddress, serverPort);
            System.out.println("连接服务器成功");

            // 获取输入流和输出流
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter writer = new PrintWriter(socket.getOutputStream());

            // 发送请求
            String request = "Hello, server!";
            writer.println(request);
            writer.flush();
            System.out.println("发送请求给服务器:" + request);

            // 接收响应
            String response = reader.readLine();
            System.out.println("接收到服务器响应:" + response);

            // 关闭连接
            socket.close();
            System.out.println("连接关闭");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

在这个示例程序中,BIO客户端与服务器的通信过程如下:

  1. 创建一个Socket对象,指定服务器地址和端口号。
  2. 通过调用Socket对象的getInputStream()方法和getOutputStream()方法,分别获取与服务器连接的输入流和输出流。输入流用于接收服务器的响应,输出流用于向服务器发送请求。
  3. 构造一个请求消息,例如Hello, server!。
  4. 使用输出流的println()方法将请求消息发送给服务器,并调用flush()方法确保消息被立即发送到服务器。
  5. 使用输入流的readLine()方法来读取服务器发送的响应消息。
  6. 打印接收到的服务器响应消息。
  7. 关闭与服务器的连接,调用Socket对象的close()方法。

你可以在该示例程序中修改请求消息和服务器地址、端口号,以适应你的实际情况。

运行实例

先启动服务器,进行端口监听,再启动客户端,连接到指定服务器地址的端口

#BioServer输出台显示

服务器启动,监听端口:8080
客户端连接成功,地址:/127.0.0.1:60038
接收到客户端数据:Hello, server!
发送响应给客户端:Hello, client!
客户端连接关闭

#BioClient输出台显示

连接服务器成功
发送请求给服务器:Hello, server!
接收到服务器响应:Hello, client!
连接关闭

结尾

尽管阻塞IO模型简单易懂,但在高并发或大规模的网络应用程序中,使用该模型可能会遇到线程创建和管理开销过大的问题。因此,在需要高并发处理的场景下,阻塞IO模型并不是最理想的选择。

为了解决这个问题,可以考虑使用其他IO模型,如非阻塞IO(NIO)或基于NIO的框架(如Netty)。

非阻塞IO模型通过使用非阻塞的IO操作和事件轮询机制,允许程序在等待IO操作完成的同时继续执行其他任务,从而提高了系统的并发能力。而基于NIO的框架则提供了更高级别的抽象和更灵活的IO操作方式,例如使用选择器(Selector)来管理多个IO通道,实现单线程处理多个IO连接。

这些模型可以使用较少的线程处理多个请求,并且能更好地利用系统资源,提高并发处理能力。

非阻塞IO和基于NIO的框架具有以下优势:

  1. 更高的并发性能:不像阻塞IO模型那样频繁地创建和管理线程,,非阻塞IO和基于NIO的框架能够通过一个线程处理多个IO连接,减少了线程创建和管理的开销,从而提高了系统的并发性能。

  2. 更灵活的IO操作方式:非阻塞IO和基于NIO的框架提供了更灵活的IO操作方式,例如事件驱动的编程模型和选择器机制,使得程序能够更高效地管理和处理多个IO连接。

  3. 资源利用率更高:更加节约资源,由于非阻塞IO和基于NIO的框架使用了较少的线程,可以更有效地利用系统资源,避免了线程创建和上下文切换的开销。

在选择适合的IO模型时,需要根据具体的应用场景和性能需求来权衡各种模型的优劣。阻塞IO模型适用于简单的、低并发的应用场景,而非阻塞IO和基于NIO的框架则更适合需要处理大量并发连接的高性能应用。# BIO

BIO英文全名是 blocking IO,也叫做 阻塞IO,是最容易理解、最容易实现的IO工作方式。

1.1、什么是阻塞IO(BIO)

当我们在谈论阻塞IO(Blocking IO)时,我们指的是一种输入输出方式,其中线程正在进行IO操作时会被阻塞(即暂停运行),直到IO 操作完成。这种阻塞是同步的,也就是说线程会等待IO操作完成后再继续执行后续的任务。

在阻塞IO中,当一个线程调用IO操作(如读取或写入数据)时,如果没有数据可用或无法立即完成IO操作,线程会被挂起,直到满足操作条件。这种挂起意味着线程无法执行其他任务,因为它一直等待IO操作完成。

举个例子说明,假设一个线程负责从网络套接字读取数据,Socket就是网络套接字。当线程调用读取数据的方法时,如果没有数据可用,线程将被阻塞,直到有数据可读为止。在此期间,线程无法执行其他任务,他会一直等待直到数据到达或IO操作超时。

阻塞IO的特点是简单易懂,但也存在一些问题。其他一个问题是当有多个IO操作需要处理时,每个操作都会阻塞对应的线程,导致线程的浪费。在高并发或大规模的应用程序中,这可能会导致性能的下降,因为线程的创建和上下文切换会带来额外的开销。

为了解决阻塞IO的性能问题,Java引入了非阻塞IO(Non-blocking IO)模型,例如NIO(New IO)和NIO.2。这些模型使用了事件驱动的方式,通过单个线程处理多个IO操作。当一个IO操作无法立即完成时,线程不会被阻塞,而是继续执行其他任务,等待IO操作完成后再处理。这种方式能更有效地利用系统资源,并提高并发处理能力。

综上所述,阻塞IO是一种简单易懂的IO操作方式,但在高并发或大规模应用程序中可能存在性能问题。了解阻塞IO的概念可以帮助我们理解其他IO模型的工作原理和优势。

1.2、BIO工作原理

当一个客户端请求到达服务器时,服务器会为该请求创建一个新的线程。这个线程将负责处理该请求的所有IO操作,包括读取请求数据、处理请求、发送响应等。这种一对一的线程模型在简单的应用场景下可能是可行的,但当并发请求增加时,线程的数量也会相应增加。

大量线程的创建和管理开销较大,会消耗大量的系统资源,包括内存和CPU。每个线程都需要占用一定的内存空间,并且线程之间的切换也需要消耗CPU资源。如果并发请求非常高,线程的数量可能会过多,导致系统资源不足,甚至引发性能下降、系统崩溃等问题。

BIO的优点是通俗易懂,适用于一些处理少量应发请求的简单服务器,比如:单线程服务器、简单的客户端-服务器通信等。对于高并发、大规模的网络应该程序,BIO模型可能无法满足要求,这会使得线程创建和管理开销太大。

1.3、BIO服务器

当谈到编写一个BIO(Blocking I/O)程序时,我们可以创建一个简单的服务器,它能够接受客户端连接请求并处理这些请求。下面是一个使用java socket编写的BIO服务器简单示例:

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

public class BioServer {

    public static void main(String[] args) {
        int port = 8080; // 服务器监听的端口号

        try {
            ServerSocket serverSocket = new ServerSocket(port);
            System.out.println("服务器启动,监听端口:" + port);

            while (true) {
                // 等待客户端连接
                Socket socket = serverSocket.accept();
                System.out.println("客户端连接成功,地址:" + socket.getInetAddress() + ":" + socket.getPort());

                // 创建线程处理客户端请求
                new Thread(() -> {
                    try {
                        // 获取输入流和输出流
                        BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                        PrintWriter writer = new PrintWriter(socket.getOutputStream());

                        // 读取客户端发送的数据
                        String request = reader.readLine();
                        System.out.println("接收到客户端数据:" + request);

                        // 处理请求并返回响应
                        String response = "Hello, client!";
                        writer.println(response);
                        writer.flush();
                        System.out.println("发送响应给客户端:" + response);

                        // 关闭连接
                        socket.close();
                        System.out.println("客户端连接关闭");
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这个示例程序创建了一个ServerSocket来监听指定端口(这里使用8080)。在主循环中,通过调用accept()方法等待客户端的连接请求,没有连接的时候,程序会一直阻塞在这里,直到收到客户端连接请求。一旦客户端连接成功,程序会创建一个新的线程来处理该客户端的请求。

在处理线程中,我们获取与客户端连接的输入流和输出流,使用BufferedReader来读取客户端发送的数据,并使用PrintWriter来进行客户端发送响应。这里处理逻辑非常简单,及仅仅返回一个固定的字符作为响应。

当请求处理结束后,关闭与客户端的连接,并继续等待下一个客户端的连接。

需要注意,这个示例程序是单线程的。当有新的客户端连接时,都会创建一个新的线程来处理请求。这种方式仅适用于简单的应用场景,在高并发下,会创建大量的线程,导致性能下降。

1.4、BIO客户端

如果想发送消息到上面的BIO服务器,我们可以使用一个简单的Socket客户端来连接服务器并发送请求。以下是一个示例代码:

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

public class BioClient {

    public static void main(String[] args) {
        String serverAddress = "localhost"; // 服务器地址
        int serverPort = 8080; // 服务器端口号

        try {
            // 连接服务器
            Socket socket = new Socket(serverAddress, serverPort);
            System.out.println("连接服务器成功");

            // 获取输入流和输出流
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter writer = new PrintWriter(socket.getOutputStream());

            // 发送请求
            String request = "Hello, server!";
            writer.println(request);
            writer.flush();
            System.out.println("发送请求给服务器:" + request);

            // 接收响应
            String response = reader.readLine();
            System.out.println("接收到服务器响应:" + response);

            // 关闭连接
            socket.close();
            System.out.println("连接关闭");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

在这个示例程序中,BIO客户端与服务器的通信过程如下:

  1. 创建一个Socket对象,指定服务器地址和端口号。
  2. 通过调用Socket对象的getInputStream()方法和getOutputStream()方法,分别获取与服务器连接的输入流和输出流。输入流用于接收服务器的响应,输出流用于向服务器发送请求。
  3. 构造一个请求消息,例如Hello, server!。
  4. 使用输出流的println()方法将请求消息发送给服务器,并调用flush()方法确保消息被立即发送到服务器。
  5. 使用输入流的readLine()方法来读取服务器发送的响应消息。
  6. 打印接收到的服务器响应消息。
  7. 关闭与服务器的连接,调用Socket对象的close()方法。

你可以在该示例程序中修改请求消息和服务器地址、端口号,以适应你的实际情况。

运行实例

先启动服务器,进行端口监听,再启动客户端,连接到指定服务器地址的端口

#BioServer输出台显示

服务器启动,监听端口:8080
客户端连接成功,地址:/127.0.0.1:60038
接收到客户端数据:Hello, server!
发送响应给客户端:Hello, client!
客户端连接关闭

#BioClient输出台显示

连接服务器成功
发送请求给服务器:Hello, server!
接收到服务器响应:Hello, client!
连接关闭

结尾

尽管阻塞IO模型简单易懂,但在高并发或大规模的网络应用程序中,使用该模型可能会遇到线程创建和管理开销过大的问题。因此,在需要高并发处理的场景下,阻塞IO模型并不是最理想的选择。

为了解决这个问题,可以考虑使用其他IO模型,如非阻塞IO(NIO)或基于NIO的框架(如Netty)。

非阻塞IO模型通过使用非阻塞的IO操作和事件轮询机制,允许程序在等待IO操作完成的同时继续执行其他任务,从而提高了系统的并发能力。而基于NIO的框架则提供了更高级别的抽象和更灵活的IO操作方式,例如使用选择器(Selector)来管理多个IO通道,实现单线程处理多个IO连接。

这些模型可以使用较少的线程处理多个请求,并且能更好地利用系统资源,提高并发处理能力。

非阻塞IO和基于NIO的框架具有以下优势:

  1. 更高的并发性能:不像阻塞IO模型那样频繁地创建和管理线程,,非阻塞IO和基于NIO的框架能够通过一个线程处理多个IO连接,减少了线程创建和管理的开销,从而提高了系统的并发性能。

  2. 更灵活的IO操作方式:非阻塞IO和基于NIO的框架提供了更灵活的IO操作方式,例如事件驱动的编程模型和选择器机制,使得程序能够更高效地管理和处理多个IO连接。

  3. 资源利用率更高:更加节约资源,由于非阻塞IO和基于NIO的框架使用了较少的线程,可以更有效地利用系统资源,避免了线程创建和上下文切换的开销。

在选择适合的IO模型时,需要根据具体的应用场景和性能需求来权衡各种模型的优劣。阻塞IO模型适用于简单的、低并发的应用场景,而非阻塞IO和基于NIO的框架则更适合需要处理大量并发连接的高性能应用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值