WebSocket入门

为什么需要WebSocket?

虽然有HTTP协议,但是一个很明显的缺点是:所有请求只能有客户端发起,向服务端请求。而服务端有任何状态变化,无法直接通知到客户端。简单处理的方法就是轮询,连续不断发起请求,但是这个非常浪费资源,因为需要不断请求连接。最常见的例子就是聊天室。

WebSocket

优点:
- 支持双向通信,实时性更强
- 更好的支持二进制
- 较少的控制开销,数据交换时,数据包请求头较小
- 支持更多扩展

websocket也是通过http请求去建立连接,请求格式如下:

GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-string
Sec-WebSocket-Version: 13

跟普通http请求的区别:
- GET请求的地址不是类似/path/,而是以ws://开头的地址
- 请求头Upgrade: websocketConnection: Upgrade表示这个连接将要被转换为WebSocket连接
- Sec-WebSocket-Key是用于标识这个连接,并非用于加密数据
- Sec-WebSocket-Version指定了WebSocket的协议版本
- 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

服务器返回数据:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string

该响应代码101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议

成功建立连接后,客户端和服务端就可以直接主动发消息给对方。消息传递的格式有两种:文本,二进制数据.通常可以发送JSON数据,方便处理

WebSocket对象

var ws = new WebSokcet(url, [protocol])

const ws = new WebSocket('ws://echo.websocket.org', ['myProtocol1', 'myProtocol2'])

WebSocket 构造函数可接受两个参数,其中,第一个参数必须是以 ws://wss:// 开头的完全限定的 URL
第二个为非必要参数,用于指定可接受的子协议,有两种可能的类型:
- String 类型,值为客户端和服务器端均能理解的协议
- Arrary 类型,包含一组客户端支持的协议(String 类型)

WebSocket 属性

Socket.readyState

只读属性 readyState 表示连接状态,可以是以下值:
- 0 | WebSocket.CONNECTING:表示连接尚未建立
- 1 | WebSocket.OPEN:表示连接已经建立
- 2 | WebSocket.CLOSEING:表示连接正在关闭
- 3 | WebSocket.CLOSED: 表示连接已经关闭或者连接不能打开

bufferedAmount

WebSocket 对象的 bufferedAmount 属性可以用检查已经进入发送队列,但是还未发送到服务器的字节数。可以用来判断发送是否结束

protocol

WebSocket 对象的 protocol 属性值为 WebSocket 打开连接握手期间,服务器端所选择的协议名

protocol 属性在最初的握手完成之前为空,如果服务器没有选择客户端提供的某个协议,则该属性保持空值

WebSockets事件处理

WebSocket 对象具有以下 4 个事件:

open 事件

当服务器响应了 WebSocket 连接请求,触发open事件并建立一个连接,此时WebSocket已经准备好发送和接收数据,open事件对应的回调函数是onopen()

ws.onopen = (event) => {
    console.log('开启连接');
}

// 或者
ws.addEventListener('open', (event) => {
    console.log('开启连接');
}, false)

message事件

message事件在接收到消息是触发,消息内容存储在事件对象eventdata中,对应的回调函数是onmessage()

ws.onmessage = (event) => {
    if (typeof event.data === 'string') {
        console.log('接收到的string消息内容为:' + event.data)
    } else {
        console.log('其他类型消息')
    }
}

除了普通文件,WebSocket消息内容还可以是二进制,这种数据作为Blob消息或者ArraryBuffer消息处理。暂不赘述。

error事件

error事件在响应意外发生故障时触发,对应的回调函数是onerror()。错误会导致WebSocket连接关闭。

close事件

close事件在连接关闭时触发,对应的回调函数是onclose()。一旦连接关闭,客户端和服务器端不在接续接收和发送消息。
close事件的3个常用属性:
- wasClean:布尔值,表示连接是否被正确关闭。如果是来自服务器的close帧的响应,则为true;如果是因为其他原因关闭,则为false
- code:服务器发送的关闭连接握手状态码
- reason:服务器发送的关闭连接握手状态

WebSocket方法

WebSocket API提供两个方法供调用。

send()

使用send()方法可以从客户端向服务端发送消息。前提是必须当WebSocket在客户端和服务端建立全双工双向连接后,才可以调用该方法。所以一般是在open事件触发之后,close触发之前调用send()发送消息

ws.onopen = (event) {
    ws.send('hello websocket');
}

close()

通过使用close()方法,可以人为的手动关闭WebSocket连接或者终止连接尝试。如果连接已关闭,则该方法什么也不做

可以向close()方法传递两个参数:
- code:Number类型,状态代码
- reason: String类型,文本字符串,传递一些关于关闭连接的信息

参考链接:
《WebSocket 教程 - 阮一峰》
《WebSocket客户端编程》

docker入门篇

阅读《Docker — 从入门到实践》

windows使用

  • 下载docker for windows,安装
  • 使用镜像加速,我自己使用的是阿里云镜像加速setting->Daemon->Registry mirrors->粘贴自己的加速器地址
  • 可以使用gui界面,Kitematic

常用命令

获取镜像

可以使用docker pull --help查看

docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]

# etc
docker pull ubuntu:16.04

运行

docker run -it --rm ubuntu:16.04 bash

# 查看当前系统版本
cat /etc/os-release
  • -it: -i: 交互式操作, -t: 终端
  • --rm: 执行退出后删除该容器实例
  • bash: shell使用的方式

镜像列表

docker image ls

# etc
REPOSITORY TAG IMAGE ID CREATED SIZE

各列意义:仓库名, 标签, 镜像id, 创建时间, 解压后文件大小
镜像ID 则是镜像的唯一标识,一个镜像可以对应多个标签

镜像大小

docker system df

删除镜像

docker image rm [选项] <镜像1> [<镜像2> ...]

启动已终止的container镜像

docker [container] start 

后台运行容器

-d参数能够让容器后台运行,保证输出结果不会打印出来。但容器是否长久运行(一直后台挂起),跟-d参数无关,需要一直有指令执行,才不会“秒退”

# -c exec执行
docker run ubuntu:17.10 /bin/sh -c "while true; do echo hello world; sleep 1; done"

# 后台运行
docker run -d ubuntu:17.10 /bin/sh -c "while true; do echo hello world; sleep 1; done"

可以通过docker [container] logs去查看容器输出信息

终止运行

docker [contanier] stop

进入容器

docker attach  

# 推荐使用
docker exec -it [container id] /bin/bash

两者的区别:前者exit退出后,会停止当前容器;而exec仍然会保持运行

删除容器

docker container rm  

# 清楚所有处于终止状态的容器
docker container prune

commit

为什么不建议使用docker commit?
使用docker commit提交后,对于其他使用者而言,这个image镜像是一个黑箱,别人无处得知执行过什么命令、如何生成的镜像

Dokcerfile 定制镜像

FROM指定基础镜像

RUN执行命令

  • shell格式:RUN <命令>
  • exec格式:RUN ["可执行文件", "参数1", "参数2"]

构建镜像

docker build [选项] <上下文路径/URL/->

# etc, 注意镜像构建上下文(context)
docker build -t nginx:v3 .

Dockerfile 指令详解

CMD 启动命令

CMD 指令就是用于指定默认的容器主进程的启动命令的,也分为shell格式以及exec格式
- shell格式:CMD <命令>
- exec格式:CMD ["可执行文件", "参数1", "参数2"...]
- 参数列表格式:CMD ["参数1", "参数2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数

一般推荐使用exec格式,这类格式在解析时会被解析成JSON数组,因此要使用双引号

如果是shell格式,实际会被包装成sh -c的格式

CMD echo $HOME

实际执行会变成:

CMD [ "sh", "-c", "echo $HOME" ]

注意:Docker不是虚拟机,容器中的应用都是前台执行,没有后台服务的概念

错误示范:

CMD service nginx start

这里容器执行后会秒退出,因为上面的命令会被转化为CMD ["sh", "-c", "service nginx start],主进程是sh,当service nginx start执行结束后,sh也就结束了,sh作为主进程结束,所以容器也会退出

正确做法:

CMD [ "nginx", "-g", "daemon off;" ]

这边执行docker run的时候,不需要再跟/bin/bash启动命令,因为会覆盖。否则就是秒结束进程

ENTRYPOINT 入口点

如果指定了ENTRYPOINTCMD就不会直接执行命令,而是讲内容作为参数传给ENTRYPOINT,实际执行指令会变为:

ENTRYPOINT "<CMD>"

场景一:让镜像变成像命令一样使用

docker run myip -i

场景二:应用运行前的准备工作

在启动主进程之前,需要一些准备工作,比如数据库的配置、初始化

ENV 设置环境变量

两种格式:
- ENV \ \
- ENV \=\ \=\ ...

# 含有空格的值使用双引号
ENV VERSION=1.0 DEBUG=on \
    NAME="Happy Feet"

ARG构建参数

格式:ARG <参数名>[=<默认值>]

构建参数和ENV效果一样,都是设置环境变量,唯一的区别是,ARG构建的环境变量,在将来容器运行的时候,不会存储这些环境变量

Dokcerfile中的ARG指令是定义参数名称,以及其默认值。可以通过构建命令docker build中用--build-arg <参数名>=<值>来覆盖

VOLUME 定义匿名卷

格式为:
- VOLUME ["<路径1>", "<路径2>"...]
- VOLUME <路径>

容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中

VOLUME /data

这里的 /data 目录就会在运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化

docker run -d -v mydata:/data xxxx

这里mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置

EXPOSE 声明端口

格式为:EXPOSE <端口1> [<端口2>...]

EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处:
- 帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射
- 在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口

要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。

WORKDIR 指定工作目录

格式为: WORKDIR <工作目录路径>

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录

USER 指定当前用户

格式:USER <用户名>

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份

HEALTHCHECK 健康检查

HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常

格式:
- HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令
- HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

ONBUILD

格式:ONBUILD <其它指令>

NOBUILD指令是别人定制镜像。即使用FROM的时候,才执行的命令

推送镜像

# 先打标签
docker tag ubuntu:17.10 username/ubuntu:17.10

# 在push
docker push username/ubuntu:17.10

配置私有仓库

配置私有仓库

数据卷

数据卷 是一个可供一个或多个容器使用的特殊目录:
- 数据卷可以在容器之间共享和重用
- 对 数据卷 的修改会立马生效
- 对 数据卷 的更新,不会影响镜像
- 数据卷 默认会一直存在,即使容器被删除

注意:数据卷 的使用,类似于 Linux 下对目录或文件进行 mount,镜像中的被指定为挂载点的目录中的文件会隐藏掉,能显示看的是挂载的 数据卷。

外部访问容器

使用- P标记时,Docker会随机映射 49000~49900 的端口到内部容器开放的网络端口

- p可以指定要映射的端口,也可以指定地址
ip:hostPort:containerPort

docker run -d -p 127.0.0.1:5000:5000 training/webapp python app.py

查看映射端口配置

docker port

docker port nostalgic_morse 5000
  • 容器有自己的内部网络和 ip 地址
  • -p 标记可以多次使用来绑定多个端口

容器互联

查看已有网络

docker network ls

新建网络

docker network create -d bridge my-net

-d 可以指定Docker网络类型,有bridge, overlay,其中 overlay 网络类型用于 Swarm mode(集群服务)

连接容器

运行一个容器并连接到新建的 my-net 网络

docker run -it --rm --name busybox1 --network my-net busybox sh

# 再运行一个容器
docker run -it --rm --name busybox2 --network my-net busybox sh

# 测试连接
# 在busybox1 容器里,执行
# /ping busybox2
PING busybox2 (172.19.0.3): 56 data bytes
64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.060 ms
64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.046 ms
64 bytes from 172.19.0.3: seq=2 ttl=64 time=0.075 ms

Compose 项目

Compose 项目是 Docker 官方的开源项目,负责实现对 Docker 容器集群的快速编排。它允许用户通过一个单独的 docker-compose.yml 模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)。
Compose 中有两个重要的概念:
- 服务 (service):一个应用的容器,实际上可以包括若干运行相同镜像的容器实例。
- 项目 (project):由一组关联的应用容器组成的一个完整业务单元,在 docker-compose.yml 文件中定义。

Compose 的默认管理对象是项目,通过子命令对项目中的一组容器进行便捷地生命周期管理。

Compose 命令说明

命令对象与格式

docker-compose 命令的基本的使用格式是:

docker-compose [-f=<arg>...] [options] [COMMAND] [ARGS...]

命令选项

  • -f, --file FILE 指定使用的 Compose 模板文件,默认为 docker-compose.yml,可以多次指定。
  • -p, --project-name NAME 指定项目名称,默认将使用所在目录名称作为项目名。
  • --x-networking 使用 Docker 的可拔插网络后端特性
  • --x-network-driver DRIVER 指定网络后端的驱动,默认为 bridge
  • --verbose 输出更多调试信息。
  • -v, --version 打印版本并退出

命令使用说明

Tips: 这里的service name是指服务的名称,不是container name 或者 container id

build

构建(重新构建)项目中的服务容器

docker-compose build [options] [SERVICE...]

服务容器一旦构建后,将会带上一个标记名,例如对于 web 项目中的一个 db 容器,可能是 web_db
选项包括:
- --force-rm 删除构建过程中的临时容器
- --no-cache 构建镜像过程中不使用 cache(这将加长构建过程)
- --pull 始终尝试通过 pull 来获取更新版本的镜像

config

验证 Compose 文件格式是否正确,若正确则显示配置,若格式错误显示错误原因

down

此命令将会停止 up 命令所启动的容器,并移除网络

exec

进入指定的容器

# 如果执行/bin/bash失败,报错OCI runtime exec failed,是因为bash不存在,替换成sh    

docker-compose exec web /bin/sh

images

列出 Compose 文件中包含的镜像

kill

格式为 docker-compose kill [options] [SERVICE...]

通过发送 SIGKILL 信号来强制停止服务容器

支持通过 -s 参数来指定发送的信号,例如通过如下指令发送 SIGINT 信号

 docker-compose kill -s SIGINT

logs

查看服务容器的输出。
默认情况下,docker-compose 将对不同的服务输出使用不同的颜色来区分。可以通过 --no-color 来关闭颜色。

格式为:docker-compose logs [options] [SERVICE...]

pause

暂停服务
格式为:docker-compose pause [SERVICE...]

port

打印某个容器端口所映射的公共端口

格式为 docker-compose port [options] SERVICE PRIVATE_PORT

选项:
- --protocol=proto 指定端口协议,tcp(默认值)或者 udp。
- --index=index 如果同一服务存在多个容器,指定命令对象容器的序号(默认为 1)

ps

pull

push

restart

stop

rm

run

格式为 docker-compose run [options] [-p PORT...] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...]

docker-compose run ubuntu ping docker.com

默认情况下,如果存在关联,则所有关联的服务将会自动被启动,除非这些服务已经在运行中。

该命令类似启动容器后运行指定的命令,相关卷、链接等等都将会按照配置自动创建。

两个不同点:

给定命令将会覆盖原有的自动运行命令;

不会自动创建端口,以避免冲突。

如果不希望自动启动关联的容器,可以使用 --no-deps 选项,例如:

docker-compose run --no-deps web python manage.py shell

将不会启动 web 容器所关联的其它容器.

选项:
- -d 后台运行容器。
- --name NAME 为容器指定一个名字。
- --entrypoint CMD 覆盖默认的容器启动指令。
- -e KEY=VAL 设置环境变量值,可多次使用选项来设置多个环境变量。
- -u, --user="" 指定运行容器的用户名或者 uid。
- --no-deps 不自动启动关联的服务容器。
- --rm 运行命令后自动删除容器,d 模式下将忽略。
- -p, --publish=[] 映射容器端口到本地主机。
- --service-ports 配置服务端口并映射到本地主机。
- -T 不分配伪 tty,意味着依赖 tty 的指令将无法运行。

scale

设置指定服务运行的容器个数

格式为 docker-compose scale [options] [SERVICE=NUM...]

通过 service=num 的参数来设置数量。例如:

docker-compose scale web=3 db=2

一般的,当指定数目多于该服务当前实际运行容器,将新创建并启动容器;反之,将停止容器。

top

unpause

启动已暂停的服务

up

格式为 docker-compose up [options] [SERVICE...]

该命令十分强大,它将尝试自动完成包括构建镜像,(重新)创建服务,启动服务,并关联服务相关容器的一系列操作

链接的服务都将会被自动启动,除非已经处于运行状态

默认情况,docker-compose up 启动的容器都在前台,控制台将会同时打印所有容器的输出信息,可以很方便进行调试

如果使用 docker-compose up -d,将会在后台启动并运行所有的容器。一般推荐生产环境下使用该选项。

选项:
- -d 在后台运行服务容器。
- --no-color 不使用颜色来区分不同的服务的控制台输出。
- --no-deps 不启动服务所链接的容器。
- --force-recreate 强制重新创建容器,不能与 --no-recreate 同时使用。
- --no-recreate 如果容器已经存在了,则不重新创建,不能与 --force-recreate 同时使用。
- --no-build 不自动构建缺失的服务镜像。
- -t, --timeout TIMEOUT 停止容器时候的超时(默认为 10 秒)

Compose 模板文件

模板文件是使用 Compose 的核心,涉及到的指令关键字也比较多。但大家不用担心,这里面大部分指令跟 docker run 相关参数的含义都是类似的。

默认的模板文件名称为 docker-compose.yml,格式为 YAML 格式。

version: '3'
services:

  webapp:
    build:
      context: ./dir
      dockerfile: Dockerfile-alternate
      args:
        buildno: 1

注意每个服务都必须通过 image 指令指定镜像或 build 指令(需要 Dockerfile)等来自动构建生成镜像

如果使用 build 指令,在 Dockerfile 中设置的选项(例如:CMD, EXPOSE, VOLUME, ENV 等) 将会自动被获取,无需在 docker-compose.yml 中再次设置。

build

指定 Dockerfile 所在文件夹的路径(可以是绝对路径,或者相对 docker-compose.yml 文件的路径)。 Compose 将会利用它自动构建这个镜像,然后使用这个镜像。

可以使用 context 指令指定 Dockerfile 所在文件夹的路径

使用 dockerfile 指令指定 Dockerfile 文件名

使用 arg 指令指定构建镜像时的变量

使用 cache_from 指定构建镜像的缓存

build:
  context: .
  cache_from:
    - alpine:latest
    - corp/web_app:3.14

cap_add, cap_drop

指定容器的内核能力(capacity)分配

command

覆盖容器启动后默认执行的命令

command: echo "hello world"

container_name

指定容器名称。默认将会使用 项目名称_服务名称_序号 这样的格式

container_name: docker-web-container

注意: 指定容器名称后,该服务将无法进行扩展(scale),因为 Docker 不允许多个容器具有相同的名称

devices

指定设备映射关系。

devices:
  - "/dev/ttyUSB1:/dev/ttyUSB0"

depends_on

解决容器的依赖、启动先后的问题。
以下例子中会先启动 redis db 再启动 web

version: '3'

services:
  web:
    build: .
    depends_on:
      - db
      - redis

  redis:
    image: redis

  db:
    image: postgres

注意:web 服务不会等待 redis db 「完全启动」之后才启动。

dns

自定义 DNS 服务器。可以是一个值,也可以是一个列表。

dns: 8.8.8.8

dns:
  - 8.8.8.8
  - 114.114.114.114

env_file

从文件中获取环境变量,可以为单独的文件路径或列表。

如果通过 docker-compose -f FILE 方式来指定 Compose 模板文件,则 env_file 中变量的路径会基于模板文件路径

如果有变量名称与 environment 指令冲突,则按照惯例,以后者为准

env_file: .env

env_file:
  - ./common.env
  - ./apps/web.env
  - /opt/secrets.env

环境变量文件中每一行必须符合格式,支持 # 开头的注释行。

# common.env: Set development environment
PROG_ENV=development

environment

设置环境变量。可以使用数组或字典两种格式

只给定名称的变量会自动获取运行 Compose 主机上对应变量的值,可以用来防止泄露不必要的数据

environment:
  RACK_ENV: development
  SESSION_SECRET:

environment:
  - RACK_ENV=development
  - SESSION_SECRET

如果变量名称或者值中用到 true|false,yes|no 等表达 布尔 含义的词汇,最好放到引号里,避免 YAML 自动解析某些内容为对应的布尔语义。

expose

暴露端口,但不映射到宿主机,只被连接的服务访问

仅可以指定内部端口为参数

expose:
 - "3000"
 - "8000"

image

指定为镜像名称或镜像 ID

读取变量

Compose 模板文件支持动态读取主机的系统环境变量和当前目录下的 .env 文件中的变量。

例如,下面的 Compose 文件将从运行它的环境中读取变量 ${MONGO_VERSION} 的值,并写入执行的指令中。

version: "3"
services:

db:
  image: "mongo:${MONGO_VERSION}"

如果执行 MONGO_VERSION=3.2 docker-compose up 则会启动一个 mongo:3.2 镜像的容器;如果执行 MONGO_VERSION=2.8 docker-compose up 则会启动一个 mongo:2.8 镜像的容器。

若当前目录存在 .env 文件,执行 docker-compose 命令时将从该文件中读取变量。

在当前目录新建 .env 文件并写入以下内容。

# 支持 # 号注释
MONGO_VERSION=3.6

执行 docker-compose up 则会启动一个 mongo:3.6 镜像的容器。

附录

常见问题总结

常见问题总结

资源链接

资源链接

进阶深入

进阶深入,参考原文档《Docker — 从入门到实践》