解决webSocket在针对指定用户推送消息时,需要保存用户id至redis中,保存redis时会报Null pointer exception

解决webSocket在针对指定用户推送消息时,需要保存用户id至redis中,保存redis时会报Null pointer exception

最近在做一个项目遇到,websocket针对指定用户推送消息时,需要保存用户的socketId至redis中,存入redis中的key是一个字符串常量"socketNumber",value值是socketId的json串,每当用户建立连接之后,调用存储socketId到redis这一步就会报空指针异常。
在这里插入图片描述

定义的key是一个静态常量,没理由存不到redis中,百思不得其解。接下来先贴一段报空指针异常的websocket服务类方法,然后分析下报错原因,给出解决方法。

websocket服务类方法

导入redisClient工具类方式如下:
在这里插入图片描述
设置redis的key是静态常量
在这里插入图片描述
redis存储用户userId方法,每次断点过redisClient.set(socketNumber ,JsonUtil.toJson(socketIds));这部分总会报空指针,但是socketNumber定义的是静态常量,没有道理值不存在。
在这里插入图片描述

@ServerEndpoint(value = "/webSocket/{socketId}", encoders = {WebSocketCustomEncoding.class})
@Component
public class WebSocketServer {
    private static final AriesJcLogger logger = AriesJcLoggerFactory.getLogger(WebSocketServer.class);
    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static AtomicInteger online = new AtomicInteger();

    //concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
    private static ConcurrentHashMap<String, Session> sessionPools = new ConcurrentHashMap<>();
    private static List<String> socketIds = new ArrayList<>();
    public static  String socketNumber ="socketNumber";
    @Autowired
    private RedisClient redisClient;

    /**
     * 发送消息方法
     *
     * @param session 客户端与socket建立的会话
     * @param message 消息
     * @throws IOException
     */
    public void sendMessage(Session session, BusinessRealTimeData message) throws IOException, EncodeException {
        if (session != null) {
//            session.getBasicRemote().sendText(message.toString());
            session.getBasicRemote().sendObject(message);
        }
    }

    /**
     * 连接建立成功调用
     *
     * @param session  客户端与socket建立的会话
     * @param socketId socketId
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "socketId") String socketId) {
        socketIds.add(socketId);
        redisClient.set(socketNumber ,JsonUtil.toJson(socketIds));
        sessionPools.put(socketId, session);
        addOnlineCount();
        logger.info("====加入新连接session:{}!当前在线人数为:{}", session.getId());
    }

    /**
     * 关闭连接时调用
     *
     * @param socketId socketId
     */
    @OnClose
    public void onClose(@PathParam(value = "socketId") String socketId) {
        redisClient.removeForSet(socketNumber, socketId);
        String plate = redisClient.get("plate_" + socketId);
        List<String> plates = JSONObject.parseArray(plate,String.class);
        redisClient.removeForSet("plate_"+socketId,plates);
        String camera = redisClient.get("camera_"+socketId);
        List<String> cameras = JSONObject.parseArray(camera,String.class);
        redisClient.removeForSet("camera_"+socketId,cameras);
        sessionPools.remove(socketId);
        subOnlineCount();
        logger.info("====关闭连接userid:{}", socketId);
    }

    /**
     * 收到客户端消息时触发(群发)
     *
     * @param message
     * @throws IOException
     */
    @OnMessage
    public void onMessage(String message) throws IOException {
        logger.debug("====来自客户端的消息session:{}---message:{}", message);
        for (Session session : sessionPools.values()) {
            try {
                BusinessRealTimeData businessRealTimeData = JSONObject.parseObject(message, BusinessRealTimeData.class);
                sendMessage(session, businessRealTimeData);
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
    }

    /**
     * 发生错误时候
     *
     * @param session
     * @param throwable
     */
    @OnError
    public void onError(Session session, Throwable throwable) {
        logger.error("websocket 发生错误", throwable);
        throwable.printStackTrace();
    }

    /**
     * 给指定用户发送消息
     *
     * @param socketId socketId
     * @param message  消息
     * @throws IOException
     */
    public void sendInfo(String socketId, BusinessRealTimeData message) {
        logger.debug("发送指定客户端的消息:{}", JsonUtil.toJson(message));
        logger.debug("发送指定客户端socketId:{}", JsonUtil.toJson(socketId));
        Session session = sessionPools.get(socketId);
        try {
            sendMessage(session, message);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void sendtoAll(BusinessRealTimeData message) throws IOException {
        logger.debug("发送客户端的消息:{}", JsonUtil.toJson(message));
        for (Session session : sessionPools.values()) {
            try {
                sendMessage(session, message);
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }

问题分析

在@ServerEndpoint注解类中使用注解@Autowired注入RedisClient,注入RedisClient失败,因此报空指针异常。
为什么在websocket中不能通过@Autowire注解注入呢?
由于websocket本身是线程安全的,建立连接时候就会创建一个端点实例,一个websocket可以建立多个连接,因此会创建多个端点实例对象。@Autowired注解在Spring初始化对象实例时,会调用此构造函数,进行对象的实例化。项目初始化时,初始化一个websocket对象,此时的websocket对象没有建立连接,成功注入容器。当用户建立了连接,又会重新创建一个新的websocket,由于此前已经注入了一个没有建立连接的websocket对象,Spring默认支持单例模式,因此后边建立连接的websocket对象就不能被注入了,因此会报空指针异常。

解决方法

如何解决无法注入的问题?可以通过使用 getBean的方法主动获取实例。

  • 创建一个获取SpringUtils工具类
@Component
public final class SpringUtils implements BeanFactoryPostProcessor {

    private static ConfigurableListableBeanFactory beanFactory;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        SpringUtils.beanFactory = beanFactory;
    }

    public static ConfigurableListableBeanFactory getBeanFactory() {
        return beanFactory;
    }

    /**
     * 获取对象
     *
     * @param name
     * @return Object 一个以所给名字注册的bean的实例
     * @throws org.springframework.beans.BeansException
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) throws BeansException {
        if (getBeanFactory() == null) {
            //zhengkai.blog.csdn.net
            System.out.println("本地调试Main模式,没有BeanFactory,忽略错误");
            return null;
        } else {
            T result = (T) getBeanFactory().getBean(name);
            return result;
        }
    }

    /**
     * 获取类型为requiredType的对象
     *
     * @param name
     * @return
     * @throws org.springframework.beans.BeansException
     */
    public static <T> T getBean(Class<T> name) throws BeansException {
        if (getBeanFactory() == null) {
            //zhengkai.blog.csdn.net
            System.out.println("本地调试Main模式,没有BeanFactory,忽略错误");
            return null;
        } else {
            T result = (T) getBeanFactory().getBean(name);
            return result;
        }
    }

    /**
     * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
     *
     * @param name
     * @return boolean
     */
    public static boolean containsBean(String name) {
        return getBeanFactory().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 getBeanFactory().isSingleton(name);
    }

    /**
     * @param name
     * @return Class 注册对象的类型
     * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
     */
    public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
        return getBeanFactory().getType(name);
    }

    /**
     * 如果给定的bean名字在bean定义中有别名,则返回这些别名
     *
     * @param name
     * @return
     * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
     */
    public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
        return getBeanFactory().getAliases(name);
    }

}

-在websocket服务类中可以通过@Resource注解通过 private RedisClient redisClient = SpringUtils.getBean(RedisClient.class);方式导入redis工具类

@ServerEndpoint(value = "/webSocket/{socketId}", encoders = {WebSocketCustomEncoding.class})
@Component
public class WebSocketServer {
    private static final AriesJcLogger logger = AriesJcLoggerFactory.getLogger(WebSocketServer.class);
    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static AtomicInteger online = new AtomicInteger();

    //concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
    private static ConcurrentHashMap<String, Session> sessionPools = new ConcurrentHashMap<>();
    private static List<String> socketIds = new ArrayList<>();
    public static  String socketNumber ="socketNumber";
    @Resource
    private RedisClient redisClient = SpringUtils.getBean(RedisClient.class);

    /**
     * 发送消息方法
     *
     * @param session 客户端与socket建立的会话
     * @param message 消息
     * @throws IOException
     */
    public void sendMessage(Session session, BusinessRealTimeData message) throws IOException, EncodeException {
        if (session != null) {
//            session.getBasicRemote().sendText(message.toString());
            session.getBasicRemote().sendObject(message);
        }
    }

    /**
     * 连接建立成功调用
     *
     * @param session  客户端与socket建立的会话
     * @param socketId socketId
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "socketId") String socketId) {
        socketIds.add(socketId);
        redisClient.set(socketNumber ,JsonUtil.toJson(socketIds));
        sessionPools.put(socketId, session);
        addOnlineCount();
        logger.info("====加入新连接session:{}!当前在线人数为:{}", session.getId());
    }

    /**
     * 关闭连接时调用
     *
     * @param socketId socketId
     */
    @OnClose
    public void onClose(@PathParam(value = "socketId") String socketId) {
        redisClient.removeForSet(socketNumber, socketId);
        String plate = redisClient.get("plate_" + socketId);
        List<String> plates = JSONObject.parseArray(plate,String.class);
        redisClient.removeForSet("plate_"+socketId,plates);
        String camera = redisClient.get("camera_"+socketId);
        List<String> cameras = JSONObject.parseArray(camera,String.class);
        redisClient.removeForSet("camera_"+socketId,cameras);
        sessionPools.remove(socketId);
        subOnlineCount();
        logger.info("====关闭连接userid:{}", socketId);
    }

    /**
     * 收到客户端消息时触发(群发)
     *
     * @param message
     * @throws IOException
     */
    @OnMessage
    public void onMessage(String message) throws IOException {
        logger.debug("====来自客户端的消息session:{}---message:{}", message);
        for (Session session : sessionPools.values()) {
            try {
                BusinessRealTimeData businessRealTimeData = JSONObject.parseObject(message, BusinessRealTimeData.class);
                sendMessage(session, businessRealTimeData);
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
    }

    /**
     * 发生错误时候
     *
     * @param session
     * @param throwable
     */
    @OnError
    public void onError(Session session, Throwable throwable) {
        logger.error("websocket 发生错误", throwable);
        throwable.printStackTrace();
    }

    /**
     * 给指定用户发送消息
     *
     * @param socketId socketId
     * @param message  消息
     * @throws IOException
     */
    public void sendInfo(String socketId, BusinessRealTimeData message) {
        logger.debug("发送指定客户端的消息:{}", JsonUtil.toJson(message));
        logger.debug("发送指定客户端socketId:{}", JsonUtil.toJson(socketId));
        Session session = sessionPools.get(socketId);
        try {
            sendMessage(session, message);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void sendtoAll(BusinessRealTimeData message) throws IOException {
        logger.debug("发送客户端的消息:{}", JsonUtil.toJson(message));
        for (Session session : sessionPools.values()) {
            try {
                sendMessage(session, message);
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
    }

    public static void addOnlineCount() {
        online.incrementAndGet();
    }

    public static void subOnlineCount() {
        online.decrementAndGet();
    }
}

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,这是一个比较复杂的功能,需要涉及到多个模块的编写。下面是一个简单的示例代码,供您参考。 1. 首先是WebSocket的部分,我们可以使用Python的tornado框架来实现: ```python import tornado.web import tornado.websocket import tornado.ioloop import redis r = redis.Redis(host='localhost', port=6379, db=0) class WebSocketHandler(tornado.websocket.WebSocketHandler): def open(self): # 获取用户信息,将其作为Redis的key值 user_id = self.get_secure_cookie('user_id').decode('utf-8') self.user_id = user_id def on_close(self): pass def check_origin(self, origin): return True def on_message(self, message): # 接收到消息后,将其存入Redis,key为用户ID,value为消息内容 r.rpush(self.user_id, message) def make_app(): return tornado.web.Application([ (r'/ws', WebSocketHandler), ]) if __name__ == '__main__': app = make_app() app.listen(8000) tornado.ioloop.IOLoop.current().start() ``` 2. 接下来是Redis的部分,我们需要实现一个推送消息的函数,同需要实现一个获取离线消息的函数: ```python import redis r = redis.Redis(host='localhost', port=6379, db=0) def push_message(user_id, message): # 将消息推送到当前用户WebSocket连接 for conn in WebSocketHandler.connections: if conn.user_id == user_id: conn.write_message(message) def get_offline_message(user_id): # 获取当前用户的离线消息,并返回一个列表 messages = r.lrange(user_id, 0, -1) r.delete(user_id) return messages ``` 3. 最后,我们需要在适当的位置调用这些函数,来实现消息推送和离线消息的获取。例如,在用户登录后,我们可以调用`get_offline_message()`函数,获取用户的离线消息: ```python class LoginHandler(tornado.web.RequestHandler): def post(self): user_id = self.get_argument('user_id') self.set_secure_cookie('user_id', user_id) # 获取用户的离线消息,并推送到当前用户WebSocket连接 offline_messages = get_offline_message(user_id) for msg in offline_messages: push_message(user_id, msg) ``` 当用户收到新消息,我们可以调用`push_message()`函数,将消息推送到当前用户WebSocket连接: ```python class SendMessageHandler(tornado.web.RequestHandler): def post(self): user_id = self.get_secure_cookie('user_id').decode('utf-8') message = self.get_argument('message') # 将消息推送到当前用户WebSocket连接 push_message(user_id, message) # 将消息存入Redis,供离线推送使用 r.rpush(user_id, message) ``` 这样,我们就完成了WebSocket+Redis实现消息推送机制以及离线消息推送的功能。当用户收到新消息,会立即推送到其当前的WebSocket连接;当用户下线后,未读的消息会存入Redis,等用户再次登录,会将其离线消息推送用户
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值