Java进阶3 - 易错知识点整理(待更新)

Java进阶3 - 易错知识点整理(待更新)

该章节是Java进阶2- 易错知识点整理的续篇
在前一章节中介绍了 ORM框架,中间件相关的面试题,而在该章节中主要记录关于项目部署中间件,网络性能优化的常见面试题。

15、Docker

参考Docker常用命令(以Anaconda为例搭建环境)

16、Netty(核心:channelPipeline双向链表(责任链),链表每个节点使用promisewait/notify(事件监听者))

参考黑马Netty笔记尚硅谷Netty笔记【硬核】肝了一月的Netty知识点【阅读笔记】Java游戏服务器架构实战,代码参考:kebukeYi / book-code

  • 【问】同步和异步的区别?阻塞和非阻塞IO的区别?(阻塞强调的是状态,而同步强调的是过程)

    Note

    • 基本概念

      • 阻塞:等待缓冲区中的数据准备好过后才处理其他的事情,否则一直等待在那里。

      • 非阻塞:当我们的进程访问我们的数据缓冲区的时候,如果数据没有准备好则直接返回,不会等待。如果数据已经准备好,也直接返回。

      • 同步:当一个进程/线程在执行某个请求的时候,如果该请求需要一段时间才能返回信息,那么这个进程/线程会一直等待下去,直到收到返回信息才继续执行下去。

      • 异步:进程不需要一直等待某个请求的处理结果,而是继续执行下面的操作,当这个请求处理完毕之后,可以通过回调函数通知该进程进行处理

      阻塞和同步(非阻塞和异步)描述相同,但强调内容不同:阻塞强调的是状态,而同步强调的是过程

    • 阻塞IO 和 非阻塞IO:(BIO vs NIO)

      • BIO(Blocking IO):

        • 传统的 java.io 包,它基于流模型实现,提供了我们最熟知的一些 IO 功能,比如File抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。

        • Java网络通信中,SocketServerSocket套接字是基于阻塞模式实现的。

          在这里插入图片描述

      • NIO(Non-Blocking IO):

        • Java 1.4中引入了对应 java.nio 包,提供了 Channel , SelectorBuffer 等抽象,它支持面向缓冲的,基于通道的 I/O 操作方法。

        • NIO 提供了与传统 BIO 模型中的 SocketServerSocket 相对应的 SocketChannelServerSocketChannel 两种不同的套接字通道实现,支持阻塞和非阻塞两种模式。对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

          在这里插入图片描述

      • BIONIO的比较:

        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),用于快速开发可维护、高性能的网络服务器和客户端NettyJava 网络应用框架中的地位就好比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进行再次封装,得到NioSocketChannelNioServerSocketChannel

      • 管道(ChannelPipeline) :管道是以一组编码器为结点的链表,用于处理客户端请求,也是真正处理业务逻辑的地方。

      • 处理器(ChannelHandler) :处理器,是管道的一个结点,一个客户端请求通常由管道里的所有处理器(handler)逐一的处理。

      • 事件KEY(selectionKey) :当通道(channel)产生事件的时候,Selector就会生成一个selectionKey事件,并唤醒事件线程去处理事件

      • 缓冲(Buffer) :NIO是面向块的IO,从通道读取数据之后会放进缓存(Buffer),向通道写数据的时候也需要先写进缓存(Buffer),总之既不能直接从通道读数据,也不能直接向通道写数据。

      • 缓冲池(BufferPool) :这是Netty针对内存的一种优化手段,通过一种池化技术去管理固定大小的内存。(当线程需要存放数据的时候,可以直接从缓冲池中获取内存,不需要的时候再放回去,这样不需要去频繁的重新去申请内存,因为申请内存是需要时间的,影响性能)

      • ServerBootstrapBootstrapBootstrapServerBootstrap 被称为引导类,指对应用程序进行配置,并使他运行起来的过程。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专门负责网络的读写

        BossGroupWorkerGroup 类型都是 NioEventLoopGroup

      • NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环,每一个事件循环是 NioEventLoop

        • NioEventLoop 表示一个不断循环的执行处理任务的线程(selector监听绑定事件是否发生),每个 NioEventLoop 都有一个 Selector,用于监听绑定在其上的 socket 的网络通讯,比如NioServerSocketChannel绑定在服务器bossgroupNioEventLoopselector上,NioSocketChannel绑定在客户端NioEventLoopselector上,然后各自的selector不断循环监听相关事件

        • NioEventLoopGroup 可以有多个线程,即可以含有多个NioEventLoop

      • 每个 BossGroup下面的NioEventLoop 循环执行的步骤有 3 步

        1. 轮询 accept 事件

        2. 处理 accept 事件,与 client建立连接,生成 NioScocketChannel,并将其注册到某个 workerGroup NIOEventLoop 上的 Selector

        3. 继续处理任务队列的任务,即 runAllTasks

      • 每个 WorkerGroup下面的NIOEventLoop循环执行的步骤

        1. 轮询 readwrite 事件

        2. 处理 I/O 事件,即 read,write 事件,在对应 NioScocketChannel 处理。

        3. 处理任务队列的任务,即 runAllTasks

      • 每个 Worker下面的NIOEventLoop 处理业务时,会使用 pipeline(管道),pipeline 中包含了 channel(通道),即通过 pipeline 可以获取到对应通道,管道中维护了很多的处理器

      • NioEventLoop 内部采用串行化设计,从消息的 读取->解码->处理->编码->发送,始终由 IO 线程 NioEventLoop 负责

      • NioEventLoopGroup 下包含多个 NioEventLoop
        每个 NioEventLoop 中包含有一个 Selector,一个 taskQueue
        每个 NioEventLoopSelector 上可以注册监听多个 NioChannel
        每个 NioChannel 只会绑定在唯一的 NioEventLoop
        每个 NioChannel 都绑定有一个自己的 ChannelPipeline
        NioChannel可以获取对应的ChannelPipelineChannelPipeline也可以获取对应的NioChannel

    • 客户端服务器分析流程如下:

      1. Server启动,NettyParentGroupBossGroup)中选出一个NioEventLoop对指定port进行监听

      2. Client启动,NettyParentGroupBossGroup)中选出个NioEventLoop连接Server

      3. Client连接Serverport创建Channel

      4. NettyChildGroupWorkGroup)中选出一个NioEventLoopchannel绑定,用于处理该Channel中所有的操作

      5. Client通过ChannelServer发送数据包。

      6. Pipeline中的处理器采用责任链的模式Channel中的数据包进行处理

      7. Server 如需向Client发送数据。则需将数据经pipeline中的处理器处理行
        ByteBuf数据包进行传输

      8. Server将数据包通过channel发送给Client

      9. Pipeline中的处理器采用责任链的模式对channel中的数据包进行处理

      在这里插入图片描述

  • 【问】如何利用Netty简单实现多个客户端和服务端的通信?

    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.18080)。

        这里的端口是客户端访问服务器端的唯一入口,当多个客户端在同一时间向服务器端发送大量请求,如果服务器端对每个客户端的请求进行一一接收,则会出现阻塞等待问题

      • 为了解决阻塞问题,客户端的不同请求通过不同的channel(即文件缓存)以文件形式保存在服务器端监听的端口中,因此这里服务器端专门开启一个线程来监听这个端口,该线程与selector绑定,让selector完成事件处理工作。

      • channel传输完毕之后(文件缓存满了,写入到文件), selector会通过channelPipeline中自定义的channelHandler对数据进行处理。

  • 【问】juc.Future、回调函数、netty的promise之间有什么区别?(Promise结合了Future和回调函数的优点实现异步;Promise接口使用了观察者模式,即通过addListener来设置监听者,监听器通过promise.await阻塞,处理线程通过promise.notify唤醒监听者)

    Note

    这里分别使用juc.FutureConsumer函数式接口,和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接口,并在此基础上派生出子接口promisepromise可以通过设置监听器来监听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可用来干什么?两者有什么区别?(ChannelFuturePromise一样,都继承于netty的Future,可用于异步处理结果的返回),参考Netty异步回调模式-Future和Promise剖析

    Note

    • NettyFuture继承JDK的Future,通过 Object 的 wait/notify机制,实现了线程间的同步;使用观察者设计模式,实现了异步非阻塞回调处理。其中:

      • ChannelFuturePromise都是NettyFuture的子接口;

      • ChannelFutureChannel绑定,用于异步处理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继承了ChannelFuturePromise是可写的ChannelFuture接口

    • ChannelFuture接口: NettyI/O操作都是异步的,例如bindconnectwrite等操作,会返回一个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框架如下:

    • 上面我们已经知道 ChannelPipelineChannelHandler的关系ChannelPipeline是一个存放各种ChannelHandler的管道容器。ChannelPipeline的执行流程如下(ChannelHandler也分为了两大类:ChannelInboundHandler是用于负责处理链路读事件的HandlerChannelOutboundHandler是用于负责处理链路写事件的Handler):

      1. NioEventLoop 触发读事件,会调用SocketChannel所关联的ChannelPipline
      2. 由上一步读取到的消息会在ChannelPipline中依次被多个ChannelInboundHandler处理。
      3. 处理完消息会调用ChannelHandlerContextwrite方法发送消息,此时触发写事件,发送的消息同样也会经过ChannelPipline中的多个ChannelOutboundHandler处理。
    • 一个channel绑定一个channelPipeline,可以通过channel获取channelPipeline进而添加channelHandlerchannelPipeline初始化代码如下:

      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();
      
    • ChannelHandlerChannelPipline中的结构:

      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是一个自定义的双向链表节点(包含prenext指针),用DefaultGameChannelHandlerContext来实现,其中每个节点封装着事件处理器ChannelHandler
      • 将链表节点DefaultGameChannelHandlerContext添加到GameChannelPipeline中,得到双向链表,不同处理方向代表不同操作(读/写)。
      • 依次为GameChannelPipeline中的处理器分配可执行的线程,用于事件监听和回调。
      • 其中Step1~Step4为ChannelHandler的封装,Step5则为ChannelHandler分配线程设置监听器
      • Step1UserEventContextAbstractGameChannelHandlerContext 的处理类:

        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;
            }
        
        }
        
        
      • Step2AbstractGameChannelHandlerContext事件处理器节点的构造器

        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;
        }
        
      • Step3DefaultGameChannelHandlerContextAbstractGameChannelHandlerContext实现类,其中封装着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;
            }
        
        }
        
      • Step4GameChannelPipeline关于处理器的双向链表

          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协议的短连接或者长连接

    • HTML5HyperText 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支持的版本号等信息,如:UpgradeConnectionWebSocket-Version等;

      • 然后,服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据;

      • 最后,客户端收到连接成功的消息后,开始借助于TCP传输信道进行全双工通信

  • 【问】websocket和webrtc技术的联系与区别?(webrtc在视频流传输时用到了websocket协议),参考WebRTC_百度百科

    Note

    • 相同点

      • 都是基于socket编程实现的,是用于前后端实时通信的的技术;都是基于浏览器的协议;

      • 原理都是在于数据流传输至服务器,然后服务器进行分发,这两个连接都是长链接

      • 这两个协议在使用时对服务器压力比较大,因为只有在对方关闭浏览器或者服务器主动关闭的时候才会关闭websocketwebrtc

    • 不同点

      • 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秒延迟,延迟可自定义

猜你喜欢

转载自blog.csdn.net/qq_33934427/article/details/127747693