⭐️名不显时心不朽,再挑灯火看文章。
个人主页 :YYYing.
⭐️Kubernetes系列专栏:从零开始的k8s学习
系列下期内容:暂无
目录
前言
一、初识容器
1、docker的形态
2、docker的安装
3、docker的使用
4、docker的架构
二、被隔离的进程
1、容器到底是什么
2、为什么要隔离
3、与虚拟机的区别是什么
4、隔离是怎么实现的
三、容器化的应用
1、什么是容器化的应用
2、常用的镜像操作有哪些
3、常用容器操作有哪些
结语
前言
你一定听过Kubernetes吧,或许更熟悉一点的,是当今许多人挂在嘴边的“K8s”。
不管你是研发、测试、还是运维,不管你是前台、后台、还是中台,不管你用的是C++、Java还是Python,不管你是搞数据库、区块链、还是人工智能,不管你是从事网站、电商、还是音视频,在这个“云原生”时代,Kubernetes都是一个绕不过去的产品,是我们工作中迟早要面对的“坎儿”。
毕竟,现代应用是什么?是微服务,是服务网格,这些统统要围绕着容器来开发、部署和运行,而使用容器就必然要用到容器编排技术,在现在只有唯一的选项,那就是Kubernetes,它的地位就如同Linux一样,成为了事实上的云原生操作系统,是构建现代应用的基石。
一、初识容器
容器这部分内容其实早在docker系列就应该讲了的,但是当时时间平衡不过来再加上有点小懒,那几个数据库和docker的系列我前面一直没做(doge,但是没关系,此系列面向的群体只是要有一定linux基础的人群(至少也得有个虚拟机),至于docker和k8s等云原生相关技术,我将会从0开始说起,这个门槛我感觉还是很低的。
1、docker的形态
在用docker之前,我们需要对docker的形态有所了解:目前市面上使用docker有两个选择,Docker Desktop和Docker Engine。
Docker Desktop是针对个人使用而设计的,支持Mac和Windows快速安装,具有直观的图形界面,还集成了许多周边工具,方便易用。
但不太推荐使用Docker Desktop,原因有两个。第一个,它是商业产品,难免会带有Docker公司的“私人气息”,有一些自己的、非通用的东西,不利于我们后续的Kubernetes学习。第二个,它只是对个人学习免费,受条款限制不能商用,我们在日常工作中难免会“踩到雷区”。
Docker Engine则和Docker Desktop正好相反,完全免费,但只能在Linux上运行,只能使用命令行操作,缺乏辅助工具,需要我们自己动手DIY运行环境。不过要是较起真来,它才是Docker当初的真正形态,“血脉”最纯正,也是现在各个公司在生产环境中实际使用的Docker产品,毕竟机房里99%的服务器跑的都是Linux。
所以我们接下来学习过程中,我将使用Docker Engine来给大家讲解。
2、docker的安装
安装这个东西不管怎么样只要能安装好并能正常运行测试就行,所以如果你觉得我的安装教程可能有些不是很全或者跟着很难受,那完全可以自行处理然后跳过这节看下面的内容。
先让我们尝试输入命令docker,会得到“命令未找到”的提示,还有如何安装的建议:
Command 'docker' not found, but can be installed with: sudo apt install docker.io所以,你只需要按照系统的提示输入命令,安装docker.io就可以了。为了方便,你还可以使用-y参数来避免确认,实现自动化操作:
sudo apt install -y docker.io #安装Docker Engine我们安装好后,必须要做一些手工调整才能用起来,所以你还要在安装完毕后执行下面的两条命令:
sudo service docker start #启动docker服务 sudo usermod -aG docker $USER #当前用户加入docker组第一个service docker start是启动Docker的后台服务,第二个usermod -aG是把当前的用户加入Docker的用户组。这是因为操作Docker必须要有root权限,而直接使用root用户不够安全,加入Docker用户组是一个比较好的选择,这也是Docker官方推荐的做法。当然,如果只是为了图省事,你也可以直接切换到root用户来操作Docker。
上面的三条命令执行完之后,我们还需要运行下面的命令,让它刷新一下权限,这样才能让修改用户组的命令usermod生效。
newgrp docker现在我们就可以来验证Docker是否安装成功了,使用的命令是docker version和docker info。
3、docker的使用
首先,我们使用命令docker ps,它会列出当前系统里运行的容器,就像我们在Linux系统里使用ps命令列出运行的进程一样。
接下来,让我们尝试另一个非常重要的命令docker pull,从外部的镜像仓库(Registry)拉取一个busybox镜像(image),你可以把它类比成是Ubuntu里的“apt install”下载软件包:
docker pull会有一些看起来比较奇怪的输出信息,现在我们暂时不用管,后续会有详细解释。我们再执行命令docker images,它会列出当前Docker所存储的所有镜像:
其他镜像先不管,可以看到,命令会显示有一个叫busybox的镜像,镜像的ID号是一串16进制数字,大小是1.41MB。
现在,我们就要从这个镜像启动容器了,命令是docker run,执行echo输出字符串:
docker run busybox echo hello world这条命令会在我们的终端上,输出计算机世界最著名的语句“hello world”:
然后我们再用docker ps命令,加上一个参数-a,就可以看到这个已经运行完毕的容器:
初次接触容器的你可能会感到很困惑,这些命令都做了什么?看起来并没有展示出什么特别神奇的本领啊?可能还不如直接写一个Shell脚本来得省事。不用着急,既然我都这么说了,那我们后续肯定来讲解其中的奥秘。
那运行都有了,那停止和删除肯定也得要有:
# 停止容器(如果容器正在运行) docker stop <容器ID或名称> # 删除容器 docker rm <容器ID或名称> # 如果容器未停止,可以直接使用强制删除 docker rm -f <容器ID或名称>不过这点我们在容器操作中也会强调。
4、docker的架构
这里我再稍微讲一下Docker Engine的架构,让你有个初步的印象,也为之后的学习做一个铺垫。
下面的这张图来自Docker官网(What is Docker? | Docker Docs),精准地描述了Docker Engine的内部角色和工作流程,对我们的学习研究非常有指导意义。
刚才我们敲的命令行docker实际上是一个客户端client ,它会与Docker Engine里的后台服务Docker daemon通信,而镜像则存储在远端的仓库Registry里,客户端并不能直接访问镜像仓库。
Docker client可以通过build、pull、run等命令向Docker daemon发送请求,而Docker daemon则是容器和镜像的“大管家”,负责从远端拉取镜像、在本地存储镜像,还有从镜像生成容器、管理容器等所有功能。
所以,在Docker Engine里,真正干活的其实是默默运行在后台的Docker daemon,而我们实际操作的命令行工具“docker”只是个“传声筒”的角色。
Docker官方还提供一个“hello-world”示例,可以为你展示Docker client到Docker daemon再到Registry的详细工作流程,你只需要执行这样一个命令:
docker run hello-world它会先检查本地镜像,如果没有就从远程仓库拉取,再运行容器,最后输出运行信息:
二、被隔离的进程
广义上来说,容器技术是动态的容器、静态的镜像和远端的仓库这三者一起组合而成的。不过,“容器”这个术语作为容器技术里的核心概念,不仅是大多数初次接触这个领域的人,即使是有一些使用经验的人,想要准确的把握其内涵和本质仍是比较困难的。
那么现在,我们就来一起看看所谓容器到底是什么。
1、容器到底是什么
从字面上来看,容器就是Container,一般把它形象地比喻成现实世界里的集装箱,它也正好和Docker的现实含义相对应,因为码头工人(那只可爱的小鲸鱼)就是不停地在搬运集装箱。
集装箱的作用是标准化封装各种货物,一旦打包完成之后,就可以从一个地方迁移到任意的其他地方。相比散装形式而言,集装箱隔离了箱内箱外两个世界,保持了货物的原始形态,避免了内外部相互干扰,极大地简化了商品的存储、运输、管理等工作。
所以我们容器也是一样的,不过它封装的货物是运行中的应用程序,也就是进程,同样他会把进程跟外界隔离开,互不影响。
我们可以看看在容器里运行的进程是个什么样子:
docker pull alpine然后我们使用docker run命令运行它的Shell程序:
docker run -it alpine sh注意我们在这里多加了一个-it参数,这样我们就会暂时离开当前的Ubuntu操作系统,进入容器内部。
现在,让我们执行cat /etc/os-release,还有ps这两个命令,看看容器里与容器外有什么不同,最后再使用exit退出:
就像这张截图里所显示的,在容器里查看系统信息,会发现已经不再是外面的Ubuntu系统了,而是变成了Alpine Linux 3.15,使用ps命令也只会看到一个完全“干净”的运行环境,除了Shell(即sh)没有其他的进程存在。
也就是说,在容器内部是一个全新的Alpine操作系统,在这里运行的应用程序完全看不到外面的Ubuntu系统,两个系统被互相“隔离”了,就像是一个“世外桃源”。
我们还可以再拉取一个Ubuntu 18.04的镜像,用同样的方式进入容器内部,然后执行apt update、apt install等命令来看看:
docker pull ubuntu:18.04 docker run -it ubuntu:18.04 sh # 下面的命令都是在容器内执行 cat /etc/os-release apt update apt install -y wget redis redis-server &具体结果我就不展示了,如果不想下太多占内存,这块弄不弄也无所谓(虽说一个镜像内存也就几十MB)。不过我想说的就是,我们可以在这个“世外桃源”做任意的事情,比如安装应用、运行Redis服务等。但无论我们在容器里做什么,都不会影响外面的Ubuntu系统(当然不是绝对的)。
到这里,我们就可以得到一个初步的结论:容器,就是一个特殊的隔离环境,它能够让进程只看到这个环境里的有限信息,不能对外界环境施加影响。
那么好,此时就会有一个问题,我们为什么要这么做,也就是为什么要创建这么一个隔离环境,直接让进程就运行在操作系统不就行了?
2、为什么要隔离
对于Linux操作系统来说,一个不受任何限制的应用程序是十分危险的。如果这个进程能够看到系统里所有的文件、所有的进程、所有的网络流量,访问内存里的任何数据,那么恶意程序很容易就会把系统搞瘫痪,正常程序也可能会因为无意的Bug导致信息泄漏或者其他安全事故。虽然Linux提供了用户权限控制,能够限制进程只访问某些资源,但这个机制还是比较薄弱的,和真正的“隔离”需求相差得很远。
而现在,使用容器技术,我们就可以让应用程序运行在一个有严密防护的“沙盒”(Sandbox)环境之内,就好像是把进程请进了“隔离酒店”,它可以在这个环境里自由活动,但绝不允许“越界”,从而保证了容器外系统的安全。
而且我们计算机是有各种各样的资源的:CPU,内存,硬盘,网卡,虽然很多大公司的高性能服务器其实按理来说这些资源还是很宽泛的,但终归是有限的,且要考虑成本,那么就注定不可能让一个程序随意无限制地占用,
所以容器技术的另一个本领就是为应用程序加上资源隔离,在系统里切分出一部分资源,让它只能使用指定的配额,虽然这使程序损失了很多自由,但却保证了系统性安全。
3、与虚拟机的区别是什么
这么看来,容器不过就是常见的“沙盒”技术中的一种,和虚拟机差不了多少,那么它与虚拟机的区别在哪里呢?又有什么样的优势呢?
其实容器和虚拟机面对的都是相同的问题,使用的也都是虚拟化技术,只是所在的层次不同,我们可以参考Docker官网上的两张图,把这两者对比起来会更利于学习理解。
(Docker官网的图示其实并不太准确,容器并不直接运行在Docker上,Docker只是辅助建立隔离环境,让容器基于Linux操作系统运行)
首先,容器和虚拟机的目的都是隔离资源,保证系统安全,然后是尽量提高资源的利用率。
想想当时你自己用VMware或VirtualBox创建虚拟机的时候,它们能够在宿主机系统里完整虚拟化出一套计算机硬件,在里面还能够安装任意的操作系统,这内外两个系统也同样是完全隔离,互不干扰。
而在数据中心的服务器上,虚拟机软件(即图中的Hypervisor)同样可以把一台物理服务器虚拟成多台逻辑服务器,这些逻辑服务器彼此独立,可以按需分隔物理服务器的资源,为不同的用户所使用。
从实现的角度来看,虚拟机虚拟化出来的是硬件,需要在上面再安装一个操作系统后才能够运行应用程序,而硬件虚拟化和操作系统都比较“重”,会消耗大量的CPU、内存、硬盘等系统资源,但这些消耗其实并没有带来什么价值,属于“重复劳动”和“无用功”,不过好处就是隔离程度非常高,每个虚拟机之间可以做到完全无干扰。
我们再来看容器(即图中的Docker),它直接利用了下层的计算机硬件和操作系统,因为比虚拟机少了一层,所以自然就会节约CPU和内存,显得非常轻量级,能够更高效地利用硬件资源。不过,因为多个容器共用操作系统内核,应用程序的隔离程度就没有虚拟机那么高了。
还有一点就是运行效率,这可以说是容器相比于虚拟机最大的优势,在这个对比图中就可以看到,同样的系统资源,虚拟机只能跑3个应用,其他的资源都用来支持虚拟机运行了,而容器则能够把这部分资源释放出来,同时运行6个应用。
当然,这个对比图只是一个形象的展示,并不是严谨的数值比较,大体有个印象就行。
一个普通的Ubuntu虚拟机安装完成之后,体积都是GB级别的,再安装一些应用很容易就会上到几十GB,我们的电脑上同时运行十来个虚拟机可能就是极限了。而一个Ubuntu镜像大小则只有几十MB,启动起来更是非常快,基本上不超过一秒钟,同时跑上百个容器也毫无问题。
虚拟机和容器这两种技术并不是互相排斥的,它们完全可以结合起来使用,就像我们现在的环境一样,用虚拟机实现与宿主机的强隔离,然后在虚拟机里使用Docker容器来快速运行应用程序。
4、隔离是怎么实现的
我们知道虚拟机使用的是Hypervisor(KVM、Xen等),那么,容器是怎么实现和下层计算机硬件和操作系统交互的呢?为什么它会具有高效轻便的隔离特性呢?
奥秘就在于Linux操作系统内核之中,为资源隔离提供了三种技术:namespace、cgroup、chroot,虽然这三种技术的初衷并不是为了实现容器,但它们三个结合在一起就会发生奇妙的“化学反应”。
namespace是2002年从Linux 2.4.19开始出现的,和编程语言里的namespace有点类似,它可以创建出独立的文件系统、主机名、进程号、网络等资源空间,相当于给进程盖了一间小板房,这样就实现了系统全局资源和进程局部资源的隔离。
chroot的历史则要比前面的namespace、cgroup要古老得多,早在1979年的UNIX V7就已经出现了,它可以更改进程的根目录,也就是限制访问文件系统,相当于给进程的小板房铺上了地砖。
综合运用这三种技术,一个具有完善的隔离特性的容器就此出现了,进程就可以搬进这个小房间工作了。
三、容器化的应用
1、什么是容器化的应用
之前我们运行容器的时候,显然不是从零开始的,而是要先拉取一个“镜像”(image),再从这个“镜像”来启动容器,像这一篇的第一章那样:
docker pull busybox docker run busybox echo hello world那么,这个镜像又是什么东西?它与容器又有什么关系?
其实我们在其他场合中也曾经见到过“镜像”这个词,比如最常见的光盘镜像,重装电脑时使用的硬盘镜像,还有虚拟机系统镜像。这些“镜像”都有一些相同点:只读,不允许修改,以标准格式存储了一系列的文件,然后在需要的时候再从中提取出数据运行起来。
容器技术里的镜像也是同样的道理。因为容器是由操作系统动态创建的,那么必然就可以用一种办法把它的初始环境给固化下来,保存成一个静态的文件,相当于是把容器给“拍扁”了,这样就可以非常方便地存放、传输、版本化管理了。
镜像可以说是一个“样板间”,把运行进程所需要的文件系统、依赖库、环境变量、启动参数等所有信息打包整合到了一起。之后镜像文件无论放在哪里,操作系统都能根据这个“样板间”快速重建容器,应用程序看到的就会是一致的运行环境了。
从功能上来看,镜像和常见的tar、rpm、deb等安装包一样,都打包了应用程序,但最大的不同点在于它里面不仅有基本的可执行文件,还有应用运行时的整个系统环境。这就让镜像具有了非常好的跨平台便携性和兼容性,能够让开发者在一个系统上开发(例如Ubuntu),然后打包成镜像,再去另一个系统上运行(例如CentOS),完全不需要考虑环境依赖的问题,是一种更高级的应用打包方式。
理解了这一点,我们再回过头来看看前面运行的Docker命令。
docker pull busybox,就是获取了一个打包了busybox应用的镜像,里面固化了busybox程序和它所需的完整运行环境。
docker run busybox echo hello world,就是提取镜像里的各种信息,运用namespace、cgroup、chroot技术创建出隔离环境,然后再运行busybox的echo命令,输出hello world的字符串。
这两个步骤,由于是基于标准的Linux系统调用和只读的镜像文件,所以,无论是在哪种操作系统上,或者是使用哪种容器实现技术,都会得到完全一致的结果。
推而广之,任何应用都能够用这种形式打包再分发后运行,这也是无数开发者梦寐以求的“一次编写,到处运行(Build once, Run anywhere)”的至高境界。所以,所谓的“容器化的应用”,或者“应用的容器化”,就是指应用程序不再直接和操作系统打交道,而是封装成镜像,再交给容器环境去运行。
现在你就应该知道了,镜像就是静态的应用容器,容器就是动态的应用镜像,两者互相依存,互相转化,密不可分。
2、常用的镜像操作有哪些
在前面的课程里你应该已经了解了两个基本命令,docker pull从远端仓库拉取镜像,docker images列出当前本地已有的镜像。
docker pull的用法还是比较简单的,和普通的下载非常像,不过我们需要知道镜像的命名规则,这样才能准确地获取到我们想要的容器镜像。
镜像的完整名字由两个部分组成,名字和标签,中间用 : 连接起来。
名字表明了应用的身份,比如busybox、Alpine、Nginx、Redis等等。标签(tag)则可以理解成是为了区分不同版本的应用而做的额外标记,任何字符串都可以,比如3.15是纯数字的版本号、jammy是项目代号、1.21-alpine是版本号加操作系统名等等。其中有一个比较特殊的标签叫“latest”,它是默认的标签,如果只提供名字没有附带标签,那么就会使用这个默认的“latest”标签。
那么现在,你就可以把名字和标签组合起来,使用 docker pull 来拉取一些镜像了:
docker pull grafana/grafana:latest docker pull mysql:5.7 docker pull nginx:1.21-alpine docker pull nginx:alpine docker pull redis:6.0.8有了一些镜像之后,我们再用docker images命令来看看它们的具体信息:
再来看另一个镜像操作命令docker rmi,它用来删除不再使用的镜像,可以节约磁盘空间,注意命令rmi,实际上是“remove image”的简写。
下面我们就来试验一下,使用名字和IMAGE ID来删除镜像:
docker rmi alpine docker rmi a46这里的第一个rmi删除了alpine镜像,因为没有显式写出标签,默认使用的就是“latest”。第二个rmi没有给出名字,而是直接使用了IMAGE ID的前三位,也就是“a46”,Docker就会直接找到这个ID前缀的镜像然后删除。
命令还有很多,不过我们此处就讲这几个,后续博客我们还会陆陆续续介绍其他命令。
3、常用容器操作有哪些
现在我们已经在本地存放了镜像,就可以使用docker run命令把这些静态的应用运行起来,变成动态的容器了。
基本的格式是“docker run 设置参数”,再跟上“镜像名或ID”,后面可能还会有附加的“运行命令”。
比如这个命令:
docker run -h srv alpine hostname这里的-h srv就是容器的运行参数,alpine是镜像名,它后面的hostname表示要在容器里运行的“hostname”这个程序,输出主机名。
docker run是最复杂的一个容器操作命令,有非常多的额外参数用来调整容器的运行状态,你可以加上--help来看它的帮助信息,今天我只说几个最常用的参数。
-it表示开启一个交互式操作的Shell,这样可以直接进入容器内部,就好像是登录虚拟机一样。(它实际上是“-i”和“-t”两个参数的组合形式)
-d表示让容器在后台运行,这在我们启动Nginx、Redis等服务器程序的时候非常有用。
--name可以为容器起一个名字,方便我们查看,不过它不是必须的,如果不用这个参数,Docker会分配一个随机的名字。
下面我们就来练习一下这三个参数,分别运行Nginx、Redis和Ubuntu:
docker run -d nginx:alpine # 后台运行Nginx docker run -d --name red_srv redis:6.0.8 # 后台运行Redis docker run -it --name ubuntu 你ubuntu的名字 sh # 使用IMAGE ID,登录Ubuntu18.04因为第三个命令使用的是-it而不是-d,所以它会进入容器里的Ubuntu系统,我们需要另外开一个终端窗口,使用docker ps命令来查看容器的运行状态:
可以看到,每一个容器也会有一个“CONTAINER ID”,它的作用和镜像的“IMAGE ID”是一样的,唯一标识了容器。
对于正在运行中的容器,我们可以使用docker exec命令在里面执行另一个程序,效果和docker run很类似,但因为容器已经存在,所以不会创建新的容器。它最常见的用法是使用-it参数打开一个Shell,从而进入容器内部,例如:
docker exec -it red_srv sh这样我们就“登录”进了Redis容器,可以很方便地查看服务的运行状态或者日志。
运行中的容器还可以使用docker stop命令来强制停止,这里我们仍然可以使用容器名字,不过或许用“CONTAINER ID”的前三位数字会更加方便。
容器被停止后使用docker ps命令就看不到了,不过容器并没有被彻底销毁,我们可以使用docker ps -a命令查看系统里所有的容器,当然也包括已经停止运行的容器:
这些停止运行的容器可以用docker start再次启动运行,如果你确定不再需要它们,可以使用docker rm命令来彻底删除。
你可能会感觉这样的容器管理方式很麻烦,启动后要ps看ID再删除,如果稍微不注意,系统就会遗留非常多的“死”容器,占用系统资源,有没有什么办法能够让Docker自动删除不需要的容器呢?
办法当然有,就是在执行docker run命令的时候加上一个--rm参数,这就会告诉Docker不保存容器,只要运行完毕就自动清除,省去了我们手工管理容器的麻烦。
我们还是用刚才的Nginx、Redis和Ubuntu这三个容器来试验一下,加上--rm参数(省略了name参数):
docker run -d --rm nginx:alpine docker run -d --rm redis:6.0.8 docker run -it --rm 你的ubuntu名字 sh然后我们用docker stop停止容器,再用docker ps -a,就会发现不需要我们再手动执行docker rm,Docker已经自动删除了这三个容器。
结语
好了,今天我们初步了解了容器技术以及容器技术中最关键的概念:动态的容器,然后使用Docker实际操作了镜像和容器,运行了被容器化的Alpine、Nginx、Redis等应用。
镜像是容器的静态形式,它打包了应用程序的所有运行依赖项,方便保存和传输。使用容器技术运行镜像,就形成了动态的容器,由于镜像只读不可修改,所以应用程序的运行环境总是一致的。
而容器化的应用就是指以镜像的形式打包应用程序,然后在容器环境里从镜像启动容器。
由于Docker的命令比较多,而且每个命令还有许多参数,一篇文章里很难把它们都详细说清楚,希望你课下参考Docker自带的帮助或者官网文档(Reference documentation | Docker Docs),再多加实操练习,相信你一定能够成为Docker高手。
最后再简单小结一下主要的内容:
- 容器技术起源于Docker,它目前有两个产品:Docker Desktop和Docker Engine,我们的课程里推荐使用免费的Docker Engine,它可以在Ubuntu系统里直接用apt命令安装。
- Docker Engine需要使用命令行操作,主命令是
docker,后面再接各种子命令。 - 查看Docker的基本信息的命令是
docker version和docker info,其他常用的命令有docker ps、docker pull、docker images、docker run。 - Docker Engine是典型的客户端/服务器(C/S)架构,命令行工具Docker直接面对用户,后面的Docker daemon和Registry协作完成各种功能。
- 容器就是操作系统里一个特殊的“沙盒”环境,里面运行的进程只能看到受限的信息,与外部系统实现了隔离。
- 容器隔离的目的是为了系统安全,限制了进程能够访问的各种资源。
- 相比虚拟机技术,容器更加轻巧、更加高效,消耗的系统资源非常少,在云计算时代极具优势。
- 容器的基本实现技术是Linux系统里的namespace、cgroup、chroot。
- 常用的镜像操作有
docker pull、docker images、docker rmi,分别是拉取镜像、查看镜像和删除镜像。 - 用来启动容器的
docker run是最常用的命令,它有很多参数用来调整容器的运行状态,对于后台服务来说应该使用-d。 docker exec命令可以在容器内部执行任意程序,对于调试排错特别有用。- 其他常用的容器操作还有
docker ps、docker stop、docker rm,用来查看容器、停止容器和删除容器。
我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。
无限进步,我们下次再见。