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处理。
Redis实现聊天功能的更多相关文章
- [Asp.net 开发系列之SignalR篇]专题二:使用SignalR实现酷炫端对端聊天功能
一.引言 在前一篇文章已经详细介绍了SignalR了,并且简单介绍它在Asp.net MVC 和WPF中的应用.在上篇博文介绍的都是群发消息的实现,然而,对于SignalR是为了实时聊天而生的,自然少 ...
- Redis多机功能介绍
Redis多机功能目的:以单台Redis服务器过渡到多台Redis服务器 Redis单机在生产环境中存在的问题 1.内存容量不足 Redis使用内存来存书数据库中的数据,但是对于一台机器来说,硬件的内 ...
- Linux下p2p的聊天功能实现
Linux下p2p的聊天功能实现细节 Do one thing at a time, and do well. 今天闲着没事,写一个P2P的点对点的聊天功能的小程序,我觉得对网络编程初学者的学习很有用 ...
- MVC实现类似QQ的网页聊天功能-ajax(下)
此篇文章主要是对MVC实现类似QQ的网页聊天功能(上)的部分代码的解释. 首先说一下显示框的滚动条置底的问题: 结构很简单一个大的div(高度一定.overflow:auto)包含着两个小的div第一 ...
- MingQQ v1.0高仿版开源了,使用WebQQ协议实现了QQ客户端基本的聊天功能...
MingQQ v1.0高仿版开源了,使用WebQQ协议实现了QQ客户端基本的聊天功能... MingQQ目前支持的功能如下:1.支持普通方式登录.验证码方式登录.注销.保持在线.改变在线状态.2.支持 ...
- 最新的chart 聊天功能( webpack2 + react + router + redux + scss + nodejs + express + mysql + es6/7)
请表明转载链接: 我是一个喜欢捣腾的人,没事总喜欢学点新东西,可能现在用不到,但是不保证下一刻用不到. 我一直从事的是依赖angular.js 的web开发,但是我怎么能一直用它呢?看看最近火的一塌糊 ...
- Spring 学习——基于Spring WebSocket 和STOMP实现简单的聊天功能
本篇主要讲解如何使用Spring websocket 和STOMP搭建一个简单的聊天功能项目,里面使用到的技术,如websocket和STOMP等会简单介绍,不会太深,如果对相关介绍不是很了解的,请自 ...
- Redis的各项功能解决了哪些问题?
先看一下Redis是一个什么东西.官方简介解释到:Redis是一个基于BSD开源的项目,是一个把结构化的数据放在内存中的一个存储系统,你可以把它作为数据库,缓存和消息中间件来使用.同时支持string ...
- Redis的事务功能详解
Redis的事务功能详解 MULTI.EXEC.DISCARD和WATCH命令是Redis事务功能的基础.Redis事务允许在一次单独的步骤中执行一组命令,并且可以保证如下两个重要事项: >Re ...
随机推荐
- day5 列表
列表 查 索引(下标),默认从0开始 切片 .count 查某个元素的出现次数 .index 根据内容找元素的对应索引位置 增加 .append() 追加在最后 .insert(index,'内容') ...
- idea 项目打包发布
clean install -Dmaven.test.skip=true -pl 项目名(maven为准) -am -amd
- 【Gym 100015B】Ball Painting(DP染色)
题 There are 2N white balls on a table in two rows, making a nice 2-by-N rectangle. Jon has a big pai ...
- POJ1163(简单的DP)
题目链接:http://poj.org/problem?id=1163 Description 73 88 1 02 7 4 44 5 2 6 5 (Figure 1) Figure 1 shows ...
- Python面向对象编程和模块
在面向对象编程中,你编写表示现实世界中的事物和情景的类,并基于这些类来创建对象. 编写类时,你定义一大类对象都有的通用行为.基于类创建对象时,每个对象都自动具备这种通用行为,然后根据需要赋予每个对象独 ...
- LOJ#2095 选数
给定n,k,l,r 问从[l, r]中选出n个数gcd为k的方案数. 解:稍微一想就能想到反演,F(x)就是[l, r]中x的倍数个数的n次方. 后面那个莫比乌斯函数随便怎么搞都行,当然因为这是杜教筛 ...
- A1056. Mice and Rice
Mice and Rice is the name of a programming contest in which each programmer must write a piece of co ...
- CodeBlocks: 生成的exe文件自定义一个图标
CodeBlocks生成的exe文件的图标默认是系统图标,如何自定义一个漂亮的小图标呢? 我是C菜鸟,平时只用CodeBlocks练习c,也不开发什么软件,这个问题就难倒我了. 到网上搜索了一下,发现 ...
- 2018.9南京网络预选赛(J)
传送门:Problem J https://www.cnblogs.com/violet-acmer/p/9720603.html 变量解释: need[ i ] : 第 i 个房间含有的旧灯泡个数. ...
- python爬虫 bs4_4select()教程
http://www.w3.org/TR/CSS2/selector.html 5 Selectors Contents 5.1 Pattern matching 5.2 Selector synta ...