IM通信类系统学习

140 阅读3分钟

从WebSocket开始

最简单方式

image.png

广播

@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
    for (String key : sessionMap.keySet()) {
        sessionMap.get(key).sendMessage(message);
    }
}

当然,这种写法在高并发的情况下会发生线程写冲突[TEXT_PARTIAL_WRITING]导致异常,可以使用线程锁处理,以session为锁对象。

使用线程锁处理写冲突

@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
    for (String key : sessionMap.keySet()) {
        WebSocketSession socketSession = sessionMap.get(key);
        if (socketSession == null) {
            sessionMap.remove(key);
            continue;
        }
        synchronized (socketSession) {
            if (!socketSession.isOpen()) {
                sessionMap.remove(key);
                continue;
            }
            socketSession.sendMessage(message);
        }

    }
}

但在高并发发消息时出现

image.png

java.io.IOException: java.io.IOException: Connection reset by peer java.lang.IllegalStateException: Message will not be sent because the WebSocket session has been closed

同时,这种模式扩展性也非常差,众所周知,websocket是一种长链接,这会导致

  1. 服务器上的内存和 CPU 资源消耗增加。如果大量用户同时在线,那么服务器的压力将会非常大。
  2. 由于每一个链接强绑定服务器,导致负载均衡变得困难,
  3. 由于 WebSocket 连接的持久性和粘滞性,当我们希望将服务器横向扩展时,

比如,当我们存在一个聊天室A,聊天室内有人员B,C; 其中B在服务器E上, C在服务器F上, 此时如果要在聊天室A内进行广播,这是行不通的;除非我们可以通过Hash将指定A的成员一定会在一个服务器上,但这也很可能导致负载不均衡。

image.png

利用MQ(kafka)

将消息转入kafka, 由kafka来解决上面由于不同服务器而带来的广播问题

image.png

在这里,每个服务器既是kafka的生产者,也是kafka的订阅者;这样,即使B,C分属两个服务器,互相直接也可以进行广播;同时,降低了横向扩展难度,降低了耦合性,只要websocket接入kafka,就可以实现个服务器之间的交流。

但是, 这也是有缺点的,无关消息多,假如有另一个人D在新的websocket服务器G上, 期望在A聊天室广播;A聊天室的人员分布在G和F上,此时E就收到了无效消息。

image.png

再加Redis

那么,能不能将消息进行分类呢?( 或许可以按聊天室划分topic?但感觉不太合理 )。而且MQ也不太适合支持消息重放。

在这里可以使用KV型的数据库,比如Redis;Kafka消费者读取消息后将消息存入Redis,然后各服务器定时去Redis内去拉数据,在Redis中以 roomID -> {{userID, message, timestamp}, ...}

image.png

例子 WebSocket Server

  1. Server会记录连接到本服务器的userID以及roomID (由Websocket的onopen携带)
  2. Server可以根据roomID来确认自己需要去订阅哪些Redis数据
  3. Server每隔1秒去读取一次 ZRANGEBYSCORE {roomID} {now()- 1 minute} {now()}
  4. 获得到的消息都是需要广播的消息,此时我们通过roomID获取其内的userID
  5. 根据userID获取session发送消息

Kafka

  1. 接收到的数据为
{
    roomID: "xxxx",
    userID: "xxxx",
    message: "xxxx"
}

Redis

时间戳由存入Redis时加入

Redis内的数据为

roomID -> {{userID, message; timestamp}, ...}

要注意的点,

  1. 各服务器都依赖于时间的准确性;
  2. 如果存在Stop The World,则可能存在漏发数据的情况
  3. 准实时

http+websocket

当然,也可以这样,发送数据使用http,接收数据使用websocket

image.png

学习来源

blog.qizong007.top/article/liv…

cloud.tencent.com/developer/a…