食无求饱,居无求安,敏于事而慎于言.

0%

问题收集

Java

算法

  • 10亿数字寻找重复数字
  • 10亿数字寻找前10大数字
  • 滴滴用户地图上显示附近司机该如何实现

mysql

redis

常用命令

Docker

其它

docker命令详解

docker 存在的意义

  • 使用dokcer加速本地开发和构建,开发人员可以构建、运行并分享Docker容器,容器可以在开发环境中构建,然后轻松地提交到测试环境中,并最终进入生产环境
  • 能够让独立服务或应用程序在不同环境中,得到相同的运行结果。
  • 用docker 创建隔离环境进行测试
  • docker 可以让开发者先在本机上构建一个复杂的程序测试,而不是一开始就在生产环境进行测试

目录

docker概念

  • Docker 的常用文档:https://docs.docker.com/
  • Docker 镜像: 用户基于镜像来运行自己的容器,可以把镜像当做容器的『源代码』,镜像体积很小,易于分享、存储和更新
  • Registry: Docker 用 Registry 保存用户构建的镜像,Registry 分为公共和私有两种:
    • Docker 公司运营的公共 Registry 叫做 Docker Hub,我们可以在上面注册账号,分享并保存自己的镜像。
    • 可以在 Docker Hub 保存自己的私有镜像或者架设自己私有的 Registry
  • Docker 容器: 把应用程序或服务打包放进去,容器是基于镜像启动的,容器中可以运行一个或多个进程。
    • 镜像是 Docker 生命周期中的构建或打包阶段
    • 容器则是启动或执行阶段

docker的使用命令

1 docker 命令介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
docker --help

管理命令:
container 管理容器
image 管理镜像
network 管理网络
命令:
attach 介入到一个正在运行的容器
build 根据 Dockerfile 构建一个镜像
commit 根据容器的更改创建一个新的镜像
cp 在本地文件系统与容器中复制 文件/文件夹
create 创建一个新容器
exec 在容器中执行一条命令
images 列出镜像
kill 杀死一个或多个正在运行的容器
logs 取得容器的日志
pause 暂停一个或多个容器的所有进程
ps 列出所有容器
pull 拉取一个镜像或仓库到 registry
push 推送一个镜像或仓库到 registry
rename 重命名一个容器
restart 重新启动一个或多个容器
rm 删除一个或多个容器
rmi 删除一个或多个镜像
run 在一个新的容器中执行一条命令
search 在 Docker Hub 中搜索镜像
start 启动一个或多个已经停止运行的容器
stats 显示一个容器的实时资源占用
stop 停止一个或多个正在运行的容器
tag 为镜像创建一个新的标签
top 显示一个容器内的所有进程
unpause 恢复一个或多个容器内所有被暂停的进程

docker run命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
docker run [OPTIONS] IMAGE [COMMAND] [ARG...]    

-d, --detach=false 指定容器运行于前台还是后台,默认为false
-i, --interactive=false 打开STDIN,用于控制台交互
-t, --tty=false 分配tty设备,该可以支持终端登录,默认为false
-u, --user="" 指定容器的用户
-a, --attach=[] 登录容器(必须是以docker run -d启动的容器)
-w, --workdir="" 指定容器的工作目录
-c, --cpu-shares=0 设置容器CPU权重,在CPU共享场景使用
-e, --env=[] 指定环境变量,容器中可以使用该环境变量
-m, --memory="" 指定容器的内存上限
-P, --publish-all=false 指定容器暴露的端口
-p, --publish=[] 指定容器暴露的端口
-h, --hostname="" 指定容器的主机名
-v, --volume=[] 给容器挂载存储卷,挂载到容器的某个目录
--volumes-from=[] 给容器挂载其他容器上的卷,挂载到容器的某个目录
--cap-add=[] 添加权限,权限清单详见:http://linux.die.net/man/7/capabilities
--cap-drop=[] 删除权限,权限清单详见:http://linux.die.net/man/7/capabilities
--cidfile="" 运行容器后,在指定文件中写入容器PID值,一种典型的监控系统用法
--cpuset="" 设置容器可以使用哪些CPU,此参数可以用来容器独占CPU
--device=[] 添加主机设备给容器,相当于设备直通
--dns=[] 指定容器的dns服务器
--dns-search=[] 指定容器的dns搜索域名,写入到容器的/etc/resolv.conf文件
--entrypoint="" 覆盖image的入口点
--env-file=[] 指定环境变量文件,文件格式为每行一个环境变量
--expose=[] 指定容器暴露的端口,即修改镜像的暴露端口
--link=[] 指定容器间的关联,使用其他容器的IP、env等信息
--lxc-conf=[] 指定容器的配置文件,只有在指定--exec-driver=lxc时使用
--name="" 指定容器名字,后续可以通过名字进行容器管理,links特性需要使用名字
--net="bridge" 容器网络设置:
bridge 使用docker daemon指定的网桥
host //容器使用主机的网络
container:NAME_or_ID >//使用其他容器的网路,共享IP和PORT等网络资源
none 容器使用自己的网络(类似--net=bridge),但是不进行配置
--privileged=false 指定容器是否为特权容器,特权容器拥有所有的capabilities
--restart="no" 指定容器停止后的重启策略:
no:容器退出时不重启
on-failure:容器故障退出(返回值非零)时重启
always:容器退出时总是重启
--rm=false 指定容器停止后自动删除容器(不支持以docker run -d启动的容器)
--sig-proxy=true 设置由代理接受并处理信号,但是SIGCHLD、SIGSTOP和SIGKILL不能被代理

2.更详细的功能参数配置

参数 解释
—api-enable-cors=false 开放远程API调用的 CORS 头信息。这个接口开关对想进行二次开发的上层应用提供了支持.
-b, —bridge=”” 挂载已经存在的网桥设备到 Docker 容器里。注意,使用 none 可以停用容器里的网络.
—bip=”” 使用 CIDR 地址来设定网络桥的 IP。注意,此参数和 -b 不能一起使用.
-D, —debug=false 开启Debug模式。例如:docker -d -D
-d, —daemon=false 开启Daemon模式.
—dns=[] 强制容器使用DNS服务器.例如: docker -d —dns 8.8.8.8
—dns-search=[] 强制容器使用指定的DNS搜索域名.例如: docker -d —dns-search example.com
-e, —exec-driver=”native” 强制容器使用指定的运行时驱动.例如:docker -d -e lxc
-G, —group=”docker” 在后台运行模式下,赋予指定的Group到相应的unix socket上。注意,当此参数 —group 赋予空字符串时,将去除组信息。
-g, —graph=”/var/lib/docker” 配置Docker运行时根目录
-H, —host=[] 在后台模式下指定socket绑定,可以绑定一个或多个 tcp://host:port, unix:///path/to/socket, fd://* 或 fd://socketfd。例如:$ docker -H tcp://0.0.0.0:2375 ps 或者 $ export DOCKER_HOST=”tcp://0.0.0.0:2375” $ docker ps
—icc=true 启用内联容器的通信.
—ip=”0.0.0.0” 容器绑定IP时使用的默认IP地址.
—ip-forward=true 启动容器的 net.ipv4.ip_forward.
—iptables=true 启动Docker容器自定义的iptable规则.
—mtu=0 设置容器网络的MTU值,如果没有这个参数,选用默认 route MTU,如果没有默认route,就设置成常量值 1500.
-p, —pidfile=”/var/run/docker.pid” 后台进程PID文件路径.
-r, —restart=true 重启之前运行中的容器.
-s, —storage-driver=”” 强制容器运行时使用指定的存储驱动,例如,指定使用devicemapper, 可以这样:docker -d -s devicemapper
—selinux-enabled=false 启用selinux支持
—storage-opt=[] 配置存储驱动的参数
—tls=false 启动TLS认证开关
—tlscacert=”/Users/dxiao/.docker/ca.pem” 通过CA认证过的的certificate文件路径
—tlscert=”/Users/dxiao/.docker/cert.pem” TLS的certificate文件路径
—tlskey=”/Users/dxiao/.docker/key.pem” TLS的key文件路径
—tlsverify=false 使用TLS并做后台进程与客户端通讯的验证
-v, —version=false 显示版本信息

*注意:其中带有[] 的启动参数可以指定多次,例如

1
2
docker run -a stdin -a stdout -a stderr -i -t ubuntu /bin/bash

docker基本信息

  1. 查看系统内核
1
uname -r
  1. 启动docker 境像
1
systemctl start docker 

3.查看docker版本

1
docker verison

4.显示docker系统的信息

1
docker info

操作docker镜像

1.检索image

1
docker search image-name

2.下载image

1
docker pull image-name

3.列出镜像列表

1
docker images 

4.删除一个或者多个镜像

1
docker rmi image-name

5.显示一个镜像的历史

1
docker history image-name  

6.通过容器创建镜像

从已经创建的容器中更新镜像,并且提交这个镜像 使用 Dockerfile 指令来创建一个新的镜像
下面通过已存在的容器创建一个新的镜像。

1
2
3
4
5
6
7
8
docker commit -m="First Image" -a="keke" 7a15f99695c0 keke/unbantu:17.10.0

上面命令参数说明:
* -m 提交的描述信息
* -a 指定镜像作者
* 7a15f99695c0 记住这个是容器id,不是镜像id
* keke/unbantu:17.10.0 创建的目标镜像名

  1. Docker 注册账户,发布的镜像都在这个页面里展示
  2. 将上面做的镜像unbantu,起个新的名字unbantu-test
1
docker tag keke/unbantu:17.10.0 keke/unbantu-test:lastest
  1. 登录docker
1
docker login

4.上传unbantu镜像

1
docker push keke/unbantu-test:lastest

启动容器

docker容器可以理解为在沙盒中运行的进程。这个沙盒包含了该进程运行所必须的资源,包括文件系统、系统类库、shell 环境等等。但这个沙盒默认是不会运行任何程序的。你需要在沙盒中运行一个进程来启动某一个容器。这个进程是该容器的唯一进程,所以当该进程结束的时候,容器也会完全的停止。

1.在容器中安装新的程序

1
2
3
4
5
docker run image-name apt-get install -y -name
```
2.在容器中运行"echo"命令,输出"hello word"
```shell
docker run image-name echo "hello word"

3.交互式进入容器中
1
docker run -i -t image_name /bin/bash  

注意:在执行apt-get 命令的时候,要带上-y参数。如果不指定-y参数的话,apt-get命令会进入交互模式,需要用户输入命令来进行确认,但在docker环境中是无法响应这种交互的。apt-get 命令执行完毕之后,容器就会停止,但对容器的改动不会丢失.

查看容器

1.列出当前所有正在运行的container

1
docker ps

2.列出所有的container

1
docker ps -a  

3.列出最近一次启动的container

1
docker ps -l  

4.保存对容器的修改
当你对某一个容器做了修改之后(通过在容器中运行某一个命令),可以把对容器的修改保存下来,这样下次可以从保存后的最新状态运行该容器。

1.保存对容器的修改; -a, —author=”” Author; -m, —message=”” Commit message

1
docker commit ID new-image-name 

5.操作容器

1.删除所有容器

1
docker rm `docker ps -a -q`

2.删除单个容器; -f, —force=false; -l, —link=false Remove the specified link and not the underlying container; -v, —volumes=false Remove the volumes associated to the container

1
docker rm Name/ID 

3.停止、启动、杀死一个容器

1
2
3
docker stop Name/ID  
docker start Name/ID
docker kill Name/ID

4.从一个容器中取日志; -f, —follow=false Follow log output; -t, —timestamps=false Show timestamps

1
docker logs Name/ID  

5.列出一个容器里面被改变的文件或者目录,list列表会显示出三种事件,A 增加的,D 删除的,C 被改变的

1
docker diff Name/ID

6.显示一个运行的容器里面的进程信息

1
docker top Name/ID  

7.从容器里面拷贝文件/目录到本地一个路径

1
2
docker cp Name:/container-path to-path  
docker cp ID:/container-path to-path

8.重启一个正在运行的容器; -t, —time=10 Number of seconds to try to stop for before killing the container, Default=10

1
docker restart Name/ID

9.附加到一个运行的容器上面; —no-stdin=false Do not attach stdin; —sig-proxy=true Proxify all received signal to the process

1
docker attach ID #重新启动并运行一个交互式会话shell

注意:使用这个命令可以挂载正在后台运行的容器,在开发应用的过程中运用这个命令可以随时观察容器內进程的运行状况.

保存和加载镜像

当需要把一台机器上的镜像迁移到另一台机器的时候,需要保存镜像与加载镜像。

1.保存镜像到一个tar包; -o, —output=”” Write to an file

1
docker save image-name -o file-path 

2.加载一个tar包格式的镜像; -i, —input=”” Read from a tar archive file

1
docker load -i file-path 

3.从机器A拷贝到机器B

1
2
3
4
5
6
docker save image-name > /home/keke/main.tar

*使用scp将main.tar拷到机器A上:

docker load < /home/keke/main.tar

登录

1.登陆registry server; -e, —email=”” Email; -p, —password=”” Password; -u, —username=”” Username

1
docker login

发布docker镜像

1
docker push new-image-name 

构建镜像

  1. Dockerfile文件使用

docker build命令会根据Dockerfile文件及上下文构建新Docker镜像。构建上下文是指Dockerfile所在的本地路径或一个URL(Git仓库地址)。构建上下文环境会被递归处理,所以,构建所指定的路径还包括了子目录,而URL还包括了其中指定的子模块。

  • 构建镜像

将当前目录做为构建上下文时,可以像下面这样使用docker build命令构建镜像:

1
2
3
$ ~/Downloads/hello-system$ sudo docker build .
Sending build context to Docker daemon 70.14kB

说明:构建会在Docker后台守护进程(daemon)中执行,而不是CLI中。构建前,构建进程会将全部内容(递归)发送到守护进程。大多情况下,应该将一个空目录作为构建上下文环境,并将Dockerfile文件放在该目录下。

在构建上下文中使用的Dockerfile文件,是一个构建指令文件。为了提高构建性能,可以通过.dockerignore文件排除上下文目录下,不需要的文件和目录。

Dockerfile一般位于构建上下文的根目录下,也可以通过-f指定该文件的位置:

1
2
$ sudo docker build -f /home/keke/Downloads/hello-system/Dockerfile .

构建时,还可以通过-t参数指定构建成后,镜像的仓库,标签等:

  • 镜像标签
1
2
$ ~/Downloads/hello-system$ sudo docker build -t keke/myapp .

如果存在多个仓库下,或使用多个镜像标签,就可以使用多个-t参数:

1
$ docker build -t keke/myapp:1.0.2 -t keke/myapp:latest .

在Docker守护进程执行Dockerfile中的指令前,首先会对Dockerfile进行语法检查,有语法错误时会返回:

1
2
3
$ docker build -t test/myapp .
Sending build context to Docker daemon 2.048 kB
Error response from daemon: Unknown instruction: RUNCMD

  • Dockerfile文件格式
    Dockerfile文件中指令不区分大小写,但为了更易区分,约定使用大写形式。

Docker 会依次执行Dockerfile中的指令,文件中的第一条指令必须是FROM,FROM指令用于指定一个基础镜像。

FROM指令用于指定其后构建新镜像所使用的基础镜像。FROM指令必是Dockerfile文件中的首条命令,启动构建流程后,Docker将会基于该镜像构建新镜像,FROM后的命令也会基于这个基础镜像。

Dockerfile文件格式如下:

1
2
3
# Comment
INSTRUCTION arguments

Dockerfile文件中指令不区分大小写,但为了更易区分,约定使用大写形式。

Docker 会依次执行Dockerfile中的指令,文件中的第一条指令必须是FROM,FROM指令用于指定一个基础镜像。

  • FROM语法格式为:
    1
    FROM <image> 或 FROM <image>:<tag>

通过FROM指定的镜像,可以是任何有效的基础镜像。FROM有以下限制:

FROM必须是Dockerfile中第一条非注释命令
在一个Dockerfile文件中创建多个镜像时,FROM可以多次出现。只需在每个新命令FROM之前,记录提交上次的镜像ID。
tag或digest是可选的,如果不使用这两个值时,会使用latest版本的基础镜像

  • RUN
    RUN用于在镜像容器中执行命令,其有以下两种命令执行方式:
    shell执行
    在这种方式会在shell中执行命令,Linux下默认使用/bin/sh -c,Windows下使用cmd /S /C。
    注意:通过SHELL命令修改RUN所使用的默认shell
1
RUN <command>

exec执行

1
RUN ["executable", "param1", "param2"]

RUN可以执行任何命令,然后在当前镜像上创建一个新层并提交。提交后的结果镜像将会用在Dockerfile文件的下一步。

通过RUN执行多条命令时,可以通过\换行执行:

1
2
RUN /bin/bash -c 'source $HOME/.bashrc; \
echo $HOME'

也可以在同一行中,通过分号分隔命令:

1
2
RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME'


RUN指令创建的中间镜像会被缓存,并会在下次构建中使用。如果不想使用这些缓存镜像,可以在构建时指定—no-cache参数,如:docker build —no-cache。

  • CMD
    CMD用于指定在容器启动时所要执行的命令。CMD有以下三种格式:
1
2
3
CMD ["executable","param1","param2"]
CMD ["param1","param2"]
CMD command param1 param2

CMD不同于RUN,CMD用于指定在容器启动时所要执行的命令,而RUN用于指定镜像构建时所要执行的命令。
CMD与RUN在功能实现上也有相似之处。如:

1
docker run -t -i keke/static /bin/true 等价于:cmd ["/bin/true"]

CMD在Dockerfile文件中仅可指定一次,指定多次时,会覆盖前的指令。
另外,docker run命令也会覆盖Dockerfile中CMD命令。如果docker run运行容器时,使用了Dockerfile中CMD相同的命令,就会覆盖Dockerfile中的CMD命令。
如,我们在构建镜像的Dockerfile文件中使用了如下指令:

1
CMD ["/bin/bash"]

使用docker build构建一个新镜像,镜像名为keke/test。构建完成后,使用这个镜像运行一个新容器,运行效果如下:

1
sudo docker run -i -t keke/test

在使用docker run运行容器时,我们并没有在命令结尾指定会在容器中执行的命令,这时Docker就会执行在Dockerfile的CMD中指定的命令。
如果不想使用CMD中指定的命令,就可以在docker run命令的结尾指定所要运行的命令:

1
sudo docker run -i  -t keke/test /bin/ps

这时,docker run结尾指定的/bin/ps命令覆盖了Dockerfile的CMD中指定的命令.

  • ENTRYPOINT
    ENTRYPOINT用于给容器配置一个可执行程序。也就是说,每次使用镜像创建容器时,通过ENTRYPOINT指定的程序都会被设置为默认程序。ENTRYPOINT有以下两种形式:
    1
    2
    ENTRYPOINT ["executable", "param1", "param2"]
    ENTRYPOINT command param1 param2
    ENTRYPOINT与CMD非常类似,不同的是通过docker run执行的命令不会覆盖ENTRYPOINT,而docker run命令中指定的任何参数,都会被当做参数再次传递给ENTRYPOINT。Dockerfile中只允许有一个ENTRYPOINT命令,多指定时会覆盖前面的设置,而只执行最后的ENTRYPOINT指令。
    docker run运行容器时指定的参数都会被传递给ENTRYPOINT,且会覆盖CMD命令指定的参数。如,执行docker run -d时, -d参数将被传递给入口点。也可以通过docker run —entrypoint重写ENTRYPOINT入口点。
    如:可以像下面这样指定一个容器执行程序:
    1
    ENTRYPOINT ["/usr/bin/nginx"]
    完整构建代码:
    1
    2
    3
    4
    5
    6
    7
    8
    FROM ...
    MAINTAINER keke "2536495681@gmail.com"
    RUN ...

    # 指定容器内的程序将会使用容器的指定端口
    # 配合 docker run -p
    EXPOSE ...

    使用docker build构建镜像,并将镜像指定为keke/test:
1
sudo docker build -t="itbilu/test" .

构建完成后,使用keke/test启动一个容器:

1
sudo docker run -i -t keke/test -g "daemon off;"

在运行容器时,我们使用了-g “daemon off;” ,这个参数将会被传递给ENTRYPOINT,最终在容器中执行的命令为/usr/sbin/nginx -g “daemon off;” 。

  • EXPOSE
    EXPOSE用于指定容器在运行时监听的端口:
    1
    EXPOSE <port> [<port>...]

EXPOSE并不会让容器的端口访问到主机。要使其可访问,需要在docker run运行容器时通过-p来发布这些端口,或通过-P参数来发布EXPOSE导出的所有端口。

  • RUN: 指定镜像被构建时要运行的命令
  • CMD: 指定容器被启动时要运行的命令
  • ENTRYPOINT: 同 CMD ,但不会被 docker run -t 覆盖
  • WORKDIR: CMD/ENTRYPOINT 会在这个目录下执行
  • VOLUME:创建挂载点,即向基于所构建镜像创始的容器添加卷
  • ADD:用于复制构建环境中的文件或目录到镜像中
  • COPY:同样用于复制构建环境中的文件或目录到镜像中
1
docker history images-name

1.从新镜像启动容器

1
docker run -d -p 4000:80 --name [name] #可以在 Dokcer 宿主机上指定一个具体的端口映射到容器的80端口上

守护容器

docker run -d container-name #创建守护容器
docker top container-name #查看容器内进程
docker exec container-name touch a.txt #在容器内部运行进程
docker stop container-name #停止容器

关于docker

觉得此文章不错可以给我star!
如果还有遇到问题可以加我微信Sen0676备注下来自github,进go实战群详细交流!

参考资料

官方英文资源

中文资源

其它资源

原文地址

https://github.com/KeKe-Li/docker-directive

使用 Docker 部署 Spring Boot 项目

Docker 介绍

Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前最流行的 Linux 容器解决方案。

Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。有了 Docker,就不用担心环境问题。

总体来说,Docker 的接口相当简单,用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。

Docker 的主要用途

  • (1)提供一次性的环境。比如,本地测试他人的软件、持续集成的时候提供单元测试和构建的环境。

  • (2)提供弹性的云服务。因为 Docker 容器可以随开随关,很适合动态扩容和缩容。

  • (3)组建微服务架构。通过多个容器,一台机器可以跑多个服务,因此在本机就可以模拟出微服务架构。

Docker 的安装(CentOS环境)

  • 安装命令
1
yum install docker
  • 安装完成后,使用下面的命令来启动 docker 服务,并将其设置为开机启动
1
2
3
4
5
6
service docker start
chkconfig docker on

#LCTT 译注:此处采用了旧式的 sysv 语法,如采用CentOS 7中支持的新式 systemd 语法,如下:
systemctl start docker.service
systemctl enable docker.service
  • 使用Docker 中国加速器
1
2
3
4
5
6
7
vi  /etc/docker/daemon.json

#添加后:
{
"registry-mirrors": ["https://registry.docker-cn.com"],
"live-restore": true
}
  • 重新启动docker
1
systemctl restart docker

输入 docker version 返回版本信息则安装正常。

安装JDK

1
yum -y install java-1.8.0-openjdk*

配置环境变量 打开 vim /etc/profile 添加一下内容

1
2
3
export JAVA_HOME=/usr/lib/jvm/jre-1.8.0-openjdk-1.8.0.242.b08-0.el7_7.x86_64
export CLASSPATH=.:$JAVA_HOME/jre/lib/rt.jar:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export PATH=$PATH:$JAVA_HOME/bin

修改完成之后,使其生效

1
source /etc/profile

输入java -version 返回版本信息则安装正常。

安装 MAVEN

下载:https://mirror.bit.edu.cn/apache/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz

1
2
3
4
## 解压
tar vxf apache-maven-3.6.3-bin.tar.gz
## 移动
mv apache-maven-3.6.3 /usr/local/maven3

修改环境变量, 在/etc/profile

1
2
3
4
MAVEN_HOME=/usr/local/maven3
export MAVEN_HOME
export PATH=${PATH}:${MAVEN_HOME}/bin

执行source /etc/profile使环境变量生效。

输入mvn -version 返回版本信息则安装正常。

到止,通过docker,jdk,maven的安装,整个构建环境算配置完成了。

创建 spring boot 项目

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<groupId>com.easy</groupId>
<artifactId>spring-boot-docker</artifactId>
<version>1.0</version>
<packaging>jar</packaging>

<name>spring-boot-docker</name>
<description>Demo project for Spring Boot</description>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<docker.image.prefix>springboot</docker.image.prefix>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- Docker maven plugin -->
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>1.0.0</version>
<configuration>
<imageName>${docker.image.prefix}/${project.artifactId}</imageName>
<dockerDirectory>src/main/docker</dockerDirectory>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
</plugin>
<!-- Docker maven plugin -->
</plugins>
</build>

</project>

Dockerfile 配置

1
2
3
4
5
FROM openjdk:8-jdk-alpine
VOLUME /tmp
EXPOSE 8282
ADD spring-boot-docker-1.0.jar app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

Dockerfile 文件介绍,构建 Jdk 基础环境,添加 Spring Boot Jar 到镜像中:

  • FROM,表示使用 Jdk8 环境 为基础镜像,如果镜像不是本地的会从 DockerHub 进行下载
  • VOLUME,VOLUME 指向了一个/tmp的目录,由于 Spring Boot 使用内置的Tomcat容器,Tomcat 默认使用/tmp作为工作目录。这个命令的效果是:在宿主机的/var/lib/docker目录下创建一个临时文件并把它链接到容器中的/tmp目录
  • EXPOSE,EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。
  • ADD,拷贝文件并且重命名
  • ENTRYPOINT,为了缩短 Tomcat 的启动时间,添加java.security.egd的系统属性指向/dev/urandom作为 ENTRYPOINT

其它示例代码

DockerController.java

1
2
3
4
5
6
7
8
@RestController
public class DockerController {

@RequestMapping("/")
public String index() {
return "Hello Docker!";
}
}

DockerApplication.java

1
2
3
4
5
6
7
8
@SpringBootApplication
public class DockerApplication {

public static void main(String[] args) {
SpringApplication.run(DockerApplication.class, args);
}
}

application.properties

1
server.port=8282

使用 Docker 部署 Spring Boot 项目

将项目 docker 拷贝至服务器中,进入项目路径下进行打包测试。

1
2
3
4
#打包
mvn package
#启动
java -jar target/spring-boot-docker-1.0.jar

看到 Spring Boot 的启动日志后表明环境配置没有问题,接下来我们使用 DockerFile 构建镜像。

1
mvn package docker:build

第一次构建可能有点慢,当看到以下内容的时候表明构建成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Step 1/5 : FROM openjdk:8-jdk-alpine
---> a3562aa0b991
Step 2/5 : VOLUME /tmp
---> Using cache
---> d070c927d0a7
Step 3/5 : EXPOSE 8282
---> Using cache
---> b16d14267527
Step 4/5 : ADD spring-boot-docker-1.0.jar app.jar
---> c4ddc409b458
Removing intermediate container c58c986e6b9a
Step 5/5 : ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -jar /app.jar
---> Running in d2b61fddd616
---> 13c600d3f625
Removing intermediate container d2b61fddd616
Successfully built 13c600d3f625
[INFO] Built springboot/spring-boot-docker
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.273 s
[INFO] Finished at: 2020-03-25T10:05:04+08:00
[INFO] ------------------------------------------------------------------------

使用docker images命令查看构建好的镜像:

1
2
3
4
5
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
springboot/spring-boot-docker latest 13c600d3f625 18 minutes ago 122 MB
docker.io/openjdk 8-jdk-alpine a3562aa0b991 10 months ago 105 MB

springboot/spring-boot-docker 就是我们构建好的镜像,下一步就是运行该镜像

1
docker run -p 8282:8282 -t springboot/spring-boot-docker

启动完成之后我们使用docker ps查看正在运行的镜像:

1
2
3
4
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a626c3dbdb1b springboot/spring-boot-docker "java -Djava.secur..." 34 seconds ago Up 34 seconds 0.0.0.0:8282->8282/tcp suspicious_murdock

可以看到我们构建的容器正在在运行,访问浏览器:http://192.168.0.x:8282/,返回

1
Hello Docker!
说明使用 Docker 部署 Spring Boot 项目成功!

资料

LeetCode 寻找两个有序数组的中位数

第4题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。

请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。

你可以假设 nums1 和 nums2 不会同时为空。

示例 1:

nums1 = [1, 3]
nums2 = [2]

则中位数是 2.0
示例 2:

nums1 = [1, 2]
nums2 = [3, 4]

则中位数是 (2 + 3)/2 = 2.5

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/median-of-two-sorted-arrays

中位数定义

将一个集合划分为两个长度相等的子集,其中一个子集中的元素总是大于另一个子集中的元素。

解题思路

将数组进行切。

  • 长度为 m 的数组,有 0 到 m 总共 m + 1 个位置可以切。

  • 把数组 A 和数组 B 分别在 i 和 j 进行切割。

  • 将 i 的左边和 j 的左边组合成「左半部分」,将 i 的右边和 j 的右边组合成「右半部分」。

当 A 数组和 B 数组的总长度是偶数时,如果我们能够保证

  • 左半部分的长度等于右半部分 $i+j=m-i+n-j$, 也就是 $j=(m+n)/2-i$

  • 左半部分最大的值小于等于右半部分最小的值 $max(A[i-1],B[j-1])<=min(A[i],B[j])$

那么,中位数就可以表示如下

$(左半部分最大值+右半部分最小值)/2$==>$(max(A[i-1],B[j-1])+min(A[i],B[j]))/2$

当 A 数组和 B 数组的总长度是奇数时,如果我们能够保证

  • 左半部分的长度比右半部分大1,即$i+j=m-i+n-j+1$ 也就是 $j=(m+n+1)/2-i$

  • 左半部分最大的值小于等于右半部分最小的值 $max(A[i-1],B[j-1]) <=min(A[i],B[j])$

那么,中位数就是

左半部分最大值,也就是左半部比右半部分多出的那一个数==>$max(A[i-1],B[j-1])$

对以上条件进行分析

第一个条件

我们其实可以合并为 $j=(m+n+1)/2-i$,因为如果$m+n$ 是偶数,由于我们取的是 int 值,所以加 1 也不会影响结果。当然,由于 $0<=i<=m$,为了保证 $0<=j<=n$,我们必须保证 $m<=n$。

  • $m≤n,i(m+m+1)/2−m=0$

  • $m≤n,i>0,j=(m+n+1)/2−i≤(n+n+1)/2−i<(n+n+1)/2=n$

最后一步由于是 int 间的运算,所以 $1/2=0$

第二个条件

奇数和偶数的情况是一样的,我们进一步分析。

为了保证 $max(A[i-1],B[j-1])<=min(A[i],B[j])$,因为 A 数组和 B 数组是有序的,所以 $A[i-1]<=A[i],B[i-1]<=B[i] 这是一定的,所以我们只需要保证 $B[j-1]<=A[i]和A[i-1]<=B[j]$,即我们要分两种情况讨论:

  • $B[j-1]>A[i]$,并且为了不越界,要保证 $j!= 0,i!=m$,此时很明显,我们需要增加 i ,为了数量的平衡还要减少 j ,幸运的是 $j=(m+n+1)/2-i$,i 增大,j 自然会减少。

  • $A[i-1]>B[j]$,并且为了不越界,要保证 $i!=0,j!=n$,此时和上边的情况相反,我们要减少 i ,增大 j 。

上边两种情况,我们把边界都排除了,需要单独讨论。

  • 当 i=0, 或者 j=0,也就是切在了最前边。此时左半部分当 j = 0 时,最大的值就是 $A[i-1]$ ;当 i=0 时 最大的值就是 $B[j-1]$。右半部分最小值和之前一样。

  • 当 i = m 或者 j = n,也就是切在了最后边。此时左半部分最大值和之前一样。右半部分当 $j=n$ 时,最小值就是 $A[i]$;当 i = m 时,最小值就是$B[j]$。

所有的思路都理清了,最后一个问题,增加 i 的方式。当然用二分了。初始化 i 为中间的值,然后减半找中间的,减半找中间的,减半找中间的直到答案。

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public double findMedianSortedArrays(int[] A, int[] B) {
int m = A.length;
int n = B.length;
if (m > n) {
return findMedianSortedArrays(B,A); // 保证 m <= n
}
int iMin = 0, iMax = m;
while (iMin <= iMax) {
int i = (iMin + iMax) / 2;
int j = (m + n + 1) / 2 - i;
if (j != 0 && i != m && B[j-1] > A[i]){ // i 需要增大
iMin = i + 1;
}
else if (i != 0 && j != n && A[i-1] > B[j]) { // i 需要减小
iMax = i - 1;
}
else { // 达到要求,并且将边界条件列出来单独考虑
int maxLeft = 0;
if (i == 0) { maxLeft = B[j-1]; }
else if (j == 0) { maxLeft = A[i-1]; }
else { maxLeft = Math.max(A[i-1], B[j-1]); }
if ( (m + n) % 2 == 1 ) { return maxLeft; } // 奇数的话不需要考虑右半部分

int minRight = 0;
if (i == m) { minRight = B[j]; }
else if (j == n) { minRight = A[i]; }
else { minRight = Math.min(B[j], A[i]); }

return (maxLeft + minRight) / 2.0; //如果是偶数的话返回结果
}
}
return 0.0;
}
}

总结

  • 时间复杂度

我们对较短的数组进行了二分查找,所以时间复杂度是 $O(log(min(m,n)))$。

  • 空间复杂度

只有一些固定的变量,和数组长度无关,所以空间复杂度是 $O(1)$。

资料

对于普通的Java对象,当new的时候创建对象,当它没有任何引用的时候被垃圾回收机制回收。而由Spring IoC容器托管的对象,它们的生命周期完全由容器控制。

bean生命周期

Bean生命周期流程

1.实例化Bean

实例化Bean对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。

对于ApplicationContext容器,当容器启动结束后,便实例化所有的bean。

容器通过获取BeanDefinition对象中的信息进行实例化。并且这一步仅仅是简单的实例化,并未进行依赖注入。 实例化对象被包装在BeanWrapper对象中,BeanWrapper提供了设置对象属性的接口,从而避免了使用反射机制设置属性。

2.设置对象属性(依赖注入)

实例化后的对象被封装在BeanWrapper对象中,并且此时对象仍然是一个原生的状态,并没有进行依赖注入。 紧接着,Spring根据BeanDefinition中的信息进行依赖注入。 并且通过BeanWrapper提供的设置属性的接口完成依赖注入。

3.注入Aware接口

紧接着,Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给bean。

  • 如果这个Bean实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,此处传递的是Spring配置文件中Bean的ID

  • 如果这个Bean实现了BeanFactoryAware接口,会调用它实现的setBeanFactory(),传递的是Spring工厂本身(可以用这个方法获取到其他Bean)

  • 如果这个Bean实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文,ApplicationContext是BeanFactory的子接口,有更多的实现方法

4.BeanPostProcessor

当经过上述几个步骤后,bean对象已经被正确构造,但如果你想要对象被使用前再进行一些自定义的处理,就可以通过BeanPostProcessor接口实现。

该接口提供了两个函数:

  • postProcessBeforeInitialzation( Object bean, String beanName )

当前正在初始化的bean对象会被传递进来,我们就可以对这个bean作任何处理。 这个函数会先于InitialzationBean执行,因此称为前置处理。 所有Aware接口的注入就是在这一步完成的。

  • postProcessAfterInitialzation( Object bean, String beanName )

当前正在初始化的bean对象会被传递进来,我们就可以对这个bean作任何处理。 这个函数会在InitialzationBean完成后执行,因此称为后置处理。

5.InitializingBean与init-method

当BeanPostProcessor的前置处理完成后就会进入本阶段。

InitializingBean接口只有一个函数:

  • afterPropertiesSet()

这一阶段也可以在bean正式构造完成前增加我们自定义的逻辑,但它与前置处理不同,由于该函数并不会把当前bean对象传进来,因此在这一步没办法处理对象本身,只能增加一些额外的逻辑。 若要使用它,我们需要让bean实现该接口,并把要增加的逻辑写在该函数中。然后Spring会在前置处理完成后检测当前bean是否实现了该接口,并执行afterPropertiesSet函数。

当然,Spring为了降低对客户代码的侵入性,给bean的配置提供了init-method属性,该属性指定了在这一阶段需要执行的函数名。Spring便会在初始化阶段执行我们设置的函数。init-method本质上仍然使用了InitializingBean接口。

6. DisposableBean和destroy-method

和init-method一样,通过给destroy-method指定函数,就可以在bean销毁前执行指定的逻辑。

代码示例

注解方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.easy.bean;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

@Component
@Slf4j
public class AnnotationBean {
@PostConstruct
public void start() {
log.info("AnnotationBean 开始初始化");
}

@PreDestroy
public void destroy() {
log.info("AnnotationBean 开始销毁");
}
}

InitializingBean, DisposableBean 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.easy.bean;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class SpringLifeCycleService implements InitializingBean, DisposableBean {

@Override
public void afterPropertiesSet() throws Exception {
log.info("SpringLifeCycleService 开始");
}

@Override
public void destroy() throws Exception {
log.info("SpringLifeCycleService 销毁");
}
}

自定义初始化和销毁方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.easy.bean;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SpringLifeCycle {
public void start() {
log.info("SpringLifeCycle 开始初始化");
}

public void destroy() {
log.info("SpringLifeCycle 开始销毁");
}
}

配置添加

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.easy.bean;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class LifeCycleConfig {
@Bean(initMethod = "start", destroyMethod = "destroy")
public SpringLifeCycle create() {
SpringLifeCycle springLifeCycle = new SpringLifeCycle();
return springLifeCycle;
}
}

或者通过xml配置如下

1
<bean class="com.easy.bean.SpringLifeCycle" init-method="start" destroy-method="destroy"></bean>

实现 xxxAware 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.easy.bean;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class SpringLifeCycleAware implements ApplicationContextAware {

private ApplicationContext applicationContext;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
log.info("SpringLifeCycleAware 开始");
}
}

这样在 springLifeCycleAware 这个 bean 初始化会就会调用 setApplicationContext 方法,并可以获得 applicationContext 对象。

BeanPostProcessor 增强处理器

实现 BeanPostProcessor 接口,Spring 中所有 bean 在做初始化时都会调用该接口中的两个方法,可以用于对一些特殊的 bean 进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.easy.bean;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class SpringLifeCycleProcessor implements BeanPostProcessor {

/**
* 预初始化 初始化之前调用
*
* @param bean
* @param beanName
* @return
* @throws BeansException
*/
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if ("annotationBean".equals(beanName)) {
log.info("SpringLifeCycleProcessor 开始初始化 beanName={}", beanName);
}
return bean;
}

/**
* 后初始化 bean 初始化完成调用
*
* @param bean
* @param beanName
* @return
* @throws BeansException
*/
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if ("annotationBean".equals(beanName)) {
log.info("SpringLifeCycleProcessor 初始化结束 beanName={}", beanName);
}
return bean;
}
}

运行示例查看控制台输出

1
2
3
4
5
6
7
8
9
2020-01-09 17:46:37.022  INFO 9544 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-01-09 17:46:37.023 INFO 9544 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1642 ms
2020-01-09 17:46:37.081 INFO 9544 --- [ main] com.easy.bean.SpringLifeCycleProcessor : SpringLifeCycleProcessor 开始初始化 beanName=annotationBean
2020-01-09 17:46:37.089 INFO 9544 --- [ main] com.easy.bean.AnnotationBean : AnnotationBean 开始初始化
2020-01-09 17:46:37.089 INFO 9544 --- [ main] com.easy.bean.SpringLifeCycleProcessor : SpringLifeCycleProcessor 初始化结束 beanName=annotationBean
2020-01-09 17:46:37.090 INFO 9544 --- [ main] com.easy.bean.SpringLifeCycleAware : SpringLifeCycleAware 开始
2020-01-09 17:46:37.091 INFO 9544 --- [ main] com.easy.bean.SpringLifeCycleService : SpringLifeCycleService 开始
2020-01-09 17:46:37.093 INFO 9544 --- [ main] com.easy.bean.SpringLifeCycle : SpringLifeCycle 开始初始化
2020-01-09 17:46:37.279 INFO 9544 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'

资料

Spring容器是什么?

Spring容器是Spring的核心,Spring容器是管理bean对象的地方,其通过IoC技术管理。Spring容器也就是一个bean工厂(BeanFactory)。应用中bean的实例化,获取,销毁等都是由这个bean工厂管理的。更进一步讲,spring容器是管理service和dao的容器。

Spring提供了两种容器类型:BeanFactory和ApplicationContext

BeanFactory

基础类型IoC容器,提供完整的IoC服务支持。如果没有特殊指定,默认采用延迟初始化策略(lazy-load)。只有当客户端对象需要访问容器中的某个受管对象的时候,才对该受管对象进行初始化以及依赖注入操作。所以,相对来说,容器启动初期速度较快,所需要的资源有限。

对于资源有限,并且功能要求不是很严格的场景,BeanFactory是比较合适的IoC容器选择。

ApplicationContext

ApplicationContext在BeanFactory的基础上构建,是相对比较高级的容器实现,除了拥有BeanFactory的所有支持,ApplicationContext还提供了其他高级特性,比如事件发布、国际化信息支持等。ApplicationContext所管理的对象,在该类型容器启动之后,默认全部初始化并绑定完成。所以,相对于BeanFactory来说,ApplicationContext要求更多的系统资源,同时,因为在启动时就完成所有初始化,容器启动时间较之BeanFactory也会长一些。

在那些系统资源充足,并且要求更多功能的场景中,ApplicationContext类型的容器是比较合适的选择。

资料

协议介绍

协议是两个网络实体进行通信的基础,数据在网络上从一个实体传输到另一个实体,以字节流的形式传递到对端。在这个字节流的世界里,如果没有协议,就无法将这个一维的字节流重塑成为二维或者多维的数据结构以及领域对象。

在通信过程中,不同的服务等级一般对应着不同的服务质量,那么选择合适的协议便是一件非常重要的事情。你可以根据你应用的创建来选择。例如,使用RMI协议,一般会受到防火墙的限制,所以对于外部与内部进行通信的场景,就不要使用RMI协议,而是基于HTTP协议或者Hessian协议。

常见的协议模式

应用层协议一般的形式有三种:定长协议、特殊结束符和协议头+payload模式。

从网络上以流的形式进行数据的读取,需要确定的是一次有意义的传输内容在读到何时结束,因为一个一个byte传输过来,需要有一个结束。而且数据在网络上的传输,存在粘包和半包的情况,能够应对这个问题的办法就是协议能够准确的识别,当粘包发生时不会多读,当半包发生时会继续读取。

定长协议

定长的协议是指协议内容的长度是固定的,比如协议byte长度是50,当从网络上读取50个byte后,就进行decode解码操作。定长协议在读取或者写入时,效率比较高,因为数据缓存的大小基本都确定了,就好比数组一样,缺陷就是适应性不足,以RPC场景为例,很难估计出定长的长度是多少。

可以参考Netty的FixedLengthFrameDecoder

特殊结束符

相比定长协议,如果能够定义一个特殊字符作为每个协议单元结束的标示,就能够以变长的方式进行通信,从而在数据传输和高效之间取得平衡,比如用特殊字符\n。

特殊结束符方式的问题是过于简单的思考了协议传输的过程,对于一个协议单元必须要全部读入才能够进行处理,除此之外必须要防止用户传输的数据不能同结束符相同,否则就会出现紊乱。

可以参考Netty的DelimiterBasedFrameDecoder

变长协议(协议头+payload)

一般是自定义协议,会以定长加不定长的部分组成,其中定长的部分需要描述不定长的内容长度。

+———+
|定长|
+———+
|内容|
+———+

可以参考Netty的LengthFieldBasedFrameDecoder

Dubbo支持的协议

  • Dubbo协议
  • Hessian协议
  • HTTP协议
  • RMI协议
  • WebService协议
  • Thrift协议
  • Memcached协议
  • Redis协议

Dubbo 协议

Dubbo缺省协议采用单一长连接和NIO异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。Dubbo缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低

缺省协议,使用基于mina1.1.7+hessian3.2.1的tbremoting交互。
连接个数:单连接
连接方式:长连接
传输协议:TCP
传输方式:NIO异步传输
序列化:Hessian二进制序列化
适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用dubbo协议传输大文件或超大字符串。
适用场景:常规远程服务方法调用

Hessian 协议

Hessian协议用于集成Hessian的服务,Hessian底层采用Http通讯,采用Servlet暴露服务,Dubbo缺省内嵌Jetty作为服务器实现基于Hessian的远程调用协议。

连接个数:多连接
连接方式:短连接
传输协议:HTTP
传输方式:同步传输
序列化:Hessian二进制序列化
适用范围:传入传出参数数据包较大,提供者比消费者个数多,提供者压力较大,可传文件
适用场景:页面传输,文件传输,或与原生hessian服务互操作

HTTP 协议

采用Spring的HttpInvoker实现

基于http表单的远程调用协议

连接个数:多连接
连接方式:短连接
传输协议:HTTP
传输方式:同步传输
序列化:表单序列化(JSON)
适用范围:传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表单或URL传入参数,暂不支持传文件
适用场景:需同时给应用程序和浏览器JS使用的服务。

RMI 协议

RMI协议采用JDK标准的java.rmi.*实现,采用阻塞式短连接和JDK标准序列化方式

Java标准的远程调用协议

连接个数:多连接
连接方式:短连接
传输协议:TCP
传输方式:同步传输
序列化:Java标准二进制序列化
适用范围:传入传出参数数据包大小混合,消费者与提供者个数差不多,可传文件。
适用场景:常规远程服务方法调用,与原生RMI服务互操作

WebService 协议

基于CXF的frontend-simple和transports-http实现

基于WebService的远程调用协议

连接个数:多连接
连接方式:短连接
传输协议:HTTP
传输方式:同步传输
序列化:SOAP文本序列化
适用场景:系统集成,跨语言调用。

Thrift 协议

Thrift是一个轻量级、跨语言的远程服务调用框架,最初由Facebook开发,后面进入Apache开源项目。它通过自身的IDL中间语言, 并借助代码生成引擎生成各种主流语言的RPC服务端/客户端模板代码。

当前 dubbo 支持的 thrift 协议是对 thrift 原生协议的扩展,在原生协议的基础上添加了一些额外的头信息,比如service name,magic number等。

资料

MySQL基架大致包括如下几大模块组件

  • (1)MySQL向外提供的交互接口(Connectors)

  • (2)管理服务组件和工具组件(Management Service & Utilities)

  • (3)连接池组件(Connection Pool)

  • (4)SQL接口组件(SQL Interface)

  • (5)查询分析器组件(Parser)

  • (6)优化器组件(Optimizer)

  • (7)缓存主件(Caches & Buffers)

  • (8)插件式存储引擎(Pluggable Storage Engines)

  • (9)物理文件(File System)

MySQL向外提供的交互接口(Connectors)

Connectors组件,是MySQL向外提供的交互组件,如java,.net,php等语言可以通过该组件来操作SQL语句,实现与SQL的交互

管理服务组件和工具组件(Management Service & Utilities)

提供对MySQL的集成管理,如备份(Backup),恢复(Recovery),安全管理(Security)等

连接池组件(Connection Pool)

负责监听对客户端向MySQL Server端的各种请求,接收请求,转发请求到目标模块。每个成功连接MySQL Server的客户请求都会被创建或分配一个线程,该线程负责客户端与MySQL Server端的通信,接收客户端发送的命令,传递服务端的结果信息等

SQL接口组件(SQL Interface)

接收用户SQL命令,如DML,DDL和存储过程等,并将最终结果返回给用户

查询分析器组件(Parser)

SQL命令传递到解析器的时候会被解析器验证和解析,首先分析SQL命令语法的合法性,并尝试将SQL命令分解成数据结构,若分解失败,则提示SQL语句不合理

优化器组件(Optimizer)

SQL语句在查询之前会使用查询优化器对查询进行优化。

缓存组件(Caches & Buffers)

如果查询缓存有命中的查询结果,查询语句就可以直接去查询缓存中取数据。这个缓存机制是由一系列小缓存组成的。比如表缓存,记录缓存,key缓存,权限缓存等

存储引擎(Engine)

MySQL属于关系型数据库,而关系型数据库的存储是以表的形式进行的,对于表的创建,数据的存储,检索,更新等都是由MySQL存储引擎完成的,这也是MySQL存储引擎在MySQL中扮演的重要角色。

MySQL之所以有多种存储引擎,是因为MySQL的开源性决定的。MySQL存储引擎,从种类上来说,大致可归结为官方存储引擎和第三方存储引起。MySQL的开源性,允许第三方基于MySQL骨架,开发适合自己业务需求的存储引擎。

MySQL存储引擎作用

MySQL存储引擎在MySQL中扮演重要角色,其作比较重要作用,大致归结为如下两方面:

作用一:管理表创建,数据检索,索引创建等

作用二:满足自定义存储引擎开发。

MySQL引擎种类

不同种类的存储引擎,在存储表时的存储引擎表机制也有所不同,从MySQL存储引擎种类上来说,可以分为官方存储引擎和第三方存储引擎。

当前,也存在多种MySQL存储引擎,如MyISAM存储引擎,InnoDB存储引擎,NDB存储引擎,Archive存储引擎,Federated存储引擎,Memory存储引擎,Merge存储引擎,Parter存储引擎,Community存储引擎,Custom存储引擎和其他存储引擎。

其中,比较常用的存储引擎包括InnoDB存储引擎,MyISAM存储引擎和Momery存储引擎。

物理文件(File System)

实际存储MySQL 数据库文件和一些日志文件等的系统,如Linux,Unix,Windows等。

物理文件包括:redolog、undolog、binlog(复制时还有relaylog)、errorlog、querylog、slowlog、data、index

资料

本示例首先介绍Java原生API实现BIO通信,然后进阶实现NIO通信,最后利用Netty实现NIO通信及Netty主要模块组件介绍。

Netty 是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。

BIO(Blocking I/O) 方案

BIO通信(一请求一应答)模型图如下

采用 BIO 通信模型 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在while(true) 循环中服务端会调用 accept() 方法等待接收客户端的连接的方式监听请求,一旦接收到一个连接请求,就可以在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待当前连接的客户端的操作执行完成, 如果要让 BIO 通信模型 能够同时处理多个客户端请求,就必须使用多线程(主要原因是socket.accept()、socket.read()、socket.write() 涉及的三个主要函数都是同步阻塞的)

代码实现

BIO服务端

BIOServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.easy.javaBio;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;

@Slf4j
public class BIOServer {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(10002);
while (true) {
Socket client = server.accept(); //等待客户端的连接,如果没有获取连接 ,在此步一直等待
new Thread(new ServerThread(client)).start(); //为每个客户端连接开启一个线程
}
//server.close();
}
}

@Slf4j
class ServerThread extends Thread {

private Socket client;

public ServerThread(Socket client) {
this.client = client;
}

@SneakyThrows
@Override
public void run() {
log.info("客户端:" + client.getInetAddress().getLocalHost() + "已连接到服务器");
BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));
//读取客户端发送来的消息
String mess = br.readLine();
log.info("客户端:" + mess);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
bw.write(mess + "\n");
bw.flush();
}
}

BIO客户端

BIOClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.easy.javaBio;

import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.net.Socket;

@Slf4j
public class BIOClient {

public static void main(String[] args) throws IOException {
Socket s = new Socket("0.0.0.0", 10002);

InputStream input = s.getInputStream();
OutputStream output = s.getOutputStream();

BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(output));
bw.write("客户端给服务端发消息测试\n"); //向服务器端发送一条消息
bw.flush();

BufferedReader br = new BufferedReader(new InputStreamReader(input)); //读取服务器返回的消息
String mess = br.readLine();
log.info("服务器:" + mess);
}
}

运行示例

运行BIO服务端,然后再运行BIO客户端,观察控制台

BIOServer控制台输出:

1
2
3
Connected to the target VM, address: '127.0.0.1:64346', transport: 'socket'
17:29:52.519 [Thread-1] INFO com.easy.javaBio.ServerThread - 客户端:YHE6OR5UXQJ6D35/192.168.9.110已连接到服务器
17:29:52.523 [Thread-1] INFO com.easy.javaBio.ServerThread - 客户端:客户端给服务端发消息测试

BIOClient控制台输出:

1
2
3
Connected to the target VM, address: '127.0.0.1:64355', transport: 'socket'
17:29:52.527 [main] INFO com.easy.javaBio.BIOClient - 服务器:客户端给服务端发消息测试
Disconnected from the target VM, address: '127.0.0.1:64355', transport: 'socket'

这表示我们实现了一个最简单的BIO通信了

这种方式为每个客户端开启一个线程,高并发时消耗资源较多,容易浪费,甚至导致服务端崩溃,对性能造成负面影响,高并发下不推荐使用。

NIO(New I/O)方案

NIO通信模型图如下

NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。

NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

NIO服务端

NIOServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package com.easy.javaBio;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.*;

@Slf4j
public class NIOServer {
private InetAddress addr;
private int port;
private Selector selector;

private static int BUFF_SIZE = 1024;

public NIOServer(InetAddress addr, int port) throws IOException {
this.addr = addr;
this.port = port;
startServer();
}

private void startServer() throws IOException {
// 获得selector及通道(socketChannel)
this.selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);

// 绑定地址及端口
InetSocketAddress listenAddr = new InetSocketAddress(this.addr, this.port);
serverChannel.socket().bind(listenAddr);
serverChannel.register(this.selector, SelectionKey.OP_ACCEPT);

log.info("NIOServer运行中...按下Ctrl-C停止服务");

while (true) {
log.info("服务器等待新的连接和selector选择…");
this.selector.select();

// 选择key工作
Iterator keys = this.selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = (SelectionKey) keys.next();

// 防止出现重复的key,处理完需及时移除
keys.remove();

//无效直接跳过
if (!key.isValid()) {
continue;
}
if (key.isAcceptable()) {
this.accept(key);
} else if (key.isReadable()) {
this.read(key);
} else if (key.isWritable()) {
this.write(key);
} else if (key.isConnectable()) {
this.connect(key);
}
}
}
}

private void connect(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
if (channel.finishConnect()) {
// 成功
log.info("成功连接了");
} else {
// 失败
log.info("失败连接");
}
}

private void accept(SelectionKey key) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel channel = serverChannel.accept();
channel.configureBlocking(false);
channel.register(this.selector, SelectionKey.OP_READ);

Socket socket = channel.socket();
SocketAddress remoteAddr = socket.getRemoteSocketAddress();
log.info("连接到: " + remoteAddr);
}

private void read(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();

ByteBuffer buffer = ByteBuffer.allocate(BUFF_SIZE);
int numRead = channel.read(buffer);
if (numRead == -1) {
log.info("关闭客户端连接: " + channel.socket().getRemoteSocketAddress());
channel.close();
return;
}
String msg = new String(buffer.array()).trim();
log.info("得到了: " + msg);

// 回复客户端
String reMsg = msg + " 你好,这是BIOServer给你的回复消息:" + System.currentTimeMillis();
channel.write(ByteBuffer.wrap(reMsg.getBytes()));
}

private void write(SelectionKey key) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocate(BUFF_SIZE);
byteBuffer.flip();
SocketChannel clientChannel = (SocketChannel) key.channel();
while (byteBuffer.hasRemaining()) {
clientChannel.write(byteBuffer);
}
byteBuffer.compact();
}

public static void main(String[] args) throws IOException {
new NIOServer(null, 10002);
}
}

使用NIO, 可以用Selector最终决定哪一组注册的socket准备执行I/O

NIO客户端

NIOClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.easy.javaBio;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;

@Slf4j
public class NIOClient {
private static int BUFF_SIZE = 1024;

public static void main(String[] args) throws IOException, InterruptedException {

InetSocketAddress socketAddress = new InetSocketAddress("0.0.0.0", 10002);
SocketChannel socketChannel = SocketChannel.open(socketAddress);

log.info("连接 BIOServer 服务,端口:10002...");

ArrayList<String> companyDetails = new ArrayList<>();

// 创建消息列表
companyDetails.add("腾讯");
companyDetails.add("阿里巴巴");
companyDetails.add("京东");
companyDetails.add("百度");
companyDetails.add("google");

for (String companyName : companyDetails) {
socketChannel.write(ByteBuffer.wrap(companyName.getBytes()));
log.info("发送: " + companyName);

ByteBuffer buffer = ByteBuffer.allocate(BUFF_SIZE);
buffer.clear();
socketChannel.read(buffer);
String result = new String(buffer.array()).trim();
log.info("收到NIOServer回复的消息:" + result);

// 等待2秒钟再发送下一条消息
Thread.sleep(2000);
}

socketChannel.close();
}
}

运行示例

首先运行我们的NIOServer,然后再运行NIOClient,观察控制台输出

NIOServer控制台输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17:35:40.921 [main] INFO com.easy.javaBio.NIOServer - NIOServer运行中...按下Ctrl-C停止服务
17:35:40.924 [main] INFO com.easy.javaBio.NIOServer - 服务器等待新的连接和selector选择…
17:36:29.188 [main] INFO com.easy.javaBio.NIOServer - 连接到: /192.168.9.110:64443
17:36:29.188 [main] INFO com.easy.javaBio.NIOServer - 服务器等待新的连接和selector选择…
17:36:29.194 [main] INFO com.easy.javaBio.NIOServer - 得到了: 腾讯
17:36:29.194 [main] INFO com.easy.javaBio.NIOServer - 服务器等待新的连接和selector选择…
17:36:31.194 [main] INFO com.easy.javaBio.NIOServer - 得到了: 阿里巴巴
17:36:31.195 [main] INFO com.easy.javaBio.NIOServer - 服务器等待新的连接和selector选择…
17:36:33.195 [main] INFO com.easy.javaBio.NIOServer - 得到了: 京东
17:36:33.195 [main] INFO com.easy.javaBio.NIOServer - 服务器等待新的连接和selector选择…
17:36:35.196 [main] INFO com.easy.javaBio.NIOServer - 得到了: 百度
17:36:35.197 [main] INFO com.easy.javaBio.NIOServer - 服务器等待新的连接和selector选择…
17:36:37.197 [main] INFO com.easy.javaBio.NIOServer - 得到了: google
17:36:37.198 [main] INFO com.easy.javaBio.NIOServer - 服务器等待新的连接和selector选择…
17:36:39.198 [main] INFO com.easy.javaBio.NIOServer - 关闭客户端连接: /192.168.9.110:64443
17:36:39.198 [main] INFO com.easy.javaBio.NIOServer - 服务器等待新的连接和selector选择…

NIOClient控制台输出

1
2
3
4
5
6
7
8
9
10
11
17:36:29.189 [main] INFO com.easy.javaBio.NIOClient - 连接 BIOServer 服务,端口:10002...
17:36:29.194 [main] INFO com.easy.javaBio.NIOClient - 发送: 腾讯
17:36:29.194 [main] INFO com.easy.javaBio.NIOClient - 收到NIOServer回复的消息:腾讯 你好,这是BIOServer给你的回复消息:1576229789194
17:36:31.194 [main] INFO com.easy.javaBio.NIOClient - 发送: 阿里巴巴
17:36:31.195 [main] INFO com.easy.javaBio.NIOClient - 收到NIOServer回复的消息:阿里巴巴 你好,这是BIOServer给你的回复消息:1576229791194
17:36:33.195 [main] INFO com.easy.javaBio.NIOClient - 发送: 京东
17:36:33.196 [main] INFO com.easy.javaBio.NIOClient - 收到NIOServer回复的消息:京东 你好,这是BIOServer给你的回复消息:1576229793195
17:36:35.196 [main] INFO com.easy.javaBio.NIOClient - 发送: 百度
17:36:35.197 [main] INFO com.easy.javaBio.NIOClient - 收到NIOServer回复的消息:百度 你好,这是BIOServer给你的回复消息:1576229795197
17:36:37.197 [main] INFO com.easy.javaBio.NIOClient - 发送: google
17:36:37.198 [main] INFO com.easy.javaBio.NIOClient - 收到NIOServer回复的消息:google 你好,这是BIOServer给你的回复消息:1576229797198

NIO服务端每隔两秒会收到客户端的请求,并对客户端的消息做出回复。

直接使用Java NIO API构建应用程序是可以的,但要做到正确和安全并不容易。特别是在高负载下,可靠和高效地处理和调度I/O操作是一项繁琐而且容易出错的任务。可以选中Netty, Apache Mina等高性能网络编程框架。

Netty 构建 NIO 通信服务 方案

使用JDK原生网络应用程序API,会存在的问题

  • NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等

  • 需要具备其它的额外技能做铺垫,例如熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序

  • 可靠性能力补齐,开发工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大

Netty对JDK自带的NIO的API进行封装,解决上述问题,主要特点有

  • 高并发

Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高 。

  • 传输快

Netty的传输快其实也是依赖了NIO的一个特性——零拷贝。

  • 封装好

Netty封装了NIO操作的很多细节,提供易于使用的API。

Netty框架的优势

  • API使用简单,开发门槛低;
  • 功能强大,预置了多种编解码功能,支持多种主流协议;
  • 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展;
  • 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优;
  • 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;
  • 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会加入;
  • 经历了大规模的商业应用考验,质量得到验证。在互联网、大数据、网络游戏、企业应用、电信软件等众多行业得到成功商用,证明了它已经完全能够满足不同行业的商业应用了。

代码实现

pom.xml依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.easy</groupId>
<artifactId>netty</artifactId>
<version>0.0.1</version>
<name>netty</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
<encoding>UTF-8</encoding>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>

<dependencies>
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.43.Final</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>

<modules>
<module>java-tcp</module>
<module>netty-server</module>
<module>netty-client</module>
</modules>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

搭建 Netty 服务端

NettyServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package com.easy.nettyServer;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.net.InetSocketAddress;

@Component
@Slf4j
public class NettyServer {
/**
* boss 线程组用于处理连接工作
*/
private EventLoopGroup boss = new NioEventLoopGroup();
/**
* work 线程组用于数据处理
*/
private EventLoopGroup work = new NioEventLoopGroup();

@Value("${netty.port}")
private Integer port;

/**
* 启动Netty Server
*
* @throws InterruptedException
*/
@PostConstruct
public void start() throws InterruptedException {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boss, work)
// 指定Channel
.channel(NioServerSocketChannel.class)
//使用指定的端口设置套接字地址
.localAddress(new InetSocketAddress(port))

//服务端可连接队列数,对应TCP/IP协议listen函数中backlog参数
.option(ChannelOption.SO_BACKLOG, 1024)

//设置TCP长连接,一般如果两个小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
.childOption(ChannelOption.SO_KEEPALIVE, true)

//将小的数据包包装成更大的帧进行传送,提高网络的负载
.childOption(ChannelOption.TCP_NODELAY, true)

.childHandler(new ServerChannelInitializer());
ChannelFuture future = bootstrap.bind().sync();
if (future.isSuccess()) {
log.info("启动 Netty Server");
}
}

@PreDestroy
public void destory() throws InterruptedException {
boss.shutdownGracefully().sync();
work.shutdownGracefully().sync();
log.info("关闭Netty");
}
}

NettyServerHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.easy.nettyServer;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 客户端连接会触发
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("Channel active......");
}

/**
* 客户端发消息会触发
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("服务器收到消息: {}", msg.toString());
ctx.write("我是服务端,我收到你的消息了!");
ctx.flush();
}

/**
* 发生异常触发
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

ServerChannelInitializer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.easy.nettyServer;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;

public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//添加编解码
socketChannel.pipeline().addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
socketChannel.pipeline().addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
socketChannel.pipeline().addLast(new NettyServerHandler());
}
}

创建 Netty 客户端

NettyClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.easy.nettyClient;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

@Component
@Slf4j
public class NettyClient {

private EventLoopGroup group = new NioEventLoopGroup();

@Value("${netty.port}")
private Integer port;

@Value("${netty.host}")
private String host;

private SocketChannel socketChannel;

/**
* 发送消息
*/
public void sendMsg(String msg) {
socketChannel.writeAndFlush(msg);
}

@PostConstruct
public void start() {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.remoteAddress(host, port)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new NettyClientInitializer());
ChannelFuture future = bootstrap.connect();
//客户端断线重连逻辑
future.addListener((ChannelFutureListener) future1 -> {
if (future1.isSuccess()) {
log.info("连接Netty服务端成功");
} else {
log.info("连接失败,进行断线重连");
future1.channel().eventLoop().schedule(() -> start(), 20, TimeUnit.SECONDS);
}
});
socketChannel = (SocketChannel) future.channel();
}
}

NettyClientHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.easy.nettyClient;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("客户端Active .....");
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("客户端收到消息: {}", msg.toString());
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

NettyClientInitializer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.easy.nettyClient;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class NettyClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast("decoder", new StringDecoder());
socketChannel.pipeline().addLast("encoder", new StringEncoder());
socketChannel.pipeline().addLast(new NettyClientHandler());
}
}

运行示例

打开浏览器,地址栏输入:http://localhost:8091/send?msg=%E4%BD%A0%E5%A5%BD,观察服务端和客户端控制台

服务端控制台输出

1
2
3
2019-12-13 18:01:37.901  INFO 11288 --- [           main] com.easy.nettyServer.NettyServer         : 启动 Netty Server
2019-12-13 18:01:45.834 INFO 11288 --- [ntLoopGroup-3-1] com.easy.nettyServer.NettyServerHandler : Channel active......
2019-12-13 18:02:07.858 INFO 11288 --- [ntLoopGroup-3-1] com.easy.nettyServer.NettyServerHandler : 服务器收到消息: 你好

客户端控制台输出

1
2
3
2019-12-13 18:01:45.822  INFO 11908 --- [ntLoopGroup-2-1] com.easy.nettyClient.NettyClient         : 连接Netty服务端成功
2019-12-13 18:01:45.822 INFO 11908 --- [ntLoopGroup-2-1] com.easy.nettyClient.NettyClientHandler : 客户端Active .....
2019-12-13 18:02:08.005 INFO 11908 --- [ntLoopGroup-2-1] com.easy.nettyClient.NettyClientHandler : 客户端收到消息: 我是服务端,我收到你的消息了!

表示使用Netty实现了我们的NIO通信了

Netty 模块组件

Bootstrap、ServerBootstrap

一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类。

Future、ChannelFuture

在Netty中所有的IO操作都是异步的,不能立刻得知消息是否被正确处理,但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过Future和ChannelFuture,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。

Channel

Netty网络通信组件,能够用于执行网络I/O操作。Channel为用户提供:

  • 当前网络连接的通道的状态(例如是否打开?是否已连接?)
  • 网络连接的配置参数 (例如接收缓冲区大小)
  • 提供异步的网络I/O操作(如建立连接,读写,绑定端口),异步调用意味着任何I/O调用都将立即返回,并且不保证在调用结束时所请求的I/O操作已完成。调用立即返回一个ChannelFuture实例,通过注册监听器到ChannelFuture上,可以I/O操作成功、失败或取消时回调通知调用方。
  • 支持关联I/O操作与对应的处理程序

不同协议、不同阻塞类型的连接都有不同的 Channel 类型与之对应,下面是一些常用的 Channel 类型

  • NioSocketChannel,异步的客户端 TCP Socket 连接
  • NioServerSocketChannel,异步的服务器端 TCP Socket 连接
  • NioDatagramChannel,异步的 UDP 连接
  • NioSctpChannel,异步的客户端 Sctp 连接
  • NioSctpServerChannel,异步的 Sctp 服务器端连接

Selector

Netty基于Selector对象实现I/O多路复用,通过 Selector, 一个线程可以监听多个连接的Channel事件, 当向一个Selector中注册Channel 后,Selector 内部的机制就可以自动不断地查询(select) 这些注册的Channel是否有已就绪的I/O事件(例如可读, 可写, 网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel

NioEventLoop

NioEventLoop中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用NioEventLoop的run方法,执行I/O任务和非I/O任务:

  • I/O任务 即selectionKey中ready的事件,如accept、connect、read、write等,由processSelectedKeys方法触发。
  • 非IO任务 添加到taskQueue中的任务,如register0、bind0等任务,由runAllTasks方法触发。

两种任务的执行时间比由变量ioRatio控制,默认为50,则表示允许非IO任务执行的时间与IO任务的执行时间相等。

NioEventLoopGroup

NioEventLoopGroup,主要管理eventLoop的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个Channel上的事件,而一个Channel只对应于一个线程。

ChannelHandler

ChannelHandler是一个接口,处理I/O事件或拦截I/O操作,并将其转发到其ChannelPipeline(业务处理链)中的下一个处理程序。

ChannelHandlerContext

保存Channel相关的所有上下文信息,同时关联一个ChannelHandler对象

ChannelPipline

保存ChannelHandler的List,用于处理或拦截Channel的入站事件和出站操作。 ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及Channel中各个的ChannelHandler如何相互交互。

资料

数列

等比数列

前N项和为 $S_n$,通项为$a_n$,公比为 $q$,且$a_1\not=0$。

  • 当$q=1$时,则$S_n=na_1$

  • 当$q\not=0$时,则$S_n=\cfrac{a_1(1-q^n)}{1-q}$

  • $a_n=a_1*q^n$

  • $a_n=a_mq^{n-m}$

等差数列

前N项和为 $S_n$,通项为$a_n$,公差为 $d$。

  • $S_n=\cfrac{(a_1+a_n)*n}{2}$

  • $a_n=a_1+(n-1)d$

  • $a_n=a_m+(n-m)d$

解析几何

点到点距离

设两个点A、B以及坐标分别为 $A(x_1,y_1)、B(x_2,y_2)$,A和B两点之间的距离为 $d$。

则 $d=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2}$

点到直线距离

直线的方程为 $l:Ax+By+c=0 ,A、B$ 均不为0,点的坐标为 $P(x_0,y_0)$,点 $P$ 到 $l$ 的距离为 $d$。

则 $d=\cfrac{|Ax_0+Bx_0+c|}{\sqrt{A^2+B^2}}$

两条平行线距离

直线的方程为 $l_1:Ax+By+c_1=0,l_2:Ax+By+c_2=0$,它们的距离为 $d$。

则 $d=\cfrac{|c_1-c_2|}{\sqrt{A^2+B^2}}$

立体几何

球体表面积

球体半径为 $r$,球体表面积为:$S$。

则 $S=4\pi r^2$

球体体积

球体半径为 $r$,体积为:$V$

则 $V=\cfrac{4}{3}\pi r^3$

示例

$\sqrt{xy}+\sqrt[a]{x}$

资料