WebSocket在分布式中简单的使用思路
如今WebSocket是公认解决轮询的良方,但是在实际运用中存在一些不可避免的问题。就拿当今开发最火的SpringCloud分布式微服务来说,WebSocket就存在一个不可避免的问题:下面是一个分布式的案例图
这是一个评判系统的简略图,producer-server来接收提交的答案批改请求(这里做了消息队列的处理,但对解说没有影响所以简化掉了),然后通过负载均衡将消息给consume-server来处理并得到结果放入数据库。问题来了,客户端请求的只能是producer服务所以不可能直接通过JSON来得到consume-server的处理结果。一般这时就需要轮询数据库来得到最后的成绩,但是评判系统遇到高并发的话就会有崩掉的风险,而且成绩还需要及时的显示在界面上所以这种方法被PASS掉了,那么就到了WebSocket上场了。
WebSocket在SpringBoot里的配置
首先是在maven里面进行引用。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
之后便是在springboot进行申明配置
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
然后是进行WebSocket的具体设置(网上一大堆)
@ServerEndpoint(value = "/webSocket/{id}",configurator = HttpSessionWSHelper.class)
@Component
public class WebSocket {
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
/**
* concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
* 若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
*/
private static CopyOnWriteArraySet<WebSocket> webSocketSet = new CopyOnWriteArraySet<>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
/**
* 连接建立成功调用的方法
* @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
@OnOpen
public void onOpen(Session session,@PathParam("id")String id) {
this.session = session;
webSocketSet.add(this); //加入set中
addOnlineCount(); //在线数加1
SocketManager.getInstance().getMapSession().put(id,session);
System.out.println(SocketManager.getInstance().getMapSession().size());
System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(@PathParam("id")String id) {
webSocketSet.remove(this); //从set中删除
subOnlineCount(); //在线数减1
SocketManager.getInstance().getMapSession().remove(id);
System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
* @param message 客户端发送过来的消息
* @param session 可选的参数
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("来自客户端的消息:" + message);
//群发消息
}
/**
* 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
* @param message
* @throws IOException
*/
public void sendMessage(String message,Session session) throws IOException {
String respMsg = null;
if ("1".equals(message)){
respMsg = "Java";
}else if ("2".equals(message)){
respMsg = "python";
}else if ("3".equals(message)){
respMsg = "groovy";
}else{
respMsg = "欢迎访问!请根据如下规则获取您想要的内容:1:java 2:python 3:groovy";
}
session.getBasicRemote().sendText(respMsg);
//this.session.getAsyncRemote().sendText(message);
}
// public Session getSession(Session session){
// return session;
// }
/**
* 发生错误时调用
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
onlineCount++;
}
public static synchronized void subOnlineCount() {
onlineCount--;
}
}
我们主要注重一下sendMessage里面session.getBasicRemote().sendText(respMsg);方法。这个很重要因为这个似乎告诉了我们我们只要拿到了session似乎就可以进行前后端的自由交互。
(这里可以把session就看成WebSocket的双工通信通道)
分布式的引用问题
然后就是引用的问题了,WebSocket要与客户端进行联系必须是在producer-server里面进行连接的建立,但是结果是在consume里面产生的那么问题来了:要想在服务间传输我们在producer里面建立的session。
错误的想法
说起服务间的传输数据,我们第一个想到的就是redis或者rabbitMQ等,这是我们第一时间想到的,但是WebSocket就很特殊:
首先,WebSocket是一个多实例对象,在Sping里面注入容器的时候必须将其定义为静态变量然后指定方法注入才能使用不然为空。
其二,WebSocket的Session不支持序列化,那么这就直接让redis以及消息队列的想法暴毙。redis直接没法传输,rabbitMQ能存储但是无法取出。
WebSocket似乎是跟Spring作对,样样不合常理。但是我们似乎忘了我们是在SpringCloud中。
解决的方法
既然无法传输到consume服务上,那就不传直接通过SpringCloud的负载均衡让最后的结果传回producer-server来进行session的操作,这样就避免了session的跨服务传输。
这样WebSocket的分布式就很好的解决了,那么还有一个系统上的问题:如何确认提交的答案的结果是谁的。这个时候我们就需要在WebSocket上面设置一个key来确定。那么我们还需要设计一个WebSocket的一个存放列表,这个时候又有一个错误想法诞生了。
错误的想法2
要用一个公用的存储容器,我第一想法就是通过Spring注册一个容器来进行存储。那么这时候WebSocket又开始跟Spring杠上了,WebSocket的配置类里面使用自动注入无效,就是说我的列表拿不进来。网上很多大神都解决了这种问题这位大神讲的很简洁:大神的解决方法以及为啥不能注入的原因
跳出Spring框架
我们使用的是java而不只是Spirng,所以我们跳出来看这个问题,实质我们只需要一个生命周期与服务器一样的公用的存储Map(或者List),联想最近看的GitHub上大佬的NeetyRPC服务,得到了启发于是写了如下容器代码。
public class SocketManager {
private Map<String, Session> mapSession = new ConcurrentHashMap<>();
private volatile static SocketManager socketManager;
public Map<String, Session> getMapSession() {
return mapSession;
}
public void setMapSession(Map<String, Session> mapSession) {
this.mapSession = mapSession;
}
public SocketManager() {
}
public static SocketManager getInstance(){
if (socketManager==null){
synchronized (SocketManager.class){
if (socketManager==null){
socketManager = new SocketManager();
}
}
}
return socketManager;
}
}
这样就在创建建立的时候进行一个存储。
@OnOpen
public void onOpen(Session session,@PathParam("id")String id) {
this.session = session;
webSocketSet.add(this); //加入set中
addOnlineCount(); //在线数加1
***SocketManager.getInstance().getMapSession().put(id,session);***
System.out.println(SocketManager.getInstance().getMapSession().size());
System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
}
下面就只需要在负载均衡的方法中拿到对应的Session就可以进行推送了
@PostMapping("/pushScore")
public void pushScore(@RequestParam("id")String id,
@RequestParam("score")String score){
Session session = SocketManager.getInstance().getMapSession().get(id);
try {
if(session == null){
System.out.println("No Connection");
return;
}
System.out.println("发送消息中");
session.getBasicRemote().sendText(score);
System.out.println("over");
} catch (IOException e) {
e.printStackTrace();
}
}
本文主要是提供思路,代码的话相信各位都能自己写出来,思路理清楚了代码都是小问题。如有差漏请大家提出来,十分感谢。