Java I/O 模型之 BIO

简介: Java BIO(Blocking I/O)是一种同步阻塞式I/O模型,从JDK1.0到JDK1.3,Java只能使用BIO实现Socket通信。

❤️ 个人主页:水滴技术
🚀 支持水滴:点赞👍 + 收藏⭐ + 留言💬
🌸 订阅专栏:Java 教程:从入门到精通

大家好,我是水滴~~

Java BIO的概念

Java BIO(Blocking I/O)是一种同步阻塞式I/O模型,从JDK1.0到JDK1.3,Java只能使用BIO实现Socket通信。

在BIO通信模型中,有一个独立的Acceptor线程,来负责监听客户端的连接,它每接收到一个客户端连接,都会为之创建一个新的线程,新线程专门负责处理与之对应客户端的通信。

在这里插入图片描述

Java API解析

在Java BIO中,服务端ServerSocket负责绑定IP地址和端口,并启动端口监听;而客户端Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。

下面,我们通过一段示例代码,来了解服务端和客户端的API。

服务器端API

服务器端是为客户端服务的,若想要客户端知道它的存在,必须向网络中公开出一个套接字地址(IP地址+端口),也就是网络中的一个端点,我们称该端点为服务端套接字(或者叫监听套接字)。

创建服务端套接字

Java BIO中使用ServerSocket类创建一个服务器端套接字。下面示例代码中,创建了一个服务器端套接字实例。此时的套接字还未绑定本地IP和端口,客户端还无法连接。

// 为服务端创建 ServerSocket,并指定端口
ServerSocket serverSocket = new ServerSocket();

绑定本地套接字地址

这里要先说一下InetSocketAddress类,从类名也可以看出,这是一个网络套接字地址(IP地址+端口号)类。该类在实例化时,可以指定具体的IP地址和端口号;也可以只指定端口号,那么其IP地址将是一个通配符地址

通过bind方法来绑定一个本地套接字地址,并启动了监听。下面示例代码中,只指定了8080端口,未指定IP地址,若服务端有多个IP地址,那么每一个IP都可以被连接。

服务端套接字绑定了本地端口后,实际客户端就已经可以连接了。你可以通过telnet命令连接试一下。

serverSocket.bind(new InetSocketAddress(8080));

监听客户端连接

通过accept方法来监听客户端的连接。该方法是一个阻塞方法,直到有一个客户端连接成功后,该方法才会返回该连接的套接字对象。也就是说,每连接一个客户端,都会生成一个与之对应的套接字对象。

该套接字与客户端的套接字是成对出现的,它们都记录了该连接的四元组信息(源IP地址、源端口、目的IP地址、目的端口)。后面与客户端的I/O操作,都是通过此套接字来完成的。

// 监听,等待客户端连接。该方法会阻塞,直到建立连接。
Socket socket = serverSocket.accept();

读取客户端数据

通过套接字可以获取输入流,调用输入流的read方法来监听客户端的输入。该方法同样是一个阻塞方法,直到有客户端数据输入。

下面代码是从输入流中读取字节数据,并将其缓存到一个字节数组中。最后将字节数据转换为字符串,便是我们要的内容。

// 通过 socket 获取输入流
InputStream inputStream = socket.getInputStream();
// 创建缓冲区数组,用于临时存储发来的数据
byte[] bytes = new byte[1024];
// 从输入流中读取数据,并将它们存储到缓冲区数组中。该方法会阻塞,直到输入数据可用、检查到文件结束或抛出异常
int len = inputStream.read(bytes);
if (len != -1) {
   
   
    // 将字节数组转换为文本内容
    String content = new String(bytes, 0, len);
}

向客户端写入数据

通过套接字获取输出流,通过调用write方法,来向客户端写入数据。写入的数据是一个字节数组,所以我们要将字符串内容转换为字节数组。

// 获取套接字的输入流
OutputStream outputStream = socket.getOutputStream();
// 向输出流写入内容
outputStream.write("收到".getBytes());

客户端API

服务端公开了套接字地址后,客户端便可以连接了。

创建客户端套接字

客户端想要连接服务端,也必须创建一个套接字,我们称之为客户端套接字。下面代码创建了一个未连接的客户端套接字。

// 创建 Socket
Socket socket = new Socket();

连接服务器端

通过connect方法来连接服务端,该方法的入参是一个服务端套接字地址(必须指定IP和端口)。该方法是一个阻塞方法,直到连接成功(可以设置超时时间)。

客户端的套接字,同样也有四元组信息,一旦连接成功,会随机分配一个未使用的端口。

// 连接远程端点
socket.connect(new InetSocketAddress("127.0.0.1", 8080));

读/写数据

客户端套接字连接成功,便可以与服务端进行读/写操作了,该操作与服务端相同,就不再赘述了。

示例代码

服务端示例代码

accept方法用来监听客户端连接,为了让所有客户端都能连接,在主线程内使用一个while循环,来依次接收所有连接。

由于read是一个阻塞方法,为了不影响其他客户端的连接,需要为每一个连接创建一个独立的线程,来处理客户端读取操作。

import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author 码农StayUp
 * @date 2021/4/6 0006
 */
public class BioServer {
   
   

    private static final int PORT = 8080;

    public static void main(String[] args) throws IOException {
   
   

        // 为服务端创建 ServerSocket
        ServerSocket serverSocket = new ServerSocket();
        // 绑定本地端口
        serverSocket.bind(new InetSocketAddress(PORT));
        System.out.printf("[%s] - 服务端启动了,端口为:%s\n", Thread.currentThread().getName(), PORT);

        // 循环接收每一个客户端连接,当没有连接时会阻塞
        while (true) {
   
   
            // 监听,等待客户端连接。该方法会阻塞,直到建立连接。
            Socket socket = serverSocket.accept();
            System.out.printf("[%s] - 有一个客户端连上来了 - %s\n", Thread.currentThread().getName(), socket.getRemoteSocketAddress());

            // 为连接创建一个独立的线程,进行接收数据
            new Thread(() -> socketHandler(socket)).start();
        }
    }

    /**
     * 处理 socket 连接
     *
     * @param socket
     */
    private static void socketHandler(Socket socket) {
   
   
        try {
   
   
            // 创建缓冲区数组,用于临时存储客户端发来的数据
            byte[] bytes = new byte[1024];
            // 通过 socket 获取输入流
            InputStream inputStream = socket.getInputStream();
            // 循环接消息,直到连接关闭
            while (true) {
   
   
                // 从输入流中读取数据,并将它们存储到缓冲区数组中。该方法会阻塞,直到输入数据可用、检查到文件结束或抛出异常
                int len = inputStream.read(bytes);
                if (len != -1) {
   
   
                    String content = new String(bytes, 0, len);
                    System.out.printf("[%s] - 接收客户端发来的内容:%s\n", Thread.currentThread().getName(), content);
                } else {
   
   
                    System.out.printf("[%s] - 连接关闭...\n", Thread.currentThread().getName());
                    break;
                }
            }
        } catch (IOException e) {
   
   
            e.printStackTrace();
        } finally {
   
   
            try {
   
   
                socket.close();
            } catch (IOException e) {
   
   
                e.printStackTrace();
            }
        }
    }
}

客户端示例代码

客户端代码相对比较简单,连接成功后进行通信即可。

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Scanner;

/**
 * @author 码农StayUp
 * @date 2021/4/6 0006
 */
public class BioClient {
   
   

    private static final String HOST = "127.0.0.1";
    private static final int PORT = 8080;

    public static void main(String[] args) throws IOException {
   
   

        // 创建 Socket
        Socket socket = new Socket();
        // 连接远程端点
        socket.connect(new InetSocketAddress(HOST, PORT));
        System.out.printf("连接成功,主机:%s,端口:%s\n", HOST, PORT);
        // 获取输出流
        OutputStream outputStream = socket.getOutputStream();
        // 获取控制输入内容
        Scanner scanner = new Scanner(System.in);
        while (true) {
   
   
            System.out.print("请输入:");
            String content = scanner.nextLine();
            if ("quit".equals(content)) {
   
   
                break;
            }
            // 将控制台内容写入输出流
            outputStream.write(content.getBytes());
        }
        outputStream.close();
        scanner.close();
        socket.close();
    }
}
相关文章
|
2月前
|
存储 缓存 Oracle
Java I/O流面试之道
NIO的出现在于提高IO的速度,它相比传统的输入/输出流速度更快。NIO通过管道Channel和缓冲器Buffer来处理数据,可以把管道当成一个矿藏,缓冲器就是矿藏里的卡车。程序通过管道里的缓冲器进行数据交互,而不直接处理数据。程序要么从缓冲器获取数据,要么输入数据到缓冲器。
Java I/O流面试之道
|
3月前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
26 1
|
5月前
|
Java
"揭秘Java IO三大模式:BIO、NIO、AIO背后的秘密!为何AIO成为高并发时代的宠儿,你的选择对了吗?"
【8月更文挑战第19天】在Java的IO编程中,BIO、NIO与AIO代表了三种不同的IO处理机制。BIO采用同步阻塞模型,每个连接需单独线程处理,适用于连接少且稳定的场景。NIO引入了非阻塞性质,利用Channel、Buffer与Selector实现多路复用,提升了效率与吞吐量。AIO则是真正的异步IO,在JDK 7中引入,通过回调或Future机制在IO操作完成后通知应用,适合高并发场景。选择合适的模型对构建高效网络应用至关重要。
102 2
|
5月前
|
网络协议 Java 关系型数据库
16 Java网络编程(计算机网络+网络模型OSI/TCP/IP+通信协议等)
16 Java网络编程(计算机网络+网络模型OSI/TCP/IP+通信协议等)
93 2
|
6月前
|
安全 Java Linux
(七)Java网络编程-IO模型篇之从BIO、NIO、AIO到内核select、epoll剖析!
IO(Input/Output)方面的基本知识,相信大家都不陌生,毕竟这也是在学习编程基础时就已经接触过的内容,但最初的IO教学大多数是停留在最基本的BIO,而并未对于NIO、AIO、多路复用等的高级内容进行详细讲述,但这些却是大部分高性能技术的底层核心,因此本文则准备围绕着IO知识进行展开。
197 1
|
6月前
|
Java Linux
Java演进问题之1:1线程模型对于I/O密集型任务如何解决
Java演进问题之1:1线程模型对于I/O密集型任务如何解决
|
6月前
|
存储 Java Unix
(八)Java网络编程之IO模型篇-内核Select、Poll、Epoll多路复用函数源码深度历险!
select/poll、epoll这些词汇相信诸位都不陌生,因为在Redis/Nginx/Netty等一些高性能技术栈的底层原理中,大家应该都见过它们的身影,接下来重点讲解这块内容。
103 0
|
9天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
11天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
11天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。