容器云系列之容器技术相关概念

最近参加容器技术有关培训,了解到容器技术的发展越来越成熟和趋于标准化。本文主要介绍容器技术的相关概念,包括Docker的一些技术点,加深对容器技术领域的理解和掌握。


1、容器技术介绍

容器云系列之容器技术相关概念

1.1 容器定义
容器是对服务器CPU和内存等资源分割和调度的基本单位,在容器出现之前在操作系统通过进程来实现,但是计算资源的隔离和灵活调度不满足发展需要。容器是为开发者和系统管理员设计的,用于构建、发布和运行分布式应用的平台。在操作系统中一个进程和操作系统构成完整环境,不同进程共享操作系统,对于容器来说一个容器和对应的主机构成完整环境。容器中提供应用程序完整的运行时环境,包括:
  • 应用程序的代码

  • 相关配置文件、库

  • 运行应用程序所需的依赖项

容器化则是一种应用程序或系统分发方法,将应用程序或系统及其依赖项与底层基础设施隔离开来。无论底层基础设施什么硬件,什么样的操作系统,只要系统支持容器。容器可以看成是操作系统级虚拟化,允许用户在容器中部署和运行分布式应用程序或系统。

1.2 容器与虚拟机区别

容器云系列之容器技术相关概念

虚拟机和容器都是资源隔离的一种方式
  • 虚拟机:通过硬件隔离技术,虚拟出物理环境进行资源的隔离,再安装一个完整的操作系统。

  • 容器:最大的区别是GuestOS在容器中不存在,通过隔离文件系统和资源使用限制独立运行进程,共享操作系统内核

  • 容器比虚拟机上更加轻量级,性能消耗更小

  • 虚拟机上很难做到内存的共用,容器中可以做到不同容器之间内存的共用

容器云系列之容器技术相关概念

1.3 当前容器技术标准

容器云系列之容器技术相关概念

传统意义上的容器指代docker的概念已经发生了变化,在当前的容器环境下,原来docker环境下的dockershim逐渐演变为通过CRI插件来和containered交互,再由containered管理多个container。这个也成为主流和标准的方式,整个流程上更加的简洁。

  • OCI开放容器标准(Open Container Initiative)

  • CRI 是支持多种容器运行时的插件接口

  • Containerd可以在宿主机中管理完整的容器生命周期

  • runC是OCI的参考实现

容器云系列之容器技术相关概念

  • 第一种是docker模式,通过dockershim跟docker引擎交互,再和Containered打交道,整个链路较长,逐渐被废弃
  • 第二种是通过CRI-Containered直接和containered交互

  • 第三种方式,是把CRI插件嵌套到Containered中

  • 第四种方式,直接通过CRi-O运行

2、Docker技术

2.1 Docker组件介绍

容器云系列之容器技术相关概念

  • 守护进程daemon:运行docker的后台进程
  • 客户端client:与用户交互,打包,拉/推镜像,运行/停止/删除容器

  • 镜像images:只读的,把环境和程序代码打成的包

  • 仓库registries:保存镜像的地方

  • 容器containers:从镜像创建的应用运行实例,在内存中实例化的应用

2.1.1 Docker镜像
1)镜像的种类
  • 基本镜像是没有父镜像的镜像,通常是操作系统的镜像,如ubuntu、alpine或debian

  • 子镜像是建立在基础镜像上的镜像,增加了额外的功能

  • 官方镜像是Docker认可的镜像

  • 用户镜像是用户创建和分享的镜像。建立在基础镜像的基础上,并增加了额外的功能镜像的格式是user/image-name

2)镜像标签
  • 使用tag为容器镜像打标签:和hash id相比语义清晰,latest自动用在最新的镜像上

  • 采用USERNAME/CONTAINER_NAME:TAG的命名方法

2.1.2 Docker仓库

用户可以创建一个本地仓库供内部使用,可以使用官方提供的工具docker-registry,并获取官方registry镜像来运行。仓库会被创建在容器的/var/lib/registry目录:

docker run --name registry -d -p 5000:5000 --restart=always v /opt/data/registry:/var/lib/registry registry
  • 推送到本地仓库:docker push localhost:5000/session-web:latest

  • 查看私有仓库中镜像:curl localhost:5000/v2/_catlog

  • 推荐使用仓库软件工具Harbor和Quay。

2.1.3 Docker运行过程
$ sudo docker run -i -t ubuntu /bin/bash
以上为例运行ubuntu镜像,按照顺序,Docker执行以下流程
  1. 拉取ubuntu镜像: Docker检查ubuntu镜像是否存在,如果在本地没有该镜像,Docker会从Docker Hub镜像仓库下载。如果镜像本地已经存在,Docker会使用它来创建新的容器。

  2. 使用本地ubuntu镜像创建一个新的容器,使用chroot创建根目录

  3. 分配文件系统并且挂载一个可读写的层: 容器会在这个文件系统中创建,并且一个可读写的层被添加到镜像中。

  4. 分配网络/桥接接口: 创建一个允许容器与本地主机通信的网络接口。

  5. 设置一个IP地址: 从池中寻找一个可用的IP地址并且服加到容器上。

  6. 指定运行的程序: 运行指定的程序/bin/bash

  7. 捕获并且提供应用输出: 连接并且记录标准输出、输入和错误可以看到程序是如何运行的

2.2 Docker核心原理
对Docker项目来说,它最核心的原理实际上就是为待创建的用户进程:
  1. 启用Linux Namespace配置

  2. 设置指定的Cgroups参数

  3. 切换进程的根目录chroot

Rootfs保证了容器的一致性,使得容器无论是在本地、物理机、云服务器上都处于同样的运行环境
2.2.1 隔离和权限控制

容器技术主要包括Cgroup和Namespace这两个内核特性:Namespace用于隔离资源、Cgroup用于限制资源。参看“容器云系列之Docker数据卷管理和资源限制”部分有关资源限制介绍。

1)Namespace
  • pid命名空间:使用在进程隔离(PID: Process ID)

  • net命名空间:使用在管理网络接口(NET:Networking)

  • ipc命名空间:使用在管理进程间通信资源 (IPC:InterProcess Communication)

  • mnt命名空间:使用在管理挂载点 (MNT: Mount)

  • uts命名空间:使用在隔离内核和版本标识 (UTS:Unix Timesharing System)

  • user命名空间:每个container可以有不同的user和group id

2)Cgroup:为每种可以控制的资源定义了子系统,限制进程组能够使用cpu、内存、磁盘、带宽等资源的上限
  • 限制资源使用,各种子系统的资源限制

  • 优先级控制,cpu使用、内存、磁盘io吞吐等

  • 资源使用报告,可以用来计费

  • 控制,挂起、恢复进程

例:查看子系统/sys/fs/cgroup/

[root@tango-01 cgroup]# cd /sys/fs/cgroup/
[root@tango-01 cgroup]# ll
total 0
drwxr-xr-x 6 root root 0 Apr 16 15:38 blkio
lrwxrwxrwx 1 root root 11 Apr 16 15:38 cpu -> cpu,cpuacct
lrwxrwxrwx 1 root root 11 Apr 16 15:38 cpuacct -> cpu,cpuacct
drwxr-xr-x 6 root root 0 Apr 16 15:38 cpu,cpuacct
drwxr-xr-x 3 root root 0 Apr 16 15:38 cpuset
drwxr-xr-x 6 root root 0 Apr 16 15:38 devices
drwxr-xr-x 3 root root 0 Apr 16 15:38 freezer
drwxr-xr-x 3 root root 0 Apr 16 15:38 hugetlb
drwxr-xr-x 6 root root 0 Apr 16 15:38 memory
lrwxrwxrwx 1 root root 16 Apr 16 15:38 net_cls -> net_cls,net_prio
drwxr-xr-x 3 root root 0 Apr 16 15:38 net_cls,net_prio
lrwxrwxrwx 1 root root 16 Apr 16 15:38 net_prio -> net_cls,net_prio
drwxr-xr-x 3 root root 0 Apr 16 15:38 perf_event
drwxr-xr-x 6 root root 0 Apr 16 15:38 pids
drwxr-xr-x 6 root root 0 Apr 16 15:38 systemd
  • Memory:内存相关的限制

  • Cpu:并不能像硬件虚拟化方案一样能够定义 CPU 能力,但是能够定义 CPU 轮转的优先级

  • Blkio:block IO相关的统计和限制,byte/operation 统计和限制(IOPS 等),读写速度限制

  • Devices:设备权限限制

3)Cgroups组织

容器云系列之容器技术相关概念

  • hierarchy: cgroups提供了一种类型的文件系统,是一组虚拟的文件系统,通过配置告诉内核,如何对进程限制使用资源。父子节点构成继承的层级关系
  • task:进程在cgroups中称为task

  • subsystem:cgroup支持的所有可配置的资源称为子系统,如:cpu、内存、网络等都是子系统

  • cgroup: 资源控制单位,任务组包含若干子系统

4)Cgroup实战

[root@tango-01 cpu]# mkdir /sys/fs/cgroup/cpu/mycgroup
[root@tango-01 cpu]# ls -l mycgroup
total 0
-rw-r--r-- 1 root root 0 Apr 16 16:45 cgroup.clone_children
--w--w--w- 1 root root 0 Apr 16 16:45 cgroup.event_control
-rw-r--r-- 1 root root 0 Apr 16 16:45 cgroup.procs
-r--r--r-- 1 root root 0 Apr 16 16:45 cpuacct.stat
-rw-r--r-- 1 root root 0 Apr 16 16:45 cpuacct.usage
-r--r--r-- 1 root root 0 Apr 16 16:45 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 Apr 16 16:45 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 Apr 16 16:45 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 Apr 16 16:45 cpu.rt_period_us
-rw-r--r-- 1 root root 0 Apr 16 16:45 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 Apr 16 16:45 cpu.shares
-r--r--r-- 1 root root 0 Apr 16 16:45 cpu.stat
-rw-r--r-- 1 root root 0 Apr 16 16:45 notify_on_release
-rw-r--r-- 1 root root 0 Apr 16 16:45 tasks

模拟高CPU占用 cat /dev/urandom | gzip -9 > /dev/null

[root@tango-01 cpu]# cat /dev/urandom | gzip -9 > /dev/null
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
13536 root 20 0 4624 612 396 R 40.7 0.0 0:11.78 gzip

限制CPU,占用率不超过20%

[root@tango-01 mycgroup]# echo 10000 > cpu.cfs_quota_us
[root@tango-01 mycgroup]# echo 50000 > cpu.cfs_period_us
[root@tango-01 mycgroup]# echo 13536 > tasks

查看CPU使用

   PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND                                                                                                                                                                                           
13536 root 20 0 4624 612 396 R 8.0 0.0 1:50.98 gzip
2.2.2 Docker卷挂载
启动容器时挂载整个“/”根目录
  • 这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”,称为rootfs(根文件系统)

  • volumn卷机制,指定特定文件或目录,独立生命周期,有两种方式:启动容器时-v挂载;在Dockerfile中VOLUME添加

2.2.3 Docker镜像层

容器云系列之容器技术相关概念

Docker 在镜像的设计中,引入了层(layer)的概念。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs
  1. 第一部分是只读层,打包好的镜像都属于只读层

  2. 第二部分是容器层,是容器rootfs的最上层。在没有写入文件前,这个目录是空的

  3. 第三部分是init层。这一层是docker单独生成的内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息

参考“容器云系列之Docker镜像和仓库管理”关于镜像分层结构的介绍。

2.3 Docker网络模式
创建Docker容器时,可以用–net选项指定容器的网络模式。Docker有以下4种网络模式:
  • host模式:使用–net=host指定,共享宿主机网络

  • bridge模式:使用–net=bridge指定,默认设置

  • container模式:使用–net=container:NAME(or)ID指定,共享另一个容器的网络

  • none模式:使用–net=none指定

参看“容器云系列之Docker网络管理及容器互联”有关容器网络的详细介绍。

2.3.1 Docker端口映射
  • 为了做到网络隔离,Docker使用Linux桥接,在宿主机虚拟容器网桥

    • 启动容器时会根据docker网桥的网段分配给容器一个IP地址,称为Container-IP。这个IP地址和宿主机IP地址不一样

    • 网桥是每个容器的默认网关

  • Docker网桥是宿主机虚拟出来的,不是真实存在的网络设备

    • 外部网络时无法寻址到的

    • 外部网络无法直接通过Container-IP访问到容器

  • 通过映射容器端口到宿主机

    • 通过-p或-P参数来启用

    • docker run -p 18080:80 nginx

[root@tango-01 ~]# docker run -p 18080:80 nginx
[root@tango-01 mycgroup]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2eca44029685 nginx "/docker-entrypoint." 49 seconds ago Up 44 seconds 0.0.0.0:18080->80/tcp cranky_mirzakhani

容器云系列之容器技术相关概念

2.3.2 Docker虚拟网桥
当Docker进程启动时,会创建为docker0的虚拟网桥
  • Docker容器会连接到这个虚拟网桥上

  • 虚拟网桥的工作方式和物理交换机类似,所有容器就通过交换机连在一起

从docker0 子网中分配一个IP给容器使用,并设置docker0的IP地址为容器的默认网关
  • 在主机上创建一对虚拟网卡veth pair设备

  • 组成了一个数据的通道,数据进出

容器云系列之容器技术相关概念

Docker将veth pair设备的一端放在新创建的容器中,并命名为eth0,另一端放在主机中,以veth*这样类似的名字命名,并将这个网络设备加入到docker0 网桥中。通过brctl show 命令查看

[root@tango-01 ~]# brctl show
bridge
name bridge id STP enabled interfaces
br-1d93f41271b4 8000.0242a3d69c1c no
br-d9ffb1af87f2 8000.024262f9a631 no veth70da614
veth78ca7c6
vethb281fb7
vethfb847e9
docker0 8000.02421ad85f66 no
2.3.3 实现端口转发功能

使用docker run -p时,docker实际是在iptables 做了DNAT规则,可以使用iptables -t nat -nL查看

[root@tango-01 mycgroup]# iptables -t nat -nL
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.19.0.3:8052
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:18080 to:172.17.0.2:80
2.3.4 连接容器
容器之间的连接有两种方式:link连接网络和自定义网络
  • link连接容器:docker run –link:alias,推荐不再使用

  • 自定义网络:创建新的Docker,在运行容器的时候使用新的网络

[root@tango-01 ~]# docker network create -d bridge my-net
f702ac3515d65156d5c7494cedf41cd984f82b0b2af02cd424e1ce03f64122ca
[root@tango-01 ~]# docker run -it --rm --name busybox1 --network my-net busybox sh
[root@tango-01 ~]# docker run -it --rm --name busybox1 --network my-net busybox sh
/ # ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:14:00:02
inet addr:172.20.0.2 Bcast:172.20.255.255 Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:19 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:2230 (2.1 KiB) TX bytes:0 (0.0 B)

lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

/ # ping 172.20.0.3
PING 172.20.0.3 (172.20.0.3): 56 data bytes
64 bytes from 172.20.0.3: seq=0 ttl=64 time=0.183 ms
64 bytes from 172.20.0.3: seq=1 ttl=64 time=0.069 ms
^C
--- 172.20.0.3 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.069/0.126/0.183 ms

运行另外一个

[root@tango-01 ~]# docker run -it --rm --name busybox2 --network my-net busybox sh
/ #
/ # ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:14:00:03
inet addr:172.20.0.3 Bcast:172.20.255.255 Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:6 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:516 (516.0 B) TX bytes:0 (0.0 B)

lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

/ #
2.4 Docker命令解析
  1. docker ps:查看正在运行的容器

[root@tango-01 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7a1329f43d63 ansible/awx:latest "/usr/bin/tini -- /u" 17 months ago Up 5 minutes 8052/tcp awx_task

docker ps –a查看该容器处于退出状态,但是容器没有真正销毁

[root@tango-01 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f88d934e7197 busybox "echo hello world" 36 seconds ago Exited (0) 14 seconds ago stoic_kepler
  1. docker pull:从镜像中心拉取一个镜像到本地

[root@tango-01 ~]# docker pull training/webapp
  1. docker images:查看本地拉取下来的镜像

[root@tango-01 ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
oceanbase/obce-mini latest 1a5ca6d233a7 8 months ago 690MB
  1. docker build:根据指定的Dockerfile构造镜像

[root@tango-01 dockerfile]# docker build -t alpine:v1.0 .
  1. docker run:运行容器

[root@tango-01 ~]# docker run busybox echo hello world
hello world
  1. docker start:启动容器

[root@tango-01 ~]# docker start dc2a72625341
dc2a72625341
  1. docker exec:开启一个交互式终端,在容器内部执行命令

[root@tango-01 ~]# docker exec -it influxdb /bin/bash
  1. docker logs:查看容器日志

[root@tango-01 ~]# docker logs -f c39cf512b3f9
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
  1. export/import:备份和恢复容器文件到tar文件,但是如果有tempfs卷则无法使用,因为不能备份内存信息。

  2. docker rm:删除容器

[root@tango-01 ~]# docker rm e7330d407e32
e7330d407e32
  1. docker commit:进入到容器中任何的变更都会被记录到容器的存储层中,commit后会将存储层保存下来成为镜像

[root@tango-01 ~]# docker run -t -i busybox /bin/sh
/ # cd /tmp
/tmp # ls / > root.txt
/tmp # ls -l
total 4
-rw-r--r-- 1 root root 43 Apr 16 07:59 root.txt
[root@tango-01 ~]# docker commit a80287c1f2da my_busybox
sha256:b7b85aec55e5474911987979012ca45640f7f7ccf04340573cbc26e638f65b08

通过diff命令可以查看修改的历史记录

[root@tango-01 ~]# docker diff a80287c1f2da
C /tmp
A /tmp/root.txt
C /root
A /root/.ash_history
[root@tango-01 ~]#
2.5 Dockerfile实战
2.5.1 Dockerfile关键字
  1. FROM:拉取基础镜像,如果没有指定docker镜像中心,默认从hub.docker.com上拉取

  2. WORKDIR:指定工作目录

  3. ADD/COPY:从你给的路径复制文件到容器内部你指定的路径中,ADD会自动解压tar、gzip、bzip2等压缩文件后进行复制。官方更推荐使用COPY

  4. EXPOSE:声明端口,docker run –p 配置映射端口时使用

  5. RUN:执行命令,在docker build时执行

  6. CMD:执行命令,在docker run时执行

2.5.2 Docker Build创建

创建Dockerfile镜像文件:

[root@tango-01 dockerfile]# vi Dockerfile 
FROM alpine
ADD root.txt /tmp

使用docker build命令构建

[root@tango-01 dockerfile]# docker build -t alpine:v1.0 .
Sending build context to Docker daemon 1.06MB
Step 1/2 : FROM alpine
---> c059bfaa849c
Step 2/2 : ADD root.txt /tmp
---> d0b7c4e5849f
Successfully built d0b7c4e5849f
Successfully tagged alpine:v1.0

运行docker

[root@tango-01 dockerfile]# docker run -t -i alpine:v1.0 /bin/sh
/ # ls /tmp
root.txt
Dockerfile使用经验:
  • 不使用多条RUN指令,防止增加不必要的镜像

  • 添加文件使用合并成为一行

  • 采用合适的根镜像:FROM ubuntu:20.04

  • 采用COPY指令而不采用ADD

  • 对镜像打标签,确定版本

2.6 Docker Compose工具

定义和运行多容器的应用程序工具,定义若干个容器作为整体运行。通过docker-compose.yml文件定义,包含version、services和networks三部分。

version: "3.9"
services:
db:
image: postgres
volumes:
- ./tmp/db:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: password
web:
build: .
command: bash -c "rm -f tmp/pids/server.pid
&& bundle exec rails s -p 3000 -b '0.0.0.0'"
volumes:
- .:/myapp
ports:
- "3000:3000"
depends_on:
- db

通过命令docker-compose up运行

3、Podman

Podman是Redhat公司推出的容器管理工具,开发、管理、运行OCI容器,起初是CRI-O的一部分,后来单独分离出来叫做libpod后来改成Podman。Podman的命令几乎同docker类似,在结构上与Docker不同,Podman不使用daemon的方式去创建容器,而是直接调用OCI runtime,比如runc。Podman由两部分组成:Podman CLI方便用户交互和conmon负责container runtime,主要包括监控、日志、TTY分配等,是所有容器进程的父进程。

容器云系列之容器技术相关概念

  • 在Docker中主要是Docker Daemon进程,负责生成容器、和内核交互、管理镜像、镜像仓库拉取镜像,Docker CLI和Docker Daemon进行通信

  • 在Podman中有buildah镜像构建工具和skepeo镜像拷贝工具分别负责镜像构建和镜像仓库通信

4、总结

以上是最近参加的容器技术培训有关容器这一块的基本概念和技术点,全文基于张建锋老师的培训材料整理。容器技术这几年发展迅猛,技术迭代更新很快,逐渐也形成一套行业标准,从Docker到K8S、Rancher以及Podman,开源和商业化产品的碰撞,行业内的产品和工具不断涌现,整个生态也是欣欣向荣。


参考资料:

  1. 容器技术培训,张建锋老师

  2. 容器云系列之Docker基本概念及部署使用

  3. 容器云系列之Docker镜像和仓库管理

  4. 容器云系列之Docker数据卷管理和资源限制

  5. 容器云系列之Docker网络管理及容器互联

  6. 容器云系列之Docker管理工具Docker-compose和Docker Machine

原文始发于微信公众号(牧羊人的方向):容器云系列之容器技术相关概念

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/65125.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!