Redis实现多人多聊天室功能

本文为大家分享了Redis支持多人多聊天室功能的设计代码,供大家参考,具体内容如下

设计原理

左边的一个数据域,代表两个聊天室,聊天室id分别是827,729

在聊天室827里,有2个人,分别是jason22,jeff24他们分别已经阅读过聊天室内的id为5和6的消息

右边的一个数据域,代表了用户在不同的聊天室,jason22参与了827与729聊天室,在这两个聊天室里,他分别阅读到了id为5和id为10的消息

另外827聊天室内id为5的消息与729聊天室内id为5的消息不一样。

同时还有三个域
msgs:chatid
这是一个zset,有序集合,member是消息体,score是消息id
代表的是某个聊天室内已经发出的消息
另外 这里面存的是有用的消息,已经被所有人都阅读的消息就会被删除

ids:chatid
是一个String型的数据,里面放的是最新的消息的编号(发消息时,自增这个字段,即可获得最新的值)

ids:chat:
是一个String型的数据,里面放的是最新的聊天室的编号(创建聊天室时,自增这个字段)

代码

OK 开始看代码

public String createChat(Jedis conn, String sender, Set<String> recipients, String message) {
//启动的时候redis里是没有ids:chat:这个键的
//自增之后返回1
    String chatId = String.valueOf(conn.incr("ids:chat:"));
    return createChat(conn, sender, recipients, message, chatId);
  } 

  /**
  *
  * @param conn
  * @param sender 发送消息的人
  * @param recipients 接受消息的人
  * @param message 待发送的消息
  * @param chatId 聊天室的编号
  * @return
  */
  public String createChat( Jedis conn, String sender,
    Set<String> recipients, String message, String chatId){
  //自己发的消息 自己也能接受到
    recipients.add(sender); 

    Transaction trans = conn.multi();
    for (String recipient : recipients){
  //聊天室的成员 最开始时 都阅读的是0号信息
      trans.zadd("chat:" + chatId, 0, recipient);
  //记录每个人参加的聊天室
      trans.zadd("seen:" + recipient, 0, chatId);
    }
    trans.exec(); 

    return sendMessage(conn, chatId, sender, message);
  } 

  public String sendMessage(Jedis conn, String chatId, String sender, String message) { 

  //锁住聊天室 为啥? 人员变动了咋办
  //这个acquireLock见上一章
    String identifier = acquireLock(conn, "chat:" + chatId);
    if (identifier == null){
      throw new RuntimeException("Couldn't get the lock");
    }
    try {
    //给要发布的消息设定一个最新的编号 第一次时 返回的是1
      long messageId = conn.incr("ids:" + chatId);
      HashMap<String,Object> values = new HashMap<String,Object>();
      values.put("id", messageId);
      values.put("ts", System.currentTimeMillis());
      values.put("sender", sender);
      values.put("message", message);
      String packed = new Gson().toJson(values); 

      //某个聊天室的消息列表
      //最旧的消息----消息json
      //默认的zset是按照score的值从小到大排序
      conn.zadd("msgs:" + chatId, messageId, packed);
    }finally{
      releaseLock(conn, "chat:" + chatId, identifier);
    }
    return chatId;
  }

发消息现在就OK了,剩下的就是用户去拉取未读的消息了。这个比较麻烦,恩,相当的麻烦

 @SuppressWarnings("unchecked")
  public List<ChatMessages> fetchPendingMessages(Jedis conn, String recipient) { 

  //获得用户在各个聊天室 已经看到的最新消息的id
  //有几个聊天室 seenSet的size就是几
    Set<Tuple> seenSet = conn.zrangeWithScores("seen:" + recipient, 0, -1);
    List<Tuple> seenList = new ArrayList<Tuple>(seenSet); 

    Transaction trans = conn.multi();
    for (Tuple tuple : seenList){
      String chatId = tuple.getElement();
      int seenId = (int)tuple.getScore();
      //获取每个聊天室里 未读的所有消息
      //min 和 max 可以是 -inf 和 +inf
      trans.zrangeByScore("msgs:" + chatId, String.valueOf(seenId + 1), "inf");
    }
    //我参加了几个聊天室 results的长度就是几
    List<Object> results = trans.exec(); 

    //com.google.gson.Gson jar包自己下载吧
    Gson gson = new Gson();
    Iterator<Tuple> seenIterator = seenList.iterator();
    Iterator<Object> resultsIterator = results.iterator(); 

    //用户最后成功拉取的未读消息 存放在chatMessages
    List<ChatMessages> chatMessages = new ArrayList<ChatMessages>();
    List<Object[]> seenUpdates = new ArrayList<Object[]>();
    List<Object[]> msgRemoves = new ArrayList<Object[]>(); 

    //这个大的while循环 用户参与了几个聊天室 就循环几次
    while (seenIterator.hasNext()){
      Tuple seen = seenIterator.next();
      Set<String> messageStrings = (Set<String>)resultsIterator.next();
      if (messageStrings.size() == 0){
      //没有未读的消息
        continue;
      } 

      //代码运行到这里
      //说明 我在某个聊天室 还有未读的消息
      //seedid记录我已经拉取到的消息 初始为0
      int seenId = 0;
      //当前处理的是哪个聊天室
      String chatId = seen.getElement(); 

      List<Map<String,Object>> messages = new ArrayList<Map<String,Object>>(); 

      //我在聊天室未读的消息列表
      for (String messageJson : messageStrings){
        Map<String,Object> message = (Map<String,Object>)gson.fromJson(
          messageJson, new TypeToken<Map<String,Object>>(){}.getType());
        int messageId = ((Double)message.get("id")).intValue(); 

        if (messageId > seenId){
          seenId = messageId;
        }
        message.put("id", messageId);
        //加入到成功拉取的列表里
        messages.add(message);
      }
      //更新我在这个聊天室读到的最新消息
      conn.zadd("chat:" + chatId, seenId, recipient); 

      //记录我在某个聊天室读到的最新记录
      seenUpdates.add(new Object[]{"seen:" + recipient, seenId, chatId}); 

      //取出第0个member-score
      Set<Tuple> minIdSet = conn.zrangeWithScores("chat:" + chatId, 0, 0);
      //为啥删除呢? 每个聊天室是一个zset表 第一条记录代表的就是 所有用户至少都读了的消息
      if (minIdSet.size() > 0){
      Tuple tuple=minIdSet.iterator().next();
      System.out.println("要删除的 tuple:"+tuple.getElement()+"--"+tuple.getScore());
        msgRemoves.add(new Object[]{"msgs:" + chatId, tuple.getScore()});
      }
      chatMessages.add(new ChatMessages(chatId, messages));
    } 

    trans = conn.multi();
    for (Object[] seenUpdate : seenUpdates){
      trans.zadd(
        (String)seenUpdate[0],
        (Integer)seenUpdate[1],
        (String)seenUpdate[2]);
    }
    for (Object[] msgRemove : msgRemoves){
      trans.zremrangeByScore(
        (String)msgRemove[0], 0, ((Double)msgRemove[1]).intValue());
    }
    trans.exec(); 

    //返回的是我这次拉取获得的 最新的消息
    return chatMessages;
  }

OK,咱们看看测试代码:

package redisinaction; 

import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set; 

import org.junit.BeforeClass;
import org.junit.Test; 

import jedis.redis_in_action.Chapter06;
import jedis.redis_in_action.Chapter06.ChatMessages;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple; 

/**
 * This class is used for ...
 * @author dlf(460795365@qq.com)
 * @version 1.0, 2016年10月17日 下午10:15:58
 */
public class Chapter06Test {
  static Jedis conn = null;
  static Chapter06 c=null; 

  @BeforeClass
  public static void initConn(){
    System.out.println("test before");
    conn = new Jedis("10.150.0.80");
    conn.auth("dlf123123"); 

    c=new Chapter06();
  } 

  @Test
   public void testMultiRecipientMessaging() {
      System.out.println("\n----- testMultiRecipientMessaging -----");
      conn.del("ids:chat:", "msgs:1", "ids:1", "seen:joe", "seen:jeff", "seen:jenny"); 

      System.out.println("Let's create a new chat session with some recipients...");
      Set<String> recipients = new HashSet<String>();
      recipients.add("jeff");
      recipients.add("jenny");
      String chatId = c.createChat(conn, "joe", recipients, "message 1");
      System.out.println("Now let's send a few messages...");
      for (int i = 2; i < 5; i++){
        c.sendMessage(conn, chatId, "joe", "message " + i);
      }
      System.out.println();
      System.out.println("看看消息库");
      //消息库里的所有消息
      Set<Tuple> messageFromBase=conn.zrangeWithScores("msgs:"+chatId, 0, -1);
      Iterator<Tuple> iterator=messageFromBase.iterator();
      while(iterator.hasNext()){
        Tuple tuple=iterator.next();
        System.out.println(tuple.getElement()+" --  "+tuple.getScore());
      }
      System.out.println("And let's get the messages that are waiting for jeff and jenny..."); 

      List<ChatMessages> r1 = c.fetchPendingMessages(conn, "jeff");
      List<ChatMessages> r2 = c.fetchPendingMessages(conn, "jenny");
    //当我拉取了joe的未读信息后 就会删除msgs:1里面的信息
    //为什么?想明白了么?
      List<ChatMessages> r3 = c.fetchPendingMessages(conn, "joe");
      System.out.println("They are the same? " + r1.equals(r2)); 

      System.out.println("Those messages are:");
      for(ChatMessages chat : r1){
        System.out.println(" chatId: " + chat.chatId);
        System.out.println("  messages:");
        for(Map<String,Object> message : chat.messages){
          System.out.println("   " + message);
        }
      } 

      System.out.println("看看还有没");
      messageFromBase=conn.zrangeWithScores("msgs:"+chatId, 0, -1);
       iterator=messageFromBase.iterator();
      while(iterator.hasNext()){
        Tuple tuple=iterator.next();
        System.out.println(tuple.getElement()+" --  "+tuple.getScore());
      }
      conn.del("ids:chat:", "msgs:1", "ids:1", "seen:joe", "seen:jeff", "seen:jenny");
    } 

}

搞定了,大家不妨把代码复制一份,自己看看
下面的是测试的结果

test before

----- testMultiRecipientMessaging -----
Let's create a new chat session with some recipients...
Now let's send a few messages...

看看消息库
{"sender":"joe","id":1,"message":"message 1","ts":1477276890018} --  1.0
{"sender":"joe","id":2,"message":"message 2","ts":1477276890113} --  2.0
{"sender":"joe","id":3,"message":"message 3","ts":1477276890115} --  3.0
{"sender":"joe","id":4,"message":"message 4","ts":1477276890116} --  4.0
And let's get the messages that are waiting for jeff and jenny...
要删除的 tuple:jenny--0.0
要删除的 tuple:joe--0.0
要删除的 tuple:jeff--4.0
They are the same? true
Those messages are:
  chatId: 1
    messages:
      {sender=joe, id=1, message=message 1, ts=1.477276890018E12}
      {sender=joe, id=2, message=message 2, ts=1.477276890113E12}
      {sender=joe, id=3, message=message 3, ts=1.477276890115E12}
      {sender=joe, id=4, message=message 4, ts=1.477276890116E12}
看看还有没

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • javascript和jQuery实现网页实时聊天的ajax长轮询

    介绍 大家都知道,HTTP协议是一个属于应用层的面向对象的协议,HTTP 协议一共有五大特点: 1.支持客户/服务器模式; 2.简单快速; 3.灵活; 4.无连接; 5.无状态. 所以一次的请求都是一个单独的事件,和前后都没有联系.所以我们在解决网页实时聊天时就遇到一个问题,如何保证与服务器的长时间联系,从而源源不段地获取信息. 一直以来的方式无非有这么几种: 1.长连接,即服务器端不断开联系,PHP服务器端用ob系列函数来不停的读取输出,但是相当耗费服务器资源. 2.Flash socket,

  • HTML5基于Tomcat 7.0实现WebSocket连接并实现简单的实时聊天

    1.什么是WebSocket? WebSocket 是一种自然的全双工.双向.单套接字连接.使用WebSocket,你的HTTP 请求变成打开WebSocket 连接(WebSocket 或者WebSocket over TLS(TransportLayer Security,传输层安全性,原称"SSL"))的单一请求,并且重用从客户端到服务器以及服务器到客户端的同一连接.WebSocket 减少了延迟,因为一旦建立起WebSocket 连接,服务器可以在消息可用时发送它们.例如,和轮

  • ASP.NET 使用application与session对象写的简单聊天室程序

    ASP.Net中有两个重要的对象,一个是application对象,一个是session对象. Application:记录应用程序参数的对象,该对象用于共享应用程序级信息. Session:记录浏览器端的变量对象,用来存储跨网页程序程序的变量或者对象. 说实话,写了快一年的asp.net,application对象还真没怎么用过.看了看书,根据这两个对象的特性写了一个简单的聊天室程序.真的是非常的简陋. 我的思路是,有两个页面Default页和ChatRoom页,页面布局如图: Default

  • Asp.net SignalR创建实时聊天应用程序

    一.概述 使用 ASP.NET 那么 SignalR 2 创建一个实时聊天应用程序.将 SignalR 添加 MVC 5 应用程序中,并创建聊天视图发送并显示消息. 在Demo中,将学习SignalR 开发任务包括 ︰ 向 MVC 5 应用程序添加那么 SignalR 图书馆. 创建集线器和浩然启动类,以将内容推送到客户端. 使用 web 页中的那么 SignalR jQuery 库发送邮件并显示更新从集线器. 下面的屏幕快照显示在浏览器中运行的已完成的聊天应用程序. 二.实现 创建一个 ASP

  • Asp.net使用SignalR实现酷炫端对端聊天功能

    一.引言 在前一篇文章已经详细介绍了SignalR了,并且简单介绍它在Asp.net MVC 和WPF中的应用.在上篇博文介绍的都是群发消息的实现,然而,对于SignalR是为了实时聊天而生的,自然少了不像QQ一样的端对端的聊天了.本篇博文将介绍如何使用SignalR来实现类似QQ聊天的功能. 二.使用SignalR实现端对端聊天的思路 在介绍具体实现之前,我先来介绍了使用SignalR实现端对端聊天的思路.相信大家在前篇文章已经看到过Clients.All.sendMessage(name,

  • 使用Meteor配合Node.js编写实时聊天应用的范例

    我经常见到被拿来与Derby.js做比较的框架是Meteor.js. 与Derby相似的是,它也能在多个客户端下实时更新views, 尽管做法上可能跟Derby有点不同. Derby可以较容易的使用多种数据库, 而Meteor则只亲近于MongoDB. 事实上, 通过如Mongoose客户端接入数据库的API与你在服务端所期望的已经非常接近了. 虽然现在meteor是个有一些缺点和争议的框架, 但Meteor看起来是非常有趣的选择用来建立有实时需求的应用. 个人还是喜欢Derby基于传统回调的编

  • Asp.net使用SignalR实现聊天室的功能

    一.引言 在前一篇文章<Asp.net使用SignalR实现酷炫端对端聊天功能>中,我向大家介绍了如何实现实现端对端聊天的功能的,在这一篇文章中将像大家如何使用SignalR实现群聊这样的功能. 二.实现思路 要想实现群聊的功能,首先我们需要创建一个房间,然后每个在线用户可以加入这个房间里面进行群聊,我们可以为房间设置一个唯一的名字来作为标识.那SignalR类库里面是否有这样现有的方法呢?答案是肯定的. // IGroupManager接口提供如下方法 // 作用:将连接ID加入某个组 //

  • asp.net mvc signalr简单聊天室制作过程分析

    signalr的神奇.实用很早就知道,但一直都没有亲自去试用,仅停留在文章,看了几篇简单的介绍文字,感觉还是很简单易用的. 由于最后有个项目需要使用到它,所以就决定写个小程序测试一下,实践出真知:别人写的文章,由于环境(版本等)不同,还是或多或少存在一些出入的. 环境:vs2013 / asp.net mvc 5 / signalr 2.2.1 / jquery 1.10.2 先上两个效果图: 系统会自动给加入聊天室的人员分配一个ID,是该人员的唯一标识(绿色为当前用户说的话,橙色为当前用户之外

  • ASP.NET网站聊天室的设计与实现(第3节)

    大家都玩过网站聊天室吧,那知道它是怎么实现的吗? 今天我们就来设计一个网站聊天室,用户输入用户名登陆聊天室,采用框架结构实现. 学习内容: 第一步,聊天室首页与简单计数器设计 1.打开VS2008.在"解决方案'101'下新建网站,命名为Chatroom.默认首页文件为Default.aspx. 2.为Default.aspx添加窗体控件,切换到"设计"视图,从左侧工具箱标准组中拖出2个Lable控件,1个Textbox控件,一个Button控件,最后给输入昵称的Textbo

  • 值得分享的php+ajax实时聊天室

    非常经典的一款php+ajax实时聊天室,其中使用PHP文件保存聊天记录,按天划分,PHP实现聊天的功能只有一个文件,整合了PHP与AJAX技术,也就是说只要运行这一个文件就可以启动PHP的聊天室了,关于代码上面也是非常的简单,但是实现了聊天室一般的功能,聊天时的昵称,更改昵称的颜色,聊天字号大小,字体,加粗,窗体的变大变小等等,如果你想搞个聊天室来玩玩,这个源码完全可以满足普通的需求. 具体的效果看如下图: 关键代码: <?php header('content-type:text/html;

随机推荐