• Redis实现聊天功能


      在学习了Redis做为消息队列之后研究 了redis聊天的功能。

      其实用关系型数据库也可以实现消息功能,自己就曾经用mysql写过一个简单的消息的功能。RDB中思路如下:

    **
    在实际中可以完全借助mysql数据库实现聊天功能,建立一个表,保存接收人的username、message、isConsumed等信息,用户登录之后采用心跳机制不停的检测数据库并消费消息。
    心跳可以做好多事,比如检测检测当前用户是否已经登录,如果已经登录剔除之前已经登录的用户,实现一个用户一次登录的功能。
    心跳可以采用JS的周期函数不停的向后台发起异步请求,后台查询未消息的消息
    **

    1.Redis实现一对一的聊天功能(基于lpush和brpop实现)

      简单的实现一个用户向另一个用户发送多条信息,实现的思路是:

    一对一聊天的思路:(采用Lpush和Brpop实现)
    1.消息生产者生产消息到redis中:生产消息的时候根据接收人的userName与消息的类型发送到对应的key,采用lpush发送消息(根据userName生成key)
    2.消息的消费者根据userName,从userName的key中消费对应的消息。如果有必要可以将消息写到RDB中避免数据的丢失。(根据userName生成key的规则获取用户对应的消息)
    3.消息的内容头部加入发送者,例如原来消息内容是:hello,为了知道消息的发送者可以改为:张三*-*hello(为了获取消息的发送者)

    下面直接上代码:

    User.java(只有一个userName有用)

    package cn.xm.jwxt.bean.system;
    
    import java.util.List;
    import java.util.Set;
    
    public class User {
      
        private String username;//用户姓名
    public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username == null ? null : username.trim();
        }
    }

    redis-chat.properties

    redis.url=127.0.0.1
    redis.port=6379
    redis.maxIdle=30
    redis.minIdle=10
    redis.maxTotal=100
    redis.maxWait=20000

    Jedis工具类:(返回Jedis连接)

    package cn.xm.redisChat.util;
    
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisPool;
    import redis.clients.jedis.JedisPoolConfig;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.Properties;
    
    /**
     * @Author: qlq
     * @Description
     * @Date: 21:32 2018/10/9
     */
    public class JedisPoolUtils {
    
        private static JedisPool pool = null;
    
        static {
    
            //加载配置文件
            InputStream in = JedisPoolUtils.class.getClassLoader().getResourceAsStream("redis-chat.properties");
            Properties pro = new Properties();
            try {
                pro.load(in);
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            //获得池子对象
            JedisPoolConfig poolConfig = new JedisPoolConfig();
            poolConfig.setMaxIdle(Integer.parseInt(pro.get("redis.maxIdle").toString()));//最大闲置个数
            poolConfig.setMaxWaitMillis(Integer.parseInt(pro.get("redis.maxWait").toString()));//最大闲置个数
            poolConfig.setMinIdle(Integer.parseInt(pro.get("redis.minIdle").toString()));//最小闲置个数
            poolConfig.setMaxTotal(Integer.parseInt(pro.get("redis.maxTotal").toString()));//最大连接数
            pool = new JedisPool(poolConfig, pro.getProperty("redis.url"), Integer.parseInt(pro.get("redis.port").toString()));
        }
    
        //获得jedis资源的方法
        public static Jedis getJedis() {
            return pool.getResource();
        }
    }

    消息生产者:(处理消息头部加上消息的发送者,并且根据接受者的userName生成key)

    package cn.xm.redisChat.one2one;
    
    import cn.xm.jwxt.bean.system.User;
    import cn.xm.redisChat.util.JedisPoolUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import redis.clients.jedis.Jedis;
    
    /**
     * @Author: qlq
     * @Description 消息生产者(根据消息的)
     * @Date: 23:02 2018/10/13
     */
    public class RedisMessageProducer {
        private static final Logger log = LoggerFactory.getLogger(RedisMessageProducer.class);
    
        /**
         * 发送消息的方法
         *
         * @param sendUser   发送消息的用户
         * @param sendToUser 接收消息的用户
         * @param messages   可变参数返送多条消息
         * @return
         */
        public static boolean sendMessage(User sendUser, User sendToUser, String... messages) {
            Jedis jedis = JedisPoolUtils.getJedis();
            try {
                String key = sendToUser.getUsername() + ":msg";
                //将消息的内容加上消息的发送人以 *-* 分割,不能用增强for循环
                for (int i = 0, length_1 = messages.length; i < length_1; i++) {
                    messages[i] = sendUser.getUsername() + "*-*" + messages[i];
                }
                Long lpush = jedis.lpush(key, messages);//返回值是还有多少消息未消费
                log.debug("user {} send message [{}] to {}", sendUser.getUsername(), messages, sendToUser.getUsername());
                log.debug("user {} has {} messages ", sendToUser.getUsername(), lpush);
            } catch (Exception e) {
                log.error("sendMessage error", e);
            } finally {
                jedis.close();
            }
            return true;
        }
    }

    消息的消费者:(采用线程池获取消息,根据接收消息的userName从对应的key中获取对应的消息,并解析消息的key和发送者和内容)

    package cn.xm.redisChat.one2one;
    
    import cn.xm.jwxt.bean.system.User;
    import cn.xm.redisChat.util.JedisPoolUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import redis.clients.jedis.Jedis;
    
    import java.util.List;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @Author: qlq
     * @Description 消息的消费者
     * @Date: 23:44 2018/10/13
     */
    public class RedisMessageConsumer {
        private static final Logger log = LoggerFactory.getLogger(RedisMessageConsumer.class);
    
        /**
         * 参数是初始化线程池子的大小
         */
        private static final ScheduledExecutorService batchTaskPool = Executors.newScheduledThreadPool(2);
    
        /**
         * 消费消息
         *
         * @param consumerUser 接收消息的用户
         */
        public static void consumerMessage(final User consumerUser) {
            final Jedis jedis = JedisPoolUtils.getJedis();
    
            //新建一个线程,线程池获取消息
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    while (true){
                        List<String> messages = jedis.brpop(0, consumerUser.getUsername() + ":msg");//0是timeout,返回的是一个集合,第一个是消息的key,第二个是消息的内容
                        String key = messages.get(0);//第一个是key
                        String message = messages.get(1);//第二个是消息
                        String sendUserName = message.substring(0, message.indexOf("*-*"));//获取消息的发送者
                        message = message.substring(message.indexOf("*-*")+3);//获取消息内容
                        log.debug("ThreadName is {},user {} consumer message {} ,sended by {}", Thread.currentThread().getName(),consumerUser.getUsername(), message, sendUserName);
                    }
                }
            };
            //线程池中获取消息
            //第一个参数是需要执行的任务,第二个参数是第一次的延迟时间,第三个参数是两次执行的时间间隔,第四个参数是时间的单位
            batchTaskPool.scheduleWithFixedDelay(runnable, 3,5, TimeUnit.SECONDS);
        }
    }

    测试类:(lisi和wangwu消费消息)

    package cn.xm.redisChat.one2one;
    
    import cn.xm.jwxt.bean.system.User;
    
    /**
     * @Author: qlq
     * @Description 消息消息
     * @Date: 0:04 2018/10/14
     */
    public class ConsumerMessageApp {
    
        public static void main(String[] args) {
            User sndToUser = new User();
            sndToUser.setUsername("lisi");
    
            User sndToUser2 = new User();
            sndToUser2.setUsername("wangwu");
    
            RedisMessageConsumer.consumerMessage(sndToUser);
            RedisMessageConsumer.consumerMessage(sndToUser2);
        }
    }

    zhangsan给lisi和wangwu发送消息

    package cn.xm.redisChat.one2one;
    
    import cn.xm.jwxt.bean.system.User;
    
    /**
     * @Author: qlq
     * @Description 生产消息测试
     * @Date: 23:59 2018/10/13
     */
    
    public class ProducerMessageApp {
        public static void main(String[] args) {
            User sndUser = new User();
            sndUser.setUsername("zhangsan");
    
            User sndToUser = new User();
            sndToUser.setUsername("lisi");
    
            User sndToUser2 = new User();
            sndToUser2.setUsername("wangwu");
    
            RedisMessageProducer.sendMessage(sndUser, sndToUser, "给李四的消息一", "给李四的消息二");
            RedisMessageProducer.sendMessage(sndUser, sndToUser2, "给王五的消息一", "给王五的消息二");
        }
    }

    1.先启动消费者

    2.启动消费者之后

    消费者控制台如下:

    生产者控制台如下:

     3.再次启动消费者之后

    消费者控制台:

     

    生产者控制台:

    至此实现了简单的一对一聊天,实际上就是简单的一个用户给另一个用户发送消息。上面采用这种方式实现的即使用户上线也会接受之前未接受的消息。只有BRPOP之后消息才会消失。

    实际中可以根据需求进行实际的开发,实际中有消息类型、内容等。

    有时间的话可以用kindeditor实现一个简单的一对一web聊天系统,这个功能待完成。==============

    2.群聊功能(基于发布/订阅实现)

    群聊的思路:采用发布订阅模式实现(publish和subscribe实现)
    1.每个channel都有一个频道,每一个channel代表一个群,用户每次订阅这个房间都代表进入这个群,可以发送与接收消息
    2.发送者每次发送消息都需要先进入房间,也就是订阅channel,之后可以向该频道发送消息
    3.接收者需要先进入房间,也就是订阅channel,然后接收消息

    也就是不管发送消息与接收消息,都需要订阅channel进入房间。用户进入某个房间可以存入到zSet,每次进入某个房间和发送消息先判断是否已经进入某个房间。比如:
    房间room1,则channel就是room1,保存其成员的set就是room1members。

     

    总的来说:

        每个房间都是一个channel,进入房间的成员订阅该channel。每个房间的成员保存在一个zset中,key可以定义为roomName+"users"。

        用户退出房间的时候需要退出该房间,也就是退订该channel,同时在zset中移除该成员。(退订只能调用JedisPubSub的unsubscribe实现)。

    也就是一个房间对应的信息如下:

      一个channel,名称为  roomName    发送的群消息到这个channel

      一个zset,对应的key为roomName+"users",保存的是进入该房间的用户,用户退出房间需要借助退出房间的消息以及退订实现

      发送群聊消息直接publish消息到该roomName即可。

    下面上代码:

    RoomUtil   用户进入某个房间与退出某个房间。退出房间需要发出退出房间的信号(也就是发送一条退订的消息)

    package cn.xm.redisChat.groupChat;
    
    import cn.xm.jwxt.bean.system.User;
    import cn.xm.redisChat.util.JedisPoolUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import redis.clients.jedis.Jedis;
    
    /**
     * @Author: qlq
     * @Description 进入房间的工具类(订阅某个channel,代表进入房间)
     * @Date: 16:15 2018/10/14
     */
    public class RoomUtil {
    
        private static final Logger log = LoggerFactory.getLogger(RoomUtil.class);
    
        /**
         * 进入房间
         *
         * @param user     用户
         * @param roomName
         */
        public static void enterRoom(User user, String roomName) {
            Jedis jedis = JedisPoolUtils.getJedis();
            String username = user.getUsername();
            Boolean sismember = jedis.sismember(roomName + "users", username);
            if (!sismember) {
                jedis.sadd(roomName + "users", username);
                log.info("{} 已经成功进入房间  {}!", username, roomName);
            } else {
                log.info("{} 已经进入房间,不能重复进入!", username);
            }
        }
    
        /**
         * 退出房间
         *
         * @param user     用户
         * @param roomName 房间名称
         */
        public static void exitRoom(User user, String roomName) {
            Jedis jedis = JedisPoolUtils.getJedis();
            String username = user.getUsername();
            Boolean sismember = jedis.sismember(roomName + "users", username);
            if (sismember) {
                //从成员组中移除
                jedis.srem(roomName + "users", username);
                //发送退订信号(房间内的成员收到该信号后退订)
                String exitSignal = user.getUsername() + ":exit:" + roomName;
                jedis.publish(roomName, exitSignal);
                log.info("{} 已经发出移除房间  {}的信号!", username, roomName);
            } else {
                log.info("{} 已经不在房间内!", username);
            }
        }
    
        /**
         * 判断用户是否在某个房间
         *
         * @param user
         * @param roomName
         * @return
         */
        public static boolean userIsInRoom(User user, String roomName) {
            Jedis jedis = JedisPoolUtils.getJedis();
            return jedis.sismember(roomName + "users", user.getUsername());
        }
    }

    消息生产者:(publish发布消息到指定的channe)

    package cn.xm.redisChat.groupChat;
    
    import cn.xm.jwxt.bean.system.User;
    import cn.xm.redisChat.util.JedisPoolUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import redis.clients.jedis.Jedis;
    
    /**
     * @Author: qlq
     * @Description 消息生产者
     * @Date: 18:09 2018/10/14
     */
    public class MessageProducer {
        private static final Logger log = LoggerFactory.getLogger(MessageProducer.class);
    
        public static void produceMsg(final User sendUser, String roomName, String... messages) {
            //发送消息
            Jedis jedis = JedisPoolUtils.getJedis();
            for (int i = 0, length_1 = messages.length; i < length_1; i++) {
                String msg = sendUser.getUsername() + "*-*" + messages[i];
                log.debug(msg);
                jedis.publish(roomName, msg);//发送消息
            }
        }
    }

    消息消费者:

      开启线程获取消息,如果收到的是自己退订的信号则自己退出房间(取消订阅该channel),订阅之后线程会一直阻塞,退订之后才会结束线程,也就是局部线程t会一直阻塞,直到收到退订信号之后才会结束线程(也就不再获取消息)

    package cn.xm.redisChat.groupChat;
    
    import cn.xm.jwxt.bean.system.User;
    import cn.xm.redisChat.util.JedisPoolUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisPubSub;
    
    /**
     * @Author: qlq
     * @Description 消息消费者(订阅频道消费消息)
     * @Date: 16:12 2018/10/14
     */
    public class MessageConsumer {
        private static final Logger log = LoggerFactory.getLogger(MessageConsumer.class);
    
        public static void consumerMsg(final User user, final String roomName) {
            if (!RoomUtil.userIsInRoom(user, roomName)) {
                RoomUtil.enterRoom(user, roomName);
            }
    
            final Jedis jedis = JedisPoolUtils.getJedis();
            //新建一个线程,线程池获取消息
            Thread t = new Thread() {
                @Override
                public void run() {
                    jedis.subscribe(new JedisPubSub() {
                        @Override
                        public void onMessage(String channel, String message) {
                            //如果收到退订信号就退订频道
                            String exitSignal = user.getUsername() + ":exit:" + roomName;
                            if (exitSignal.equals(message)) {
                                unsubscribe(channel);
                                log.info("============" + exitSignal + "============,channel->{}", channel);
                            } else if (!message.contains(":exit:")) {
                                log.info("{} consume msg:{},room is->{}", user.getUsername(), message, channel);
                            }
                        }
    
                        @Override
                        public void unsubscribe(String... channels) {
                            log.info("==============unsubscribe {}========", channels);
                            super.unsubscribe(channels);
                        }
                    }, roomName);
                }
            };
            t.start();
        }
    }

    测试类:

    package cn.xm.redisChat.groupChat;
    
    import cn.xm.jwxt.bean.system.User;
    import org.junit.Test;
    
    /**
     * @Author: qlq
     * @Description
     * @Date: 21:46 2018/10/14
     */
    public class MsgProduceApp {
        public static void main(String[] args) {
            String roomName1 = "room1", roomName2 = "room2";
            User sendUser = new User();
            sendUser.setUsername("zhangsan");
    
            MessageProducer.produceMsg(sendUser, roomName1, "消息一", "消息二");
            MessageProducer.produceMsg(sendUser, roomName2, "消息一一", "消息二二");
        }
    
        /**
         * 将用户lisi移除房间2
         */
        @Test
        public void fun2() {
            User user = new User();
            user.setUsername("lisi");
            RoomUtil.exitRoom(user, "room2");
        }
    }
    package cn.xm.redisChat.groupChat;
    
    import cn.xm.jwxt.bean.system.User;
    
    /**
     * @Author: qlq
     * @Description
     * @Date: 18:19 2018/10/14
     */
    public class MsgConsumeApp {
    
        public static void main(String[] args) {
            String roomName1 = "room1", roomName2 = "room2";
            User sendUser = new User();
            sendUser.setUsername("lisi");
            User sendUser2 = new User();
            sendUser2.setUsername("wangwu");
    
            MessageConsumer.consumerMsg(sendUser, roomName1);
            MessageConsumer.consumerMsg(sendUser, roomName2);
    
            MessageConsumer.consumerMsg(sendUser2, roomName2);
        }
    }

    测试过程如下:

    1.先调用MsgConsumeApp订阅房间

    2.调用MsgProduceApp生产消息

    生产者控制台:

    消费者控制台:

     3.调用fun2 退出房间:

    消费者控制台:

     4.再次调用生产者生产消息:(lisi不接收房间2的消息)

       至此完成了群聊功能,实际上群聊还可以用线程池处理接收消息的线程,暂时用远程的Thread处理。

  • 相关阅读:
    Getting Started with MongoDB (MongoDB Shell Edition)
    Ioc
    BsonDocument
    Find or Query Data with C# Driver
    Insert Data with C# Driver
    Connect to MongoDB
    What's the difference between returning void and returning a Task?
    Import Example Dataset
    jQuery来源学习笔记:整体结构
    Word文件交换的电脑打开字体、排版变化的原因和解决方法!
  • 原文地址:https://www.cnblogs.com/qlqwjy/p/9784956.html
Copyright © 2020-2023  润新知