Docker狂神说笔记

狂神说Docker

名言金句:弱小和无知不是生存的阻碍,傲慢才是。

docker阿里云镜像地址:

yum-config-manager \    --add-repo \    http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo


Docker学习

  • Docker概述
  • Docker安装
  • Docker命令
    • 镜像命令
    • 容器命令
    • 操作命令
  • Docker镜像
  • 容器数据卷
  • DockerFile
  • Docker网络原理
  • IDEA整合Docker
  • Docker Compose
  • Docker Swarm
  • CI\CD Jenkins

知道的越多,不知道的越多!




Docker概述

Docker为什么会出现?

一款产品:开发-上线 两套环境!应用环境!应用配置!

开发—运维,问题:我在我的电脑上可以运行!版本更新,导致服务不可用!对于运维来说,考验就十分大!

环境配置是十分麻烦的,每一个及其都要部署环境(集群Redis、ES、Hadoop…)!耗时费力

发布一个项目(jar+(Redis、MySQL、jdk、ES)),项目能不能都待上环境安装打包!

之前在服务器配置一个应用的环境Redis、MySQL、jdk、ES、Hadoop,配置超麻烦,不能跨平台。

Windows开发,最后发布到Linux!

传统:开发jar,运维来做;

现在:开发打包部署上线,一套流程做完!

DevOps(开发+运维)

Docker给以上的问题,提出了解决方案!

在这里插入图片描述

Docker的思想就来自于集装箱;

JRE —多个应用(端口冲突) —原来都是交叉的!

隔离:Docker核心思想!打包装箱!每个箱子都是相互隔离的。
水果 生化武器
Docker通过隔离机制,可以将服务器利用到极致!

本质:所有的技术都是因为出现了一些问题,我们需要去解决,才去学习。


Docker的历史

2010年,几个搞IT的年轻人,就在美国成立了一家公司dotcloud;
做一些pass的云计算服务!LXC(Linux软件容器)有关的容器技术!
他们将自己的技术(容器化技术)命名就是Docker!
Docker刚刚诞生的时候,没有引起行业的这一!dotCloud,救活不下去!
开放源代码!
2013年,Docker开源!
越来越多的人发现Docker的有点!火了,Docker每个月会更新一个版本!


Docker能干嘛

之前的虚拟机技术

在这里插入图片描述
虚拟机技术的缺点:

  1. 资源占用十分多;
  2. 冗余步骤多;
  3. 启动很慢;

容器化技术

容器化技术不是模拟一个完整的操作系统;
在这里插入图片描述
内核
内核,是一个操作系统的核心。是基于硬件的第一层软件扩充,提供操作系统的最基本的功能,是操作系统工作的基础,它负责管理系统的进程、内存、设备驱动程序、文件和网络系统,决定着系统的性能和稳定性。

比较Docker与虚拟机技术的不同:

  • 传统虚拟机:通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统,然后在这个系统上安装和运行软件;
  • Docker:容器内的应用直接运行在宿主机的内核上,容器没有自己的内核,也没有虚拟我们的硬件,所以就轻便了;
  • Docker:每个容器间是互相隔离的,每个容器内都有一个属于自己的文件系统,都有一个独立的环境,互不影响;



DevOps(开发、运维)

应用更快捷的交付和部署
传统:一堆帮助文档,安装程序;
Docker:打包镜像发布测试,一键运行;

更便捷的升级和扩容
使用了Docker之后,我们部署应用就和搭积木一样!
SpringBoot 1.5、Redis 5、Tomcat 8
项目打包为一个镜像,扩展 服务器A!服务器B

更简单的系统运维
在容器化之后,我们的开发,测试环境都是高度一致的;

更高效的计算机资源利用
Docker是内核级别的虚拟化,可以再一个物理机上运行多个容器实例,服务器的性能可以被压榨到极致!



Docker的安装

Docker的基本组成

在这里插入图片描述
镜像(image):
docker镜像就好比是一个模板,可以通过这个模板来创建容器服务,Tomcat镜像===>run===>tomcat01容器(提供服务器),通过这个镜像可以创建多个容器(最终服务运行或者项目运行就是在容器中的)。

容器(container):
Docker利用容器技术,独立运行一个或者一组应用,通过镜像来创建的,
启动、停止、删除;基本命令;
目前就可以把这个容器理解为就是一个简易的linux系统;

仓库:
仓库就是存放镜像的地方;
仓库分为公有仓库和私有仓库;
Docker Hub(默认是国外的);
阿里云(国内镜像,可提速);




安装Docker

环境准备

  1. 需要会一点点的Linux的基础;
  2. CentOS 7
  3. 我们使用Xshell连接远程服务器进行操作!

环境查看

# 系统内核是3.10以上的
[root@localhost ~]# uname -r
3.10.0-862.el7.x86_64
[root@localhost ~]# cat /etc/os-release
NAME="CentOS Linux"
VERSION="7 (Core)"
ID="centos"
ID_LIKE="rhel fedora"
VERSION_ID="7"
PRETTY_NAME="CentOS Linux 7 (Core)"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:centos:centos:7"
HOME_URL="https://www.centos.org/"
BUG_REPORT_URL="https://bugs.centos.org/"

CENTOS_MANTISBT_PROJECT="CentOS-7"
CENTOS_MANTISBT_PROJECT_VERSION="7"
REDHAT_SUPPORT_PRODUCT="centos"
REDHAT_SUPPORT_PRODUCT_VERSION="7"

安装

帮助文档:

1、卸载旧的版本

 yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-engine

2、需要的安装包

yum install -y yum-utils

3、设置镜像的仓库

yum-config-manager \    --add-repo \    http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

更新yum软件包索引

yum makecache fast

4、安装docker(docker-ce:社区、ee:企业版)

yum install docker-ce docker-ce-cli containerd.io 

5、启动docker

systemctl start docker

6、查看docker是否安装成功

docker version

在这里插入图片描述
7、运行docker容器

docker run hello-world

在这里插入图片描述

8、查看一下下载的这个hello-world镜像

docker images

在这里插入图片描述
了解:卸载docker

1. 卸载依赖
yum remove docker-ce docker-ce-cli containerd.io

2. 删除资源
rm -rf /var/lib/docker

/var/lib/docker:docker的默认工作路径



回顾HelloWorld流程

在这里插入图片描述

底层原理

Docker是怎么工作的?
Docker是一个Client-Server结构的系统,Docker的守护进行(即Server)运行在宿主机上,通过Socket从客户端访问!
DockerServer接收到Docker-Client的指令,就会执行这个命令!
在这里插入图片描述

Docker为什么比VM快?

  1. Docker有着比虚拟机更少的抽象层。
  2. Docker利用的是宿主机的内核,VM需要是Guest OS(子操作系统)。
    在这里插入图片描述
    所以说,新建一个容器的时候,Docker不需要像虚拟机一样重新加载一个操作系统内核,避免引导。虚拟机是加载Guest OS,分钟级别的,而Docker是利用宿主机的操作系统,省略了这个复杂的过程,秒级!
    在这里插入图片描述





Docker的常用命令

官网文档:https://docs.docker.com/engine/reference/commandline/

帮助命令

docker version     # 显示Docker的版本信息
docker info        # 显示Docker的系统信息,包括镜像与容器数量
docker 命令 --help # 万能命令

镜像命令

docker images:查看所有本地主机上的镜像

[root@localhost ~]# docker images
REPOSITORY    TAG       IMAGE ID       CREATED         SIZE
hello-world   latest    9c7a54a9a43c   2 months ago    13.3kB
centos        latest    5d0da3dc9764   22 months ago   231MB

#  解释
REPOSITORY  镜像的仓库源
TAG         镜像的标签
IMAGE ID    镜像的id
CREATED     镜像的创建时间
SIZE        镜像的大小

#  可选相关
	-a, --all   # 列出所有镜像
	-q, --quiet # 只显示镜像的id

docker search搜索镜像

[root@localhost ~]# docker search tomcat  # 搜索tomcat镜像

docker pull 下载镜像

[root@localhost ~]# docker pull tomcat  # 下载tomcat镜像(没有指定版本,默认是最新版本)
Using default tag: latest
latest: Pulling from library/tomcat
9d19ee268e0d: Pull complete 
f2b566cb887b: Pull complete 
b375e6654ef5: Downloading  159.7MB/192.6MB
b375e6654ef5: Downloading  160.8MB/192.6MB
b375e6654ef5: Downloading  161.3MB/192.6MB
b375e6654ef5: Pull complete 
19452d1108a6: Pull complete 
b82f37793aff: Pull complete 
3c7dae5d5e11: Pull complete 
6e76f5caab20: Pull complete 
Digest: sha256:2729ebbf9c2fa54b83298eabfd3c1f67417e3a91094ebeb080c8d9e43dc1ce78
Status: Downloaded newer image for tomcat:latest
docker.io/library/tomcat:latest

注意:

  1. 采用的是分层下载,docker image的核心,联合文件系统;
  2. Digest:签名;
  3. docker.io/library/tomcat:latest:真实地址;

docker rmi 删除镜像:

[root@localhost ~]# docker rmi -f hello-world  # 此处的镜像名也可以换成镜像id(删除单个镜像)
[root@localhost ~]# docker rmi -f hello-world centos  # 同时删除多个镜像
[root@localhost ~]# docker rmi -f $(docker images -aq)   # 删除所有的镜像


容器命令

说明:我们有了镜像才可以创建容器,linux,下载一个centos镜像来测试学习
1.、下载镜像

docker pull centos

2、新建容器并启动

docker run [可选参数] image

# 参数说明
--name="Name"   容器名称tomcat01  tomcat02用来区分容器
-d              后台方式运行
-it             使用交互方式运行,进入容器查看内容
-p              指定容器的端口号 -p  8080:8080
	-p ip:主机端口:容器端口
	-p 主机端口:容器端口(常用)
	-p 容器端口
-P 随机指定端口

3、测试,启动并进入容器

[root@localhost ~]# docker run -it centos /bin/bash
[root@486c97420bd0 /]# ls
bin  etc   lib	  lost+found  mnt  proc  run   srv  tmp  var
dev  home  lib64  media       opt  root  sbin  sys  usr
[root@486c97420bd0 /]# exit  # 退出命令

4、列出所有运行的容器(docker ps)

[root@localhost ~]# docker ps
CONTAINER ID   IMAGE     COMMAND       CREATED         STATUS         PORTS     NAMES
94b04e652301   centos    "/bin/bash"   4 seconds ago   Up 4 seconds             sleepy_curie

dokcer ps -a  # 列出当前正在运行的容器+带出历史运行过的容器
	      -q  # 只显示容器的编号

5、退出容器

exit             # 直接容器停止并退出
Ctrl+P+Q   # 容器不停止退出

6、删除容器

docker rm -f 容器id             # 删除指定的容器(可以不加上-f,如此则不能删除正在运行的容器)
docker rm -f $(docker ps -aq)   # 删除所有的容器

7、启动和停止容器的操作

docker start 容器id     # 启动容器
docker restart 容器id   # 重启容器
docker stop 容器id      # 停止当前正在运行的容器
docker kill 容器id      # 强制停止当前容器


常用其他命令

后台启动容器

#命令docker run -d 镜像名
[root@localhost ~]# docker run -d centos
f7fac32343a688f861ca6b83410622f82fb779f33b4d95e7254b975c4563886d

#调用docker ps 发现centos停止了

#常见的坑:docker容器使用后台运行,就必须要有一个前台进程,docker发现没有应用,就会自动停止;
#同样的nginx 容器启动后,发现自己没有提供服务,就会立刻停止,就是没有程序了;

查看日志

docker logs -f -t -tail 10 容器id

#另开一个终端,输入命令:
[root@localhost ~]# docker run -it centos /bin/bash

#进入之后,输入以下命令:
[root@b5f6d7e5b519 /]# while true;do echo kuangshen;sleep 1;done

#在原终端输入,查看日志命令:
[root@localhost ~]# docker logs -t -f --tail 10 b5f6d7e5b519
2023-07-27T12:45:13.263282615Z kuangshen
2023-07-27T12:45:14.265265038Z kuangshen
...

查看容器中进程信息

#命令docker top 容器id
[root@localhost ~]# docker top 8a524532e706
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
root                12611               12593               0                   20:50               pts/0               00:00:00            /bin/bash

查看镜像的元数据

#命令docker inspect 容器id
[root@localhost ~]# docker inspect 8a524532e706
[
    {
        "Id": "8a524532e706606c79202e4e0b6d475291b1d27bb480db904007290c31f69db4",
        "Created": "2023-07-27T12:50:43.282010909Z",
        "Path": "/bin/bash",
        "Args": [],
        "State": {
            "Status": "running",
            "Running": true,
            "Paused": false,
            "Restarting": false,
            "OOMKilled": false,
            "Dead": false,
            "Pid": 12611,
            "ExitCode": 0,
            "Error": "",
            "StartedAt": "2023-07-27T12:50:43.609984356Z",
            "FinishedAt": "0001-01-01T00:00:00Z"
        },
        "Image": "sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6",
        "ResolvConfPath": "/var/lib/docker/containers/8a524532e706606c79202e4e0b6d475291b1d27bb480db904007290c31f69db4/resolv.conf",
        "HostnamePath": "/var/lib/docker/containers/8a524532e706606c79202e4e0b6d475291b1d27bb480db904007290c31f69db4/hostname",
        "HostsPath": "/var/lib/docker/containers/8a524532e706606c79202e4e0b6d475291b1d27bb480db904007290c31f69db4/hosts",
        "LogPath": "/var/lib/docker/containers/8a524532e706606c79202e4e0b6d475291b1d27bb480db904007290c31f69db4/8a524532e706606c79202e4e0b6d475291b1d27bb480db904007290c31f69db4-json.log",
        "Name": "/sweet_brattain",
        "RestartCount": 0,
        "Driver": "overlay2",
        "Platform": "linux",
        "MountLabel": "",
        "ProcessLabel": "",
        "AppArmorProfile": "",
        "ExecIDs": null,
        "HostConfig": {
            "Binds": null,
            "ContainerIDFile": "",
            "LogConfig": {
                "Type": "json-file",
                "Config": {}
            },
            "NetworkMode": "default",
            "PortBindings": {},
            "RestartPolicy": {
                "Name": "no",
                "MaximumRetryCount": 0
            },
            "AutoRemove": false,
            "VolumeDriver": "",
            "VolumesFrom": null,
            "ConsoleSize": [
                24,
                80
            ],
            "CapAdd": null,
            "CapDrop": null,
            "CgroupnsMode": "host",
            "Dns": [],
            "DnsOptions": [],
            "DnsSearch": [],
            "ExtraHosts": null,
            "GroupAdd": null,
            "IpcMode": "private",
            "Cgroup": "",
            "Links": null,
            "OomScoreAdj": 0,
            "PidMode": "",
            "Privileged": false,
            "PublishAllPorts": false,
            "ReadonlyRootfs": false,
            "SecurityOpt": null,
            "UTSMode": "",
            "UsernsMode": "",
            "ShmSize": 67108864,
            "Runtime": "runc",
            "Isolation": "",
            "CpuShares": 0,
            "Memory": 0,
            "NanoCpus": 0,
            "CgroupParent": "",
            "BlkioWeight": 0,
            "BlkioWeightDevice": [],
            "BlkioDeviceReadBps": [],
            "BlkioDeviceWriteBps": [],
            "BlkioDeviceReadIOps": [],
            "BlkioDeviceWriteIOps": [],
            "CpuPeriod": 0,
            "CpuQuota": 0,
            "CpuRealtimePeriod": 0,
            "CpuRealtimeRuntime": 0,
            "CpusetCpus": "",
            "CpusetMems": "",
            "Devices": [],
            "DeviceCgroupRules": null,
            "DeviceRequests": null,
            "MemoryReservation": 0,
            "MemorySwap": 0,
            "MemorySwappiness": null,
            "OomKillDisable": false,
            "PidsLimit": null,
            "Ulimits": null,
            "CpuCount": 0,
            "CpuPercent": 0,
            "IOMaximumIOps": 0,
            "IOMaximumBandwidth": 0,
            "MaskedPaths": [
                "/proc/asound",
                "/proc/acpi",
                "/proc/kcore",
                "/proc/keys",
                "/proc/latency_stats",
                "/proc/timer_list",
                "/proc/timer_stats",
                "/proc/sched_debug",
                "/proc/scsi",
                "/sys/firmware"
            ],
            "ReadonlyPaths": [
                "/proc/bus",
                "/proc/fs",
                "/proc/irq",
                "/proc/sys",
                "/proc/sysrq-trigger"
            ]
        },
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/8bfb921e310b7232ffcccb78e209461fd9cc8ec21cbae4112692d76c9e582609-init/diff:/var/lib/docker/overlay2/1af059a2e44d0a09b9672b2c72768a920d0d015359a90888a3d9e4e83913c18b/diff",
                "MergedDir": "/var/lib/docker/overlay2/8bfb921e310b7232ffcccb78e209461fd9cc8ec21cbae4112692d76c9e582609/merged",
                "UpperDir": "/var/lib/docker/overlay2/8bfb921e310b7232ffcccb78e209461fd9cc8ec21cbae4112692d76c9e582609/diff",
                "WorkDir": "/var/lib/docker/overlay2/8bfb921e310b7232ffcccb78e209461fd9cc8ec21cbae4112692d76c9e582609/work"
            },
            "Name": "overlay2"
        },
        "Mounts": [],
        "Config": {
            "Hostname": "8a524532e706",
            "Domainname": "",
            "User": "",
            "AttachStdin": true,
            "AttachStdout": true,
            "AttachStderr": true,
            "Tty": true,
            "OpenStdin": true,
            "StdinOnce": true,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": [
                "/bin/bash"
            ],
            "Image": "centos",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": {
                "org.label-schema.build-date": "20210915",
                "org.label-schema.license": "GPLv2",
                "org.label-schema.name": "CentOS Base Image",
                "org.label-schema.schema-version": "1.0",
                "org.label-schema.vendor": "CentOS"
            }
        },
        "NetworkSettings": {
            "Bridge": "",
            "SandboxID": "34c854b249a154eb72474fffe4a43ae2d0db25afd6d1e3521acf4be33da98328",
            "HairpinMode": false,
            "LinkLocalIPv6Address": "",
            "LinkLocalIPv6PrefixLen": 0,
            "Ports": {},
            "SandboxKey": "/var/run/docker/netns/34c854b249a1",
            "SecondaryIPAddresses": null,
            "SecondaryIPv6Addresses": null,
            "EndpointID": "1dd6552d21d4f53abda6abb167e661b82ab51668a337f11f938081bc9a2d10d2",
            "Gateway": "172.17.0.1",
            "GlobalIPv6Address": "",
            "GlobalIPv6PrefixLen": 0,
            "IPAddress": "172.17.0.2",
            "IPPrefixLen": 16,
            "IPv6Gateway": "",
            "MacAddress": "02:42:ac:11:00:02",
            "Networks": {
                "bridge": {
                    "IPAMConfig": null,
                    "Links": null,
                    "Aliases": null,
                    "NetworkID": "a62c1bab69896f54a38f459edf7ce6c84b02e46c68f57ffd88d66c6ed2da94b6",
                    "EndpointID": "1dd6552d21d4f53abda6abb167e661b82ab51668a337f11f938081bc9a2d10d2",
                    "Gateway": "172.17.0.1",
                    "IPAddress": "172.17.0.2",
                    "IPPrefixLen": 16,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "MacAddress": "02:42:ac:11:00:02",
                    "DriverOpts": null
                }
            }
        }
    }
]

进入当前正在运行的容器
方式一:
我们通常容器都是使用后台方式运行的,需要进入容器,修改一些配置
命令:docker exec -it 容器id /bin/bash

#测试
[root@localhost ~]# docker ps
CONTAINER ID   IMAGE     COMMAND       CREATED          STATUS          PORTS     NAMES
5ae382de8660   centos    "/bin/bash"   21 seconds ago   Up 20 seconds             wizardly_nightingale
[root@localhost ~]# docker exec -it 5ae382de8660 /bin/bash
[root@5ae382de8660 /]# ls
bin  dev  etc  home  lib  lib64  lost+found  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

#查看所有进程信息
[root@5ae382de8660 /]# ps -ef
UID         PID   PPID  C STIME TTY          TIME CMD
root          1      0  0 12:59 pts/0    00:00:00 /bin/bash
root         15      0  0 13:00 pts/1    00:00:00 /bin/bash
root         30     15  0 13:02 pts/1    00:00:00 ps -ef

#退出容器
[root@5ae382de8660 /]# exit
exit
[root@localhost ~]# 

方式二:
命令:docker attach 容器id

[root@localhost ~]# docker attach 5ae382de8660
[root@5ae382de8660 /]# ls
bin  dev  etc  home  lib  lib64  lost+found  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

从容器内拷贝文件到主机上
命令:docker cp 容器id:容器内路径 目的主机路径

[root@localhost ~]# docker ps
CONTAINER ID   IMAGE     COMMAND       CREATED          STATUS          PORTS     NAMES
5b4122b05506   centos    "/bin/bash"   12 seconds ago   Up 11 seconds             beautiful_dewdney
[root@localhost ~]# docker exec -it 5b4122b05506 /bin/bash
[root@5b4122b05506 /]# ls
bin  etc   lib	  lost+found  mnt  proc  run   srv  tmp  var
dev  home  lib64  media       opt  root  sbin  sys  usr
[root@5b4122b05506 /]# cd tmp
[root@5b4122b05506 tmp]# ls
ks-script-4luisyla  ks-script-o23i7rc2	ks-script-x6ei4wuu
[root@5b4122b05506 tmp]# touch lq.txt
[root@5b4122b05506 tmp]# ls
ks-script-4luisyla  ks-script-o23i7rc2	ks-script-x6ei4wuu  lq.txt
[root@5b4122b05506 tmp]# exit
exit
[root@localhost ~]# ps
   PID TTY          TIME CMD
 11873 pts/1    00:00:00 bash
 14635 pts/1    00:00:00 ps
[root@localhost ~]# docker ps
CONTAINER ID   IMAGE     COMMAND       CREATED              STATUS              PORTS     NAMES
5b4122b05506   centos    "/bin/bash"   About a minute ago   Up About a minute             beautiful_dewdney
[root@localhost ~]# docker cp 5b4122b05506:/tmp/lq.txt /tmp
Successfully copied 1.54kB to /tmp

拷贝是一个手动过程,未来我们使用容器卷技术,可以实现自动;




作业练习

docker安装nginx

1、搜索镜像  docker search 镜像名(此处省略,默认下载最新的)
2.、下载镜像 docker pull 镜像名
3、运行测试

[root@localhost ~]# docker pull nginx

[root@localhost ~]# docker run -d --name nginx01 -p 3304:80 nginx

[root@localhost ~]# docker top ed28250c389c
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
root                15140               15121               0                   21:23               ?                   00:00:00            nginx: master process nginx -g daemon off;
101                 15193               15140               0                   21:23               ?                   00:00:00            nginx: worker process

[root@localhost ~]# curl localhost:3304
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

端口暴露的概念
在这里插入图片描述
思考问题:我们每次改动nginx配置文件,都需要进入容器内部?十分麻烦,我要是可以在容器外部提供一个映射路径,达到在外部修改文件,容器内部就可以自动修改?-v 容器卷




Docker镜像讲解

镜像是什么

镜像是一种轻量级,可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件,它包含运行某个软件所需的所有内容,包括代码、运行时库、环境变量和配置文件。

所有的应用,直接打包docker镜像,就可以直接跑起来!

镜像的获取方式:

  • 从远程仓库下载
  • 拷贝
  • 制作一个镜像DockerFile

Docker镜像加载原理

UnionFS(联合文件系统)

UnionFS(联合文件系统):Union文件系统(UnionFS)是一种分层,轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下。Union文件系统是Docker镜像的基础,镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。

特性:一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录;

Docker镜像加载原理

docker的镜像实际上由一层一层的文件系统组成,这种层级的文件系统就是UnionFS。

系统启动引导加载过程

bootfs(boot file system) 主要包含bootloader(加载器)和kernel(内核), bootloader(加载器)主要是引导加载kernel(内核), Linux刚启动时会加载bootfs文件系统,Docker镜像的最底层是bootfs。这一层与我们典型的Linux/Unix系统是一样的,包含bootloader(加载器)和kernel(内核)。当boot加载完成之后整个内核就都在内存中了,此时内存的使用权已由bootfs转交给内核,此时系统也会卸载bootfs(平时开机即启动系统时需要用到bootloader(加载器),开机的速度非常的久,但Docker的容器的内核用的是主机的内核,主机已经启动内核,容器不需要再用到bootloader(加载器)来启动内核,所以Docker容器的启动速度非常的快)

rootfs (root file system),在bootfs之上。包含的就是典型Linux系统中的/dev, /proc, /bin, /etc等标准目录和文件,简单来说rootfs就是各种不同的操作系统发行版,比如Ubuntu , Centos等等。(rootfs可以很小,只需包含最基本的命令、工具和程序即可,而内核直接使用主机的kernel(内核)即可,自己只需要提供rootfs即可,所以传统安装操作系统镜像需要几个G的大小,而Docker用来安装容器的操作系统镜像只需要几百MB即可;而且对于不同的Linux发行版,它们的内核都是Linux的内核,所以bootfs基本保持一致,rootfs会有差别,因此不同的Linxu发行版可以公用bootfs)

bootfs是系统引导过程中所使用的文件系统,rootfs是操作系统运行时所使用的文件系统;
在这里插入图片描述

分层理解

分层的镜像
我们可以去下载一个进行,注意观察下载的日志输出,可以看到是一层一层下载!
在这里插入图片描述

思考:为什么Docker镜像要采用这种分层的结构呢?
最大的好处,我觉得莫过于是资源共享了!比如有多个镜像都从相同的Base镜像构建而来的,那么宿主机只需要在磁盘上保留一份base镜像,同时内存中也只需要加载一份base镜像,这样就可以为所有的容器服务了,而且镜像的每一层都可以被共享。
查看镜像分层的方式可以通过:docker image inspect命令

[root@localhost ~]# docker image inspect redis:latest

在这里插入图片描述

理解:
所有的Docker镜像都起始于一个基础镜像层,当进行修改或增加新的内容时,就会在当前镜像层之上,创建新的镜像层。
举一个简单的例子,例如基于Ubuntu Linux 16.04创建一个新的镜像,这就是新镜像的第一层;如果在该镜像中添加python包,就会在基础镜像层之上创建第二个镜像层;如果继续添加一个安全补丁,就会创建第三个镜像层。
在添加额外镜像层的同事,镜像始终保持着当前所有镜像层的组合。


特点
Docker镜像都是只读的,当容器启动时,一个新的可写层被加载到镜像的顶部!
这一层就是我们通常说的容器层,容器之下都叫镜像层。



如何提交一个自己的镜像
commit镜像

docker commit 提交容器成为一个新的版本

命令和git原理类似
docker commit -m="提交的描述信息"  -a="作者" 容器id 目标镜像名:[tag]

实战测试

#启动一个tamcat
[root@localhost ~]# docker run -it -p 8080:8080 tomcat

#发现这个默认的tomcat是没有webapp应用,镜像的原因,官方的镜像默认webapps下面是没有文件的!

#我自己拷贝基本的文件(从webapps.dist)
root@0f329b194534:/usr/local/tomcat# cp -r webapps.dist/* webapps/

#提交自己的镜像
[root@localhost ~]# docker commit -a="lq" -m="add webapps ap" 0f329b194534 tomcat02:1.0

在这里插入图片描述

学习方式说明:理解概念,但是一定要时间,最后时间和理论相结合,一次搞定这个知识!
如果你想要保存当前容器的状态,就可以通过commit来提交,获得一个镜像,
就好比我们以前学习VM时候,快照!
到了这里才算是入门Docker!认真吸收练习!




容器数据卷

什么是容器数据卷


docker的理念回顾

将应用和环境打包成一个镜像!

数据?如果数据都在容器中,那么我们容器删除,数据就会丢失!需求:数据可以持久化

MySQL,容器删了,删库跑路!需求:MySQL数据可以存储在本地!

容器之间可以有一个数据共享的技术!Docker容器中产生的数据,同步到本地!

这就是卷技术!目录的挂在,将我们容器内的目录,挂载到Linux上面(宿主机)!
在这里插入图片描述
总结一句话:容器的持久化和同步操作!容器间也可以数据共享的



使用数据卷

方式一:直接使用命令来挂载

docker run -it -v 主机目录:容器内目录

修改主机目录下的文件,容器内的目录文件也会发生相应的变化(包括删除)。



实战:安装MySQL

#获取镜像
docker pull mysql

#运行容器,需要做数据挂载!#安装启动mysql,需要配置密码的,这是要注意点!
#官方测试:docker run --name mysql01 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:tag

#启动我们的mysql
-d 后台运行
-p 端口映射
-v 卷挂载
-e 环境配置
--name 容器名字
docker run -d -p 3306:3306 -v /home/mysql/conf:/etc/mysql/conf.d -v /hone/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 --name mysql01 mysql

注意:即使我们将容器删除之后,挂载到本地的数据卷依旧没有丢失,这就实现了容器数据持久化功能!

具名和匿名挂载

# 匿名挂载
-v  容器内路径!
-P 随机端口
docker run -d -P --name nginx01 -v /etc/nginx nginx

# 查看所有的volume的情况
docker volume ls
DRIVER    VOLUME NAME
local     2b89da43cd43b0e513ab227ad16a4e56a66da53493b4f8a58c0e8b9e4de2d299

# 这里发现,上面这种就是匿名挂载,我们在 -v 只写了容器内的路径,没有些容器外的路径!

# 具名挂载
docker run -d -P --name nginx02 -v juming-nginx:/etc/nginx nginx

# 通过-v 卷名:容器内路径
#查看一下这个卷
docker volume inspect juming-nginx

所有的docker容器内的卷,没有指定目录的情况下都在/var/lib/docker/volumes/xxx/_data
我们通过具名挂载可以方便的找到我们的一个卷,大多数情况再使用的是:具名挂载!

# 如何确定是具名挂载还是匿名挂载,还是执行路径挂载!
-v 容器内路径   # 匿名挂载
-v 卷名:容器内路径   # 具名挂载
-v 宿主机路径:容器内路径  # 指定路径挂载!

拓展:

# 通过 -v 容器内路径:ro rw 改变读写权限
ro  readonly  # 只读
rw  readwrite # 可读可写

# 一旦设置了容器权限,容器对我们挂载出来的内容就有了限定
docker run -d -P --name nginx02 -v juming-nginx:/etc/nginx:ro nginx
docker run -d -P --name nginx02 -v juming-nginx:/etc/nginx:rw nginx

# ro 只要看到ro就说明这个路径只能通过宿主机来操作,容器内无法操作!



DockerFile

初试Dockerfile

Dockerfile就是用来构建docker镜像的构建文件!命令脚本!先体验一下!
通过这个脚本可以生成镜像,镜像是一层一层的,脚本一个个的命令,每个命令都是一层!

# 创建一个dockerfile文件,名字可以随机,建议Dockerfile
#文件内容
FROM centos
# 挂载容器内的目录,比如:volume01不存在,则会在根陆目录下创建
VOLUME ["volume01","volume02"]

CMD echo "---end---"
CMD /bin/bash
注意:这里的每一个命令,都是镜像的一层

查看一下挂在卷的路径在这里插入图片描述

测试一下刚才的文件是否同步出去了!
这种方式我们未来使用的十分多,因为我们通常会构建自己的镜像!
假设构建镜像时没有挂载卷,要手动镜像挂载 -v 卷名:容器内路径



数据卷容器

多个mysql同步数据
在这里插入图片描述

 #通过--volumes-from 进行继承,实现容器之间的数据共享
docker run -it --name docker02 --volumes-from docker01 139235ade348  /bin/bash

容器之间的数据是互相拷贝的

多个mysql同步数据!

docker run -d -p 3310:3306 -v /etc/mysql/conf.d -v /var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 --name mysql01  mysql:5.7

docker run -d -p 3310:3306  -e MYSQL_ROOT_PASSWORD=123456 --name mysql02 --volumes-from mysql01  mysql:5.7

这个时候,就可以实现两个容器的数据同步

结论:

容器之间配置信息的传递,数据卷容器的声明周期一直持续到没有容器使用为止。

但是一旦持久化到了本地,这个时候,本地的数据是不会删除的!





DockerFile

dockerfile是用来构建docker镜像的文件!命令参数脚本!
构建步骤:
1、编写一个dockerfile文件;
2、docker build 构建成为一个镜像;
3、docker run 运行镜像;
4、docker push 发布镜像(DockerHub、阿里云镜像仓库)


DockerFile构建过程

基础知识:
1、每个保留关键字(指令)都是必须是大写字母;
2、执行从上到下顺序执行;
3、# 表示注释;
4、每一个指令都会创建提交一个新的镜像层,并提交!
在这里插入图片描述
dockerfile是面向开发的,我们以后要发布项目,做镜像,就需要编写DockerFile文件,这个文件十分简单!

Docker镜像逐渐成为企业交付的标准,必须要掌握!

步骤:开发、部署、运维缺一不可!

DockerFile:构建文件,定义了一切步骤,源代码

DockerImages:通过DockerFile构建生成的镜像,最终发布和运行的产品!

Docker容器:容器就是镜像运行起来的服务器



DockerFile的指令

指令描述
FROM它的妈妈是谁(基础镜像)
MAINTAINER告诉别人,你创造了它(维护者信息)
RUN你先让它干啥(把命令前面加上RUN)
ADD往它肚子里放点文件(COPY文件,会自动解压)
WORKDIR我是cd,今天刚化了妆(当前工作目录)
VOLUME给我一个存放行李的地方(目录挂载)
EXPOSE我要打开的门是啥(端口)
CMD指定这个容器启动的时候要运行的命令,只有最后一个会生效,可被替代
ENTRYPOINT指定这个容器启动时候要运行的命令
COPY类似ADD,将我们文件拷贝到镜像中
ENV构建的时候设置环境变量


实战测试

Docker Hub中99%镜像都是从这个基础镜像过来的FROM scratch,然后配置需要的软件和配置来进行构建

centos镜像构建的dockerfile

FROM scratch
ADD centos-7-x86_64-docker.tar.xz /

LABEL \
    org.label-schema.schema-version="1.0" \
    org.label-schema.name="CentOS Base Image" \
    org.label-schema.vendor="CentOS" \
    org.label-schema.license="GPLv2" \
    org.label-schema.build-date="20201113" \
    org.opencontainers.image.title="CentOS Base Image" \
    org.opencontainers.image.vendor="CentOS" \
    org.opencontainers.image.licenses="GPL-2.0-only" \
    org.opencontainers.image.created="2020-11-13 00:00:00+00:00"

CMD ["/bin/bash"]

创建一个自己的centos(dockerfile文件)

FROM centos
MAINTAINER author

ENV MYPATH /usr/local
WORKDIR $MYPATH

RUN yum -y install vim
RUN yum -y install net-tools

EXPOSE 80

CMD echo $MYPATH
CMD echo "-----end-----"
CMD /bin/bash 

构建命令

docker build -f dockerfile -t mycentos:1.0.0 .

Tomcat实战

1、准备镜像文件,tomcat压缩包,jdk的压缩包;
2、编写dockerfile文件,官方命名Dockerfile,build会自动寻找这个文件,就不需要-f指定了!

FROM centos:latest
MAINTAINER 123456@qq.com

COPY readme.txt /usr/local/redme.txt

ADD jdk-8u381-linux-x64.tar.gz /usr/local/
ADD apache-tomcat-9.0.78.tar.gz /usr/local/

ENV MYPATH /usr/local
WORKDIR $MYPATH

ENV JAVA_HOME /usr/local/jdk1.8.0_381
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
ENV CATALINA_HOME /usr/local/apache-tomcat-9.0.78
ENV CATALINA_BASE /usr/local/apache-tomcat-9.0.78
ENV PATH $PATH;$JAVA_HOME/bin:$CATALINA_HOME/lib:$CATALINA_HOME/bin

EXPOSE 8080
# 此处的CMD命令不知为什么没有生效
CMD ["sh","/usr/local/apache-tomcat-9.0.78/bin/startup.sh"]

3、构建镜像

[root@localhost tomcat]# docker build -t mytomcat:1.0.0 .

4、启动镜像;
5、访问测试
6、发布项目(由于做了卷挂载,我们直接在本地编写项目就可以发布了!)
地址:https://hub.docker.com/

[root@localhost tomcat]# docker login --help

Usage:  docker login [OPTIONS] [SERVER]

Log in to a registry.
If no server is specified, the default is defined by the daemon.

Options:
  -p, --password string   Password
      --password-stdin    Take the password from stdin
  -u, --username string   Username

7、登录完毕后就可以提交镜像了,就是一步:docker push mytomcat:1.0.0
提交的时候也是按照镜像的层级来进行提交的!

阿里云镜像

1、登录阿里云
2、找到容器镜像服务
3、创建命名空间
4、创建容器镜像
5、浏览阿里云




Docker网络

清空所有环境
docker rm -f $(docker ps -a)
docker rmi -f $(docker images -aq)

测试
在这里插入图片描述
三个网络

问题:docker是如何处理容器网络访问的?
在这里插入图片描述

# 启动一个tomcat
[root@localhost ~]# docker run -d -it -p 8080:8080 --name tomcat01 tomcat

# 查看容器的内部网络地址(进入容器内部)
ip addr

root@b236a2aba624:/usr/local/tomcat# ip addr  发上线容器启动的时候会得到一个ip地址,docker分配的!
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
6: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever


# 如何ip命令找不到,则执行如下名之后,在执行ip addr
apt-get update & apt-get install -y iproute2

# 思考:linux能不能ping通容器内部!
[root@localhost ~]# ping 172.17.0.2
PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data.
64 bytes from 172.17.0.2: icmp_seq=1 ttl=64 time=0.041 ms
64 bytes from 172.17.0.2: icmp_seq=2 ttl=64 time=0.040 ms
64 bytes from 172.17.0.2: icmp_seq=3 ttl=64 time=0.042 ms
64 bytes from 172.17.0.2: icmp_seq=4 ttl=64 time=0.040 ms

# linux可以ping通docker容器内部

原理

1、我们每启动一个docker容器,docker就会给docker容器分配一个ip,我们只要安装了docker,就会有一个网卡docker0桥接模式,使用的技术是veth-pair技术!

  • 一个容器带来的网卡,都是一对对的;
  • veth-pair就是一堆的虚拟设备解耦,他们都是成对出现的,一段连接着协议,一段彼此相连;
  • 正因为有这个特性,veth-pair充当一个桥梁,连接各种虚拟网络设备的;
  • OpenStac,Docker容器之间的连接,OVS的连接,都是使用veth-pair技术;

在这里插入图片描述
结论:tomcat01和tomcat02是公用的一个路由器,docker0
所有的容器不指定网络的情况下,都是docker0路由的,docker会给我们的容器分配一个默认的可用IP

小结
Docker使用的是Linux的桥接,宿主机中是一个Docker容器的网桥docker0;

在这里插入图片描述
Docker中的所有的网络接口都是虚拟的。虚拟的转发效率高!(内网传递文件!)
只要容器删除,对应网桥一对就没了!

–link

思考一个场景,我们编写了一个微服务,database url=ip:,项目不重启,数据库ip换掉了,我们希望可以处理这个问题,可以名字来进行访问容器?

[root@localhost ~]# docker exec -it tomcat01 ping tomcat02
ping: tomcat02: Name or service not known

# 如何解决呢?
# 通过--link既可以解决网络联通问题
[root@localhost ~]# docker run -d -P --name tomcat03 --link tomcat01 tomcat
f30f91b517dd43f8689e7f20e7b02dc381f78cba492bc3687978b5346504e5f4

[root@localhost ~]# docker exec -it tomcat03 ping tomcat01
PING tomcat01 (172.17.0.2) 56(84) bytes of data.
64 bytes from tomcat01 (172.17.0.2): icmp_seq=1 ttl=64 time=0.187 ms
64 bytes from tomcat01 (172.17.0.2): icmp_seq=2 ttl=64 time=0.051 ms
64 bytes from tomcat01 (172.17.0.2): icmp_seq=3 ttl=64 time=0.052 ms

其实这个tomcat03就是在本地配置了tomcat02的配置?

# 查看hosts配置,在这里原理发现!
[root@localhost ~]# docker exec -it tomcat03 cat /etc/hosts
127.0.0.1	localhost
::1	localhost ip6-localhost ip6-loopback
fe00::0	ip6-localnet
ff00::0	ip6-mcastprefix
ff02::1	ip6-allnodes
ff02::2	ip6-allrouters
172.17.0.2	tomcat01 b236a2aba624
172.17.0.3	f30f91b517dd

本质探究:–link就是我们在hosts配置中添加了一个172.17.0.2 tomcat01 b236a2aba624
我们现在在玩Docker已经不建议使用–link了!
自定义网络!

自定义网络

查看所有的docker网络

[root@localhost ~]# docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
f1e20468cd30   bridge    bridge    local
b9404c3c4ce9   host      host      local
0c1cc9011159   none      null      local

网络模式

  • bridge:桥接docker(默认,自己自定义网络也使用bridge模式)
  • none:不配置网络
  • host:和宿主机共享网络
  • container:容器网络联通!(用的少,局限很大)

测试

# 我们直接启动的命令 --net bridge,而这个就是我们的docker0
docker run -d -P --name tomcat01 tomcat
docker run -d -P --name tomcat01 --net bridget tomcat

# docker0特点:默认,域名不能访问,--link可以打通连接!

# 我们可以自定义一个网络
# --driver bridge  网络模式
# --subnet 192.168.0.0/16  子网掩码(16是起始位置,16:256*256,24则是:256)
# --gateway 192.168.0.1  网关
[root@localhost ~]# docker network create --driver bridge --subnet 192.168.0.0/16 --gateway 192.168.0.1 mynet
6f1fbc563df6baa7eba88c28266cc72d1dcdac7e85984368aab092f158650c6b
[root@localhost ~]# docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
f1e20468cd30   bridge    bridge    local
b9404c3c4ce9   host      host      local
6f1fbc563df6   mynet     bridge    local
0c1cc9011159   none      null      local
[root@localhost ~]# docker run -d -P --name tomcat-net-01 --net mynet tomcat
aee0dd88c681d2aa64533f949bb2f20ad47a39264de3ab80fbdea547d98298fc
[root@localhost ~]# docker run -d -P --name tomcat-net-02 --net mynet tomcat
ef9dcb27150c3e0b04d2e20d29d5dc9f9b45ca30176d689a819ebb5c97fa4886
[root@localhost ~]# docker network inspect mynet
[
    {
        "Name": "mynet",
        "Id": "6f1fbc563df6baa7eba88c28266cc72d1dcdac7e85984368aab092f158650c6b",
        "Created": "2023-08-13T11:13:25.869847895+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "192.168.0.0/16",
                    "Gateway": "192.168.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "aee0dd88c681d2aa64533f949bb2f20ad47a39264de3ab80fbdea547d98298fc": {
                "Name": "tomcat-net-01",
                "EndpointID": "4da28302016a97c9e5437202286b8a5d35c925daf1592122c405816564aad281",
                "MacAddress": "02:42:c0:a8:00:02",
                "IPv4Address": "192.168.0.2/16",
                "IPv6Address": ""
            },
            "ef9dcb27150c3e0b04d2e20d29d5dc9f9b45ca30176d689a819ebb5c97fa4886": {
                "Name": "tomcat-net-02",
                "EndpointID": "82fcb869bd200ca62a06199823104fb083296e3d0200b8d3139982aabba461f4",
                "MacAddress": "02:42:c0:a8:00:03",
                "IPv4Address": "192.168.0.3/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

自定义的网络即使不使用–link,也可以ping通名字;

我们自定义的网络docker都已经帮我们维护好了对应的关系,推荐我们平时这样使用网络!

好处:
redis:不同的集群使用不同的网络,保证集群是安全和健康的;
mysql:不同的集群使用不同的网络,保证集群是安全和健康的



网络连通

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

# 测试打通 tomcat01 与mynet
# 联通之后就是将tomcat01放到mynet网络下
# 一个容器两个ip地址
# 阿里云服务:公网ip  私网ip

在这里插入图片描述

[root@localhost ~]# docker exec -it tomcat01 ping tomcat-net-01
PING tomcat-net-01 (192.168.0.3) 56(84) bytes of data.
64 bytes from tomcat-net-01.mynet (192.168.0.3): icmp_seq=1 ttl=64 time=0.063 ms
64 bytes from tomcat-net-01.mynet (192.168.0.3): icmp_seq=2 ttl=64 time=0.088 ms
64 bytes from tomcat-net-01.mynet (192.168.0.3): icmp_seq=3 ttl=64 time=0.051 ms

结论:加入要跨网络操作别人,就需要使用docker network connect连通!



SpringBoot微服务打包Docker镜像

1、创建一个springboot项目;
2、pom同级目录下创建Dockerfile文件;

FROM openjdk:8

COPY *.jar /app.jar

CMD ["--server.port=8080"]

EXPOSE 8080

ENTRYPOINT ["java","-jar","/app.jar"]

3、将springboot项目打出来的jar与Dockerfile文件,传输至Linux机器上;
4、构建docker镜像;

docker build -f Dockerfile -t xxx:1.0.0 .

5、发布自定义的docker镜像

[root@localhost idea]# docker push springboot-docker:1.0.0
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值