使用Undertow WebSockets有效发送大型数据集

时间:2019-11-06 09:13:09

标签: java websocket undertow

我有一个很大的ConcurrentHashMap(cache.getCache()),其中存放了所有数据(大约500+ MB大小,但是随着时间的推移会增加)。客户端可以通过使用纯java HttpServer实现的API来访问此文件。 这是简化的代码:

JsonWriter jsonWriter = new JsonWriter(new OutputStreamWriter(new BufferedOutputStream(new GZIPOutputStream(exchange.getResponseBody())))));
new GsonBuilder().create().toJson(cache.getCache(), CacheContainer.class, jsonWriter);

还有一些客户端发送的过滤器,这样它们实际上并不会每次都获取所有数据,但是HashMap会不断更新,因此客户端必须经常刷新以获取最新数据。这效率低下,所以我决定使用WebSockets将数据更新实时推送到客户端。

我之所以选择Undertow,是因为我可以简单地从Maven导入它,而无需在服务器上进行任何额外的配置。

在WS connect上,我将通道添加到HashSet并发送整个数据集(客户端在获取初始数据之前先发送带有一些过滤器的消息,但我从示例中删除了这部分):

public class MyConnectionCallback implements WebSocketConnectionCallback {
  CacheContainer cache;
  Set<WebSocketChannel> clients = new HashSet<>();
  BlockingQueue<String> queue = new LinkedBlockingQueue<>();

  public MyConnectionCallback(CacheContainer cache) {
    this.cache = cache;
    Thread pusherThread = new Thread(() -> {
      while (true) {
        push(queue.take());
      }
    });
    pusherThread.start();
  }

  public void onConnect(WebSocketHttpExchange webSocketHttpExchange, WebSocketChannel webSocketChannel) {
    webSocketChannel.getReceiveSetter().set(new AbstractReceiveListener() {
      protected void onFullTextMessage(WebSocketChannel channel, BufferedTextMessage message) {
        clients.add(webSocketChannel);
        WebSockets.sendText(gson.toJson(cache.getCache()), webSocketChannel, null);
      }
    }
  }

  private void push(String message) {
    Set<WebSocketChannel> closed = new HashSet<>();
    clients.forEach((webSocketChannel) -> {
        if (webSocketChannel.isOpen()) {
            WebSockets.sendText(message, webSocketChannel, null);
        } else {
            closed.add(webSocketChannel);
        }
    }
    closed.foreach(clients::remove);
  }

  public void putMessage(String message) {
    queue.put(message);
  }
}

每次对缓存进行更改后,我都会获取新值并将其放入队列中(我不直接序列化myUpdate对象,因为updateCache方法中还包含其他逻辑)。只有一个线程负责更新缓存:

cache.updateCache(key, myUpdate);
Map<Key,Value> tempMap = new HashMap<>();
tempMap.put(key, cache.getValue(key));
webSocketServer.putMessage(gson.toJson(tempMap));

这种方法遇到的问题:

  1. 在初始连接时,整个数据集都会转换为String,我担心太多的请求会导致服务器变为OOM。 WebSockets.sendText仅接受String和ByteBuffer
  2. 如果我先将频道添加到客户端集中,然后再发送数据,则在发送初始数据之前,可能会向客户端推送请求,并且客户端将处于无效状态
  3. 如果我先发送初始数据,然后将通道添加到客户端集,则发送初始数据期间出现的推送消息将丢失,并且客户端将处于无效状态

我针对问题2和问题3提出的解决方案是将消息放入队列(我将Set<WebSocketChannel>转换为Map<WebSocketChannel,Queue<String>>并仅在客户收到了初始数据集,但我在这里欢迎其他任何建议。

对于问题1,我的问题是通过WebSocket发送初始数据的最有效方法是什么?例如,使用JsonWriter直接写到WebSocket之类的东西。

我意识到客户端可以使用API​​进行初始调用并订阅WebSocket进行更改,但是这种方法使客户端负责拥有正确的状态(它们需要订阅WS,将WS消息排队,获取初始数据使用API​​,然后在获取初始数据后将排队的WS消息应用于其数据集),由于数据敏感,我不想将控制权交给他们。

2 个答案:

答案 0 :(得分:1)

似乎#2和#3的问题与不同线程能够同时将数据状态发送给客户端有关。因此,除了您的方法之外,您还可以考虑其他两种同步方法。

  1. 使用互斥锁来保护对数据和客户端发送的访问。这会将读取的数据序列化并将其发送到客户端,因此(伪)代码变为:
protected void onFullTextMessage(...) {
   LOCK {
     clients.add(webSocketChannel);
     WebSockets.sendText(gson.toJson(cache.getCache()), webSocketChannel, null);
   }
}

void push(String message) {
    Set<WebSocketChannel> closed = new HashSet<>();
    LOCK {
      clients.forEach((webSocketChannel) -> {
          if (webSocketChannel.isOpen()) {
              WebSockets.sendText(message, webSocketChannel, null);
          } else {
              closed.add(webSocketChannel);
          }
      }
    }
    closed.foreach(clients::remove);
}
  1. 创建一个新的类和服务线程,该线程全权负责管理对数据缓存的更改并将这些更改推送给客户端;它将使用内部同步队列来异步处理方法调用,并跟踪所连接的客户端,它将具有以下接口:
public void update_cache(....);
public void add_new_client(WebSocketChannel);

...这些调用中的每个调用都会在对象内部线程上完成一个操作。这保证了初始快照和更新的顺序,因为只有一个线程负责更改缓存并将这些更改传播给订户。

对于#1,如果您使用的是方法#2,则可以缓存数据的序列化状态,以便在以后的快照中重用(前提是同时未更改)。如评论中所述:仅当以后的客户端具有相同的过滤器配置时,此方法才有效。

答案 1 :(得分:0)

要解决#2和#3问题,我在每个仅在发送初始数据时被解锁的客户端上设置了推锁标志。设置推锁后,到达的消息将放置在该客户端队列中。然后,已排队的消息将在任何新消息之前发送。

我通过直接使用ByteBuffer而不是String缓解了问题#1。这样,由于编码,我可以节省一些内存(默认情况下,字符串使用UTF-16)

最终代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
<div >
      <button id="showed"  onclick="show()" >click to show</button>
      <div id="hidden1" style="display: none">
          <button  onclick="hide()" >click to close</button>
          <h1 >Content1</h1>
      </div>

  </div>
</body>
</html>