docker入門筆記

《Docker技術入門與實踐》筆記

2 核心概念

Docker的三個核心概念分別為:鏡像、容器和倉庫。

2.1 鏡像

Docker鏡像類似于虛擬機鏡像,可以理解為一個只讀的模板。

2.2 容器

容器的定義:容器有效地將由單個操作系統管理的資源劃分到孤立的組中,以更好地在孤立的組之間平衡有沖突的資源使用需求。與虛擬化相比,這樣既不需要指令級模擬,也不需要即時編譯。容器可以在核心CPU本地運行指令,而不需要任何專門的解釋機制。此外,也避免了準虛擬化和系統調用替換中的復雜性。

可以將Docker容器理解為一種輕量級的沙盒。每個容器內運行著一個應用,不同的容器相互隔離,容器之間也可以通過網絡互相通信。容器的創建和停止都十分快速,幾乎跟創建和終止原生應用一致;另外,容器自身對系統資源的額外需求也十分有限,遠遠低于傳統虛擬機。很多時候,甚至直接把容器當作應用本身也沒有任何問題。

沙盒是一種虛擬技術,多用于計算機安全技術,其原理是通過重定向技術,把程序生成和修改的文件重定向到自身文件夾中,當某個程序試圖發揮作用時,安全軟件可先讓它在沙盒中運行,如有惡意行為,則禁止程序進一步運行,不會對系統造成危害。

2.3 倉庫

注冊服務器(Registry)與倉庫(Repository)
  • docker倉庫是集中存放鏡像文件的場所
  • 倉庫注冊服務器(Registry)是存放倉庫(Repository)的地方
  • 一個Registry可以存放多個Repository,每個Repository中存放一類鏡像,用標簽(tag)進行區分

3 使用docker鏡像

docker運行容器前需要本地存在對應的鏡像,如果鏡像沒保存在本地,docker會嘗試先從默認鏡像倉庫(Docker Hub公共注冊服務器中的倉庫),用戶也可以通過配置,使用自定義的鏡像倉庫。

3.1 獲取鏡像

官網Docker Hub(需要VPN才能注冊)提供了鏡像給用戶下載。我們可以直接從官網拉下鏡像,命令格式:

$ docker pull NAME[:TAG]

例子:
獲取ubuntu 14.04的基礎鏡像:

$ docker pull ubuntu:14.04

若省略了TAG,則默認下載最新版本。嚴格的講,鏡像倉庫名稱還應該加上倉庫地址(即注冊服務器)作為前綴,只是默認從Docker Hub上下載,所以該前綴可以忽略,實際是:

$ docker pull registry.hub.docker.com/ubuntu:14.04

如果從非官方倉庫下載鏡像,則需要在倉庫名稱前指定完整的倉庫地址。

鏡像文件一般由若干層組成,每個層由一個唯一id(256位)表示。當不同鏡像包括相同的層時,本地僅存儲該層的一份內容,減少了存儲空間。

利用鏡像創建容器,運行bash應用:

$ docker run -it ubuntu:15.04 bash
root@3d5884dd705a:/# ls

bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

3.2 查看鏡像信息

列出本地主機上已有鏡像信息:

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              15.04               d1b55fd07600        2 years ago         131MB
  • REPOSITORY:來自哪個倉庫。
  • TAG:標簽信息,標記同一倉庫的不同鏡像。
  • IMAGE ID:鏡像ID(唯一標志符),一般可以使用該ID的前若干個字符組成的可區分串來替代完整的ID。
  • CREATED:創建時間,說明鏡像最后更新時間。
  • SIZE:鏡像大小(邏輯大小),實際物理上占用的空間會小于各鏡像的邏輯大小之和。

image子命令:

  • -a,--all=true|false:列出所有鏡像文件(包括臨時文件),默認為否
  • --digests=true|false:列出摘要,默認為否
  • -f,--filter=[]:過濾列出的鏡像
  • --format="TEMPLATE":控制輸出格式
  • --no-trunc=true|false:對輸出結果中太長的部分是否進行截斷,默認為是
  • -q,--quit=true|false:僅輸出ID信息,默認為否

為鏡像添加一個tag,與原先的tag指向同一個鏡像,因此實際占用內存只有一個鏡像大小:

$ docker tag ubuntu:15.04 myubuntu:15.04
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
myubuntu            15.04               d1b55fd07600        2 years ago         131MB
ubuntu              15.04               d1b55fd07600        2 years ago         131MB

查看一個鏡像的詳細信息:

$ docker inspect ubuntu:15.04

以上返回結果是json格式的消息,可以只獲取其中一項,如操作系統:

$ docker inspect ubuntu:15.04 -f {{".Os"}}

查看鏡像歷史:

$ docker history ubuntu:15.04

3.3 搜索鏡像

搜索遠端倉庫中共享的鏡像,默認搜索官網倉庫Docker Hub的鏡像。

例子:搜索所有自動創建的星超過100的帶ubuntu的鏡像:

$ docker search --filter=is-automated=true --filter=stars=3 ubuntu
DESCRIPTION   STARS  OFFICIAL AUTOMATED
...

3.4 刪除鏡像

  1. 用標簽刪除鏡像:
$ docker rmi [-f] ubuntu:15.04
  • 當同個鏡像擁有多個標簽時,docker rmi只是刪除該鏡像多個標簽中的指定標簽而已,并不影響鏡像文件。
  • 但當鏡像只剩下一個標簽時,docker rmi會徹底刪除鏡像。
  1. 用ID刪除鏡像,會先嘗試刪除所有指向該鏡像的標簽,然后刪除該鏡像文件本身:
$ docker rmi [-f] d1b55fd07600

當鏡像創建的容器存在時,鏡像文件無法直接刪除,需要增加-f參數。但正確的做法是先刪除鏡像的所有容器,再刪除鏡像。

3.5 創建鏡像

創建鏡像的主要方法有:

  • 基于已有鏡像的容器創建
  • 基于本地模板導入
  • 基于Dockerfile創建
  1. 基于已有鏡像的容器創建:
    先對已有鏡像進行修改,然后再提交:
$ docker commit -m "... ..." -a "author name" 容器ID Name:TAG
  • -a,--author="":作者信息
  • -c,--change=[]:提交時執行Dockerfile指令
  • -m,--message="":提交消息
  • -p,--pause=true:提交時暫停容器運行
  1. 基于本地模板導入:
    可以直接從一個操作系統模板導入一個鏡像,模板可以從OpenVZ下載。導入的方法如下:
$ docker import [OPTIONS] file|URL|-[REPOSITORY[:TAG]]

例子:

$ cat ubuntu-14.04-x86.tar.gz | docker import - ubuntu:14.04
或
$ docker import ubuntu-14.04-x86.tar.gz ubuntu:14.04

ubuntu-14.04-x86.tar.gz為下載的模板。

3.6 存出和載入鏡像

  1. 存出鏡像
    將鏡像ubuntu:15.04導出為ubuntu_15.04.tar:
$ docker save -o ubuntu_15.04.tar ubuntu:15.04
  1. 載入鏡像
$ docker load --input ubuntu_15.04.tar
或:
$ docker load < ubuntu_15.04.tar

3.7 上傳鏡像

需要先登陸:

$ docker login

添加標簽:

$ docker tag ubuntu:15.04 hanzai/ubuntu:15.04       // hanzai是我的賬號名

上傳鏡像:

$ dokcer push NAME[:TAG]   // 默認上傳到Docker Hub官方倉庫
或:
$ docker push [REGISTRY_HOST[:REGISTRY_PORT]/]NAME[:TAG]


$ docker push hanzai/ubuntu:15.04

4 操作Docker容器

容器是鏡像的一個運行實例,不同的是,鏡像是靜態的只讀文件,而容器帶有運行時需要的可寫文件層。

4.1 創建容器

$ docker create -it ubuntu:14.04   // 創建容器,會為該容器分配一個名稱,可用docker ps查看,也可以用--name string參數來指定容器名稱
d4f5a0e3ae733....                  // 容器ID
$ docker start  d4f5               // 啟動容器

或
$ docker run -it ubuntu:14.04 /bin/bash   // 創建并啟動容器
# exit 或 Ctrl+d                          // 退出容器

或
$ docker run -d ubuntu:14.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"                    
                                  // 后臺以守護進程的形式運行
ab855a5b5af2b...                  // 容器ID
$ docker logs ab855               // 查看日志
hello world
hello world
...

使用docker run時,后臺運行的標準操作為:

  • 檢查本地是否存在制定的鏡像,不存在就從公有倉庫下載
  • 利用鏡像創建,并啟動一個容器
  • 分配一個文件系統給容器,并在只讀的鏡像層外面掛載一層可讀寫層
  • 從宿主主機配置的網橋接口中橋接一個虛擬接口到容器中去
  • 從網橋的地址池配置一個ip地址給容器
  • 執行用戶指定的應用程序
  • 執行完畢后容器被自動終止

4.2 終止容器

$ docker stop [-t|--time[=10]] [CONTAINER]  // 等待一段時間(默認10秒)后關閉容器
$ docker restart [CONTAINER]                // 重啟容器

4.3 進入容器

使用-d參數啟動容器后會進入后臺,用戶無法看到容器中的信息,也無法進行操作.

  1. attach命令
$ docker attach [--detach-keys[=[]]] [--no-stdin] [--sig-proxy[=true]] CONTAINER_NAME   // CONTAINER_NAME可以使用docker ps查詢
--detach-keys[=[]] : 退出attach模式的快捷鍵序列,默認CTRL-p或CTRL-q.
--no-stdin=true|false : 是否關閉標準輸入,默認打開.
--sig-proxy=true|false : 是否代理收到的系統信號給應用進程,默認true.

當多個窗口同時使用attach連接到同一個容器時,所有窗口都會同步顯示.

  1. exec命令
$ docker exec [-d|--detach] [--detach-keyss[=[]]] [-i|--interactive] [--privileged] [-t|--tty] [-u|--user[=USER]] CONTAINER COMMAND [ARG...]
-i,--interactive : 打開標準輸入接受用戶輸入命令,默認false.
--privileged : 是否給執行命令最高權限,默認false.
-t,--tty : 分配偽終端,默認false.
-u,--user[=USER] : 執行命令的用戶名或ID.

如:
$ docker exec -it ab855a5b5af2b /bin/bash

4.4 刪除容器

刪除處于終止或退出狀態的容器:

$ docker rm [-f|--force] [-l|--link] [-v|--volumes] CONTAINER [CONTAINER...]
-f,--force : 是否強行終止并刪除一個運行中的容器.
-l,--link : 刪除容器連接,但保留容器.
-v,--volumes : 刪除容器掛載的數據卷.

4.5 導入和導出容器

  1. 導出容器
    導出容器是指導出一個已經創建的容器到一個文件中,不管容器是否處于運行狀態.
$ docker export -o filename.tar CONTAINER
或
$ docker export CONTAINER > filename.tar
  1. 導入容器
    導出的文件可以使用docker import導入變成鏡像:
$ docker import [-c|--change[=[]]] [-m|--message[=MESSAGE]] file|URL|[REPOSITORY[:TAG]]
例如:
$ docker import filename.tar REPOSITORY:TAG

5 訪問Docker倉庫

  • docker倉庫是集中存放鏡像文件的場所,分公有倉庫和私有倉庫
  • 倉庫注冊服務器(Registry)是存放倉庫(Repository)的地方
  • 一個Registry可以存放多個Repository,每個Repository中存放一類鏡像,用標簽(tag)進行區分

5.1 Docker Hub

登陸:

$ docker login

鏡像資源分為兩類:

  • 基礎鏡像/根鏡像:由Docker公司創建,驗證,支持,提供.往往使用一個單詞作為鏡像名稱.
  • 用戶創建的鏡像:由Docker用戶創建并維護,帶有用戶名稱為前綴,表明是某用戶下的某倉庫.

docker支持自動創建功能,允許用戶通過Docker Hub指定跟蹤一個目標網站(目前支持Github或BitBucket)上的項目,一旦項目發生新的提交,則自動執行創建.

6 Docker數據管理

容器中的數據管理方式主要有:

  • 數據卷:容器內數據直接映射到本地主機環境;
  • 數據卷容器:使用特定容器維護數據卷。

6.1 數據卷

數據卷是一個可供容器使用的特殊目錄,它將主機操作系統目錄直接映射進容器。其提供了很多特性:

  • 數據卷可以在容器之間共享和重用,容器間傳遞數據將變得高效方便;
  • 對數據卷內數據的修改會立馬生效,無論是容器內操作還是本地操作;
  • 對數據卷的更新不會影響鏡像,解耦了應用和數據;
  • 卷會一直存在,直到沒有容器使用,可以安全地卸載它。
  1. 在容器內創建一個數據卷:
使用ubuntu:14.04鏡像創建一個ubuntu容器,使用-v參數創建一個數據卷掛載到容器的/hanry目錄中:
$ docker run -it --name ubutnu -v /hanry ubuntu:14.04 /bin/bash
  1. 掛載一個主機目錄作為數據卷:
使用ubuntu:14.04鏡像創建一個ubuntu容器,使用-v參數指定掛載一個本地已有目錄F:\forDocker到容器的/hanry目錄中作為數據卷:
$ docker run -it --name ubutnu -v F:\forDocker:/hanry ubuntu:14.04 /bin/bash

docker掛載數據卷的默認權限是讀寫(rw),可以通過設置修改:

$ docker run -it --name ubutnu -v F:\forDocker:/hanry:ro ubuntu:14.04 /bin/bash

在這個過程中可能會遇到目錄無法共享或防火墻問題,可參考以下方式解決:
解決共享磁盤問題的方案

6.2 數據卷容器

如果用戶需要在多個容器之間共享一些持續更新的數據,最簡單的方式是使用數據卷容器。數據卷容器也是一個容器,但它的目的是專門用來提供數據卷供其他容器掛載。

首先,創建一個數據卷容器dbdata,并在其中創建一個數據卷掛載到/dbdata:
$ docker run -it -v /dbdata --name dbdata ubuntu:14.04

然后,在其他容器中使用--volumes-from參數來掛載dbdata容器中的數據卷:
$ docker run -it --volumes-from dbdata --name db1 ubuntu:14.04
$ docker run -it --volumes-from dbdata --name db2 ubuntu:14.04

這樣,在dbdata、db1、db2三個容器中的/dbdata目錄下修改數據,其他容器都能看到。

若刪除掛載的容器,數據卷不會自動刪除。若要刪除一個數據卷,必須在刪除最后一個還掛載著它的容器時顯式使用-v參數來刪除:

$ docker rm -v CONTAINER   // 在刪除容器時,順便刪除容器掛載的數據卷

6.3 利用數據卷容器來遷移數據

  1. 備份
1、從dbdata容器中掛載數據卷,也就是掛載到目錄/dbdata
2、將本地目錄F:\Docker2掛載到容器中的/backup目錄作為數據卷
3、使用鏡像ubuntu:14.04創建并啟動一個worker容器
4、worker容器啟動后,將/dbdata目錄下內容打包壓縮到容器內的/backup/backup.tar,也就是主機F:\Docker2目錄下的backup.tar
$ docker run -it --volumes-from dbdata -v F:\Docker2:/backup --name worker ubuntu:14.04 tar cvf /backup/backup.tar /dbdata

或:
$ docker run -it --volumes-from dbdata -v F:\Docker2:/backup --name worker ubuntu:14.04 /bin/bash
# tar cvf /backup/backup.tar /dbdata
  1. 恢復
1、從dbdata容器中掛載數據卷,也就是掛載到目錄/dbdata
2、將本地目錄F:\Docker2掛載到容器中的/backup目錄作為數據卷
3、使用鏡像ubuntu:14.04創建并啟動一個容器
4、容器啟動后,將主機F:\Docker2目錄下,也就是/backup目錄下的backup.tar解壓
$ docker run -it --volumes-from dbdata -v F:\Docker2:/backup ubuntu:14.04 tar xvf /backup/backup.tar

或:
$ docker run -it --volumes-from dbdata -v F:\Docker2:/backup ubuntu:14.04 /bin/bash
# tar xvf /backup/backup.tar

7 端口映射與容器 互聯

Docker提供了兩種滿足服務訪問的功能:

  • 允許映射容器內應用服務端口到本地宿主主機
  • 互聯機制實現多個容器間通過容器名來快速訪問

7.1 端口映射實現訪問容器

當容器運行一些網絡應用,要讓外部訪問這些應用時,可以通過-P或-p參數來指定映射端口。使用-P參數時,會隨機映射一個端口到內部容器開放網絡的端口(使用docker ps -l或者docker logs -f CONTAINER命令查看);使用-p參數可以指定要映射的端口,并且在一個指定端口上只能綁定一個容器,支持的格式有:

  • 映射到指定地址的指定端口 IP:HostPort:ContainerPort
  • 映射到指定地址的任意端口 IP::ContainerPort
  • 映射到所有接口地址 HostPort:ContainerPort
將本地端口映射到容器的端口,可以多次使用-p參數多次綁定:
$ docker run -d -p 5000:5000 -p 3000:80 training/webapp python app.py

映射一個特定的地址:
$ docker run -d -p 127.0.0.1:5000:5000 training/webapp python app.py
指定udp端口:
$ docker run -d -p 127.0.0.1:5000:5000/udp training/webapp python app.py

綁定127.0.0.1的任意端口到容器的5000端口,本地主機會自動分配一個端口:
$ docker run -d -p 127.0.0.1::5000 training/webapp python app.py

查看端口
$ docker port CONTAINER port

7.2 互聯機制實現便捷訪問

容器的互聯會在源和接收容器之間創建連接關系,接收容器可以通過容器名快速訪問到源容器,而不用指定具體的IP地址。

使用--link name:alias參數可以讓容器之間安全地進行交互。name是要連接的容器的名稱,alias是這個連接的別名。

$ docker run -d --name db training/postgres
$ docker run -d -P --name web --link db:db training/webapp python app.py

docekr通過兩種方式為容器公開連接信息:

  • 更新環境變量:
$ docker run -rm --name web2 --link db:db training/webapp env
輸出中DB_開頭的環境變量是供web容器連接db容器使用的。
  • 更新/etc/hosts文件。
docker會添加host信息到父容器的/etc/hosts文件中:
$ docker run -it -rm --link db:db training/webapp /bin/bash
# cat /etc/hosts
// 里邊包含兩個host信息,一個是web容器,使用自己的id作為默認主機名;一個是db容器的IP和主機名。
# ping db   // 可以直接使用容器名進行通信。

8 使用Dockerfile創建鏡像

Dockerfile是一個文本格式的配置文件,用戶可以使用Dockerfile來快速創建自定義的鏡像。

8.1 基本結構

Dockerfile由一行行命令語句組成,以#開頭表示注釋。一般包含四部分:基礎鏡像信息、維護者信息、鏡像操作指令和容器啟動時執行指令。

# 使用的基礎鏡像,必須放在第一行
FROM ubutntu

# 維護者信息
MAINTAINER docker_user docker_user@email.com

# 鏡像操作指令。
# RUN指令將對鏡像執行跟隨的命令。每執行一條RUN指令,鏡像就添加新的一層,并提交。
RUN echo "deb http://archive.ubuntu.com/ubuntu/ raring main universe" >> /etc/apt/sources.list
RUN apt-get update && apt-get install -y nginx
RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf

# 容器啟動時執行的指令
CMD /usr/sbin/nginx

8.2 指令說明

指令1

指令2
  1. FROM
    FROM指令是最重要的一個且必須為Dockerfile文件開篇的第一個非注釋行,用于為映像文件構建過程指定基準鏡像,后續的指令運行于此基準鏡像所提供的運行環境。如果本地不存在,則會從Docker Hub Registry上拉取所需的鏡像文件。
    如果在同一個Dockerfile中創建多個鏡像,可以使用多個FROM指令。
FROM <image>
FROM <image>:<tag>
FROM <image>@<digest>
  1. MAINTAINER
    指定維護者信息,會被寫入生成鏡像的Author屬性中,格式:
MAINTAINER <name>
MAINTAINER image_creator@docker.com
  1. RUN
    運行指定命令。格式:
RUN <command>  // 默認在shell終端中運行命令
RUN ["<executable>","<param1>","<param2>"]  // 該指令會被解析為Json數組,必須使用雙引號。使用exec執行,不啟動shell。

每條RUN指令在當前鏡像的基礎上執行指定命令,并提交為新的鏡像。若命令較長可以使用\來換行。

  1. CMD
    指定容器啟動時默認執行的命令,格式:
CMD ["<executable>","<param1>","<param2>"] // 使用exec執行,推薦使用
CMD <command> <param1> <param2>  // 在/bin/sh中執行
CMD ["<param1>","<param2>"]

每個Dockerfile只能有一條CMD命令,若有多條CMD命令,則只執行最后一條。

  1. LABEL
    指定生成鏡像的元數據標簽信息,格式:
LABEL <key>=<value> <key>=<value> ...
  1. EXPOSE
    聲明鏡像內服務所監聽的端口,格式:
EXPOSE <port> [<port> ...]

只是聲明,并不會自動完成端口映射。

  1. ENV
    為鏡像定義所需的環境變量,并可被Dockerfile文件中位于其后的其他指令(ENV、ADD、COPY等)所調用。格式:
ENV <key> <value>
ENV <key>=<value> ...
  1. ADD
    格式:
ADD <src> <dest>

復制指定的<src>路徑下的內容到容器的<dest>路徑下,其中<src>可以是Dockerfile所在目錄的一個相對路徑,也可以是一個URL,還可以是tar文件(會自動解壓到<dest>路徑下);<dest>可以是鏡像內的絕對路徑,或相對于工作目錄(WORKDIR)的相對路徑。

  1. COPY
    格式:
CPOY <src> <dest>

用于從Docker主機的<src>復制文件至創建的鏡像的<dest>,若<dest>不存在則自動創建。

  1. ENTRYPOINT
    指定鏡像的默認入口命令,會在啟動容器時作為根命令執行,所有傳入值作為該命令的參數,格式:
ENTRYPOINT  <command> <param1> <param2>
ENTRYPOINT ["executable","parm1","parm2"]

每個Dockerfile只能有一條ENTRYPOINT命令,若有多條ENTRYPOINT命令,則只執行最后一條。運行是可以被--entrypoint參數覆蓋。

  1. VOLUME
    創建一個數據卷掛載點,格式:
VOLUME ["/data"]

可以從本地主機或其他容器掛載數據卷,一般用來存放數據庫和需要保存的數據等。

  1. USER
    指定運行images的用戶名或UID,默認的運行用戶為root。后續指令也會用指定的身份。格式:
USER <UID> | <User_Name>
  1. WORKDIR
    為Dockerfile中所有的RUN、CMD、ENTRYPOINT、COPY和ADD設定工作目錄。格式:
WORKDIR  <dirpath>

可以使用多個WORKDIR指令。后續命令若是相對路徑,則會基于之前的命令指定的路徑。

  1. ARG
    指定鏡像內使用的參數,在執行docker build命令時才以--build-arg <varname>=<name>傳入,格式:
ARG <name>[=<default value>]
  1. ONBUILD
    若所創建的鏡像作為其他鏡像的基礎鏡像時,所執行的創建操作指令。格式:
ONBUILD <instruction>
  1. STOPSIGNAL
    指定所創建鏡像啟動的容器接收退出的信號值。格式:
STOPSIGNAL signal
  1. HEALTHCHECK
    容器啟動時如何進行健康檢查,格式:
HEALTHCHECK [options] CMD command // 根據所執行命令返回值是否為0來判斷
HEALTHCHECK NONE // 禁止基礎鏡像中的健康檢查
  1. SHELL
    指定其他命令使用shell時的默認shell類型,格式:
SHELL ["executable","parm1","parm2"]
默認:SHELL ["/bin/sh","-c"]

8.3 創建鏡像

$ docker build [options] path

讀取path下的Dockerfile,并將該路徑下的所以內容發送給Docker服務端,有服務端來創建鏡像。

/tmp/docker_builder下的Dockerfile,-t指定生成鏡像標簽信息
$ docker build -t build_repo/first_image /tmp/docker_builder

8.4 .dockerignore文件

可以使用.dockerignore文件來讓Docker忽略指定的目錄和文件,如:

# comment
    */temp*
    */*/temp*
    tmp?
    ~*
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容