从WebSocket开始
最简单方式
广播
@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);
}
}
}
但在高并发发消息时出现
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是一种长链接,这会导致
- 服务器上的内存和 CPU 资源消耗增加。如果大量用户同时在线,那么服务器的压力将会非常大。
- 由于每一个链接强绑定服务器,导致负载均衡变得困难,
- 由于 WebSocket 连接的持久性和粘滞性,当我们希望将服务器横向扩展时,
比如,当我们存在一个聊天室A,聊天室内有人员B,C; 其中B在服务器E上, C在服务器F上, 此时如果要在聊天室A内进行广播,这是行不通的;除非我们可以通过Hash将指定A的成员一定会在一个服务器上,但这也很可能导致负载不均衡。
利用MQ(kafka)
将消息转入kafka, 由kafka来解决上面由于不同服务器而带来的广播问题
在这里,每个服务器既是kafka的生产者,也是kafka的订阅者;这样,即使B,C分属两个服务器,互相直接也可以进行广播;同时,降低了横向扩展难度,降低了耦合性,只要websocket接入kafka,就可以实现个服务器之间的交流。
但是, 这也是有缺点的,无关消息多,假如有另一个人D在新的websocket服务器G上, 期望在A聊天室广播;A聊天室的人员分布在G和F上,此时E就收到了无效消息。
再加Redis
那么,能不能将消息进行分类呢?( 或许可以按聊天室划分topic?但感觉不太合理 )。而且MQ也不太适合支持消息重放。
在这里可以使用KV型的数据库,比如Redis;Kafka消费者读取消息后将消息存入Redis,然后各服务器定时去Redis内去拉数据,在Redis中以 roomID -> {{userID, message, timestamp}, ...}
例子 WebSocket Server
- Server会记录连接到本服务器的userID以及roomID (由Websocket的onopen携带)
- Server可以根据roomID来确认自己需要去订阅哪些Redis数据
- Server每隔1秒去读取一次
ZRANGEBYSCORE {roomID} {now()- 1 minute} {now()} - 获得到的消息都是需要广播的消息,此时我们通过roomID获取其内的userID
- 根据userID获取session发送消息
Kafka
- 接收到的数据为
{
roomID: "xxxx",
userID: "xxxx",
message: "xxxx"
}
Redis
时间戳由存入Redis时加入
Redis内的数据为
roomID -> {{userID, message; timestamp}, ...}
要注意的点,
- 各服务器都依赖于时间的准确性;
- 如果存在Stop The World,则可能存在漏发数据的情况
- 准实时
http+websocket
当然,也可以这样,发送数据使用http,接收数据使用websocket