Vert.x学习(三)—— WebSocket、数据共享、流式传输、Record Parser

WebSocket

  • 定义:在 HTTP 服务端和 HTTP 客户端(通常是浏览器)之间实现全双工 Socket 连接。

  • 服务端启动WebSocket:

    //配置webSocket处理器
    server.webSocketHandler(webSocket -> {});
    //配置握手
    server.webSocketHandshakeHandler(handshake -> {
        if (handshake.path().equals("/myapi")) {
            //拒绝握手
            handshake.reject();
        }else{
            //接受连接,并结束握手
            Future<ServerWebSocket> accept = handshake.accept();
        }
    });
    
  • 客户端启动WebSocketClient:

    WebSocketClientOptions options = new WebSocketClientOptions()
            .setDefaultHost("localhost")
            .setDefaultPort(8080);
    WebSocketClient webSocketClient = vertx.createWebSocketClient();
    webSocketClient.connect("/ws", res -> {
        if(res.succeeded()){
            WebSocket webSocket = res.result();
        }
        else{
            log.error(res.cause());
        }
    });
    
  • 写入二进制帧

    多帧写入

    //二进制帧
    WebSocketFrame frame1 = WebSocketFrame.binaryFrame(buffer1, false);
    webSocket.writeFrame(frame1);
    //continuation frame:发送剩余的二进制帧
    WebSocketFrame frame2 = WebSocketFrame.continuationFrame(buffer2, false);
    webSocket.writeFrame(frame2);
    // 写最终帧,标记为final
    WebSocketFrame frame3 = WebSocketFrame.continuationFrame(buffer2, true);
    webSocket.writeFrame(frame3);
    

    单帧写入:webSocket.writeFinalBinaryFrame(buff);

  • 读取二进制帧:webSocket.frameHandler(frame -> {});

  • 处理完记得使用close方法关闭连接

  • 管道传输

    FileSystem fs = vertx.fileSystem();
    AsyncFile asyncFile = fs.openBlocking("/file.txt", new OpenOptions());
    asyncFile.pipeTo(webSocket);
    

数据共享

  • 定义:在应用程序的不同部分之间、同一 Vert.x 实例中的不同应用程序之间、Vert.x 集群中的不同实例之间 安全地共享数据

  • Local maps:

    • 允许在同一Vertx实例中的不同Event Loop之间共享

    • 可作为键值和值的类型:不可变(String,int)和实现Shareable接口的类型(Buffer,JSON对象或自定义实现);确保在Vertx在不同Event Loop线程不共享可变的数据

      //获取共享数据
      SharedData sharedData = vertx.sharedData();
      //获取名称自定义的LocalMap
      LocalMap<String, String> map1 = sharedData.getLocalMap("mymap1");
      //存放数据
      map1.put("foo", "bar");
      LocalMap<String, Buffer> map2 = sharedData.getLocalMap("mymap2");
      //Buffer将会在添加到Map之前拷贝,因为不是不可变的数据类型
      map2.put("eek", Buffer.buffer().appendInt(123));
      
      // 之后,在其他应用中获取该LocalMap
      map1 = sharedData.getLocalMap("mymap1");
      String val = map1.get("foo");
      map2 = sharedData.getLocalMap("mymap2");
      Buffer buff = map2.get("eek");
      System.out.println(val);
      
  • Async Map:

    • 允许在集群中的多个vertx节点中共享

    • 可作为键值和值的类型:不可变(String,int)、实现 ClusterSerializable 接口的类型(Buffer、json对象)、实现 java.io.Serializable 接口的类型

      SharedData sharedData = vertx.sharedData();
      //获取与其他节点的共享数据
      sharedData.<String, String>getAsyncMap("mymap", res -> {
        if (res.succeeded()) {
          AsyncMap<String, String> map = res.result();
        } else {
          // 发生错误
        }
      });
      //如果应用不需要和其它vertx节点共享数据,可以只获取一个本地的 map
      sharedData.<String, String>getLocalAsyncMap(...)
      
  • 异步锁:

    • 定义:异步锁 允许在集群中获取独占锁。 适用于:同一时刻仅在一个节点上执行某些操作或访问某个资源。

      SharedData sharedData = vertx.sharedData();
      //获取锁时得到通知
      sharedData.getLock("mylock", res -> {
        if (res.succeeded()) {
          // 获得锁
          Lock lock = res.result();
      
          // 5秒后我们释放该锁
          vertx.setTimer(5000, tid -> lock.release());
      
        } else {
          // 发生错误
        }
      });
      //如果应用不需要和其它vertx节点共享数据,可以只获取一个本地的异步锁
      sharedData.getLocalLock()
      
  • 异步的原子计数器:

    SharedData sharedData = vertx.sharedData();
    
    sharedData.getCounter("mycounter", res -> {
      if (res.succeeded()) {
        Counter counter = res.result();
      } else {
        // 发生错误
      }
    });
    //如果应用不需要和其它vertx节点共享数据,可以只获取一个本地的原子计数器
    sharedData.getLocalCounter(...)
    

Vertx访问文件系统(FileSystem)

UDP

  • UDP以数据报为单位进行传输,通常情况不能发送大于MTU的数据报(数据链路层中的单次传输数据的最大尺寸)

  • 发送数据报

    //创建 DatagramSocke
    DatagramSocket socket = vertx.createDatagramSocket(new DatagramSocketOptions());
    Buffer buffer = Buffer.buffer("content");
    //发送数据报
    socket.send(buffer, 1234, "127.0.0.1", asyncResult -> {
      System.out.println("Send succeeded? " + asyncResult.succeeded());
    });
    
  • 接收数据报

    DatagramSocket socket = vertx.createDatagramSocket(new DatagramSocketOptions());
    socket.listen(1234, "0.0.0.0", asyncResult -> {
      if (asyncResult.succeeded()) {
        socket.handler(packet -> {
          // 对包进行处理
        });
      } 
    });
    
  • 发送多播数据报:通过加入到同一个可发送数据包的多播组,允许多个Socket接收相同的数据包。

    Buffer buffer = Buffer.buffer("content");
    // 发送Buffer到多播地址
    socket.send(buffer, 1234, "230.0.0.1", asyncResult -> {});
    
  • 接收多播数据报

    DatagramSocket socket = vertx.createDatagramSocket(new DatagramSocketOptions());
    socket.listen(1234, "0.0.0.0", asyncResult -> {
        if (asyncResult.succeeded()) {
          socket.handler(packet -> {
            // 对数据包进行处理
          });
    
          // 加入多播组
          socket.listenMulticastGroup("230.0.0.1", asyncResult2 -> {
              if (asyncResult2.succeeded()) {
                // 现在将接收组的数据包
    
                // 接收完后可取消监听
                socket.unlistenMulticastGroup("230.0.0.1", asyncResult3 -> {});
              } 
          });
        }
    });
    

  • 作用:在 Vert.x 中,异步的写调用是立即返回的,而写操作实际是在内部队列中排队写入。若写入对象的速度比实际写入底层数据资源速度快, 那么写入队列就会无限增长, 最终导致内存耗尽。所以vertx提供了控制流式传输的接口(类比管道):ReadStream(输入流)、WriteStream(输出流)。

  • 流式传输的过程:

    • 节点 A (数据源/生产者):节点 A 从某个地方获取数据(读取本地文件、接收网络数据等)。然后将这些数据写入到它的输出流 (Output Stream)。这个输出流代表了数据离开节点 A 的通道。

    • 传输通道:节点 A 的输出流通过某种机制连接到节点 B 的输入流。这可以是一个网络连接(如 TCP/IP socket)、一个操作系统管道 (pipe)、一个内存缓冲等。

    • 节点 B (数据接收端/消费者):节点 B 从它的输入流 (Input Stream)读取数据。这个输入流代表了数据进入节点 B 的通道。

  • 代码:

    方式一

    NetServer server = vertx.createNetServer(
        new NetServerOptions().setPort(1234).setHost("localhost")
    );
    server.connectHandler(sock -> {
      sock.handler(buffer -> {
        //传输数据
        sock.write(buffer);
        //如果 WriteStream 已满
        if (sock.writeQueueFull()) {
          //暂停读取
          sock.pause();
          //当写队列准备接收更多的数据时,drainHandler事件处理器将被调用
          sock.drainHandler(done -> {
            //恢复NetSocket 的状态
            sock.resume();
          });
        }
      });
    }).listen();
    

    方式二:

    server.connectHandler(sock -> {
      sock.pipeTo(sock);
    }).listen();
    

    方式三:

    server.connectHandler(sock -> {
    
      // 创建异步操作管道
      Pipe<Buffer> pipe = sock.pipe();
    
      // 打开目标文件
      fs.open("/path/to/file", new OpenOptions(), ar -> {
        if (ar.succeeded()) {
          AsyncFile file = ar.result();
    
          // 用管道传输socket当中的信息到文件中,并最终关闭文件
          pipe.to(file);
        } else {
          //关闭管道
          sock.close();
        }
      });
    }).listen();
    
  • ReadStream

    ReadStream(可读流) 接口的实现类包括: HttpClientResponseDatagramSocketHttpClientRequestHttpServerFileUploadHttpServerRequestMessageConsumerNetSocketWebSocketTimeoutStreamAsyncFile

    • handler: 设置一个处理器,从 ReadStream 读取对象
    • pause: 暂停处理器,暂停时,处理器中将不会收到任何对象
    • fetch: 指定从 ReadStream 中抓取多大字节的数据,即在下一次handle中会收到对应字节数的数据。
    • resume: 恢复处理器,若任何对象到达目的地则handler将被触发;等价于 fetch(Long.MAX_VALUE)
    • exceptionHandler: ReadStream发生异常时被调用
    • endHandler: 当数据读取完毕时被调用。

    ReadStreamflowingfetch 两个模式:

    • 最初 ReadStreamflowing 模式
    • ReadStream 处于 flowing 模式, ReadStream 中的元素被传输到handler
    • ReadStream 处于 fetch 模式,只会将指定数量的元素传输到handler

    pause, resumefetch 会改变 ReadStream 的模式

    • resume() 设置 ReadStreamflowing 模式
    • pause() 设置 ReadStreamfetch 模式 并设置demand值为0
    • fetch(long) 请求指定数量的 ReadStream 元素并将该数量加到目前的demand值当中
  • WriteStream

    WriteStream(可写流)接口的实现类包括:HttpClientRequestHttpServerResponse WebSocketNetSocketAsyncFile

    • write: 往WriteStream写入一个对象,该方法将永远不会阻塞, 内部是排队写入。
    • setWriteQueueMaxSize: 设置写入队列容量—— writeQueueFull 在队列 写满 时返回 true
    • writeQueueFull: 若写队列被认为已满,则返回 true
    • exceptionHandlerWriteStream 发生异常时调用。
    • drainHandler: 判定 WriteStream 有剩余空间时调用。

Record Parser

  • 定义:一个自由分割、读取指定数据大小的工具

  • 每次读取分割符之前的数据

    //初始化分隔符
    RecordParser parser = RecordParser.newDelimited("\n", h -> {
      //总共返回两次
      //第一次返回HELLO、第二次返回HOW ARE YOU
      System.out.println(h.toString());
    });
    //手动指定分隔符
    parser.delimitedMode();
    
    parser.handle(Buffer.buffer("HELLO\nHOW ARE Y"));
    parser.handle(Buffer.buffer("OU?\nI AM"));
    
  • 每次读取固定长度的数据

    //初始化长度
    RecordParser parser = RecordParser.newFixed(4, h -> {
      System.out.println(h.toString());
    });
    //手动设置长度
    parser.fixedSizeMode();
    

Json解析器

  • 定义:非阻塞解析器,用来处理体积非常大的JSON

  • 代码:

    JsonParser parser = JsonParser.newParser();
    
    parser.handler(event -> {
        switch (event.type()) {
            case START_ARRAY:
                log.info("[");
                break;
            case START_OBJECT:
                log.info("{");
                // 切换到 value-mode: 后续 VALUE 事件中直接返回字段的完整嵌套对象/数组值 (JsonObject/JsonArray),
                // 并跳过这些嵌套结构内部的 START/END/VALUE 事件。适用于直接提取嵌套体。
                parser.objectValueMode();
                break;
            //每处理一个字段,都会触发该事件
            case VALUE:
                //直接提前整个JSON对象
                if(event.fieldName().equals("address")){
                    Address address = event.mapTo(Address.class);
                    log.info(address);
                }
                log.info(event.fieldName() + ":"+ event.value());
                break;
            case END_OBJECT:
                log.info("}");
                // 设置为 event-mode,所以解析器重新触发 start-end 事件,如果在之前的事件中设置过objectValueMode,则必须调用
                parser.objectEventMode();
                break;
            case END_ARRAY:
                log.info("]");
                break;
        }
    });
    Buffer jsonBuffer = Buffer.buffer(
            "[" +
                "{" +
                    "\"firstName\": \"Bob\"," +
        			//嵌套对象Address
                    "\"address\": {" + 	
                        "\"city\": \"Paris\"," +
                        "\"country\": \"France\"" +
                        "}" +
                "}," +
                "{" +
                    "\"firstName\": \"Luke\"," +
                    "\"address\": {" +
                        "\"city\": \"Daisy Town\"," +
                        "\"country\": \"USA\"" +
                    	"}" +
                "}" +
            "]"
    );
    
    parser.handle(jsonBuffer);
    parser.end();
    

运行阻塞式代码

  • 为避免在Event Loop 中直接调用阻塞式操作,通过以下几种方式进行调用

  • executeBlocking

    //返回Future对象
    vertx.executeBlocking(() -> {
        System.out.println("执行阻塞任务");
        return null;
    },false) //如果 executeBlocking 在同一个上下文环境中(如:同一个 Verticle 实例)被调用了多次, 那么这些不同的 executeBlocking 代码块会顺序执行,此时不关心执行顺序,可以设置为false,这时会并行处理
    
  • Worker Executor

    WorkerExecutor executor = vertx.createSharedWorkerExecutor("my-worker-pool");
    executor.executeBlocking(()-> {
        System.out.println("执行阻塞任务");
        return null;
    });
    //Worker Executor 在不需要的时候必须被关闭:
    executor.close();
    

集群管理器

  • 功能:

    • 集群中 Vert.x 节点的发现和分组
    • 维护集群范围中的主题订阅者列表(我们可知道哪些节点对哪个Event Bus地址感兴趣)
    • 分布式Map的支持
    • 分布式锁
    • 分布式计数器
  • 使用:Vert.x默认使用Hazelcast集群管理器, 但它可以简单被替换成其他实现类。 Vert.x在运行时使用Java的服务加载器ServiceLoader功能在类路径中查找 ClusterManager 的实例,从而定位集群管理器。所以在 Maven/Gradle 项目中,只需引入集群管理器的依赖即可。或者以编程的方式:

    Vertx vertx = Vertx.builder().withClusterManager(...).build()

日志记录

  • 由于异步的Event Loop线程无法输出堆栈信息(System.out)和debug调试,所以应经常用异常处理器和vertx提供的日志工具来打印信息

    import io.vertx.core.impl.logging.Logger;
    import io.vertx.core.impl.logging.LoggerFactory;
    

你可能感兴趣的:(框架,学习,websocket,java,网络,后端)