• Docker--未完结


    一.Docker是干什么的

    在没亲自使用过之前,再多的术语也仅仅是抽象,只有写的人或者使用过的人能看懂。
    所以,作为新手来说,先知道Docker是用于部署项目就够了,下面展示如何用Docker部署项目及Docker常用命令。

    二、安装Docker

    1. # 1、yum 包更新到最新
    2. yum update
    3. # 2、安装需要的软件包, yum-util 提供yum-config-manager功能,另外两个是devicemapper驱动依赖的
    4. yum install -y yum-utils device-mapper-persistent-data lvm2
    5. # 3、 设置yum源
    6. yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
    7. # 4、 安装docker,出现输入的界面都按 y
    8. yum install -y docker-ce
    9. # 5、 查看docker版本,验证是否验证成功
    10. docker -v
    11. #6 然后我们就可以启动docker了
    12. systemctl start docker

    关联知识点:Docker进程相关的命令

    1. #1.启动docker服务
    2. systemctl start docker
    3. #2.停止docker服务
    4. systemctl stop docker
    5. #3.重启docker服务
    6. systemctl restart docker
    7. #4.查看docker服务状态
    8. systemctl status docker
    9. #5.设置开机启动docker服务
    10. systemctl enable docker

    三.用Docker部署应用

    简单说就是先搜索镜像,然后再拉取(下载)镜像,最后再根据镜像创建容器。

    1、部署MySQL

    【1】搜索mysql镜像
    docker search mysql
    
    【2】拉取mysql镜像(这里拉取最新版)
    docker pull mysql:latest
    
    【3】创建容器
    docker run --name mysql-server  -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306  -d mysql:latest
    

    –name:容器的名字
    -e 设置mysql密码,要记住你设置的密码
    -p 3306:3306 将docker中的3306端口映射到宿主机的3306端口,外界要访问的就是3306端口
    -d 根据什么镜像来创建的

    【4】开始使用mysql
    docker exec -it mysql-server bash
    mysql -uroot -p

    或者将两个命令合起来使用

    docker exec -it mysql-server mysql -uroot -p

    注:

    (1)使用docker exec命令,可以在容器中启动一个新的进程,并且可以与该进程进行交互。(2)在Docker中,-it是两个选项的组合:-i-t
    综合使用-it选项,可以在容器中运行交互式进程,并且通过终端与之进行交互,比如在终端中键入命令或者接收命令的输出。在上述命令中,docker exec -it mysql-server mysql -uroot -p打开了一个交互式的MySQL会话,可以使用MySQL的命令行工具与数据库进行交互。

    【5】用datagrip连接mysql

    除了常规的要设置腾讯云防火墙和linux的防火墙,连接时候还要将高级的选项除了useSSL要为false,allowPublicKeyRetrieval也要改为true。

    2、部署redis

    【1】搜索redis镜像
    docker search redis
    
    【2】拉取redis镜像
    docker pull redis:5.0
    【3】用该redis镜像创建容器运行
    1. #从docker的6379端口映射到宿主机的6379端口,命名为c_redis
    2. docker run -id --name=c_redis -p 6379:6379 redis:5.0

    四.镜像和容器

    1.概念

    可以看到有两个非常重要的概念:镜像和容器。

    【1】Docker 镜像可以看作是一个应用程序运行所需的所有文件和依赖项的打包版本,你可以把它当成安装包。类比于照片,Docker 镜像就像是一张静态的照片,记录了应用程序的全部内容和状态。可以通过构建  Dockerfile 文件来创建镜像,也可以向 Docker Hub 等镜像仓库下载别人构建好的镜像。我们上面就是拉取别人的镜像。


    【2】Docker 容器则是根据镜像创建的运行实例,其主要作用是隔离不同的应用程序或应用程序的不同版本,以便它们不会相互干扰,你可以把它当成是用安装包安装的程序。类比于卡通里的小木屋,Docker 容器就像是一个动态的小木屋,可以在其中运行应用程序并与外部系统交互。

    为什么要隔离呢?比如有两个写好了两个springboot程序,一个运行在java8的环境,一个运行在java17的环境,那样的话如果没有docker或者虚拟机,那么两个程序肯定要有一个不能运行。
    其他软件之间也可能会发生这种问题,不同的app所需的依赖会冲突,导致部分程序不能运行,这不是我们想看到的。

    而Docker容器都有自己的文件系统和网络接口,(可以看成是类似于虚拟机)和其他容器以及宿主机完全隔离。

    从logo我们可以看到,如果docker是那只鲸鱼,那么容器就是上面的集装箱。

    2.相关命令

    接下来介绍镜像和容器相关的命令:

    与镜像相关:

    1. #查看本地所有的镜像
    2. docker images
    3. ##查看所用镜像的id
    4. docker images -q
    5. #搜索镜像:从网络中查找需要的镜像
    6. docker search 镜像名称
    7. #拉取镜像:从Docker仓库下载镜像到本地,镜像名称格式为 名称:版本号
    8. #如果版本号不指定则是最新的版本,如果不知道镜像版本,可以去docker hub 搜索对应镜像查看。
    9. docker pull 镜像名称
    10. #删除镜像:删除本地镜像
    11. docker rmi 镜像id #删除指定本地镜像
    12. docker rmi 'docker images -q' #删除所有本地镜像

    与容器相关:

    1. #查看容器
    2. docker ps #查看正在运行的容器
    3. docker ps -a #查看所有容器
    4. #创建容器
    5. docker run 参数
    6. #进入容器,退出容器不会关闭
    7. docker exec 参数
    8. #停止容器
    9. docker stop 容器名称/id
    10. #启动容器
    11. docker start 容器名称/id
    12. #删除容器:如果容器是运行状态则删除失败,需要停止容器才能删除
    13. docker rm 容器名称/id
    14. #查看容器信息,如网络信息等
    15. docker inspect 容器名称

    docker run的参数说明:
    -i:保持容器运行。通常与-t同时使用。加入it这两个参数后,容器创建后自动进入容器后,退出容器后,容器自动关闭。
    -t:为容器重新分配一个伪输入终端,通常与-i同时使用。
    -d:以守护(后台)模式运行容器。创建一个容器在后台运行,需要使用docker exec进入容器。退出后,容器不会关闭。
    -name:为创建容器命名。

    五.Dockerfile--构建属于自己的镜像

    【1】Dockerfile概念

    Dockerfile是一个文本文件,用来构建镜像。包含了一条条指令,通过这些指令规定了基础镜像,镜像的环境与数据,并且规定了通过镜像生成容器运行的命令 等信息,最终构建出一个新的镜像。

    可以统一开发测试运维的环境,为整个团队提供一个完全一致的开发环境,实现应用的无缝移植。

    【2】案例:用dockerfile部署springboot容器

    (1)idea中用maven的package命令打包,如果之前打包过可能要用clear命令一下。

    (2)用ssh工具,例如finalshell上传到服务器(我是root目录下创建个docker-files文件夹来存储)

    (3)在这个文件夹下创建springboot_dockerfile文件,编写dockerfile--不用记,重点知道关键字是什么意思

    1. FROM openjdk:17-jdk
    2. MAINTAINER flyingpig <1839976096@qq.com>
    3. ADD uuAttendance-0.0.1-SNAPSHOT.jar app.jar
    4. EXPOSE 9090
    5. CMD nohup java -jar app.jar > uuAttendance.log

    然后就编辑完毕,退出先cd到刚刚创建的root/docker-files

    cd /root/docker-files
    

    使用编写好的dockerfile构建镜像:

    docker build -f ./springboot_dockerfile -t app .

    (4)然后就能运行镜像了(但是会遇到问题,看下面)

     docker run --name uuAttendance app:latest

    【3】dockerfile中语句的解释

    各个步骤的解释【自己根据实际情况进行修改】:

    1. #指定父镜像,指定dockerfile基于那个image构建
    2. FROM openjdk:17-jdk
    3. #定义作者信息,用来标明这个dockerfile谁写的
    4. MAINTAINER flyingpig <1839976096@qq.com>
    5. #将jar包添加到镜像中(生成镜像)
    6. #其中uuAttendance-0.0.1-SNAPSHOT.jar是原来jar包的名称,app.jar是添加到容器后的jar包名称
    7. ADD uuAttendance-0.0.1-SNAPSHOT.jar app.jar
    8. #定义容器启动执行的命令
    9. CMD nohup java -jar /app.jar > /uuAttendance.log
    10. 通过dockerfile构建镜像:docker bulid -f dockerfile的文件路径 -t 镜像名称:版本
    11. 不写版本表示最新版
    12. docker build -f ./springboot_dockerfile -t app .

    我们还可以添加workdir来指定工作目录,方便后面的挂载,比如下面指定的是/app为工作目录,后面的时候挂载只需要将/app挂载到宿主机目录即可【挂载的知识详细看后面的数据卷】:

    1. FROM openjdk:17-jdk
    2. MAINTAINER flyingpig <1839976096@qq.com>
    3. #指定容器内部的工作目录 如果没有创建则自动创建,
    4. #通过目录指定在哪个目录下工作,比如下面的jar包就添加到了app目录
    5. WORKDIR /app
    6. ADD websocket-0.0.1-SNAPSHOT.jar app.jar
    7. EXPOSE 8888
    8. CMD java -jar /app/app.jar > /app/uuAttendance.log

    可以看到,上面的文件规定了我们把什么东西扔进镜像(jar包和jdk),和通过镜像生成容器所运行的代码(运行jar包)。

    【4】发布自己的镜像到docker hub上(代办)

    【5】遇到的问题

    然后你运行你的springboot项目,如果说你的项目没调用到数据库,那么一点问题都没有。如果说你有用到mysql数据库,并且mysql的地址使用localhost,那么的话程序就无法正常运行,打开日志,发现报错:

    Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure
    

    接下去我们讲讲docker的网络问题。这是不是顺其自然的学习下面这个知识点了,O(∩_∩)O哈哈~。

    六.Docker网络--Docker如何处理容器的网络访问

    【1】为什么会出现上面的问题呢?

    其实默认情况下容器和容器可以进行网络通信,但是每次创建容器都是Docker给容器分配的IP地址,而不是localhost。
    比如上面的我们在连接数据库的时候都是使用localhost的地址:jdbc:mysql://localhost:3306/<数据库名>,但是容器使用的是docker分配给属于它自己的ip,所以连接不上。要改为jdbc:mysql://:3306/<数据库名>才能连接的上。
    但是每次创建容器分配的ip都是不一样的,导致我们如果更换容器就需要去重新修改代码中的ip地址,很麻烦。

    这些情况我们都可以创建自定义网络来解决这些问题。把需要互相连通的容器加入到同一个网络,这样容器和容器之间就可以通过容器名代替ip地址进行互相访问

    【2】然后讲讲怎么用偷懒的方法解决上述的问题:

    最简单的解决方法就是在每次docker run的时候添加--net=host,直接用host作为网络。这样容器与主机共享网络命名空间,这意味着容器内的应用程序使用主机的网络配置,包括IP地址和端口。
    如果这样写的话端口映射就可以删了,没用。

    例如:

    docker run --name mysql-server --net=host -e MYSQL_ROOT_PASSWORD=123456 -d mysql:latest
    
    docker run -id --net=host --name=c_redis redis:5.0
     docker run --name uuAttendance --net=host app:latest

    ’这样就可以实现不同容器之间使用host网络,从而可以直接使用localhost访问。

    【3】但是这样并不推荐,更好的方法是建立一个网络

    1.运行以下命令来创建一个自定义网络:

    1. #创建一个网络命名为my_network
    2. docker network create my_network

    2.创建网络后,你可以在启动容器时将其连接到该网络。例如:

    docker run -id --net=my_network --name=c_redis -p 6379:6379 redis:5.0

    在这个网络中的容器就可以相互连接啦。

    七.Docker容器的数据卷

     1.什么是数据卷以及数据卷的作用

    【1】什么是数据卷

    数据卷的功能:数据卷(volume)是容器内目录与宿主机目录之间映射的桥梁。
    它可以
    使得容器目录和宿主机(我们的linux)目录相互绑定。
    绑定之后,这两个目录的数据和文件是同步的,只要有一个目录的文件和数据修改另一个目录会立即同步。

    首先我们要先知道什么是容器内目录。

    其实容器就相当于是虚拟机,每个容器都有自己独立的一套目录。我们可以通过下面命令查看容器内目录:

    1. docker exec -it /bin/bash
    2. ls /

    宿主机目录就是我们Linux主机上的目录。

    而数据卷目录就是使得容器内目录与宿主机目录相互绑定来实现数据同步。

    【2】数据卷的好处

    (1)数据持久化
    因为Docker容器被删除后,在容器中的数据就不在了,但宿主机目录依然存在。所以使用数据卷可以实现容器数据持久化,防止数据丢失。
    比如如果mysql容器如果被删了,宿主机目录还存在,那么就可以找回原来mysql中的数据。
    (2)便于对容器的文件进行操作
    以前我们要操作docker某个软件/容器的文件,就得进入docker容器自己的终端,就像上面那样,但是有了数据卷之后,由于数据同步的功能,我们操作容器的文件就直接在宿主机的目录进行操作就行了。
    比如nginx我们经常需要更换html中的静态资源来展示不同的页面,还需要去log目录查看日志。有了是数据卷之后,我们将其挂载到宿主机中的目录就直接操作宿主机目录即可。
    (3)容器之间数据交换
    一个数据卷可以被多个容器和目录同时挂载。也就是说一个宿主机目录中同时拥有多个容器中目录的所有文件和数据,由于数据同步的功能,就可以实现不同容器中的数据共享。

    2.怎么配置数据卷--以nginx为例

    【1】如何设置数据卷?

    数据卷大致可以分为具名挂载和匿名挂载。

    1. #创建启动容器时,使用-v参数设置数据卷
    2. docker run 容器名称或id -v 宿主机目录(文件):容器内目录(文件)
    3. # 一般的话挂载分为3种
    4. -v 容器内目录(文件) # 匿名挂载
    5. -v /宿主机目录(文件):容器内目录(文件) # 指定路径匿名挂载
    6. -v 卷名:容器内目录(文件) # 具名挂载

    (1)具名挂载的时候会创建根据你提供的名字创建一个数据卷,然后将你指定的容器内目录与/var/lib/docker/volumes/<数据卷名>/_data绑定。
    而匿名挂载则是直接将指定的宿主机目录与容器内目录绑定,但是数据卷却没有名字。

    注意事项:1.目录必须是绝对路径,这样才区分卷名和宿主机目录
    2.如果目录不存在,会自动创建,不管是宿主机目录还是容器内目录。
    3.一个数据卷可以被多个容器内目录挂载,一个容器也可以挂载多个数据卷。
    但是要注意,一个容器目录只能挂载一个数据卷,不然会报错!!!

    看到这里你应该不是很清楚,那我们就拿nginx举个例子,记得看完下面的例子过后再回来看看!!!

    【2】nginx场景举例

    我们知道Nginx中有几个关键的目录:html目录是放置一些静态资源,conf目录是放置配置文件,而log目录是放置日志文件。

    如果我们要让Nginx代理我们的静态资源,需要将资源放到html目录;如果我们要修改Nginx的配置,最好是找到conf下的nginx.conf文件。

    但是容器运行的Nginx所有的文件都在容器内部,这样的话我们每次都要进入nginx中进行文件的增删改查操作,那如果我们可以在linux终端直接操作而不是进入容器自己的终端进行文件操作就好了。所以我们要利用数据卷将nginx中的目录与宿主机目录关联,方便我们操作。如图:

    【3】nginx部署具体过程

    1.搜索nginx镜像

    docker search nginx
    2.拉取nginx镜像
    docker pull nginx
    3.创建容器,设置端口映射、目录映射

    1. # 在/root目录下创建nginx目录用于存储nginx数据信息
    2. mkdir ~/nginx
    3. cd ~/nginx
    4. mkdir conf
    5. cd conf
    6. # 在~/nginx/conf/下创建nginx.conf文件,粘贴下面内容
    7. vim nginx.conf
    8. user nginx;
    9. worker_processes  1;
    10. error_log /var/log/nginx/error.log warn;
    11. pid       /var/run/nginx.pid;
    12. events {
    13.   worker_connections  1024;
    14. }
    15. http {
    16.   include       /etc/nginx/mime.types;
    17.   default_type application/octet-stream;
    18.   log_format main  '$remote_addr - $remote_user [$time_local] "$request" '
    19.                      '$status $body_bytes_sent "$http_referer" '
    20.                      '"$http_user_agent" "$http_x_forwarded_for"';
    21.   access_log /var/log/nginx/access.log main;
    22.   sendfile       on;
    23.    #tcp_nopush     on;
    24.   keepalive_timeout  65;
    25.    #gzip on;
    26.   include /etc/nginx/conf.d/*.conf;
    27. }
    ​4.运行容器
    
    docker run -id --name=c_nginx -p 80:80 -v /root/nginx/conf/nginx.conf:/etc/nginx/nginx.conf -v /root/nginx/logs:/var/log/nginx -v /root/nginx/html:/usr/share/nginx/html nginx
    参数说明:
    -p 80:80:将容器的 80端口映射到宿主机的 80 端口
    -v /root/nginx/conf/nginx.conf:/etc/nginx/nginx.conf:将主机当前目录下的 /root/nginx/conf/nginx.conf 挂载到容器的 :/etc/nginx/nginx.conf配置文件。
    -v /root/nginx/logs:/var/log/nginx:将主机/root/nginx/logs 目录挂载到容器的/var/log/nginx日志目录。
    -v /root/nginx/html:/usr/share/nginx/html :将主机当前目录下的/root/nginx/html目录挂载到容器的/usr/share/nginx/html静态资源目录。
    
    

    上面实现了宿主机目录和容器目录的绑定,但是是匿名数据卷的话没有名字,我们可以使用具名挂载给数据卷设定名字:

    docker run -id --name=c_nginx -p 80:80 -v conf:/etc/nginx -v logs:/var/log/nginx -v html:/usr/share/nginx/html nginx

    上面我们创建了conf数据卷,将容器内目录/etc/nginx/与宿主机目录var/lib/docker/volumes/conf/_data绑定;
    创建了logs目录,将容器内目录/var/log/nginx与宿主机目录var/lib/docker/volumes/logs/_data绑定;
    创建了html目录,将容器内目录/usr/share/nginx/html与宿主机目录var/lib/docker/volumes/html/_data绑定。


    注:docker没办法做到既指定数据卷的名字,又指定容器对应的目录。

    3.数据卷相关命令

    docker volume create

    创建数据卷

    docker volume ls

    查看所有数据卷

    docker volume rm

    删除指定数据卷

    docker volume inspect

    查看某个数据卷的详情

    docker volume prune

    清除数据卷

    docker volume create--容器与数据卷的挂载要在创建容器时配置,对于创建好的容器,是不能设置数据卷的。创建容器的过程中,数据卷会自动创建

    docker volume ls--当创建容器挂载完数据卷后,我们可以通过docker volume ls命令查看docker中存在的数据卷,但是只有具名数据卷,而没有匿名数据卷,也比较好理解,匿名数据卷知识一种绑定,严格意义上来讲应该也不算数据卷。

    docker volume rm--当我们删除容器后,匿名卷消失了,但是匿名卷和具名卷的目录都不会消失。我们可以通过rm的命令来删除具名卷,具名卷删除后具名卷对应的目录也消失了。

    docker volume inspect--通过inspect查看一下某个数据卷的相关信息。

    补充:我们可以通过docker inspect <容器名>查看一下某容器的所有数据卷信息(包括匿名数据卷和具名数据卷)

    例如:

    1. # 查看MySQL容器详细信息
    2. docker inspect mysql-server
    3. # 关注其中.Config.Volumes部分和.Mounts部分

    通过查看信息中的Mounts部分我们可以知道该容器挂载相关的信息,里面的name是数据卷的名字,source是指这个卷对应的宿主机的目录,destination则是挂载到容器内的目录。

    我们可以看到里面有一个名字很长的数据卷,但是我们创建的时候没挂载,这是怎么回事呢?这是因为mysql创建的时候自动帮我们挂载了数据卷。

    八.Docker Compose

    我们部署一个java项目,经常包含多个容器mysql,redis,mq,java项目,es等一个个。如果还像之前那样手动的逐一部署,就太麻烦了。

    而Docker Compose就可以帮助我们实现多个Docker容器的快速部署。它允许用户通过一个单独的 docker-compose.yml 配置文件来定义一组相关联的应用容器,进而实现快速部署一组关联的容器。

    1.编写compose

    docker-compose.yml文件的基本语法可以参考官方文档:

    Compose file version 3 reference | Docker Docs

    docker-compose文件中可以定义多个相互关联的应用容器,每一个应用容器被称为一个服务(service)。由于service就是在定义某个应用的运行时参数,因此与docker run参数非常相似。

    对比如下:

    docker run 参数

    docker compose 指令

    说明

    --name

    container_name

    容器名称

    -p

    ports

    端口映射

    -e

    environment

    环境变量

    -v

    volumes

    数据卷配置

    --network

    networks

    网络

    举例来说:

    1. #用docker run部署MySQL的命令如下:
    2. docker run -d \
    3. --name mysql \
    4. -p 3306:3306 \
    5. -e MYSQL_ROOT_PASSWORD=123 \
    6. -v ./mysql/data:/var/lib/mysql \
    7. -v ./mysql/conf:/etc/mysql/conf.d \
    8. -v ./mysql/init:/docker-entrypoint-initdb.d \
    9. --network flyingpig
    10. mysql
    11. #如果用docker-compose.yml文件来定义,就是这样:
    12. version: "3.8"
    13. services:
    14. mysql:
    15. image: mysql
    16. container_name: mysql
    17. ports:
    18. - "3306:3306"
    19. environment:
    20. MYSQL_ROOT_PASSWORD: 123
    21. volumes:
    22. - "./mysql/conf:/etc/mysql/conf.d"
    23. - "./mysql/data:/var/lib/mysql"
    24. networks:
    25. - new
    26. networks:
    27. new:
    28. name: flyingpig

    下面是我写的一个有mysql,redis和springboot下面的docker-compose
    【网络的话是直接与主机共享网络】

    1. version: '3'
    2. services:
    3. mysql-server:
    4. image: mysql:latest
    5. container_name: mysql-server
    6. network_mode: host
    7. environment:
    8. - MYSQL_ROOT_PASSWORD=123456
    9. restart: unless-stopped
    10. redis-server:
    11. image: redis:5.0
    12. container_name: redis-server
    13. network_mode: host
    14. restart: unless-stopped
    15. springboot-server:
    16. build:
    17. context: .
    18. dockerfile: ./springboot_dockerfile
    19. image: app:latest
    20. container_name: springboot-server
    21. network_mode: host
    22. volumes:
    23. - /root/springboot_data:/app
    24. restart: unless-stopped

    2.运行compose

    基本语法如下:docker compose [OPTIONS] [COMMAND]

    类型

    参数或指令

    说明

    Options

    -f

    指定compose文件的路径和名称

    -p

    指定project名称。project就是当前compose文件中设置的多个service的集合,是逻辑概念

    Commands

    up

    创建并启动所有service容器

    down

    停止并移除所有容器、网络

    ps

    列出所有启动的容器

    logs

    查看指定容器的日志

    stop

    停止容器

    start

    启动容器

    restart

    重启容器

    top

    查看运行的进程

    exec

    在指定的运行中容器中执行命令

    九.Docker与虚拟机比较

    与虚拟机一样,Docker的每个容器都是一个独立的沙箱,每个Docker容器之间的环境相互不影响。比如这个容器可以运行Java8的web项目,那个容器可以运行Java17的web项目之间不会产生冲突,而如果没有Docker,那么这两个环境只能运行一个。

    而且相比与重量级运行不同操作系统,模拟硬件的虚拟机,容器更加轻便,更加接近于原生。

    十.Docker总结

    所以我们再次回到文章开头的那个话题,Docker是干什么的?

    总结而言:Docker主要为我们提供了两个好处:
    (1)一个是简化部署。Docker提供了镜像和容器的功能,我们可以将我们写好的软件及其环境打包成镜像,然后我们只要通过这个镜像创建对应的容器实例就可以很方便的进行软件的部署。同时,docker还提供了docker compose,我们只需要通过一个文件,就可以一键部署很多软件。
    (2)另一个就是提供了环境隔离。与虚拟机一样,Docker的每个容器都是一个独立的沙箱,每个Docker容器之间的环境相互不影响。比如这个容器可以运行Java8的web项目,那个容器可以运行Java17的web项目之间不会产生冲突,而如果没有Docker,那么这两个环境只能运行一个。
    而且相比与虚拟机,容器更加轻便,更加接近于原生。

    现在你再来看这只鲸鱼是不是觉得这只鲸鱼很可爱?

    本文完。

  • 相关阅读:
    pdf文件怎么转化为word,pdf转换成word的方法
    卷妹带你回顾Java基础(一)每日更新Day2
    SpringMVC的请求(上)
    聊聊在不确定环境下的个人成长
    在 CentOS 8 上为Apache HTTPD 配置 HTTPS
    走进一心堂,读懂数字化转型新风向
    实现Spring的Ordered接口,控制Bean的初始化优先级最高
    Linux /etc/passwd和/etc/shadow
    windows查询端口占用
    从零开始的C++(十)
  • 原文地址:https://blog.csdn.net/bjjx123456/article/details/132674834