作者:微信小助手
发布时间:2022-04-12T14:30:10
最近做项目时遇到了需要多用户之间通信的问题,涉及到了WebSocket握手请求,以及集群中WebSocket Session共享的问题。
期间我经过了几天的研究,总结出了几个实现分布式WebSocket集群的办法,从zuul到spring cloud gateway的不同尝试,总结出了这篇文章,希望能帮助到某些人,并且能一起分享这方面的想法与研究。
以下是我的场景描述
在我的实现里,每个应用服务器都负责http and ws请求,其实也可以将ws请求建立的聊天模型单独成立为一个模块。从分布式的角度来看,这两种实现类型差不多,但从实现方便性来说,一个应用服务http+ws请求的方式更为方便。下文会有解释。
本文涉及的技术栈:
相信能走到这一步的人都了解过我上面列举的技术栈了,如果还没有,可以先去网上找找入门教程了解一下。下面的内容都与上述技术相关,题主默认大家都了解过了...
下面我将描述session特性,以及根据这些特性列举出n个解决分布式架构中处理ws请求的集群方案。
WebSocketSession与HttpSession
在Spring所集成的WebSocket里面,每个ws连接都有一个对应的session:WebSocketSession,在Spring WebSocket中,我们建立ws连接之后可以通过类似这样的方式进行与客户端的通信:
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
System.out.println("服务器接收到的消息: "+ message );
//send message to client
session.sendMessage(new TextMessage("message"));
}
那么问题来了:ws的session无法序列化到redis,因此在集群中,我们无法将所有WebSocketSession都缓存到redis进行session共享。每台服务器都有各自的session。于此相反的是HttpSession,redis可以支持httpsession共享,但是目前没有websocket session共享的方案,因此走redis websocket session共享这条路是行不通的。
有的人可能会想:我可不可以将sessin关键信息缓存到redis,集群中的服务器从redis拿取session关键信息然后重新构建websocket session...我只想说这种方法如果有人能试出来,请告诉我一声...
以上便是websocket session与http session共享的区别,总的来说就是http session共享已经有解决方案了,而且很简单,只要引入相关依赖:spring-session-data-redis
和spring-boot-starter-redis
,大家可以从网上找个demo玩一下就知道怎么做了。而websocket session共享的方案由于websocket底层实现的方式,我们无法做到真正的websocket session共享。
Netty与Spring WebSocket
刚开始的时候,我尝试着用netty实现了websocket服务端的搭建。在netty里面,并没有websocket session这样的概念,与其类似的是channel,每一个客户端连接都代表一个channel。前端的ws请求通过netty监听的端口,走websocket协议进行ws握手连接之后,通过一些列的handler(责链模式)进行消息处理。与websocket session类似地,服务端在连接建立后有一个channel,我们可以通过channel进行与客户端的通信
/**
* TODO 根据服务器传进来的id,分配到不同的group
*/
private static final ChannelGroup GROUP = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
//retain增加引用计数,防止接下来的调用引用失效
System.out.println("服务器接收到来自 " + ctx.channel().id() + " 的消息: " + msg.text());
//将消息发送给group里面的所有channel,也就是发送消息给客户端
GROUP.writeAndFlush(msg.retain());
}
那么,服务端用netty还是用spring websocket?以下我将从几个方面列举这两种实现方式的优缺点。
玩过netty的人都知道netty是的线程模型是nio模型,并发量非常高,spring5之前的网络线程模型是servlet实现的,而servlet不是nio模型,所以在spring5之后,spring的底层网络实现采用了netty。如果我们单独使用netty来开发websocket服务端,速度快是绝对的,但是可能会遇到下列问题:
spring websocket已经被springboot很好地集成了,所以在springboot上开发ws服务非常方便,做法非常简单
第一步:添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
第二步:添加配置类
@Configuration
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/")
.setAllowedOrigins("*");
}
@Bean
public WebSocketHandler myHandler() {
return new MessageHandler();
}
}
第三步:实现消息监听类
@Component
@SuppressWarnings("unchecked")
public class MessageHandler extends TextWebSocketHandler {
private List<WebSocketSession> clients = new ArrayList<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
clients.add(session);
System.out.println("uri :" + session.getUri());
System.out.println("连接建立: " + session.getId());
System.out.println("current seesion: " + clients.size());
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
clients.remove(session);
System.out.println("断开连接: " + session.getId());
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
String payload = message.getPayload();
Map<String, String> map = JSONObject.parseObject(payload, HashMap.class);
System.out.println("接受到的数据" + map);
clients.forEach(s -> {
try {
System.out.println("发送消息给: " + session.getId());
s.sendMessage(new TextMessage("服务器返回收到的信息," + payload));
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
从这个demo中,使用spring websocket实现ws服务的便利性大家可想而知了。为了能更好地向spring cloud大家族看齐,我最终采用了spring websocket实现ws服务。
因此我的应用服务架构是这样子的:一个应用既负责restful服务,也负责ws服务。没有将ws服务模块拆分是因为拆分出去要使用feign来进行服务调用。第一本人比较懒惰,第二拆分与不拆分相差在多了一层服务间的io调用,所以就没有这么做了。
要实现websocket集群,我们必不可免地得从zuul转型到spring cloud gateway。原因如下:
zuul1.0版本不支持websocket转发,zuul 2.0开始支持websocket,zuul2.0几个月前开源了,但是2.0版本没有被spring boot集成,而且文档不健全。因此转型是必须的,同时转型也很容易实现。
在gateway中,为了实现ssl认证和动态路由负载均衡,yml文件中以下的某些配置是必须的,在这里提前避免大家采坑