文章目录
weksocket问题
Github下载地址:https://github.com/dBlueTears/study-websocket
CSDN下载地址:https://download.csdn.net/download/qq_42390993/89307040
问题一:
weksocket部署在多台机器上,第一次请求负载到A服务器时,session在A服务器线程上,第二次请求,负载到B服务器上,需要通过id查找当前用户的session时,是查找不到的。
问题二:
系统为多点登录,同一个用户打开多个浏览器时,
消息只会推送给最新打开或刷新的浏览器,而其它浏览器则不能接收到服务端推送的消息。
实现
- 一个用户多浏览器消息推送
- 通过Redis发布者/订阅者模式实现websocket的session共享
代码如下(示例):
package com.study.socket.constant;
/**
* @description: 常量类
*/
public class Constants {
/**
* UTF-8 字符集
*/
public static final String UTF8 = "UTF-8";
/** redis 订阅消息通道标识*/
public final static String REDIS_CHANNEL = "onMessage";
/** 消息体的key*/
public final static String REDIS_MESSAGE_KEY = "key";
/** 消息体的值*/
public final static String REDIS_MESSAGE_VALUE = "value";
}
package com.study.socket.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @description: websocket的配置类
*/
/**
* 开启WebSocket支持
*/
@Configuration
@EnableWebSocket
public class WebSocketConfiguration {
/**
* ServerEndpointExporter 作用
* 这个Bean会自动注册使用@ServerEndpoint注解声明的websocket endpoint
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
package com.study.socket.config;
import com.study.socket.constant.Constants;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
/**
* redis配置
*
*/
@Configuration
@EnableCaching
public class RedisConfig
{
/**
* redis消息监听器容器 可以添加多个监听不同话题的redis监听器,只需要把消息监听器和相应的消息订阅处理器绑定
* 消息监听器通过反射技术调用消息订阅处理器的相关方法进行一些业务处理
* @param connectionFactory
* @param listenerAdapter
* @return
*/
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter)
{
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 可以添加多个 messageListener,配置不同的交换机
container.addMessageListener(listenerAdapter, new PatternTopic(Constants.REDIS_CHANNEL));// 订阅最新消息频道
return container;
}
/**
* 消息监听器适配器,绑定消息处理器,利用反射技术调用消息处理器的业务方法
* @param receiver
* @return
*/
@Bean
MessageListenerAdapter listenerAdapter(RedisReceiver receiver)
{
// 消息监听适配器
return new MessageListenerAdapter(receiver, "onMessage");
}
@Bean
StringRedisTemplate template(RedisConnectionFactory factory)
{
return new StringRedisTemplate(factory);
}
}
package com.study.socket.server;
import com.alibaba.fastjson.JSON;
import com.study.socket.utils.SpringUtils;
import com.study.socket.constant.Constants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.validation.constraints.NotNull;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
/**
* WebSocket服务类
*/
@Slf4j
@Component
@ServerEndpoint("/websocket/{userId}")
public class WebSocketServer {
private static final long sessionTimeout = 600000;
/**
* 当前在线连接数
*/
private static AtomicInteger onlineCount = new AtomicInteger(0);
/**
* 存放同连接 对应的多个客户端的连接数量
*/
private static ConcurrentHashMap<String, Integer> count = new ConcurrentHashMap<String, Integer>();
/**
* 存放每个客户端对应的 WebSocketServer 对象
* String 链接的标识
* CopyOnWriteArraySet<WebSocketServer> 存放相同连接的多个对应的客户端
*/
private static ConcurrentHashMap<String, CopyOnWriteArraySet<WebSocketServer>> webSocketMap = new ConcurrentHashMap<>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 接收的标识信息
*/
private String userId;
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
session.setMaxIdleTimeout(sessionTimeout);
this.session = session;
this.userId = userId;
if (exitUser(userId)) {
CopyOnWriteArraySet<WebSocketServer> webSocketSet = webSocketMap.get(userId);
//从set中增加
webSocketSet.add(this);
//在线数加1
userCountIncrease(userId);
} else {
initUserInfo(userId);
}
try {
sendMessage("欢迎" + userId + "加入连接!");
} catch (IOException e) {
log.error(userId + ",网络异常!!!!!!");
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
CopyOnWriteArraySet<WebSocketServer> webSocketSet = webSocketMap.get(userId);
//从set中删除
webSocketSet.remove(this);
//在线数减1
userCountDecrement(userId);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("接收客户端消息:" + userId + ",报文:" + message);
CopyOnWriteArraySet<WebSocketServer> webSocketSet = webSocketMap.get(userId);
for (WebSocketServer item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
continue;
}
}
}
/**
* 发生错误时调用
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("错误:" + this.userId + ",原因:" + error.getMessage());
error.printStackTrace();
}
/**
* @description: 分布式 使用redis 去发布消息
* redis广播消息
* 指定的用户发布消息
*/
public static void sendMessage(@NotNull String key, String message) {
log.info("服务端发送消息:" + key + ",报文:" + message);
String newMessage= null;
try {
newMessage = new String(message.getBytes(Constants.UTF8), Constants.UTF8);
} catch (UnsupportedEncodingException e) {
log.info("sendMessageByRedis exception:" + e);
}
Map<String,String> map = new HashMap<String, String>();
map.put(Constants.REDIS_MESSAGE_KEY, key);
map.put(Constants.REDIS_MESSAGE_VALUE, newMessage);
//广播
StringRedisTemplate template = SpringUtils.getBean(StringRedisTemplate.class);
template.convertAndSend(Constants.REDIS_CHANNEL, JSON.toJSONString(map));
}
/**
* 多用户发送
* @param userIds
* @param message
*/
public static void sendMessageByUsers(String[] userIds, String message) {
for (String userId : userIds) {
sendMessage(userId, message);
}
}
/**
* @description: 单机使用 外部接口通过指定的客户id向该客户推送消息。
*/
public static void sendMessageByWayBillId(@NotNull String key, String message) {
if (webSocketMap.containsKey(key)) {
// 获取 userId 对应的所有 WebSocketServer 实例
CopyOnWriteArraySet<WebSocketServer> servers = webSocketMap.get(key);
for (WebSocketServer server : servers) {
if (ObjectUtils.isNotEmpty(server)) {
try {
server.sendMessage(message);
log.info(key+"发送消息:"+message);
} catch (IOException e) {
e.printStackTrace();
log.error(key+"发送消息失败");
}
} else {
log.error(key+"未连接");
}
}
}
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* 在线连接+1和相同连接下的在线数+1
* @param userId
*/
public void userCountIncrease(String userId) {
addOnlineCount();
log.info(userId + "连接,当前在线数为:" + getOnlineCount());
if (count.containsKey(userId)) {
count.put(userId, count.get(userId) + 1);
} else {
count.put(userId, 1);
}
log.info(userId + "连接,相同连接下客户端数为:" + getCount(userId));
}
/**
* 在线连接-1和相同连接下的在线数-1
* @param userId
*/
public void userCountDecrement(String userId) {
subOnlineCount();
log.info(userId +"退出,当前在线人数为:" + getOnlineCount());
if (count.containsKey(userId)) {
count.put(userId, count.get(userId) - 1);
}
log.info(userId + "退出,相同连接下客户端数为:" + getCount(userId));
}
/**
* 获取相同连接的客户端数
* @param userId
*/
public Integer getCount(String userId) {
Integer sum = 0;
if (count.containsKey(userId)) {
sum = count.get(userId);
}
return sum;
}
/**
* 初始化相同连接的客户端对象
* @param userId
*/
private void initUserInfo(String userId) {
//初始化对象
CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>();
//从set中增加
webSocketSet.add(this);
webSocketMap.put(userId, webSocketSet);
//在线数加1
userCountIncrease(userId);
}
public static synchronized AtomicInteger getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount.getAndIncrement();
}
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount.getAndDecrement();
}
public boolean exitUser(String userId) {
return webSocketMap.containsKey(userId);
}
}
package com.study.socket.config;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.study.socket.server.WebSocketServer;
import com.study.socket.constant.Constants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;
/**
* 消息监听对象,接收订阅消息
*/
@Slf4j
@Component
public class RedisReceiver implements MessageListener {
@Autowired
private WebSocketServer webSocketServer;
/**
* 处理接收到的订阅消息
*/
@Override
public void onMessage(Message message, byte[] pattern)
{
String channel = new String(message.getChannel());// 订阅的频道名称
String msg = "";
try {
msg = new String(message.getBody(), Constants.UTF8);//注意与发布消息编码一致,否则会乱码
if (StringUtils.isNotEmpty(msg)){
if (channel.endsWith(Constants.REDIS_CHANNEL)){
JSONObject jsonObject = JSON.parseObject(msg);
webSocketServer.sendMessageByWayBillId(
jsonObject.get(Constants.REDIS_MESSAGE_KEY).toString()
,jsonObject.get(Constants.REDIS_MESSAGE_VALUE).toString());
} else {
//TODO 其它订阅的消息处理
}
}else{
log.info("消息内容为空,不处理。");
}
}
catch (Exception e)
{
log.error("处理消息异常:"+e.toString());
e.printStackTrace();
}
}
}
package com.study.socket.utils;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* spring工具类 方便在非spring管理环境中获取bean
*/
@Component
public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware
{
/** Spring应用上下文环境 */
private static ConfigurableListableBeanFactory beanFactory;
private static ApplicationContext applicationContext;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException
{
SpringUtils.beanFactory = beanFactory;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
{
SpringUtils.applicationContext = applicationContext;
}
/**
* 获取对象
*
* @param name
* @return Object 一个以所给名字注册的bean的实例
* @throws org.springframework.beans.BeansException
*
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) throws BeansException
{
return (T) beanFactory.getBean(name);
}
/**
* 获取类型为requiredType的对象
*
* @param clz
* @return
* @throws org.springframework.beans.BeansException
*
*/
public static <T> T getBean(Class<T> clz) throws BeansException
{
T result = (T) beanFactory.getBean(clz);
return result;
}
/**
* 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
*
* @param name
* @return boolean
*/
public static boolean containsBean(String name)
{
return beanFactory.containsBean(name);
}
/**
* 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
*
* @param name
* @return boolean
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*
*/
public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException
{
return beanFactory.isSingleton(name);
}
/**
* @param name
* @return Class 注册对象的类型
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*
*/
public static Class<?> getType(String name) throws NoSuchBeanDefinitionException
{
return beanFactory.getType(name);
}
/**
* 如果给定的bean名字在bean定义中有别名,则返回这些别名
*
* @param name
* @return
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*
*/
public static String[] getAliases(String name) throws NoSuchBeanDefinitionException
{
return beanFactory.getAliases(name);
}
/**
* 获取aop代理对象
*
* @param invoker
* @return
*/
@SuppressWarnings("unchecked")
public static <T> T getAopProxy(T invoker)
{
return (T) AopContext.currentProxy();
}
}
package com.study.socket.controller;
import com.study.socket.server.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
@RestController
public class SocketController {
@Autowired
private WebSocketServer webSocketServer;
@GetMapping("/info")
public String sendInfo(@RequestParam("userId") String userId, @RequestParam("message") String message) throws IOException {
webSocketServer.sendMessage(userId, message);
return message;
}
}
package com.study.socket;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WebsocketApplication {
public static void main(String[] args) {
SpringApplication.run(WebsocketApplication.class, args);
}
}