Java Socket 网络编程实例(阻塞IO、非阻塞IO、多路复用Selector、AIO)

文章目录

  • 1. 概述
  • 2. TCP 阻塞式IO 网络编程实例
    • 2.1 TCP网络编程服务端
    • 2.2 ByteBufferUtil
    • 2.3 客户端代码
    • 2.4 运行截图
  • 3. TCP 非阻塞式IO 网络编程实例
    • 3.1 服务端
    • 3.2 客户端
    • 3.3 运行截图
  • 4. 多路复用
    • 4.1 服务器端
    • 4.2 客户端
    • 4.3 运行截图
  • 5. AIO
    • 5.1 AIO 服务端
    • 5.2 客户端
    • 5.3 运行截图
  • 6. Channel / Buffer
    • 6.1 Channel
    • 6.2 ByteBuffer
  • 参考文献

1. 概述

  • 网络编程, 就是编写程序, 使两台联网的电脑可以交换数据,
  • 套接字是网络数据传输用的软件设备, 用来连接网络的工具
  • 在 linux中 socket被认为是文件中的一种, 在网络数据传输过程中, 使用文件I/O的相关函数
  • socket 帮助程序员封装了网络的底层细节,如:错误检测、包大小、包分解、包重传、网络地址等,让程序员将网络连接看作可以读/写字节的流
  • 套接字常用网络协议: TCP、UDP

之前还有一篇文章: Linux C++ Socket 套接字、select、poll、epoll 实例

套接字进行网络连接流程, 如下图:

服务器端:

  1. 创建服务器套接字 socket()
  2. 绑定端口 bind()
  3. 监听端口 listen()
  4. 接受客户端请求 accept()
  5. 读取客户端请求的数据 read()
  6. 返回客户端要响应的数据 write()
  7. 关闭与客户端的连接 close()
  8. 关闭服务器套接字 close()

客户端:

  1. 创建客户端套接字 socket()
  2. 连接服务端 connect()
  3. 请求服务端数据, 发送操作数和操作符到服务器 write()
  4. 从服务器读取操作结果 read()
  5. 关闭客户端套接字 close()

流程图如下, 具体代码示例可以看下面的 2. TCP 阻塞式IO 网络编程实例在这里插入图片描述

2. TCP 阻塞式IO 网络编程实例

accept 和 read 都是阻塞的, 当 accept 到新连接, 或者 read 到数据程序才往下走

为了提高服务端处理能力, 一个客户端连接一个线程处理

不能一个线程处理多个客户端, 某个客户端会阻塞这个线程处理其他客户端

在这里插入图片描述

2.1 TCP网络编程服务端

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;

public class BlockServer {
    public static void main(String[] args) throws IOException {
        // 0. ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(16);
        // 1. 创建了服务器
        ServerSocketChannel ssc = ServerSocketChannel.open();

        // 2. 绑定监听端口
        ssc.bind(new InetSocketAddress(8080));

        // 3. 连接集合
        List<SocketChannel> channels = new ArrayList<>();
        while (true) {
            // 4. accept 建立与客户端连接, SocketChannel 用来与客户端之间通信
            System.out.println("等待客户端连接...");
            SocketChannel sc = ssc.accept(); // 阻塞方法,线程停止运行
            System.out.println("接收到客户端连接: " + sc);
            channels.add(sc);
            for (SocketChannel channel : channels) {
                // 5. 接收客户端发送的数据
                System.out.println("开始读取客户端中的数据:" + channel);
                channel.read(buffer); // 阻塞方法,线程停止运行
                buffer.flip();

                String request = ByteBufferUtil.read(buffer);
                System.out.println(request);
                buffer.clear();
                System.out.println("已经读取完客户端中的数据:" + channel);
            }
        }
    }
}

2.2 ByteBufferUtil

public class ByteBufferUtil {
    public static String read(ByteBuffer byteBuffer) throws CharacterCodingException {
        CharBuffer charBuffer = StandardCharsets.UTF_8.decode(byteBuffer);
        return charBuffer.toString();
    }

    public static ByteBuffer read(String string) throws CharacterCodingException {
        return StandardCharsets.UTF_8.encode(string);
    }

    public static void main(String[] args) throws CharacterCodingException {
        System.out.println(ByteBufferUtil.read(ByteBufferUtil.read("test")));
    }

}

2.3 客户端代码

public class BlockClient {
    public static void main(String[] args) throws IOException {
        SocketChannel sc = SocketChannel.open();
        System.out.println("开始连接服务端...");
        sc.connect(new InetSocketAddress("localhost", 8080));
        String str = "test";
        System.out.println("连接服务端成功,写入数据: " + str);
        sc.write(ByteBufferUtil.read(str));
    }
}

2.4 运行截图

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

3. TCP 非阻塞式IO 网络编程实例

不停的轮询, 看看有没有accept 到新连接, 没有连接不阻塞等待, 继续去看看已经建立的连接有没有read到客户端的新数据, read到新数据处理, read不到不处理

为了提高服务端处理能力, 可以一个客户端连接一个线程处理, 线程不停的轮询自己要处理的客户端

也可以一个线程处理多个客户端, 相较于上面的阻塞I/O模型, 非阻塞不至于某个客户端阻塞这个线程处理其他客户端

在这里插入图片描述

3.1 服务端

ssc.configureBlocking(false); 设置为非阻塞模式

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class NonBlockServer {
    public static void main(String[] args) throws IOException, InterruptedException {
        // 0. ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(16);
        // 1. 创建了服务器
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false); // 非阻塞模式
        // 2. 绑定监听端口
        ssc.bind(new InetSocketAddress(8080));
        // 3. 连接集合
        List<SocketChannel> channels = new ArrayList<>();
        while (true) {
            // 4. accept 建立与客户端连接, SocketChannel 用来与客户端之间通信
            SocketChannel sc = ssc.accept(); // 非阻塞,线程还会继续运行,如果没有连接建立,但sc是null
            if (sc != null) {
                System.out.println("接收到客户端连接: " + sc);
                sc.configureBlocking(false); // 非阻塞模式
                channels.add(sc);
            }
            for (SocketChannel channel : channels) {
                System.out.println("开始读取客户端中的数据:" + channel);
                // 5. 接收客户端发送的数据
                int read = channel.read(buffer);// 非阻塞,线程仍然会继续运行,如果没有读到数据,read 返回 0
                if (read > 0) {
                    buffer.flip();
                    System.out.println((ByteBufferUtil.read(buffer)));
                    buffer.clear();
                    System.out.println("已经读取完客户端中的数据:" + channel);
                } else {
                    TimeUnit.MILLISECONDS.sleep(100);
                }
            }
        }
    }
}

3.2 客户端

客户端同上

3.3 运行截图

在这里插入图片描述

在这里插入图片描述

4. 多路复用

可以调用 select/poll/epoll , 阻塞在select/poll/epoll, select/poll/epoll 监听多个客户端连接事件或写入的数据, 然后这些事件可再有多个线程分一分处理掉

在这里插入图片描述

4.1 服务器端

打开选择器并将其与通道注册,监听接受连接操作:

Selector selector = Selector.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_ACCEPT);

监听选择器上的事件,返回已就绪的通道数量:

int count = selector.select();

获取所有事件(连接、读取):

Set<SelectionKey> keys = selector.selectedKeys();
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

public class SelectorServer {
    public static void main(String[] args) {
        try (ServerSocketChannel channel = ServerSocketChannel.open()) {
            // 绑定端口并打印通道信息
            channel.bind(new InetSocketAddress(6666));
            System.out.println(channel);

            // 打开选择器并将其与通道注册,监听接受连接操作
            Selector selector = Selector.open();
            channel.configureBlocking(false);
            channel.register(selector, SelectionKey.OP_ACCEPT);

            // 无限循环,等待选择器上的事件
            while (true) {
                // 监听选择器上的事件,返回已就绪的通道数量
                int count = selector.select();
                System.out.println("select count: " + count);

                // 如果没有就绪的通道,则继续循环等待
                if (count <= 0) {
                    continue;
                }

                // 获取并迭代处理所有就绪的事件
                // 获取所有事件
                Set<SelectionKey> keys = selector.selectedKeys();
                // 遍历所有事件,逐一处理
                Iterator<SelectionKey> iter = keys.iterator();
                while (iter.hasNext()) {
                    SelectionKey key = iter.next();

                    // 处理接受连接事件
                    // 判断事件类型
                    if (key.isAcceptable()) {
                        ServerSocketChannel c = (ServerSocketChannel) key.channel();
                        // 必须处理
                        SocketChannel sc = c.accept();
                        sc.configureBlocking(false);
                        sc.register(selector, SelectionKey.OP_READ);
                        System.out.println("连接已建立:" + sc);
                    }
                    // 处理读取数据事件
                    else if (key.isReadable()) {
                        SocketChannel sc = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(128);
                        int read = sc.read(buffer);
                        if (read == -1) {
                            // 如果读取返回-1,表示连接已关闭
                            key.cancel();
                            sc.close();
                        } else {
                            // 否则,将缓冲区反转并打印读取的数据
                            buffer.flip();
                            System.out.println(new String(buffer.array(), StandardCharsets.UTF_8));
                        }
                    }
                    // 事件处理完毕后,从迭代器中移除,避免重复处理
                    // 处理完毕,必须将事件移除
                    iter.remove();
                }
            }
        } catch (IOException e) {
            // 打印IO异常堆栈跟踪
            e.printStackTrace();
        }
    }
}

4.2 客户端

import netty.ByteBufferUtil;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class SelectorClient {

    public static void main(String[] args) throws IOException {
        // 创建Socket通道并连接到服务器
        SocketChannel sc = SocketChannel.open();
        sc.connect(new InetSocketAddress("localhost", 6666));

        // 初始化输入和输出ByteBuffer
        ByteBuffer inputBuffer = ByteBuffer.allocate(512);
        ByteBuffer serverOutput = ByteBuffer.allocate(512);

        // 循环接收用户输入并发送给服务器
        while (true) {
            // 使用Scanner获取用户输入
            Scanner in = new Scanner(System.in);
            String input = in.nextLine();
            System.out.println("user input: " + input);

            // 清空输入缓冲区,放入用户输入,然后反转准备写入
            inputBuffer.clear();
            inputBuffer.put(input.getBytes(StandardCharsets.UTF_8));
            inputBuffer.flip();

            // 将输入数据写入Socket通道
            sc.write(inputBuffer);
            System.out.println("send to server " + input);

            // 循环读取服务器响应
            while (true) {
                // 清空服务器响应缓冲区,准备读取数据
                serverOutput.clear();
                // 从Socket通道读取数据
                sc.read(serverOutput);

                // 如果没有读取到数据,继续尝试读取
                if (!serverOutput.hasRemaining()) {
                    continue;
                }

                // 反转缓冲区,读取数据并打印
                serverOutput.flip();
                System.out.println("server response " + ByteBufferUtil.read(serverOutput));

                // 读取完成后退出内层循环
                break;
            }
        }
    }
}

4.3 运行截图

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

5. AIO

异步I/O模型

告诉内核启动某个操作, 并且把数据copy到用户缓冲区再通知我们

在这里插入图片描述

5.1 AIO 服务端

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.Charset;

/**
 * AIO服务器类,用于演示异步IO的服务器端实现。
 * 使用AsynchronousServerSocketChannel处理客户端连接和数据传输。
 */
public class AIOServer {
    /**
     * 程序入口,初始化并启动AIO服务器。
     * 绑定服务器端口并等待客户端连接。
     *
     * @param args 命令行参数
     * @throws IOException 如果绑定端口失败
     */
    public static void main(String[] args) throws IOException {
        AsynchronousServerSocketChannel ssc = AsynchronousServerSocketChannel.open();
        ssc.bind(new InetSocketAddress(6666));
        ssc.accept(null, new AcceptHandler(ssc));
        while (true) ;
    }

    /**
     * 关闭客户端通道的方法。
     * 用于处理读取或写入操作失败时关闭通道。
     *
     * @param sc 客户端通道
     */
    private static void closeChannel(AsynchronousSocketChannel sc) {
        try {
            System.out.printf("[%s] %s close\n", Thread.currentThread().getName(), sc.getRemoteAddress());
            sc.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 读取数据的完成处理器,实现读取客户端数据并响应的逻辑。
     */
    private static class ReadHandler implements CompletionHandler<Integer, ByteBuffer> {
        private final AsynchronousSocketChannel sc;

        public ReadHandler(AsynchronousSocketChannel sc) {
            this.sc = sc;
        }

        /**
         * 当读取操作完成时被调用。
         * 解析读取的数据并写回响应到客户端。
         *
         * @param result     读取操作的结果
         * @param attachment 读取操作的附加上下文
         */
        @Override
        public void completed(Integer result, ByteBuffer attachment) {
            try {
                if (result == -1) {
                    return;
                }
                System.out.printf("[%s] %s read\n", Thread.currentThread().getName(), sc.getRemoteAddress());
                attachment.flip();
                String request = Charset.defaultCharset().decode(attachment).toString();
                System.out.println(request.toString());
                attachment.clear();

                attachment.put(("你好:" + request).getBytes());
                attachment.flip();
                sc.write(attachment);
                attachment.clear();

                // 读取下一个读时间
                sc.read(attachment, attachment, new ReadHandler(sc));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        /**
         * 当读取操作失败时被调用。
         * 关闭客户端通道并打印异常堆栈跟踪。
         *
         * @param exc        引发的异常
         * @param attachment 读取操作的附加上下文
         */
        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {
            closeChannel(sc);
            exc.printStackTrace();
        }
    }

    /**
     * 接受连接的完成处理器,用于处理客户端的连接请求。
     */
    private static class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, Object> {
        private final AsynchronousServerSocketChannel ssc;

        public AcceptHandler(AsynchronousServerSocketChannel ssc) {
            this.ssc = ssc;
        }

        /**
         * 当接受操作完成时被调用。
         * 设置读取缓冲区并开始读取客户端发送的数据。
         *
         * @param sc         接受到的客户端通道
         * @param attachment 接受操作的附加上下文
         */
        @Override
        public void completed(AsynchronousSocketChannel sc, Object attachment) {
            try {
                System.out.printf("[%s] %s connected\n", Thread.currentThread().getName(), sc.getRemoteAddress());
            } catch (IOException e) {
                e.printStackTrace();
            }
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // 读事件由 ReadHandler 处理
            System.out.println("开始读");
            sc.read(buffer, buffer, new ReadHandler(sc));
            System.out.println("读完成");

            // 处理完第一个 accept 时,需要再次调用 accept 方法来处理下一个 accept 事件
            ssc.accept(null, this);
        }

        /**
         * 当接受操作失败时被调用。
         * 打印异常堆栈跟踪。
         *
         * @param exc        引发的异常
         * @param attachment 接受操作的附加上下文
         */
        @Override
        public void failed(Throwable exc, Object attachment) {
            exc.printStackTrace();
        }
    }
}

5.2 客户端

同 4.2

5.3 运行截图

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

6. Channel / Buffer

6.1 Channel

Channel: 传输数据的通道

其实和数据流挺像的,不过数据流是单向的而Channel 是双向的,可以向channel中写数据,也可以从channel中读取数据

NIO 基础组件之 Channel

6.2 ByteBuffer

ByteBuffer是Buffer子类,是字节缓冲区,特点如下所示。

大小不可变。一旦创建,无法改变其容量大小,无法扩容或者缩容;
读写灵活。内部通过指针移动来实现灵活读写;
支持堆上内存分配和直接内存分配

一文搞懂ByteBuffer使用与原理

参考文献

  • UNIX 网络编程 卷1: 套接字联网API
  • TCP/IP网络编程 尹圣雨 著 金国哲 译
  • Linux IO模式及 select、poll、epoll详解
  • 浅谈select,poll和epoll的区别
  • 黑马 Netty 课程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/671675.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Python】Python异步编程

Python 异步编程 异步编程 异步编程是一种编程范式&#xff0c;通过非阻塞的方式执行任务&#xff0c;允许程序在等待某些操作&#xff08;如I/O操作、网络请求、数据库查询等&#xff09;完成时&#xff0c;继续执行其他任务。这与同步编程&#xff08;或阻塞编程&#xff09…

【图像处理与机器视觉】XJTU期末考点

题型 选择&#xff1a;1 分10 填空&#xff1a;1 分15 简答题&#xff08;也含有计算和画图&#xff09;&#xff1a;10 分*4 计算题&#xff1a;15 分20 分 考点 选择题&#xff08;部分&#xff09; 数字图像处理基础 p(x,y),q(s,t)两个像素之间的距离由公式&#xff1a…

知乎x-zse-96、x-zse-81

声明 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01;wx a15018601872 本文章未…

GraphQL(2):使用express和GraphQL编写helloworld

1 安装express、graphql以及express-graphql 在项目的目录下运行一下命令。 npm init -y npm install express graphql express-graphql -S 2 新建helloworld.js 代码如下&#xff1a; const express require(express); const {buildSchema} require(graphql); const grap…

spring mvc 中怎样定位到请求调用的controller

前言 在java web开发过程中&#xff0c;正常情况下controller都是我们自己写的&#xff0c;我们可以很方便的定位到controller的位置。但是有些时候我们引入的其他依赖中可能也有controller&#xff0c;为了找到并方便的调试jar包中的controller&#xff0c;我们一般会进行全局…

基于Java的零食管理系统的设计与实现(论文+源码)_kaic

摘 要 随着科技的进步&#xff0c;以及网络的普及&#xff0c;都为人们的生活提供了极大的方便。因此&#xff0c;在管理”三姆”宿舍在线零食商店时&#xff0c;与现代的网络联系起来是非常必要的&#xff0c;本次设计的系统在研发过程中应用到了Java技术&#xff0c;这在一定…

windows10系统64位安装delphiXE11.2完整教程

windows10系统64位安装delphiXE11.2完整教程 https://altd.embarcadero.com/download/radstudio/11.0/radstudio_11_106491a.iso XE11.1 https://altd.embarcadero.com/download/radstudio/11.0/RADStudio_11_2_10937a.iso XE11.2 关键使用文件在以下内容&#xff1a;windows10…

现代信号处理12_谱估计的4种方法(CSDN_20240602)

Slepian Spectral Estimator(1950) 做谱估计的目标是尽可能看清楚信号功率谱在某一个频率上的情况&#xff0c;假设我们想了解零频时的分布&#xff0c;最理想的情况是滤波器的传递函数H(ω) 是一个冲激函数&#xff0c;这样就没有旁瓣&#xff0c;也就没有泄漏&#xff1b;其次…

回溯算法 -- 216. 组合总和 III

目录 一.题目描述 二.解题思路 三.回溯三部曲 3.1确定递归函数的参数 3.2确认递归的终止条件 3.3确定单层循环逻辑 四.具体的代码 一.题目描述 找出所有相加之和为 n 的 k 个数的组合&#xff0c;且满足下列条件&#xff1a; 只使用数字1到9每个数字 最多使用一次 返…

经典的滑动窗口的题目 力扣 2799. 统计完全子数组的数目(面试题)

给你一个由 正 整数组成的数组 nums 。 如果数组中的某个子数组满足下述条件&#xff0c;则称之为 完全子数组 &#xff1a; 子数组中 不同 元素的数目等于整个数组不同元素的数目。 返回数组中 完全子数组 的数目。 子数组 是数组中的一个连续非空序列。 示例 1&#xff1…

【1】AI介绍

迎接 AGI 时代 AGI(Artificial General Intelligence),人工通用智能,AGI是一种可以执行复杂任务的人工智能,能够完全模仿人类智能的行为。应用领域涉及医疗、交通、智能家居等多个与人类活动密切相关的领域。 AGI 多久会到来? 乐观预测:明年(未来已来)主流预测:3-5…

【云原生】Docker Compose 使用详解

目录 一、前言 二、Docker Compose 介绍 2.1 Docker Compose概述 2.2 Docker Compose特点 2.3 Docker Compose使用场景 三、Docker Compose 搭建 3.1 安装docker环境 3.2 Docker Compose安装方式一 3.2.1 下载最新版/如果不是最新可替换最新版本 3.2.2 设置权限 3.2.…

Zigbee +PC上位机 无线控制二维云台开发笔记

今日尝试开发一款简单好学的PC上位机无线控制二维云台的小试验品&#xff1a; 主要开发环境与工具介绍&#xff1a; 单片机 STM32F103C8T6 使用标准库函数编程 Visual Studio 2022软件C# Winform 开发 上位机控制软件 DL_20 无线串口模块 &#xff0b; USB-TTL 模块 实现无线通…

使用 Scapy 库编写 Ping of Death 攻击脚本

一、介绍 1.1 概述 Ping of Death&#xff08;PoD&#xff09;攻击是一种历史悠久的拒绝服务&#xff08;DoS&#xff09;攻击&#xff0c;攻击者通过发送特制的畸形ICMP Echo请求数据包&#xff0c;导致目标系统无法正确处理&#xff0c;从而导致系统崩溃、重启或无法响应正…

用于相似图片搜索引擎的Python OpenCV图像直方图

图像直方图 那么&#xff0c;图像直方图到底是什么&#xff1f; 图片 图像的构成是由像素点构成的&#xff0c;每个像素点的值代表着该点的颜色&#xff08;灰度图或者彩色图&#xff09;。所谓直方图就是对图像的中的这些像素点的值进行统计&#xff0c;得到一个统一的整体的…

手眼标定学习笔记

目录 标定代码&#xff1a; 手眼标定原理学习 什么是手眼标定 手眼标定的目的 eye in hand eye to hand AXXB问题的求解 标定代码&#xff1a; GitHub - pumpkin-ws/HandEyeCalib 推荐博文&#xff1a; https://zhuanlan.zhihu.com/p/486592374 手眼标定原理学习 参…

YOLOv8: 标注石头、识别边缘及计算面积的方案

YOLOv8: 标注石头、识别边缘及计算面积的方案 引言 YOLO&#xff08;You Only Look Once&#xff09;是一种非常有效的实时目标检测算法&#xff0c;自其首次发布以来就受到了广泛的关注和应用。YOLOv8 是这一系列算法的最新版本&#xff0c;继承了之前版本的高效性和准确性&a…

如何在一台电脑上安装多个版本的JDK并且切换使用?

如何在一台电脑上安装多个版本的JDK并且切换使用&#xff1f; 文章目录 如何在一台电脑上安装多个版本的JDK并且切换使用&#xff1f;1.目录管理2.下载JDK3.配置环境变量4. 验证 1.目录管理 我们需要首先对不同版本的JDK进行版本管理&#xff0c;如下所示&#xff0c;我在C盘路…

vue3状态管理,pinia的使用

状态管理 我们知道组件与组件之间可以传递信息&#xff0c;那么我们就可以将一个信息作为组件的独立状态&#xff08;例如&#xff0c;单个组件的颜色&#xff09;或者共有状态&#xff08;例如&#xff0c;多个组件是否显示&#xff09;在组件之传递&#xff0c;这样的话我们希…

STM32作业实现(七)OLED显示数据

目录 STM32作业设计 STM32作业实现(一)串口通信 STM32作业实现(二)串口控制led STM32作业实现(三)串口控制有源蜂鸣器 STM32作业实现(四)光敏传感器 STM32作业实现(五)温湿度传感器dht11 STM32作业实现(六)闪存保存数据 STM32作业实现(七)OLED显示数据 STM32作业实现(八)触摸按…