目录
1.什么是Docker?☀️
在实际开发过程中,你或许遇到过下面的场景:
由于电脑环境配置不一样,或是软件版本不兼容,导致项目移交到另一台电脑上的时候,项目就无法正常启动了。需要在新系统上重新配置项目的运行环境,而有时候新配置的环境又会与原先配置好的环境产生冲突,很是头疼!
而随着软件项目的规模和复杂度增加,项目对各种软件包(库、框架等)的依赖也变得错综复杂。不同项目可能需要不同版本的同一个依赖包,这在传统的系统环境中很难同时满足,容易出现版本冲突。于是Docker就此诞生了!
---------------------------------------------------------------------------------------------------------------------------------
Docker 是一个开源的应用容器引擎,基于GO语言并遵从 Apache2.0 协议开源。
- Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。
- 容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app),更重要的是容器性能开销极低。
- Docker 从 17.03 版本之后分为 CE(Community Edition: 社区版) 和 EE(Enterprise Edition: 企业版),我们用社区版就可以了。
Docker当中主要的对象有三个:镜像(image),容器(container)和仓库(registry) ,其中镜像和容器的关系,就好比我们编程当中的类与对象。
- 镜像: 一个静态的模板,好比烹饪秘方,上面记录着应该用哪些食材,用什么方法进行制作,我们可以根据这个模板,来创建容器。
- 容器:一个动态运行的小系统,依据镜像当中所记载的环境需要,而创建出来的一个实例!
- 仓库:存放大量镜像的地方,仓库又可分为公有仓库和私有仓库,docker默认情况都是访问Docker Hub仓库。
由上面我们可以看出,容器来源于镜像,所以我们打包项目依赖的时候,得先编写好镜像,再将镜像发给对方,对方依据我们编写的镜像生成一个独立的容器,项目就可以在这个容器中正常运行了。
或许这时,你会提出一个问题,那容器和虚拟机有什么区别呢,欸,还是有些区别滴👇:
对比点 Docker | 容器 | 虚拟机 |
---|---|---|
隔离性 | 进程级别隔离,容器间共享宿主机内核,隔离性相对较弱,可能存在安全风险 | 硬件级别隔离,每个虚拟机运行独立操作系统,隔离性好,安全性高 |
启动时间 | 秒级,无需启动完整操作系统 | 分钟级,需启动完整操作系统 |
系统资源占用 | 轻量级,仅包含应用及其依赖,共享宿主机内核,占用空间小,运行效率高 | 重量级,运行完整操作系统及虚拟硬件,占用大量内存、存储,资源利用率低 |
性能开销 | 接近原生性能,性能损耗小 | 因虚拟硬件层,性能损耗较大 |
灵活性 | 部署轻量应用灵活,支持微服务架构,快速创建销毁容器 | 部署大型、复杂应用灵活,可运行不同操作系统,但创建销毁较慢 |
适用场景 | 微服务架构应用、持续集成 / 持续部署(CI/CD)流程、开发测试环境快速搭建 | 多租户环境、运行不同操作系统或遗留系统、对隔离性安全性要求极高场景 |
2.Docker的安装 (Ubuntu版本)☀️
请移步--> docker安装
3.Docker的C/S架构 ☀️
实际上Docker当中也是通过命令进行操作的,在介绍命令之前,我想先简单介绍一下docker当中命令执行的一个过程。这就不得不介绍一下Docker 的 C/S(客户端 - 服务器)架构
- Docker 客户端(Docker Client) :是用户与 Docker 守护进程交互的接口,用户通过它发送命令和请求给 Docker 守护进程,例如创建、启动、停止容器等操作。
- Docker 守护进程(Docker Daemon) :是 Docker 的服务端,它运行在主机上,负责管理 Docker 镜像、容器等资源,并处理来自 Docker 客户端的请求。
- Docker 仓库(Docker Registry) :用于存储 Docker 镜像,方便镜像的分发和共享,可以是官方的 Docker Hub 或私有的仓库。
而这种架构的优势就在于
- 简化管理 :用户无需直接操作底层的容器运行时,通过客户端就能管理和操作容器,降低了使用难度。
- 多用户支持 :多个用户可以通过客户端同时与守护进程交互,实现对容器资源的集中管理。
- 跨平台性 :Docker 客户端可以运行在多种操作系统上,通过 C/S 架构,用户能够在不同平台上统一管理容器资源。
而当我们进行docker命令操作时,命令会从本地的客户端(client)发出,本机上的守护进程(daemon)接收到命令后,就会执行对应的操作,如果是pull,search等镜像资源的请求,需要访问仓库,那么守护进程就会访问对应的仓库获取有关的镜像信息,再传输回来。
官方图示:
4.Docker命令 ☀️
4.1 镜像命令
命令 | 作用 |
---|---|
docker images | 查看本地镜像 |
docker search | 搜索仓库当中镜像 |
docker pull | 从仓库中下载镜像到本地 |
docker rmi | 本地删除镜像 |
--------------------------------------------------------------------------------------------------------------------------------
⭐ docker images
这里解释一下这表每一列都表示什么
- REPOSITORY: 镜像的仓库源
- TAG :镜像的标签,你可以理解为版本号,latest一般表示最新版
- IMAGE ID :镜像的id
- CREATED :镜像的创建时间
- SIZE :镜像的大小
--------------------------------------------------------------------------------------------------------------------------------
⭐docker search
(如果你所使用的daemon.json中的镜像源是我上面那个教程当中的,search命令如下,得在你查找的镜像前添加regiter.librax.org,如果是可以直接访问Docker Hub,或者使用其它源,一般是不用,格式为 docker search 【镜像名字】)
执行结果显示仓库当中的mysql的镜像列表,有个STARS列,就好比Github上项目的星星数,我们可以添加参数 --filter=STARS=【num】 # 搜索出来的镜像就是stars大于num的,比如说只要看星星数量大于5000的:
有时候命令连接不上,你也可以直接访问Docker Hub网站进行镜像搜索查看(需要魔法)
--------------------------------------------------------------------------------------------------------------------------------
⭐docker pull
(如果你同的是我上面的镜像源,pull的时候,得在镜像前面加上library)
格式:docker pull 【镜像名】:【TAG】(TAG就是镜像的标签,相当于版本)
这里我们从仓库取一个最新版(latest标签)的rabbitmq(消息队列)到本地 ,看到下载过程,你会发现怎么第一个下载的东东怎么是Already exists?它是如何下载的?欸,这就提到Docker镜像的文件组成了。
实际上镜像会复用宿主机上已经有的配置,如果镜像当中的某个配置(镜像层),宿主机上有了且可以与镜像分享,那么镜像就不会导入这部分内容,只会将其余部分以堆叠的形式导入,这也就是为什么一个Ubuntu镜像大小只有700~800MB,而一个虚拟机大小有1GB~5GB,容器直接复用宿主机的 Linux 内核,无需包含独立的内核。
那为什么一个Tomcat的安装包只有几十MB,而Tomcat的镜像却又几百MB?
那是因为镜像必须包含完整运行时的环境!我们设置镜像的目的,就是为了能够让项目在其它系统上运行,因此镜像当中内容必须完整,不然创造出来的容器可能无法正常运行!
- Tomcat 安装包:仅包含 Tomcat 二进制文件,没有任何依赖,JDK等依赖需要自己额外安装
- Tomcat镜像:不仅包含Tomcat二进制文件,还包括JDK等其它所需的运行环境
--------------------------------------------------------------------------------------------------------------------------------
⭐docker rmi
格式:docker rmi 【镜像ID】,删除对应的镜像
注意,如果镜像创建的容器正在运行,那么镜像无法删除,得先删除对应的容器,才可以继续删除镜像。
4.2 容器命令
命令 | 作用 |
---|---|
docker run | 创建容器并运行 |
docker ps | 列出正在运行的容器 |
docker rm | 删除容器 |
docker start / stop / restart / kill | 启动 / 停止 / 重启 / 强制停止 容器 |
docker inspect | 查看容器信息 |
--------------------------------------------------------------------------------------------------------------------------------
⭐docker run
docker run 【选项】 image:tag
选项
-it:进入交互式运行(会进入容器内部的终端,可以输入命令与容器内部的内容交互)
-d:进入守护式运行(相当于让容器后台自动运行)
-p:指定容器的端口(四种方式)小写字母p
-p ip:主机端口:容器端口
-p 主机端口:容器端口
-p 容器端口
容器端口
--name="":定义容器的名字
// 没有tag,默认就是按本地所拥有的镜像执行
我们来看看什么是交互式执行,什么是守护式执行
从上图我们可以看到,守护式执行(-d),运行容器后,我们依旧在自己的系统终端,容器在后台默默执行,但交互式执行 (-it),我们来到了一个新的终端(命令行前面的内容切换了),这个终端其实就是容器内部系统的终端,你在这里输入的操作命令都是在容器内执行,和你外部系统无关,这一个完全独立的小系统。输入 exit 命令便可退出容器内部的终端,回到我们的主系统。
那如果之后在想进入运行容器的内部,可以使用 docker attach 【容器ID】进入到容器终端
--------------------------------------------------------------------------------------------------------------------------------
⭐docker ps
docker ps可以显示正在运行的容器,-a选项,可以查看所有的容器
--------------------------------------------------------------------------------------------------------------------------------
⭐删除,启动,停止等操作
docker rm 容器id # 删除容器(不能删除正在运行的容器)如果要强制删除:docker rm -f 容器id
docker rm -f $(docker ps -aq) # 删除全部容器
docker ps -a -q|xargs docker rm # 删除所有容器
docker start 容器id # 启动容器
docker restart 容器id # 重启容器
docker stop 容器id # 停止当前正在运行的容器
docker kill 容器id # 强制停止当前容器
docker inspect 容器id # 查看容器信息
5.容器数据卷☀️
基于上面的内容,想必你对容器也有了一定概念,那我问你:
- 容器删除后,容器当中的存储数据会消失吗?
- 容器和主机之间可以交换文件吗?
- 容器和容器之间可以交换文件吗?
回答我!Look in my eyes! 咳咳,下面我们来回答一下上面的问题,(1)由于容器本身就是一个小系统,删除容器,其实差不多就是删库跑路了,所以存储数据是会随着容器删除一起消失的。(3)我们在第一节说过,每个容器之间都是相互独立的,没有对应接口,所以容器之间并不能直接交换文件。(2)但是容器和主机之间是可以交换文件的!所以我们可以通过 容器->主机->容器 的方式,实现两个容器的文件间接交换。那如何实现呢?
-----------------------------------------答案就是数据卷-----------------------------------------------------
数据卷就是宿主机中的一个文件或目录
当容器目录和宿主机目录绑定后,双方的修改会立即同步
一个数据卷可以被多个容器挂载
一个容器也可以挂载多个数据卷
数据卷基本使用
docker run -it -v 主机目录:容器内目录 镜像名 /bin/bash
// 容器内目录,不设置,默认为容器的更根目录
docker run -it -v 主机目录 镜像名 /bin/bash
我们使用 docker inspect 命令查看配置了数据卷的容器,可以在Mounts(挂载信息)中看到我们的配置内容。
数据卷容器
数据卷容器是 Docker 中用于管理数据的一种特殊容器。它主要用于在容器之间共享和持久化数据。数据卷容器本身通常不运行任何应用程序,而是专门用来存储和共享数据。
--volumes-from 参数可以将数据卷挂载到其他容器中,上述命令,就是将c3容器的数据卷(主机中的/volume)挂载到c1和c2容器中,这样子三个容器都能访问/volume中的数据,实现了容器的数据共享)
6.DockerFile☀️
上面讲了那么多关于容器的知识,那它的妈妈——镜像呢?我们该如何创建镜像呢?在实际开发中,我们怎么打包我们的项目环境,将它制作成一个镜像呢?答案:编写DockerFile!
DockerFile实际上就是用来构建docker镜像的命令参数脚本!那么我们该如何编写呢?
1.首先,创建一个空白的文本文件,并将其命名为
Dockerfile
(注意没有文件扩展名,名字随便取),这个文件通常位于你的项目的根目录下。2.接着编写DockerFile文件,有关脚本命令如下:
FROM # 基础镜像,一切从这里开始构建 LABEL / MAINTAINER # 镜像是谁写的:姓名+邮箱,现在好像一般用LABEL RUN # 镜像构建的时候需要运行的命令 ADD # 步骤:tomcat镜像,这个tomcat压缩包!添加内容 WORKDIR # 镜像的工作目录 VOLUME # 挂载的目录 EXPOSE # 暴露端口配置 CMD # 指定这个容器启动的时候要运行的命令,只有最后一个会生效,可被替代 ENTRYPOINT # 指定这个容器启动的时候要运行的命令,可以追加命令 ONBUILD # 当构建一个被继承DockerFile这个时候就会运行ONBUILD的指令。触发指令。 COPY # 类似ADD,将我们文件拷贝到镜像中 ENV # 构建的时候设置环境变量!
- 每个保留关键字(指令)都是必须是大写字母
- 执行从上到下顺序执行
- 每一个指令都会创建提交一个新的镜像层,并提交!
3.编写完DockerFile文件后,使用 docker build 命令构建镜像 (命令最后有个一点噢)
docker build -t 镜像名:标签 .
- -t:指定镜像的名称和标签(可选),用于标识镜像。
- . :表示 Dockerfile 所在的上下文路径(通常为当前目录)。就是告诉系统使用当前目录下的DockerFile创建镜像。
4.构建完成后,我们使用 docker images 命令就可以查看到我们构建的镜像了。
DockerFile编写-举例说明
下面我举两个具体例子,来说明DockerFile的编写,制作镜像
示例1:制作一个Ubuntu镜像,配置好环境变量MYPATH为/usr/local,并且将其设置为镜像的工作目录,并且配置好vim命令和namp命令,配置好SSH远程连接服务,暴露22端口。
(大家可以尝试先自己写一下这个例子的DockerFile)
DockerFile文件编写如下:
FROM ubuntu:latest // 使用的是本地的镜像,如果本地没有,docker就会去仓库拉取
LABEL maintainer "FJNU_example"
ENV MYPATH /usr/local
WORKDIR $MYPATH
RUN apt-get update && \
apt-get install -y vim && \
apt-get install -y nmap && \
apt-get install -y openssh-server && \
service ssh start
EXPOSE 22
CMD /bin/bash
接着使用docker build命令生成镜像,由于我的DockerFile文件名字叫TEST,所以得通过 -f 参数选择编写的文件,才能运行。如果你的DockerFile文件名就叫Dockerfile,那可以不加-f参数
使用docker images就可以查看到我们创建的镜像了
创建容器运行试试
--------------------------------------------------------------------------------------------------------------------------------
示例2:将本地存储着数据的数据库打包,其中包含两个数据表User和Goods,里面已经存储着一些数据,将他们打包成镜像。
首先编一个 init.sql 的初始化文件
-- 创建数据库
CREATE DATABASE User;
CREATE DATABASE Goods;
-- 创建用户并授予权限
CREATE USER 'FJNU_STUDENT'@'%' IDENTIFIED BY '123456';
GRANT ALL PRIVILEGES ON User.* TO 'FJNU_STUDENT'@'%';
GRANT ALL PRIVILEGES ON Goods.* TO 'FJNU_STUDENT'@'%';
-- 选择数据库并创建表
USE User;
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
password VARCHAR(50) NOT NULL
);
INSERT INTO users (username, password) VALUES ('user1', 'password1'), ('user2', 'password2');
USE Goods;
CREATE TABLE goods (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
price DECIMAL(10, 2) NOT NULL
);
INSERT INTO goods (name, price) VALUES ('商品1', 100.00), ('商品2', 200.50);
在同级目录下,编写Dockerfile
# 使用官方的 MySQL 镜像作为基础镜像
FROM mysql:latest
# 设置环境变量
ENV MYSQL_ROOT_PASSWORD=root_password
ENV MYSQL_USER=FJNU_STUDENT
ENV MYSQL_PASSWORD=123456
# 复制初始化 SQL 文件到容器中
COPY init.sql /docker-entrypoint-initdb.d/
# 暴露 MySQL 的默认端口
EXPOSE 3306
其中/docker-entrypoint-initdb.d/ 是 MySQL 官方镜像中一个特定的目录。在容器首次启动时,如果该目录下存在 SQL 脚本文件,MySQL 会自动执行这些脚本。这是 MySQL 官方镜像提供的一个特性,用于初始化数据库、创建用户和表等操作。
接着创建镜像,并运行容器
docker build -t mysql-custom:latest .
docker run -d -p 3306:3306 --name mysql-container mysql-custom:latest
接着我们可以连接容器的数据库
docker exec -it mysql-container mysql -uFJNU_STUDENT -p
输入密码后,就可以进入我们的mysql容器了
上面所使用的docker exec命令,可以进入正在运行的容器内的交互式 shell ,格式如下:
docker exec -it 运行中的容器名称或ID /bin/bash
镜像上传到仓库
// 登录到仓库
docker login
// 标记镜像
docker tag 本地镜像名[:标签] 仓库地址/仓库名/镜像名[:标签]
// 上传镜像
docker push 仓库地址/仓库名/镜像名[:标签]
- 在上传镜像之前,需要先登录到 Docker 仓库。如果还没有账号,需要先在 Docker Hub 或其他仓库网站上注册。
- 如果要将镜像推送到非默认的仓库(如私有仓库或企业仓库),需要对镜像进行标记,指定其仓库地址和仓库名称。如果是 Docker Hub,默认可以省略;对于其他仓库,比如阿里云容器镜像仓库、GitHub Container Registry 等,需要提供完整的仓库地址
7.Docker网络☀️
当你运行Tomcat等与网络有关的容器时,你是否相关容器是怎么连接网络的?它的网络是哪来?容器之间以及与主机之间可以进行网络连通吗?那我们现在就来研究一下这些问题。
基本联通原理
先做一个小测试吧:
1.首先先整理一下我们的容器,使用docker ps -aq可以显示所有容器的ID
接着使用 docker stop $(docker ps -aq) 停止所有的容器运行
docker rm $(docker ps -aq) 删除全部的容器。
2.创建两个tomcat容器,并运行
3.使用 ifconfig 命令,可以发现我们多了三个网卡信息,这是哪来的呢?(别急,慢慢看)
4.我们查看一下两个容器的ip,并依据ip进行网络连通性测试(ping),发现容器之间可以根据ip互相ping通,而容器与主机之间也可以ping通,他们可以实现网络互联
如果tomcat中缺少包,而无法执行ip addr和ping命令,可以用以下方法(以一个容器为例):
// 进入容器的命令行 docker exec -it tomcat01 /bin/bash // 执行命令 apt-get update apt-get install -y inetutils-ping apt-get install -y iproute2 //退出 exit
下面我们来说明一下,为什么多出来的三个网卡是什么,为什么他们之间可以互相连通。
docker0
是在 Docker 守护进程启动时自动创建的虚拟网桥。它作为连接主机网络和容器网络的桥梁,负责在主机和容器之间转发网络流量。 每当我们创建一个运行容器,Docker守护进程就会为容器的网络接口(就是上面的eth0)分配一个ip。而在docker中,网络连接采用了 veth-pair 的虚拟网络技术。
veth-pair好比一个桥梁,它的两端分别位于 容器的网络命名空间 和 宿主机的网络命名空间 中,
以上面的例子为例,tomcat容器当中的eth0就是veth-pair的一端,而宿主机当中的docker0就是另一端,所以容器和主机之间,就可以通过这两个接口,进行网络的连通。而容器之间的网络,实际上就是通过主机的docker0网桥作为中介,互相联通。一幅图解释👇
自定义网络
Docker 自定义网络是 Docker 提供的一种功能,允许用户创建自己的网络环境,用于连接容器。相较于默认网络,自定义网络提供了更好的隔离性、灵活性和可管理性,能满足不同场景下容器间通信的需求。
Docker 网络模式主要分为以下几种:
- 桥接模式(Bridge):这是默认的网络模式。Docker 在主机上创建一个虚拟网桥(docker0),并为每个容器分配一个虚拟网络接口(veth pair),一端连接到虚拟网桥,另一端连接到容器内的网络接口。容器通过虚拟网桥与主机网络进行通信。
- 主机模式(Host):容器与主机共享网络命名空间,容器的网络接口直接绑定到主机的网络接口上,没有网络隔离。
- 容器模式(Container):容器共享另一个容器的网络命名空间,多个容器使用相同的 IP 地址和端口范围。
- 无网络模式(None):容器不配置任何网络接口,只能通过本地回环(loopback)接口进行通信。
网络命令 | 作用 | 示例 |
---|---|---|
docker network ls | 列出所有网络 | docker network ls |
docker network inspect | 查看网络详细信息 | docker network inspect mynet |
docker network create | 创建新网络 | docker network create mynet |
docker network rm | 删除网络 | docker network rm mynet |
docker network connect | 将容器连接到网络 | docker network connect mynet mycontainer |
docker network disconnect | 断开容器与网络的连接 | docker network disconnect mynet mycontainer |
8.Docker Compose☀️
Docker Compose 是一种用于定义和管理多容器 Docker 应用程序的工具。它允许用户通过一个配置文件(docker-compose.yml
)来定义多个相互关联的服务、网络和卷,从而简化了复杂应用的配置和启动流程。使用 Docker Compose,可以轻松地在一个命令中启动或停止整个应用环境,非常适合开发和测试场景。而最新的 docker 已经集成了 docker-compose 功能。
Docker Compose 的配置文件通常命名为 docker-compose.yml,采用 YAML 格式。以下是文件的基本结构和常见配置项:
version: "3.8" # 指定 Compose 文件版本
services: # 定义服务
service1:
image: 镜像名:标签 # 指定服务使用的镜像
container_name: 容器名 # 指定容器名称
ports:
- "主机端口:容器端口" # 端口映射
environment:
- ENV_VAR=value # 设置环境变量
volumes:
- 主机路径:容器路径 # 数据卷挂载
depends_on: # 定义服务依赖关系
- service2
networks:
- network1 # 指定服务连接的网络
service2:
build: ./dir # 指定 Dockerfile 所在目录
command: 启动命令 # 覆盖默认的启动命令
networks: # 定义网络
network1:
driver: bridge # 网络驱动类型
volumes: # 定义数据卷
vol1:
driver: local # 数据卷驱动类型
常用的命令:
命令 | 作用 |
---|---|
docker-compose up | 启动所有服务,默认会重建镜像,使用 -d 参数可以后台运行容器。 |
docker-compose down | 停止并移除所有容器、网络、卷,恢复到执行前的状态。 |
docker-compose build | 重新构建服务的镜像。 |
docker-compose restart | 重启服务中的容器。 |
docker-compose logs | 查看服务容器的日志输出。 |
docker-compose exec | 进入正在运行的服务容器,执行指定命令。 |
案例:
假设有一个基于 Spring Boot 的 Java Web 应用和一个 MySQL 数据库的应用场景。我们可以使用 Docker Compose 来简化应用的配置和启动流程。
项目结构:
myapp/
├── docker-compose.yml
├── app/
│ ├── Dockerfile
│ └── src/ # 应用源代码
└── db/
└── init.sql # 数据库初始化脚本
编写docker-composer.yml
version: "3.8"
services:
app:
build: ./app # 指定 Dockerfile 所在目录
container_name: myapp_container
ports:
- "8080:8080" # 映射主机的 8080 端口到容器的 8080 端口
environment:
- SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/mydb # 数据库连接地址
- SPRING_DATASOURCE_USERNAME=root # 数据库用户名
- SPRING_DATASOURCE_PASSWORD=root_password # 数据库密码
depends_on: # 定义服务依赖关系
- db
networks:
- app-network
db:
image: mysql:8.0 # 使用 MySQL 镜像
container_name: db_container
environment:
MYSQL_ROOT_PASSWORD: root_password # 设置 MySQL root 用户密码
MYSQL_DATABASE: mydb # 创建数据库
volumes:
- db-data:/var/lib/mysql # 数据卷挂载,用于数据持久化
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql # 初始化数据库脚本
networks:
- app-network
networks:
app-network:
driver: bridge # 使用桥接网络
volumes:
db-data: # 定义数据卷
driver: local
在项目目录下执行以下命令启动应用:
docker-compose up
使用 -d 参数可以在后台运行容器:
docker-compose up -d
停止并移除所有容器、网络、卷:
docker-compose down
-----------------------------------完结撒花,恭喜您成功入门Docker🌷--------------------------------------------