Java进阶3 - 易错知识点整理(待更新)
该章节是Java进阶2- 易错知识点整理的续篇;
在前一章节中介绍了 ORM
框架,中间件相关的面试题,而在该章节中主要记录关于项目部署中间件,网络性能优化的常见面试题。
文章目录
15、Docker
参考Docker常用命令(以Anaconda为例搭建环境),
-
【问】docker如何拉取镜像?
Note:docker pull 镜像名:tags eg: docker pull continuumio/anaconda3 #默认拉取最新版本
-
【问】docker如何查看,查找,删除本地镜像?
Note:docker images -a #查看镜像列表 docker search 镜像名 #查找镜像 docker rmi 镜像名 #删除镜像
-
【问】docker如何更新本地镜像?
Note:docker run -t -i 镜像名:版本号 /bin/bash eg: docker run -t -i ubuntu:15.10 /bin/bash
-
【问】docker如何通过镜像实例化容器?(该步骤在创建容器过程中,免去了
docker pull 镜像名:版本号
的操作),参考修改Docker容器的映射IP地址域端口号
Note:docker run -it --name 容器名 镜像名:版本号 /bin/bash eg: docker run -it --name="anaconda" -p 8888:8888 continuumio/anaconda3 /bin/bash ---
其中各个参数含义如下:
-i
: 交互式操作。-t
: 终端。--name="anaconda"
:是给容器起名字-p 8888:8888
:是将容器的0.0.0.0:8888
端口映射到本地的8888
端口(注意docker容器内是没有ip
的,它的ip
和宿主机一样)/bin/bash
:放在镜像名后的是命令,这里我们希望有个交互式 Shell,因此用的是/bin/bash
。
-
【问】docker如何启动,停止容器?
Note:启动容器:docker start 容器名/容器id 停止容器:docker stop 容器名/容器id
-
【问】docker如何进入容器内部?(进入容器内就正常在linux系统下执行命令即可,建议先查看linux版本,比如
cat /etc/debian_version
)
Note:docker exec -it 容器id /bin/bash
-
【问】docker如何实现宿主机和docker容器的数据互传?
Note:- 将
docker
容器内的文件拷贝到宿主机中# 将容器b7200c1b6150的文件test.json传到主机/tmp/,在Ubuntu命令行中输入 $ docker cp b7200c1b6150:/opt/gopath/src/github.com/hyperledger/fabric/test.json /tmp/
- 将宿主机中的文件拷贝到
docker
容器中# 将主机requirements.txt传到容器9cf7b3c196f3的/home目录下,在宿主机命令行中输入 $ docker cp /home/wangxiaoxi/Desktop/requirements.txt 9cf7b3c196f3:/home/
- 将
-
【问】docker如何查看,删除本地容器?
Note:删除容器:docker rm -f 容器id 查看所有的容器:docker ps -a
-
【问】docker如何将容器打包成镜像?
Note:docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]] #OPTIONS说明: # -a :提交的镜像作者; # -c :使用Dockerfile指令来创建镜像; # -m :提交时的说明文字; # -p :在commit时,将容器暂停。 docker commit -a "wangxiaoxi" -m "fallDetection_toolkit_env" 2b1ad7022d19 fall_detection_env:v1
-
【问】docker如何将容器导出为tar,并由tar导入?
Note:导出容器快照:docker export 1e560fca3906 > ubuntu.tar 导入容器快照:cat docker/ubuntu.tar | docker import - test/ubuntu:v1
docker import
是指将快照文件ubuntu.tar
导入到镜像test/ubuntu:v1
中。 -
【问】docker如何编写
Dockerfile
,以及如何利用Dockerfile
构建本地镜像?,参考docker镜像的创建commit及dockerfile,Dockerfile文件详解
Note:- 常用指令:
FROM
:基础镜像,当前新镜像是基于哪个镜像
MAINTAINER
:镜像维护者的姓名和邮箱地址
RUN
:容器构建时需要运行的命令
EXPOSE
:当前容器对外暴露出的端口
WORKDIR
:指定在创建容器后,终端默认登陆的进来工作目录,一个落脚点
ENV
:用来在构建镜像过程中设置环境变量
ADD
:将宿主机目录下的文件拷贝进镜像且 ADD 命令会自动处理 URL 和解压 tar 压缩包
COPY
:类似 ADD,拷贝文件和目录到镜像中。(COPY src dest 或 COPY [“src”,“dest”])
VOLUME
:容器数据卷,用于数据保存和持久化工作
CMD
:指定一个容器启动时要运行的命令,Dockerfile 中可以有多个 CMD 指令,但只有最后一个生效,CMD 会被 docker run 之后的参数替换
ENTRYPOINT
:指定一个容器启动时要运行的命令,ENTRYPOINT 的目的和 CMD 一样,都是在指定容器启动程序及参数
ONBUILD
:当构建一个被继承的 Dockerfile 时运行命令,父镜像在被子继承后父镜像的 onbuild 被触发。 - 通过
Dockerfile
构建镜像# . 表示当前路径(一定不要在最后忘了'.',否则会报"docker build" requires exactly 1 argument.) docker build -f mydockerfile -t mycentos:0.1 . #-f表示dockerfile路径 -t表示镜像标签
- 常用指令:
-
【问】docker如何将镜像打包成tar包?
Note:# docker save -o 文件名.tar 镜像名 docker save -o /media/wangxiaoxi/新加卷/docker_dir/docker/test.tar hello-world #恢复镜像 docker load -i [docker备份文件.tar]
16、Netty(核心:channelPipeline
双向链表(责任链),链表每个节点使用promise
的wait/notify
(事件监听者))
参考黑马Netty笔记,尚硅谷Netty笔记,【硬核】肝了一月的Netty知识点,【阅读笔记】Java游戏服务器架构实战,代码参考:kebukeYi / book-code
-
【问】同步和异步的区别?阻塞和非阻塞IO的区别?(阻塞强调的是状态,而同步强调的是过程)
Note:
-
基本概念:
-
阻塞:等待缓冲区中的数据准备好过后才处理其他的事情,否则一直等待在那里。
-
非阻塞:当我们的进程访问我们的数据缓冲区的时候,如果数据没有准备好则直接返回,不会等待。如果数据已经准备好,也直接返回。
-
同步:当一个进程/线程在执行某个请求的时候,如果该请求需要一段时间才能返回信息,那么这个进程/线程会一直等待下去,直到收到返回信息才继续执行下去。
-
异步:进程不需要一直等待某个请求的处理结果,而是继续执行下面的操作,当这个请求处理完毕之后,可以通过回调函数通知该进程进行处理。
阻塞和同步(非阻塞和异步)描述相同,但强调内容不同:阻塞强调的是状态,而同步强调的是过程。
-
-
阻塞IO 和 非阻塞IO:(BIO vs NIO)
-
BIO(Blocking IO):
-
传统的 java.io 包,它基于流模型实现,提供了我们最熟知的一些 IO 功能,比如File抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。
-
在
Java
网络通信中,Socket
和ServerSocket
套接字是基于阻塞模式实现的。
-
-
NIO(Non-Blocking IO):
-
在
Java 1.4
中引入了对应java.nio
包,提供了Channel
,Selector
,Buffer
等抽象,它支持面向缓冲的,基于通道的 I/O 操作方法。 -
NIO 提供了与传统 BIO 模型中的
Socket
和ServerSocket
相对应的SocketChannel
和ServerSocketChannel
两种不同的套接字通道实现,支持阻塞和非阻塞两种模式。对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
-
-
BIO
和NIO
的比较:IO模型 BIO NIO 通信 面向流 面向缓冲 处理 阻塞 IO 非阻塞IO 触发 无 选择器
-
-
-
【问】什么是CPU密集型/IO密集型?(有多种类型的任务则需要考虑使用多个线程池)
Note:
-
游戏业务处理框架中对线程数量的管理需要考虑任务的类型:I/O密集型,计算密集型还是两者都有;如果有多种类型的任务则需要考虑使用多个线程池:
-
业务处理是计算密集型(比如游戏中总战力的计算、战报检验、业务逻辑处理,如果有N个处理器,建议分配N+1个线程)
-
数据库操作是IO密集型,比如数据库和Redis读写、网络I/O操作(不同进程通信)、磁盘I/O操作(日志文件写入)等
分配两个独立的线程池可以使得业务处理不受数据库操作的影响
-
-
在对线程的使用上,一定要严格按照不同的任务类型,使用对应的线程池。在游戏服务开发中,要严格规定开发人员不可随意创建新的线程。如果有特殊情况,需要特殊说明,并做好其使用性的评估,防止创建线程的地方过多,最后不可控制。
-
-
【问】Netty是什么?为什么要学习Netty?(异步,基于事件驱动的网络框架)
Note:
-
Netty 是一个异步的、基于事件驱动的网络应用框架,在
java.nio
基础上进行了封装(客户端SocketChannel
封装成了NioSocketChannel
,服务器端的ServerSocketChannel
封装成了NioServerSocketChannel
),用于快速开发可维护、高性能的网络服务器和客户端。Netty
在Java
网络应用框架中的地位就好比Spring
框架在JavaEE
开发中的地位。 -
为了保证网络通信的需求,以下的框架都使用了 Netty:
Cassandra
- nosql 数据库Spark
- 大数据分布式计算框架Hadoop
- 大数据分布式存储框架RocketMQ
- ali 开源的消息队列ElasticSearch
- 搜索引擎gRPC
- rpc 框架Dubbo
- rpc 框架Spring 5.x
- flux api 完全抛弃了 tomcat ,使用 netty 作为服务器端Zookeeper
- 分布式协调框架
-
-
【问】Netty的核心组件有哪些?(线程池 + selector + channel(底层是文件缓存)+ 任务队列 + channelPipeline(责任链,包含多个handler处理不同事件)),参考Netty如何封装Socket客户端Channel,Netty的Channel都有哪些类型?,Netty的核心组件,netty执行流程及核心模块详解(入门必看系列)
Note:
-
核心组件基本概念:
-
事件循环组(EventLoopGroup):可以将事件循环组简单的理解为线程池,它里面包含了多个事件循环线程(也就是
EventLoop
),初始化事件循环组的时候可以指定创建事件循环个数。 -
每个事件循环线程绑定一个任务队列,该任务队列用于处理非IO事件,比如通道注册,端口绑定等等,事件循环组中的
EventLoop
线程均处于活跃状态,每个EventLoop
线程绑定一个选择器(Selector
),一个选择器(Selector
)注册了多个通道(客户端连接),当通道产生事件的时候,绑定在选择器上的事件循环线程就会激活,并处理事件。 -
对于
BossGroup
事件循环组来说,里面的事件循环只监听通道的连接事件(即accept()
)。 -
对于
WorkerGroup
事件循环组来说,里面的事件循环只监听读事件(read()
)。如果监听到通道的连接事件(accept()
),会交给BossGroup
事件循环组中某个事件循环处理,处理完之后生成客户端通道(channel)注册至WorkerGroup事件循环组中的某个事件循环,并绑定读事件,这个事件循环就会监听读事件,客户端发起读写请求的时候,这个事件循环就会监听到并处理。-
选择器(selector) :
Selector
绑定一个事件循环线程(EventLoop
),其上可以注册多个通道(可以简单的理解为客户端连接),Selector
负责监听通道的事件(连接、读写),当客户端发起读写请求的时候,Selector
所绑定的事件线程(EventLoop
)就会唤醒,并从通道中读取事件进行处理。 -
任务队列和尾任务队列:一个事件循环绑定一个任务队列和尾队列,用于存储通道事件。
-
通道(channel):Linux程序在执行任何形式的 IO 操作时,都是在操作文件(比如可以通过
sed|awk
命令查看进程情况,查看进程的内容实际上还是个文件)。由于在UNIX系统中支持TCP/IP协议栈,就相当于引入了新的IO操作,也就是Socket IO,这个IO操作专用于网络传输。因此Linux系统把Socket也看作是一种文件。我们在使用Socket IO发送数据的时候,实际上就是操作文件:
-
首先打开文件,将数据写进文件(文件的上层也有一层缓存,叫文件缓存),再将文件缓存中的数据拷贝至网卡的发送缓冲区;
-
再通过网卡将缓冲区的数据发送至对方的网卡的接收缓冲区,对方的网卡接收到数据后,打开文件,将数据拷贝到文件,再将文件缓存中的数据拷贝至用户缓存,然后再处理数据。
Channel
是对Socket
的封装,因此它的底层也是在操作文件,所以操作Channel
的就是在操作Socket
,操作Socket
(本身就是一种文件)就是在操作文件。Netty
分别对JDK中客户端SocketChannel
和服务器端的ServerSocketChannel
进行再次封装,得到NioSocketChannel
和NioServerSocketChannel
。 -
-
-
管道(ChannelPipeline) :管道是以一组编码器为结点的链表,用于处理客户端请求,也是真正处理业务逻辑的地方。
-
处理器(ChannelHandler) :处理器,是管道的一个结点,一个客户端请求通常由管道里的所有处理器(handler)逐一的处理。
-
事件KEY(selectionKey) :当通道(
channel
)产生事件的时候,Selector
就会生成一个selectionKey
事件,并唤醒事件线程去处理事件。 -
缓冲(Buffer) :NIO是面向块的IO,从通道读取数据之后会放进缓存(Buffer),向通道写数据的时候也需要先写进缓存(Buffer),总之既不能直接从通道读数据,也不能直接向通道写数据。
-
缓冲池(BufferPool) :这是
Netty
针对内存的一种优化手段,通过一种池化技术去管理固定大小的内存。(当线程需要存放数据的时候,可以直接从缓冲池中获取内存,不需要的时候再放回去,这样不需要去频繁的重新去申请内存,因为申请内存是需要时间的,影响性能) -
ServerBootstrap 与 Bootstrap:
Bootstrap
和ServerBootstrap
被称为引导类,指对应用程序进行配置,并使他运行起来的过程。Netty
处理引导的方式是使你的应用程序和网络层相隔离。-
Bootstrap
是客户端的引导类,Bootstrap
在调用bind()
(连接UDP)和connect()
(连接TCP)方法时,会新创建一个Channel
,仅创建一个单独的、没有父 Channel 的Channel
来实现所有的网络交换。 -
ServerBootstrap
是服务端的引导类,ServerBootstrap
在调用bind()
方法时会创建一个ServerChannel
来接受来自客户端的连接,并且该 ServerChannel 管理了多个子 Channel 用于同客户端之间的通信。
-
-
ChannelFuture:
Netty 中所有的 I/O 操作都是异步的,即操作不会立即得到返回结果,所以 Netty 中定义了一个ChannelFuture
对象作为这个异步操作的“代言人”,表示异步操作本身。如果想获取到该异步操作的返回值,可以通过该异步操作对象的addListener()
方法为该异步操作添加 NIO 网络编程框架 Netty 监听器,为其注册回调:当结果出来后马上调用执行。Netty
的异步编程模型都是建立在Future
与回调call_back()
概念之上的。
-
-
组件与组件之间的关系如下:
- 一个事件循环组(
EventLoopGroup
)包含多个事件循环(EventLoop
) -1 ... *
; - 一个选择器(
selector
)只能注册进一个事件循环(EventLoop
)-1 ... 1
; - 一个事件循环(
EventLoop
)包含一个任务队列和尾任务队列 -1 ... 1
; - 一个通道(
channel
)只能注册进一个选择器(selector
)-1 ... 1
; - 一个通道(
channel
)只能绑定一个管道(channelPipeline
) -1 ... 1
; - 一个管道(
channelPipeline
)包含多个服务编排处理器(channelHandler
) - Netty通道(
NioSocketChannel/NioServerSocketChannel
)和原生NIO通道(SocketChannel/SocketServerChannel
)一一对应并绑定 -1 ... 1
; - 一个通道可以关注多个IO事件;
- 一个事件循环组(
-
-
【问】Netty 执行流程是怎样的?(自顶向下分析 / 客户端服务器分析),参考一文了解Netty整体流程,netty执行流程及核心模块详解(入门必看系列)
Note:
-
自顶向下分析流程:NioEventLoopGroup -> NioEventLoop -> selector -> channel,NioEventLoop监听不同channel(BossGroup中的NioEventLoop监听accept,work Group中的NioEventLoop监听read/write事件)
-
Netty 抽象出两组线程池 ,
BossGroup
专门负责接收客户端的连接,WorkerGroup
专门负责网络的读写;BossGroup
和WorkerGroup
类型都是NioEventLoopGroup
-
NioEventLoopGroup
相当于一个事件循环组,这个组中含有多个事件循环,每一个事件循环是NioEventLoop
;-
NioEventLoop
表示一个不断循环的执行处理任务的线程(selector
监听绑定事件是否发生),每个NioEventLoop
都有一个Selector
,用于监听绑定在其上的socket
的网络通讯,比如NioServerSocketChannel
绑定在服务器bossgroup
的NioEventLoop
的selector
上,NioSocketChannel
绑定在客户端的NioEventLoop
的selector
上,然后各自的selector
就不断循环监听相关事件。 -
NioEventLoopGroup
可以有多个线程,即可以含有多个NioEventLoop
-
-
每个
BossGroup
下面的NioEventLoop
循环执行的步骤有 3 步-
轮询
accept
事件 -
处理
accept
事件,与client
建立连接,生成NioScocketChannel
,并将其注册到某个workerGroup NIOEventLoop
上的Selector
-
继续处理任务队列的任务,即
runAllTasks
-
-
每个
WorkerGroup
下面的NIOEventLoop
循环执行的步骤-
轮询
read
,write
事件 -
处理
I/O
事件,即read,write
事件,在对应NioScocketChannel
处理。 -
处理任务队列的任务,即
runAllTasks
-
-
每个
Worker
下面的NIOEventLoop
处理业务时,会使用pipeline
(管道),pipeline
中包含了channel
(通道),即通过pipeline
可以获取到对应通道,管道中维护了很多的处理器。 -
NioEventLoop
内部采用串行化设计,从消息的 读取->解码->处理->编码->发送,始终由 IO 线程NioEventLoop
负责 -
NioEventLoopGroup
下包含多个NioEventLoop
每个NioEventLoop
中包含有一个Selector
,一个taskQueue
每个NioEventLoop
的Selector
上可以注册监听多个NioChannel
每个NioChannel
只会绑定在唯一的NioEventLoop
上
每个NioChannel
都绑定有一个自己的ChannelPipeline
NioChannel
可以获取对应的ChannelPipeline
,ChannelPipeline
也可以获取对应的NioChannel
。
-
-
客户端服务器分析流程如下:
-
Server
启动,Netty
从ParentGroup
(BossGroup
)中选出一个NioEventLoop
对指定port
进行监听。 -
Client
启动,Netty
从ParentGroup
(BossGroup
)中选出个NioEventLoop
连接Server
。 -
Client
连接Server
的port
,创建Channel
-
Netty
从ChildGroup
(WorkGroup
)中选出一个NioEventLoop
与channel
绑定,用于处理该Channel
中所有的操作。 -
Client
通过Channel
向Server
发送数据包。 -
Pipeline
中的处理器采用责任链的模式对Channel
中的数据包进行处理 -
Server 如需向Client发送数据。则需将数据经
pipeline
中的处理器处理行
成ByteBuf
数据包进行传输。 -
Server
将数据包通过channel
发送给Client
-
Pipeline
中的处理器采用责任链的模式对channel
中的数据包进行处理
-
-
-
Note:
-
参考上一问中关于客户端/服务端的Netty执行流程,给出如下代码
-
服务端:
package org.example.code001_helloworld; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.string.StringDecoder; public class HelloServer { public static void main(String[] args) throws InterruptedException{ //通过ServerBootStrap引导类创建channel ServerBootstrap sb = new ServerBootstrap() .group(new NioEventLoopGroup()) //2、选择事件循环组为NioEventLoopGroup,返回ServerBootstrap .channel(NioServerSocketChannel.class) //3、选择通道实现类为NioServerSocketChannel,返回ServerBootstrap .childHandler( //为channel添加处理器,返回返回ServerBootstrap new ChannelInitializer<NioSocketChannel>(){ //4、初始化处理器,用来监听客户端创建的SocketChannel protected void initChannel(NioSocketChannel ch) throws Exception { ch.pipeline().addLast(new StringDecoder()); // 5、处理器1用于将ByteBuf解码为String ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() { // 6、处理器2即业务处理器,用于处理上一个处理器的处理结果 @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) { System.out.println(msg); //输出客户端往NioSocketChannel中发送端数据 } }); } } ); ); // sb.bind("127.0.0.1",8080); //监听客户端的socket端口 ChannelFuture channelFuture = sb.bind("127.0.0.1",8080); //监听客户端的socket端口(默认127.0.0.1) //设置监听器 channelFuture.addListener(new GenericFutureListener<Future<? super Void>>() { @Override public void operationComplete(Future<? super Void> future) throws Exception { if(future.isSuccess()){ System.out.println("端口绑定成功"); }else{ System.out.println("端口绑定失败"); } } }); while(true){ Thread.sleep(1000); //睡眠5s System.out.println("我干我的事情"); } } }
-
客户端:
package org.example.code001_helloworld; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.string.StringEncoder; import java.util.Date; public class HelloClient{ public static void main(String[] args) throws InterruptedException { int i = 3; while(i > 0) { Channel channel = new Bootstrap() //客户端启动类,用于引导创建channel;其中Bootstrap继承于AbstractBootstrap<Bootstrap, Channel>,即一个map集合 .group(new NioEventLoopGroup()) // 1、选择事件循环组类为NioEventLoopGroup,返回Bootstrap .channel(NioSocketChannel.class) // 2、选择socket实现类为NioSocketChannel,返回Bootstrap .handler(new ChannelInitializer<Channel>() { // 3、添加处理器,返回Bootstrap:创建ChannelInitializer抽象类的匿名内部类,重写initChannel,处理器是Channel的集合 @Override //在连接建立后被调用 protected void initChannel(Channel ch) { ch.pipeline().addLast(new StringEncoder()); } }) .connect("127.0.0.1", 8080) // 4、与server建立连接,返回ChannelFuture .sync() // 5、同步阻塞,等待连接建立,返回ChannelFuture .channel(); // 6、成功创建通道,返回Channel,通道即为socket文件 channel.writeAndFlush(new Date() + ": hello world! My name is wang" + i); //7、向channel中写入数据,发送给server i--; } } }
-
先开启服务器端,再开启三个客户端;服务端通过自定义处理器,从8080端口中监听到客户端发送过来的数据,并打印到控制台。
我干我的事情 我干我的事情 我干我的事情 我干我的事情 Thu Nov 10 23:17:03 CST 2022: hello world! My name is wang1 Thu Nov 10 23:17:03 CST 2022: hello world! My name is wang3 Thu Nov 10 23:17:03 CST 2022: hello world! My name is wang2 我干我的事情 我干我的事情 我干我的事情 我干我的事情
-
-
对上面的代码流程进行简单解析:
-
服务器端先从线程池中选择一个线程,用于监听服务器端绑定的
ip
和端口(即127.0.0.1
和8080
)。这里的端口是客户端访问服务器端的唯一入口,当多个客户端在同一时间向服务器端发送大量请求,如果服务器端对每个客户端的请求进行一一接收,则会出现阻塞等待问题。
-
为了解决阻塞问题,客户端的不同请求通过不同的
channel
(即文件缓存)以文件形式保存在服务器端监听的端口中,因此这里服务器端专门开启一个线程来监听这个端口,该线程与selector
绑定,让selector
完成事件处理工作。 -
待
channel
传输完毕之后(文件缓存满了,写入到文件),selector
会通过channelPipeline
中自定义的channelHandler
对数据进行处理。
-
-
-
Note:
这里分别使用
juc.Future
,Consumer
函数式接口,和netty的promise
,模拟数据库数据查询过程:-
在等待获取
juc.Future
返回结果时,主线程是阻塞的。package org.example.code000_JUC_test; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.FutureTask; public class code001_future_test { //模拟数据库查询操作 public String getUsernameById(Integer id) throws InterruptedException{ Thread.sleep(1000); //模拟IO过程 return "wangxiaoxi"; } public static void main(String[] args) { final code001_future_test obj = new code001_future_test(); //FutureTask处理的数据类型即为Callable异步返回的数据类型 FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>(){ public String call() throws Exception { System.out.println("thread1正在异步执行中"); String username = obj.getUsernameById(1); System.out.println("callable over"); return username; } }); //创建线程并异步执行 Thread thread = new Thread(futureTask); thread.start(); try{ System.out.println("mainThread开始操作"); String res = futureTask.get(); //主线程会阻塞,同步等待线程1执行结束,并返回值 System.out.println("thread1处理完毕," + "用户名为:" + res); int i = 5; while(i > 0){ System.out.println("mainThread正在执行中"); Thread.sleep(1000); i--; } System.out.println("mainThread结束操作"); }catch (InterruptedException e){ e.printStackTrace(); }catch (Exception e){ e.printStackTrace(); } } } --- mainThread开始操作 thread1正在异步执行中 callable over thread1处理完毕,用户名为:wangxiaoxi mainThread正在执行中 mainThread正在执行中 mainThread正在执行中 mainThread正在执行中 mainThread正在执行中 mainThread结束操作
-
通过回调函数(
Consumer<String> consumer
+lambda8
表达式)的方式来处理返回结果,此时主线程仍然可以完成其他操作,无需阻塞等待其他线程的返回结果。但是会存在consumer
被多个线程同时使用的并发问题。package org.example.code000_JUC_test; import java.util.function.Consumer; public class code002_consumer_test { //模拟数据库查询操作, 其中consumer是回调函数, 所以该函数无返回值 public void getUsernameById(Integer id, Consumer<String> consumer) throws InterruptedException{ Thread.sleep(1000); //模拟IO过程 String username = "wangxiaoxi"; consumer.accept(username); } public static void main(String[] args) throws InterruptedException{ final code002_consumer_test obj = new code002_consumer_test(); Consumer<String> consumer = ((s) -> { System.out.println("thread1处理完毕,用户名为:" + s); }); //通过回调函数异步执行 Thread thread = new Thread(new Runnable(){ public void run() { try { System.out.println("thread1正在异步执行中"); obj.getUsernameById(1,consumer); //函数式编程: consumer有入参,无返回值 } catch (InterruptedException e) { throw new RuntimeException(e); } } }); thread.start(); System.out.println("mainThread开始操作"); int i = 5; while(i > 0){ System.out.println("mainThread正在执行中"); Thread.sleep(1000); i--; } System.out.println("mainThread结束操作"); } } --- mainThread开始操作 mainThread正在执行中 thread1正在异步执行中 mainThread正在执行中 thread1处理完毕,用户名为:wangxiaoxi mainThread正在执行中 mainThread正在执行中 mainThread正在执行中 mainThread结束操作
-
netty
重写了juc.Future
接口,并在此基础上派生出子接口promise
,promise
可以通过设置监听器来监听promise
是否已被其他线程处理(此时listener
通过promise.await
阻塞等待promise
处理结果,待promise
已被其他线程处理,则该线程会通过promise.notify
唤醒listener
,通知其对结果进行处理;如果future.isSuccess()
则表示处理成功,如果future.Cancelled()
则表示处理失败)。package org.example.code000_JUC_test; import io.netty.util.concurrent.DefaultEventExecutor; import io.netty.util.concurrent.DefaultEventExecutorGroup; import io.netty.util.concurrent.DefaultPromise; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.EventExecutorGroup; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import io.netty.util.concurrent.Promise; import java.util.function.Consumer; public class code003_netty_test { public Future<String> getUsernameById(Integer id,Promise<String> promise) throws InterruptedException{ //模拟从数据库线程池中取出某一个线程进行操作 new Thread(new Runnable() { @Override public void run() { System.out.println("thread2正在异步执行中"); try { Thread.sleep(1000); //模拟IO过程 } catch (InterruptedException e) { throw new RuntimeException(e); } String username = "wangxiaoxi"; System.out.println("thread2处理完毕"); promise.setSuccess(username); } }).start(); return promise; //返回promise的线程和处理promise线程并不是同一个线程 } public static void main(String[] args) throws InterruptedException{ code003_netty_test obj = new code003_netty_test(); EventExecutor executor = new DefaultEventExecutor(); //通过netty创建线程 executor.execute(new Runnable() { @Override public void run() { System.out.println("thread1正在异步执行中"); //异步调用返回值(继承于netty.Future,可用于设置监听器) Promise<String> promise = new DefaultPromise<String>(executor); //设置监听器,阻塞等待(object.await)直到promise返回结果并对其进行处理 try { obj.getUsernameById(1,promise).addListener(new GenericFutureListener<Future<? super String>>() { @Override public void operationComplete(Future<? super String> future) throws Exception { System.out.println("thread1.listener监听完毕"); if(future.isSuccess()){ System.out.println("thread1.listener监听到promise的返回值"); String username = (String)future.get(); System.out.println("thread1处理完毕,用户名为:" + username); } } }); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); System.out.println("mainThread开始操作"); int i = 5; while(i > 0){ System.out.println("mainThread正在执行中"); Thread.sleep(1000); i--; } System.out.println("mainThread结束操作"); } } --- mainThread开始操作 mainThread正在执行中 thread1正在异步执行中 thread2正在异步执行中 mainThread正在执行中 thread2处理完毕 thread1.listener监听完毕 thread1.listener监听到promise的返回值 thread1处理完毕,用户名为:wangxiaoxi mainThread正在执行中 mainThread正在执行中 mainThread正在执行中 mainThread结束操作
promise
结合了回调函数和Future
的优点,回调函数的创建和处理可以不在同一个线程中(线程1创建promise
,线程1的子线程2用于处理promise
,因此不存在并发上的问题)
-
-
【问】ChannelFuture和Promise可用来干什么?两者有什么区别?(
ChannelFuture
和Promise
一样,都继承于netty的Future
,可用于异步处理结果的返回),参考Netty异步回调模式-Future和Promise剖析Note:
-
Netty
的Future
继承JDK的Future
,通过 Object 的wait/notify
机制,实现了线程间的同步;使用观察者设计模式,实现了异步非阻塞回调处理。其中:-
ChannelFuture
和Promise
都是Netty
的Future
的子接口; -
ChannelFuture
和Channel
绑定,用于异步处理Channel
事件;但不能根据Future
的执行状态设置返回值。 -
Promise
对Netty的Future
基础上进行进一步的封装,增加了设置返回值和异常消息的功能,根据不同数据处理的返回结果定制化Future
的返回结果,比如:@Override public void channelRegister(AbstractGameChannelHandlerContext ctx, long playerId, GameChannelPromise promise) { // 在用户GameChannel注册的时候,对用户的数据进行初始化 playerDao.findPlayer(playerId, new DefaultPromise<>(ctx.executor())).addListener(new GenericFutureListener<Future<Optional<Player>>>() { @Override public void operationComplete(Future<Optional<Player>> future) throws Exception { Optional<Player> playerOp = future.get(); if (playerOp.isPresent()) { player = playerOp.get(); playerManager = new PlayerManager(player); promise.setSuccess(); fixTimerFlushPlayer(ctx);// 启动定时持久化数据到数据库 } else { logger.error("player {} 不存在", playerId); promise.setFailure(new IllegalArgumentException("找不到Player数据,playerId:" + playerId)); } } }); }
当消息设置成功后会立即通知
listener
处理结果;一旦setSuccess(V result)
或setFailure(V result)
后,那些await()
或sync()
的线程就会从等待中返回。 -
ChannelPromise
继承了ChannelFuture
和Promise
,是可写的ChannelFuture
接口
-
-
ChannelFuture
接口:Netty
的I/O
操作都是异步的,例如bind
,connect
,write
等操作,会返回一个ChannelFuture
接口。Netty源码中大量使用了异步回调处理模式,比如在接口绑定任务时,可以通过设置Listener实现异步处理结果的回调,这个过程被称为被动回调。... ChannelFuture channelFuture = sb.bind("127.0.0.1",8080); //监听客户端的socket端口 //设置监听器 channelFuture.addListener(new GenericFutureListener<Future<? super Void>>() { @Override public void operationComplete(Future<? super Void> future) throws Exception { if(future.isSuccess()){ System.out.println("端口绑定成功"); }else{ System.out.println("端口绑定失败"); } } }); ...
ChannelFuture
和IO操作中的channel
通道关联在一起了,用于异步处理channel事件,这个接口在实际中用的最多。ChannelFuture
接口相比父类Future
接口,就增加了channel()
和isVoid()
两个方法ChannelFuture
接口定义的方法如下:public interface ChannelFuture extends Future<Void> { // 获取channel通道 Channel channel(); @Override ChannelFuture addListener(GenericFutureListener<? extends Future<? super Void>> listener); @Override ChannelFuture addListeners(GenericFutureListener<? extends Future<? super Void>>... listeners); @Override ChannelFuture removeListener(GenericFutureListener<? extends Future<? super Void>> listener); @Override ChannelFuture removeListeners(GenericFutureListener<? extends Future<? super Void>>... listeners); @Override ChannelFuture sync() throws InterruptedException; @Override ChannelFuture syncUninterruptibly(); @Override ChannelFuture await() throws InterruptedException; @Override ChannelFuture awaitUninterruptibly(); // 标记Futrue是否为Void,如果ChannelFuture是一个void的Future,不允许调// 用addListener(),await(),sync()相关方法 boolean isVoid(); }
ChannelFuture
就两种状态Uncompleted(未完成)和Completed(完成),Completed
包括三种,执行成功,执行失败和任务取消。注意:执行失败和任务取消都属于Completed。 -
Promise
接口:Promise
是个可写的Future
,接口定义如下public interface Promise<V> extends Future<V> { // 执行成功,设置返回值,并通知所有listener,如果已经设置,则会抛出异常 Promise<V> setSuccess(V result); // 设置返回值,如果已经设置,返回false boolean trySuccess(V result); // 执行失败,设置异常,并通知所有listener Promise<V> setFailure(Throwable cause); boolean tryFailure(Throwable cause); // 标记Future不可取消 boolean setUncancellable(); @Override Promise<V> addListener(GenericFutureListener<? extends Future<? super V>> listener); @Override Promise<V> addListeners(GenericFutureListener<? extends Future<? super V>>... listeners); @Override Promise<V> removeListener(GenericFutureListener<? extends Future<? super V>> listener); @Override Promise<V> removeListeners(GenericFutureListener<? extends Future<? super V>>... listeners); @Override Promise<V> await() throws InterruptedException; @Override Promise<V> awaitUninterruptibly(); @Override Promise<V> sync() throws InterruptedException; @Override Promise<V> syncUninterruptibly(); }
-
Future
接口只提供了获取返回值的get()
方法,不可设置返回值。 -
Promise
接口在Future
基础上,还提供了设置返回值和异常信息,并立即通知listeners。而且,一旦setSuccess(...)
或setFailure(...)
后,那些await()
或sync()
的线程就会从等待中返回。同步阻塞有两种方式:sync()和await(),区别:
sync()
方法在任务失败后,会把异常信息抛出;await()
方法对异常信息不做任何处理,当我们不关心异常信息时可以用await()
。通过阅读源码可知
sync()
方法里面其实调的就是await()
方法。// DefaultPromise 类 @Override public Promise<V> sync() throws InterruptedException { await(); rethrowIfFailed(); return this; }
-
-
通过继承
Promise
接口,得到关于ChannelFuture
的可写的子接口ChannelPromise
;Promise
的实现类为DefaultPromise
,通过Object的wait/notify
来实现线程的同步,通过volatile
关键字保证线程间的可见性。ChannelPromise
的实现类为DefaultChannelPromise
,其继承关系如下:
-
-
【问】ChannelPipeline的执行过程(ChannelHandler在ChannelPipeline中被封装成ChannelHandlerContext,通过tail和head标识来实现读写处理),参考Netty的TCP粘包和拆包解决方案,黑马Netty教程
Note:
-
Selector
轮询到网络IO事件后,会调用该Channel
对应的ChannelPipeline
来依次执行对应的ChannelHandler
。基于事件驱动的Netty
框架如下:
-
上面我们已经知道
ChannelPipeline
和ChannelHandler
的关系 :ChannelPipeline
是一个存放各种ChannelHandler
的管道容器。ChannelPipeline
的执行流程如下(ChannelHandler
也分为了两大类:ChannelInboundHandler
是用于负责处理链路读事件的Handler
,ChannelOutboundHandler
是用于负责处理链路写事件的Handler
):NioEventLoop
触发读事件,会调用SocketChannel
所关联的ChannelPipline
- 由上一步读取到的消息会在
ChannelPipline
中依次被多个ChannelInboundHandler
处理。 - 处理完消息会调用
ChannelHandlerContext
的write
方法发送消息,此时触发写事件,发送的消息同样也会经过ChannelPipline
中的多个ChannelOutboundHandler
处理。
-
一个
channel
绑定一个channelPipeline
,可以通过channel
获取channelPipeline
进而添加channelHandler
;channelPipeline
初始化代码如下:eventGroup = new NioEventLoopGroup(gameClientConfig.getWorkThreads());// 从配置中获取处理业务的线程数 bootStrap = new Bootstrap(); bootStrap.group(eventGroup).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true).option(ChannelOption.SO_KEEPALIVE, true).option(ChannelOption.CONNECT_TIMEOUT_MILLIS, gameClientConfig.getConnectTimeout() * 1000).handler(new ChannelInitializer<Channel>() { @Override protected void initChannel(Channel ch) throws Exception { ch.pipeline().addLast("EncodeHandler", new EncodeHandler(gameClientConfig));// 添加编码 ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024 * 1024 * 4, 0, 4, -4, 0));// 添加解码 ch.pipeline().addLast("DecodeHandler", new DecodeHandler());// 添加解码 ch.pipeline().addLast("responseHandler", new ResponseHandler(gameMessageService));//将响应消息转化为对应的响应对象 // ch.pipeline().addLast(new TestGameMessageHandler());//测试handler ch.pipeline().addLast(new IdleStateHandler(150, 60, 200));//如果6秒之内没有消息写出,发送写出空闲事件,触发心跳 ch.pipeline().addLast("HeartbeatHandler",new HeartbeatHandler());//心跳Handler ch.pipeline().addLast(new DispatchGameMessageHandler(dispatchGameMessageService));// 添加逻辑处理 } }); ChannelFuture future = bootStrap.connect(gameClientConfig.getDefaultGameGatewayHost(), gameClientConfig.getDefaultGameGatewayPort()); channel = future.channel();
-
ChannelHandler
在ChannelPipline
中的结构:ChannelHandler
在加入ChannelPipline
之前会被封装成一个ChannelHandlerContext
节点类加入到一个双向链表结构中。除了头尾两个特殊的ChannelHandlerContext
实现类,我们自定义加入的ChannelHandler
最终都会被封装成一个DefaultChannelHandlerContext
类。
-
当有读事件被触发时,
ChannelHandler
(会筛选类型为ChannelInboundHandler
的Handler) 的 触发顺序是HeaderContext
->TailContext
-
当有写事件被触发时,
ChannelHandler
(会筛选类型为ChannelOutboundHandler
的Handler) 的 触发顺序与读事件相反是TailContext
->HeaderContext
可以看到,nio 工人和 非 nio 工人也分别绑定了 channel(LoggingHandler 由 nio 工人执行,而自己的 handler 由非 nio 工人执行)
-
-
-
【问】ChannelPipeline中的事件是什么?(事件可以理解成一次IO操作,比如数据库查询、网络通信等;该函数可通过Promise对象完成回调)
Note:
-
自定义事件类 -
GetPlayerInfoEvent
如下,可用于标识相同类型的I/O事件操作,比如在getPlayerName()
和getPlayerLevel()
时会触发相同的事件标识,这时监听该事件标识的线程会对监听到的结果进行处理(如上图中不同通道中的处理器节点可以用相同EventLoop
事件线程来执行):public class GetPlayerInfoEvent { private Long playerId; public GetPlayerInfoEvent(Long playerId) { super(); this.playerId = playerId; } public Long getPlayerId() { return playerId; } }
-
在不基于注解下,将事件发送到
channelPipeline
,核心方法如下:@Override public void userEventTriggered(AbstractGameChannelHandlerContext ctx, Object evt, Promise<Object> promise) throws Exception { if (evt instanceof IdleStateEvent) { logger.debug("收到空闲事件:{}", evt.getClass().getName()); ctx.close(); } else if (evt instanceof GetPlayerInfoEvent) { GetPlayerByIdMsgResponse response = new GetPlayerByIdMsgResponse(); response.getBodyObj().setPlayerId(this.player.getPlayerId()); response.getBodyObj().setNickName(this.player.getNickName()); Map<String, String> heros = new HashMap<>(); this.player.getHeros().forEach((k,v)->{ //复制处理一下,防止对象安全溢出。 heros.put(k, v); }); //response.getBodyObj().setHeros(this.player.getHeros());不要使用这种方式,它会把这个map传递到其它线程 response.getBodyObj().setHeros(heros); promise.setSuccess(response); } UserEventContext<PlayerManager> utx = new UserEventContext<>(playerManager, ctx); dispatchUserEventService.callMethod(utx, evt, promise); }
其中:
UserEventContext
是对AbstractGameChannelHandlerContext
进一步的封装AbstractGameChannelHandlerContext
是一个自定义的双向链表节点(包含pre
,next
指针),用DefaultGameChannelHandlerContext
来实现,其中每个节点封装着事件处理器ChannelHandler
;- 将链表节点
DefaultGameChannelHandlerContext
添加到GameChannelPipeline
中,得到双向链表,不同处理方向代表不同操作(读/写)。 - 依次为
GameChannelPipeline
中的处理器分配可执行的线程,用于事件监听和回调。 - 其中
Step1~Step4
为ChannelHandler的封装,Step5
则为ChannelHandler分配线程设置监听器
-
Step1:
UserEventContext
是AbstractGameChannelHandlerContext
的处理类:public class UserEventContext<T> { private T dataManager; private AbstractGameChannelHandlerContext ctx; public UserEventContext(T dataManager, AbstractGameChannelHandlerContext ctx) { super(); this.dataManager= dataManager; this.ctx = ctx; } public T getDataManager() { return dataManager; } public AbstractGameChannelHandlerContext getCtx() { return ctx; } }
-
Step2:
AbstractGameChannelHandlerContext
事件处理器节点的构造器public AbstractGameChannelHandlerContext(GameChannelPipeline pipeline, EventExecutor executor, String name, boolean inbound, boolean outbound) { this.name = ObjectUtil.checkNotNull(name, "name"); this.pipeline = pipeline; this.executor = executor; this.inbound = inbound; this.outbound = outbound; }
-
Step3:
DefaultGameChannelHandlerContext
是AbstractGameChannelHandlerContext
实现类,其中封装着channelHandler
public class DefaultGameChannelHandlerContext extends AbstractGameChannelHandlerContext{ private final GameChannelHandler handler; public DefaultGameChannelHandlerContext(GameChannelPipeline pipeline, EventExecutor executor, String name, GameChannelHandler channelHandler) { super(pipeline, executor, name,isInbound(channelHandler), isOutbound(channelHandler));//判断一下这个channelHandler是处理接收消息的Handler还是处理发出消息的Handler this.handler = channelHandler; } private static boolean isInbound(GameChannelHandler handler) { return handler instanceof GameChannelInboundHandler; } private static boolean isOutbound(GameChannelHandler handler) { return handler instanceof GameChannelOutboundHandler; } @Override public GameChannelHandler handler() { return this.handler; } }
-
Step4:
GameChannelPipeline
关于处理器的双向链表public class GameChannelPipeline { static final InternalLogger logger = InternalLoggerFactory.getInstance(DefaultChannelPipeline.class); private static final String HEAD_NAME = generateName0(HeadContext.class); private static final String TAIL_NAME = generateName0(TailContext.class); private final GameChannel channel; private Map<EventExecutorGroup, EventExecutor> childExecutors; //GameChannelPipeline构造器 protected GameChannelPipeline(GameChannel channel) { this.channel = ObjectUtil.checkNotNull(channel, "channel"); tail = new TailContext(this); head = new HeadContext(this); head.next = tail; tail.prev = head; } ... //生成处理器节点 private AbstractGameChannelHandlerContext newContext(GameEventExecutorGroup group, boolean singleEventExecutorPerGroup, String name, GameChannelHandler handler) { return new DefaultGameChannelHandlerContext(this, childExecutor(group, singleEventExecutorPerGroup), name, handler); } ... //将处理器节点添加到channelPipeline上 public final GameChannelPipeline addFirst(GameEventExecutorGroup group, boolean singleEventExecutorPerGroup, String name, GameChannelHandler handler) { final AbstractGameChannelHandlerContext newCtx; synchronized (this) { name = filterName(name, handler); newCtx = newContext(group, singleEventExecutorPerGroup, name, handler); addFirst0(newCtx); } return this; } }
-
Step5:为每个
channelHandler
设置监听器,GameChannelPipeline
中的childExecutor
方法如下:private EventExecutor childExecutor(GameEventExecutorGroup group, boolean singleEventExecutorPerGroup) { if (group == null) { return null; } if (!singleEventExecutorPerGroup) { return group.next(); } Map<EventExecutorGroup, EventExecutor> childExecutors = this.childExecutors; if (childExecutors == null) { // Use size of 4 as most people only use one extra EventExecutor. childExecutors = this.childExecutors = new IdentityHashMap<EventExecutorGroup, EventExecutor>(4); } // Pin one of the child executors once and remember it so that the same child executor // is used to fire events for the same channel. EventExecutor childExecutor = childExecutors.get(group); if (childExecutor == null) { childExecutor = group.next(); childExecutors.put(group, childExecutor); } return childExecutor; }
-
在基于注解下,只需要在当前事件方法上,在对象方法上标识
GetPlayerInfoEvent
事件类,对象方法getPlayerInfoEvent
会将事件发送到channelPipeline
上,在处理过程中会有专门的事件监听器进行监听:@UserEvent(GetPlayerInfoEvent.class) public void getPlayerInfoEvent(UserEventContext<PlayerManager> ctx, GetPlayerInfoEvent event, Promise<Object> promise) { GetPlayerByIdMsgResponse response = new GetPlayerByIdMsgResponse(); Player player = ctx.getDataManager().getPlayer(); response.getBodyObj().setPlayerId(player.getPlayerId()); response.getBodyObj().setNickName(player.getNickName()); Map<String, String> heros = new HashMap<>(); player.getHeros().forEach((k, v) -> { // 复制处理一下,防止对象安全溢出。 heros.put(k, v); }); // response.getBodyObj().setHeros(this.player.getHeros());不要使用这种方式,它会把这个map传递到其它线程 response.getBodyObj().setHeros(heros); promise.setSuccess(response); }
UserEventContext
作用同上,封装着ChannelHandler
,并将ChannelHandler
插入到GamePipeline
中。
-
-
【问】如何理解事件系统的流程?(事件触发 - 监听者处理事件)
Note:- 在服务启动的时候,功能模块需要注册对事件监听的接口,监听的内容包括事件和事件源(事件产生的对象)。当事件触发的时候,就会调用这些监听的接口,并把事件和事件源传到接口的参数里面,然后在监听接口里面处理收到的事件。
- 事件只能从事件发布者流向事件监听者,不可以反向传播。
17、WebSocket
参考WebSocket知识点整理,轮询/长轮询(Comet)/长连接(SSE)/WebSocket(全双工),简单的搭建Websocket(java+vue)
-
【问】什么是websocket?原理是什么?(HTML5中用到的技术,是一种
tcp
全双工通信协议,支持实时通讯),参考WebSocket 百度百科,HTML5_百度百科Note:
-
在
websocket
出现之前,web
交互一般是基于http
协议的短连接或者长连接; -
HTML5
是 HyperText Markup Language 5 的缩写,HTML5
技术结合了HTML4.01
的相关标准并革新,符合现代网络发展要求,在 2008 年正式发布。HTML5 由不同的技术构成,其在互联网中得到了非常广泛的应用,提供更多增强网络应用的标准。HTML5
在 2012 年已形成了稳定的版本。2014年10月28日,W3C
发布了HTML5的最终版。 -
HTML5
于2011年定义了WebSocket
协议(WebSocket
通信协议于2011年被IETF 6455,并由RFC7936补充规范。WebSocket
API也被W3C
定为标准),其本质上是一个基于tcp的协议,通过HTTP/1.1
协议的101
状态码进行握手,能够实现浏览器与服务器全双工通信,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。 -
websocket
是一种全新的持久化协议,不属于http
无状态协议,协议名为"ws
";
-
-
【问】socket和http的区别?(
socket
不是协议,而是一个API,是对TCP/IP
协议的封装)Note:
-
socket
并不是一个协议:-
Http
协议是简单的对象访问协议,对应于应用层。Http
协议是基于TCP
连接的,主要解决如何包装数据;TCP
协议对应于传输层,主要解决数据如何在网络中传输; -
Socket
是对TCP/IP
协议的封装,Socket
本身并不是协议,而是一个调用接口(API),通过Socket
实现TCP/IP
协议。
-
-
socket
通常情况下是长连接:-
Http
连接:http
连接就是所谓的短连接,及客户端向服务器发送一次请求,服务器端响应后连接即会断掉。 -
socket
连接:socket
连接是所谓的长连接,理论上客户端和服务端一旦建立连接,则不会主动断掉;但是由于各种环境因素可能会是连接断开,比如说:服务器端或客户端主机down
了,网络故障,或者两者之间长时间没有数据传输,网络防火墙可能会断开该链接已释放网络资源。所以当一个
socket
连接中没有数据的传输,那么为了维持连续的连接需要发送心跳消息,具体心跳消息格式是开发者自己定义的。
-
-
-
【问】websocket与http的关系?(3次握手的时候是基于
HTTP
协议,传输时基于TCP
信道,不需要HTTP
协议)Note:
-
相同点:
-
都是基于
tcp
的,都是可靠性传输协议; -
都是应用层协议;
-
-
不同点:
-
WebSocket
是双向通信协议,模拟Socket
协议,可以双向发送或接受信息;而
HTTP
是单向的; -
WebSocket
是需要浏览器和服务器握手进行建立连接的而
http
是浏览器发起向服务器的连接,服务器预先并不知道这个连接
-
-
联系:
WebSocket
在建立握手时,数据是通过HTTP
传输的。但是建立之后,在真正传输时候是不需要HTTP
协议的; -
总结(总体过程):
-
首先,客户端发起
http
请求,经过3次握手后,建立起TCP
连接;http
请求里存放WebSocket
支持的版本号等信息,如:Upgrade
、Connection
、WebSocket-Version
等; -
然后,服务器收到客户端的握手请求后,同样采用
HTTP
协议回馈数据; -
最后,客户端收到连接成功的消息后,开始借助于TCP传输信道进行全双工通信。
-
-
-
【问】websocket和webrtc技术的联系与区别?(webrtc在视频流传输时用到了websocket协议),参考WebRTC_百度百科
Note:
-
相同点:
-
都是基于
socket
编程实现的,是用于前后端实时通信的的技术;都是基于浏览器的协议; -
原理都是在于数据流传输至服务器,然后服务器进行分发,这两个连接都是长链接;
-
这两个协议在使用时对服务器压力比较大,因为只有在对方关闭浏览器或者服务器主动关闭的时候才会关闭
websocket
或webrtc
;
-
-
不同点:
-
websocket
保证双方可以实时的互相发送数据,具体发啥自己定 -
webrtc
则主要从浏览器获取摄像头(网页考试,刷题系统 一般基于这个技术) 一般webrtc
技术(音视频采集,编解码,网络传输和渲染,音视频同步等),是一个关于摄像头的协议,在网络传输上要配合websocket
技术才能使用,毕竟光获取了个摄像头也没啥用啊,得往服务器发。
-
-
-
【问】http存在什么问题?即时通讯包括哪些连接维持的方法?(http存在问题:无状态协议,解析请求头header耗时(比如包含身份认证信息),单向消息发送),参考轮询、长轮询(comet)、长连接(SSE)、WebSocket
Note:
-
http
存在的问题:-
http
是一种无状态协议,每当一次会话完成后,服务端都不知道下一次的客户端是谁,需要每次知道对方是谁,才进行相应的响应,因此本身对于实时通讯就是一种极大的障碍; -
http
协议采用一次请求,一次响应,每次请求和响应就携带有大量的header
头,对于实时通讯来说,解析请求头也是需要一定的时间,因此,效率也更低下 -
最重要的是,
http
协议需要客户端主动发,服务端被动发,也就是一次请求,一次响应,不能实现主动发送。
-
-
实现即时通讯常见的有四种方式,分别是:轮询、长轮询(comet)、长连接(SSE)、WebSocket。
-
轮询(客户端在时间间隔内发起请求,客户端接收到数据后关闭连接):
很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由客户端浏览器对服务器发出
HTTP
请求,然后由服务器返回最新的数据给客户端的浏览器。-
优点:后端编码比较简单
-
缺点:这种传统的模式带来很明显的缺点,即客户端的浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
-
-
长轮询(客户端发起一个请求,服务器端维持连接,客户端接收到数据后关闭连接):
客户端向发起一个到服务端的请求,然后服务端一直保持连接打开,直到数据发送到客户端为止。
-
优点:避免了服务端在没有信息更新时的频繁请求,节省流量
-
缺点:服务器一直保持连接会消耗资源,需要同时维护多个线程,而服务器所能承载的 TCP 连接是有上限的,所以这种轮询很容易导致连接上限。
-
-
长连接(通过通道来维持连接,客户端可以断开连接,但服务器端不可以)
客户端和服务端建立连接后不进行断开,之后客户端再次访问这个服务端上的内容时,继续使用这一条连接通道
-
优点:消息即时到达,不发无用请求
-
缺点:与长轮询一样,服务器一直保持连接是会消耗资源的,如果有大量的长连接的话,对于服务器的消耗是巨大的,而且服务器承受能力是有上限的,不可能维持无限个长连接。
-
-
WebSocket(支持双向实时通信,客户端和服务器端一方断开连接,则连接中断)
客户端向服务器发送一个携带特殊信息的请求头(
Upgrade:WebSocket
)建立连接,建立连接后双方即可实现自由的实时双向通信。优点:
- 较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。
- 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于
HTTP
请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet
等类似的长轮询比较,其也能在短时间内更多次地传递数据。 - 保持连接状态。与
HTTP
不同的是,Websocket
需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP
请求可能需要在每个请求都携带状态信息 (如身份认证等)。
缺点:相对来说,开发成本和难度更高
-
轮询、长轮询、长连接和
WebSocket
的总结比较:轮询(Polling) 长轮询(Long-Polling) WebSocket 长连接(SSE) 通信协议 http http tcp http 触发方式 client(客户端) client(客户端) client、server(客户端、服务端) client、server(客户端、服务端) 优点 兼容性好容错性强,实现简单 比短轮询节约资源 全双工通讯协议,性能开销小、安全性高,可扩展性强 实现简便,开发成本低 缺点 安全性差,占较多的内存资源与请求数 安全性差,占较多的内存资源与请求数 传输数据需要进行二次解析,增加开发成本及难度 只适用高级浏览器 延迟 非实时,延迟取决于请求间隔 非实时,延迟取决于请求间隔 实时 非实时,默认3秒延迟,延迟可自定义
-
-