在Redis中,有个Pub/Sub,他的主要的工作流程如:

redis订阅一个模式频道如:chat_*,然后由小a想找人聊天了,就发送一个消息“现在有人聊天吗?chat_a”,末尾的chat_a为标识,表示你要在chat_* 这个圈子里面说。这个时候,chat_*这个圈子的管理员,就会对所有加入这个圈子的人发送一条消息。消息内容就是小a说的话。说白了,就是有个大喇叭,你说话声音不够大,但是你想让所有人都听到你的消息,那么你就要先对喇叭说话,然后喇叭把你的话扩散。。。。

还是根据代码说,直接描述比抽象函数还要抽象。

首先我们先在配置文件里面配置下订阅的频道对应的监听:

   <!--chat-->
<bean id="msgListener" class="com.anhoo.util.MyMsgListener"/> <bean id="listenerContainer" class="org.springframework.data.redis.listener.RedisMessageListenerContainer">
<property name="connectionFactory" ref="jedisConnFactory"/>
<property name="messageListeners">
<map>
<entry key-ref="msgListener" value-ref="patternTopic"/>
</map>
</property>
</bean> <bean id="patternTopic" class="org.springframework.data.redis.listener.PatternTopic">
<constructor-arg value="chat_*"/>
</bean>

2行是根据监听消息的接口写的监听类,当监听到有消息的时候,就会调用onMessage类

public class MyMsgListener implements MessageListener {
@Autowired
SubService subService;
@Override
public void onMessage(Message message, byte[] bytes) {
subService.isCall(message);
// System.out.println(SerializeUtil.unserialize(bytes).toString());
System.out.println("当前的Message值为:" + message.toString());
}
}

4-11行 是pub/sub监听配置,redis的配置文件是  jedisConnFactory,监听的频道模式为 patternTopic,监听到的消息处理类为 msgListener

13-15行 是监听频道的设置

消息的entity为:

public class MessageEntity implements Serializable {

    private String user;
private String content; //省略get set
}

Controller层代码,主要有三个方法,sendMessage 是在页面中发送发送消息,(前台有一个ajax方法一直在请求)callMsg 当监听到新的消息的时候,返回监听到的消息,addChatUser 当有新的用户加入的时候,做记录,1是方便以后根据用户返回数据,2是防止重复的用户名。

@Controller
@RequestMapping("/back")
public class ChatController { @Autowired
PubService pubService; @Autowired
SubService subService; @Autowired
ChatService chatService; @ResponseBody
@RequestMapping("/send")
public ResultMsg sendMessage(MessageEntity messageEntity) {
ResultMsg resultMsg = new ResultMsg();
if (messageEntity != null && !messageEntity.getUser().equals("") && !messageEntity.getContent().equals("")) {
pubService.sendMessage(messageEntity);
resultMsg.setMsg("发送成功!");
resultMsg.setCode(ResultCode.SUCCESS);
return resultMsg;
}
resultMsg.setMsg("输入信息有误!");
resultMsg.setCode(ResultCode.FAIL);
return resultMsg;
} @ResponseBody
@RequestMapping("/callBack")
public ResultMsg callMsg(String user) throws InterruptedException {
ResultMsg resultMsg = new ResultMsg();
Logger logger = LogManager.getLogger(ChatController.class);
MessageEntity message;
message = subService.callBack(user);
if (message != null) {
resultMsg.setCode(ResultCode.SUCCESS);
resultMsg.setContent(message);
return resultMsg;
} else {
resultMsg.setCode(ResultCode.FAIL);
return resultMsg;
}
} @ResponseBody
@RequestMapping("/join")
public ResultMsg addChatUser(String user) {
ResultMsg resultMsg = new ResultMsg();
if (chatService.addUser(user) > 0) {
resultMsg.setCode(ResultCode.SUCCESS);
} else {
resultMsg.setCode(ResultCode.FAIL);
resultMsg.setMsg("昵称已经存在,请重新输入!");
}
return resultMsg;
}
}

Service 有三个 接口类为:

ChatService
int addUser(String user); PubService
void sendMessage(MessageEntity messageEntity); SubService
void isCall(Message message);
MessageEntity callBack(String user) throws InterruptedException;

ChatService接口的具体方法代码

@Service
public class ChatServiceImpl implements ChatService { @Autowired
StringRedisTemplate stringRedisTemplate; @Override
public int addUser(String user) {
//判断userList 已经其中的用户是否已经存在
if (stringRedisTemplate.hasKey("userList") && stringRedisTemplate.opsForZSet().score("userList", user) != null) {
return 0;
} else {
//增加新的用户,但是要判断下,是否是第一次刚启动的时候
int currentInidex;
if (stringRedisTemplate.hasKey("msgList")) {
currentInidex = (int) (-1 - stringRedisTemplate.opsForList().size("msgList"));
} else {
currentInidex = -1;
}
stringRedisTemplate.opsForZSet().add("userList", user, currentInidex);
return 1;
}
}
}

PubService接口的具体方法代码:

@Service
public class PubServiceImpl implements PubService { @Autowired
StringRedisTemplate stringRedisTemplate; @Override
public void sendMessage(MessageEntity messageEntity) {
//消息的频道为chat_*
String channel = "chat_";
String content = messageEntity.getContent();
//使得发送消息的 频道为chat_用户名 例如chat_jack 为了后面能根据这个得到 jack用户
stringRedisTemplate.convertAndSend(channel + messageEntity.getUser(), content);
}
}

SubService接口的具体方法代码:

@Service
public class SubServiceImpl implements SubService { @Autowired
StringRedisTemplate stringRedisTemplate; @Autowired
JedisConnectionFactory jedisConnFactory; @Override
public void isCall(Message message) {
MessageEntity messageEntity = new MessageEntity();
//请参考配置文件,本例中key,value的序列化方式均为string。
//其中key必须为stringSerializer。和redisTemplate.convertAndSend对应
messageEntity.setUser(stringRedisTemplate.getStringSerializer().deserialize(message.getChannel()).split("_")[1]);
messageEntity.setContent(stringRedisTemplate.getValueSerializer().deserialize(message.getBody()).toString());
stringRedisTemplate.opsForList().leftPush("msgList", JSON.toJSONString(messageEntity)); // Jedis jedis = (Jedis) jedisConnFactory.getConnection().getNativeConnection();
// stringRedisTemplate.opsForValue().set("broadcast",jedis.pubsubNumPat().toString() );
// System.out.println(jedis.pubsubNumPat().toString());
// jedis.close(); } @Override
public MessageEntity callBack(String user) throws InterruptedException { //模拟1s 查看一次 不至于一直在连接redis 低于1s的频率连接redis会报错
Thread.sleep(1000);
// String msgTxt = stringRedisTemplate.opsForList().rightPop("msgList");
//获取当前user 对应的消息 坐标值
Double index = stringRedisTemplate.opsForZSet().score("userList", user); long l = new Double(index).longValue();
if (stringRedisTemplate.hasKey("msgList")) {
String msgTxt = stringRedisTemplate.opsForList().index("msgList", l); //只有当msgList 有新的消息的时候,才会获取消息
if (msgTxt != null && msgTxt != "") {
// list.remove(user);
MessageEntity messageEntity = JSON.parseObject(msgTxt, MessageEntity.class); //消息坐标加-1
stringRedisTemplate.opsForZSet().incrementScore("userList", user, -1);
return messageEntity;
}
}
return null; }
}

service层用到的redis主要是 list和zset,当有用户发送消息的时候,就把消息放到list中,获取第一条可以是: opsForList().index("msgList", -1) ,第二条为:opsForList().index("msgList", -2),第三台为opsForList().index("msgList", -3)……以此类推,又因为我们前端有个ajax一直发送请求,按道理是只要我们list中有消息,我们就把他拿出来,在页面展示。但是这里又不能实时的判断当前是不是所有的用户都获取过一次,而且仅仅只能为一次。这个时候就根据list的长度以及zset的score来判断了。过程为:当有用户加入的时候, 如果是第一个用户,那么就把他的zset的score设为-1,此时list中的消息为空,只有当我们发送一条消息的时候,onMessage做出响应,再把发送的消息存到list中,这个时候,一直发送请求的ajax发现,此时消息的长度为了1(可以通过 opsForList().index("msgList", -1)得到),而且当前用户的score标志为-1,正好他们一致。那么就把这个消息取出来,在前台页面展示,然后把score自自增-1,等待list里面再次有消息放入(长度为2,可以通过opsForList().index("msgList", -2)获取)的时候才满足取出消息的条件。。以此循环;如果不是第一个加入的,就把现在消息的长度放到score中,只有当接受下一条数据的时候才展示。

前台代码。js部分:

$(function () {
$("#user").focus();
}); function loading(user) {
eoooxy.ajax("post", "/back/callBack", {"user":user}, function (r) {
if (!eoooxy.isEmpty(r) && r.code == '100') {
var o = r.content;
var h = "<div style='margin: 10px 20px 10px 20px;'><label>" + o.user + "</label><br><label>" + o.content + "</label></div>";
$("#chatSpace").append(h);
$("#chatSpace")[0].scrollTop = $("#chatSpace")[0].scrollHeight;
//$("#content").focus();
loading(user);
} else {
console.log("当前没有消息,继续请求……");
loading(user);
}
}, "json"/*, function (XMLHttpRequest, status) {
if (status == 'timeout') {//超时,status还有success,error等值的情况
loading();
}
}, 3000*/)
} function chatting() {
if (eoooxy.isEmpty($("#user").val())) {
alert("必须先输入昵称,然后点击开始聊天!");
return false;
}
var data = {"user": $("#user").val()};
eoooxy.ajax("post", "/back/join", data, function (r) {
if (!eoooxy.isEmpty(r) && r.code == '100') {
$("#user").attr("disabled", "disabled");
loading($("#user").val());
} else {
alert(r.msg);
}
}, "json")
} function sendInfo() {
var message = $("#content");
var data = {"user": $("#user").val(), "content": $("#content").val()};
// var data = {"user": "jack", "content": $("#content").val()};
eoooxy.ajax("post", "/back/send", data, function (r) {
if (!eoooxy.isEmpty(r) && r.code == '100') {
message.val('');
$("#chatSpace")[0].scrollTop = $("#chatSpace")[0].scrollHeight;
message.focus();
} else {
alert(r.msg);
} })
}

jsp部分:

<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" %>
<html>
<head>
<%@include file="/WEB-INF/view/common/meta.jsp" %>
<title>聊天室</title>
${f:addJs("resources/js/back/chat.js")}
</head>
<body>
<div style="width: auto;" align="center">
<h1>多人聊天室</h1>
<span>
<input id="user" style="resize: none;outline: none;" placeholder="昵称,必须输入">
<button onclick="chatting()">开始聊天</button>
</span>
<div id="chatSpace"
style="width: 600px;height: 500px; border: solid 1px #CCCCCC; overflow-y: auto;text-align: left"> </div>
<div style="width: 600px; height: 100px;border: solid 1px #CCCCCC; margin-top: 20px;">
<textarea id="content"
style="width: 590px;height: 60px; resize: none;border: 0px;outline: none;margin: 5px;"></textarea>
<button style="float: right;margin-right: 10px;" id="btn_send" onclick="sendInfo()">确定</button>
</div>
</div>
</body>
</html>

以上就是根据pub/sub 以及ajax长连接写的一个在线实时聊天系统(实际上延迟1s),如果有错,请指出,谢谢!

因为这边采用的是ajax长连接(就是一直问:有没有消息啊,有没有消息啊,有的话拿走然后继续问。。),所以会占用资源。如果我们能更好的优化他,我们可以使用H5的新的特性WebSocket来构建实时的聊天系统,具体这边我就不介绍了,因为我也还没搞透彻,没有调查没有发言权。。。

示例代码git连接:https://github.com/eoooxy/anhoo

根据redis的pub/sub机制,写一个即时在线聊天应用的更多相关文章

  1. Redis的Pub/Sub机制存在的问题以及解决方案

    Redis的Pub/Sub机制使用非常简单的方式实现了观察者模式,但是在使用过程中我们发现,它仅仅是实现了发布订阅机制,但是很多的场景没有考虑到.例如一下的几种场景: 1.数据可靠性无法保证 一个re ...

  2. 用c写一个小的聊天室程序

    1.聊天室程序——客户端 客户端我也用了select进行I/O复用,同时监控是否有来自socket的消息和标准输入,近似可以完成对键盘的中断使用. 其中select的监控里,STDOUT和STDIN是 ...

  3. redis的Pub/Sub

    redis的Pub/Sub机制类似于广播架构,Subscriber相当于收音机,可以收听多个channel(频道),Publisher(电台)可以在channel中发布信息. 命令介绍 PUBLISH ...

  4. 用Go语言实现一个简单的聊天机器人

    一.介绍 目的:使用Go语言写一个简单的聊天机器人,复习整合Go语言的语法和基础知识. 软件环境:Go1.9,Goland 2018.1.5. 二.回顾 Go语言基本构成要素:标识符.关键字.字面量. ...

  5. socket实例C语言:一个简单的聊天程序

    我们老师让写一个简单的聊天软件,并且实现不同机子之间的通信,我用的是SOCKET编程.不废话多说了,先附上代码: 服务器端server.c #include <stdio.h> #incl ...

  6. python_way ,day11 线程,怎么写一个多线程?,队列,生产者消费者模型,线程锁,缓存(memcache,redis)

    python11 1.多线程原理 2.怎么写一个多线程? 3.队列 4.生产者消费者模型 5.线程锁 6.缓存 memcache redis 多线程原理 def f1(arg) print(arg) ...

  7. 搞定redis面试--Redis的过期策略?手写一个LRU?

    1 面试题 Redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现? 2 考点分析 1)我往redis里写的数据怎么没了? 我们生产环境的redis怎么经常会丢掉一些数据?写进去了 ...

  8. 用 C# 写一个 Redis 数据同步小工具

    用 C# 写一个 Redis 数据同步小工具 Intro 为了实现 redis 的数据迁移而写的一个小工具,将一个实例中的 redis 数据同步到另外一个实例中.(原本打算找一个已有的工具去做,找了一 ...

  9. 【redis前传】自己手写一个LRU策略 | redis淘汰策略

    title: 自己手写一个LRU策略 date: 2021-06-18 12:00:30 tags: - [redis] - [lru] categories: - [redis] permalink ...

随机推荐

  1. java反射基础

    转载请注明出处:https://i.cnblogs.com/EditPosts.aspx?opt=1最近在接触到框架的底层的时候,遇到了反射,便想好好的学习和总结一下反射,帮助理解java框架的运行流 ...

  2. 02_Python简单爬虫(熊猫直播LOL的up主,谁最强!)

    声明: 本文仅用于Python练手,并无任何恶意攻击行为! # 导入request模块 from urllib import request # 导入re模块 import re class Spid ...

  3. MYSQL语句:创建、授权、查询、修改、统计分析等 二 用户的创建、权限设置、删除

    接着上面一的内容 4.设置更改用户密码 命令格式:SET PASSWORD FOR 'username'@'host'=PASSWORD('newpassword'); 如果是当前登录用户用:SET ...

  4. 机器学习-数据可视化神器matplotlib学习之路(三)

    之前学习了一些通用的画图方法和技巧,这次就学一下其它各种不同类型的图.好了先从散点图开始,上代码: from matplotlib import pyplot as plt import numpy ...

  5. 安装ceres-solver

    安装依赖: sudo apt-get install -y google-mock libboost-all-dev libeigen3-dev libgflags-dev libgoogle-glo ...

  6. Centos修改系统语言

    使用man page帮助时,发现居然是中文的,不过想想即便英语再水,也要逼着自己去适应.于是百度找了一下修改系统语言的方法. 首先使用 locale 命令查看当前的系统语言 然后修改时一般有两种方法, ...

  7. 【Golang】幽灵变量(变量覆盖)问题的一劳永逸解决方法

    背景 在我们公司,测试定位问题的能力在考核中占了一定的比例,所以我们定位问题的主动性会比较高.因为很多开发同学都是刚开始使用golang,所以bug频出,其中又以短变量声明语法导致的错误最多.所以就专 ...

  8. RabbitMQ入门_07_Fanout 与 Topic

    A. 用广播的方式实现发布订阅 参考资料:https://www.rabbitmq.com/tutorials/tutorial-three-java.html Fanout 类型的 Exchange ...

  9. 算法笔记--树的直径 && 树形dp && 虚树 && 树分治 && 树上差分 && 树链剖分

    树的直径: 利用了树的直径的一个性质:距某个点最远的叶子节点一定是树的某一条直径的端点. 先从任意一顶点a出发,bfs找到离它最远的一个叶子顶点b,然后再从b出发bfs找到离b最远的顶点c,那么b和c ...

  10. asp.net Core MVC + form validation + ajax form 笔记

    asp.net Core MVC 有特别处理form,controller可以自己处理model的验证,最大的优势是写form时可以少写代码 先了解tag helper ,这东西就是element上的 ...