本文为大家分享了Redis支持多人多聊天室功能的设计代码,供大家参考,具体内容如下
概述
设计原理
环信IM的功能一直在演化,比如推送功能和聊天室功能是不久前的版本中添加的。
左边的一个数据域,代表两个聊天室,聊天室id分别是827,729
在前面对环信IM有了基本了解后,下来我们通过分析Demo,来看看它是如何使用UI库和SDK库,它的主要结构怎么设计的,从而为开发FSIM做好基础。
在聊天室827里,有2个人,分别是jason22,jeff24他们分别已经阅读过聊天室内的id为5和6的消息
主要数据结构和操作
右边的一个数据域,代表了用户在不同的聊天室,jason22参与了827与729聊天室,在这两个聊天室里,他分别阅读到了id为5和id为10的消息
1. 消息Message
另外827聊天室内id为5的消息与729聊天室内id为5的消息不一样。
对应的数据结构是EMMessage。
同时还有三个域 msgs:chatid 这是一个zset,有序集合,member是消息体,score是消息id 代表的是某个聊天室内已经发出的消息 另外 这里面存的是有用的消息,已经被所有人都阅读的消息就会被删除
按类型分为文字消息EMChatText,图片消息EMChatImage,位置消息EMChatLocation,语音消息EMChatVoice,视频消息EMChatVideo,文件消息EMChatFile,透传消息EMChatCommand(服务器发送指令给客户端做特殊操作);按传送范围,分为单聊eMessageTypeChat,群聊eConversationTypeGroupChat,聊天室eConversationTypeChatRoom等类型。
ids:chatid 是一个String型的数据,里面放的是最新的消息的编号(发消息时,自增这个字段,即可获得最新的值)
在单聊环境下插入一条文本消息的代码示例:
ids:chat: 是一个String型的数据,里面放的是最新的聊天室的编号(创建聊天室时,自增这个字段)
代码
1--EMChatText *txt = [[EMChatText alloc] initWithText:@"test1"];
2--EMTextMessageBody *body = [[EMTextMessageBody alloc] initWithChatObject:txt];
3--EMMessage *message = [[EMMessage alloc] initWithReceiver:_conversation.chatter bodies:@[body]];
4--message.messageType = eMessageTypeChat;
5--message.deliveryState = eMessageDeliveryState_Delivered;
6--[[EaseMob sharedInstance].chatManager insertMessageToDB:message];
OK 开始看代码
消息的操作,除了创建,还有发送,接收在线或者离线消息,消息解析,消息已送达和消息已读回执,这也就是聊天chat的主要操作。
public String createChat(Jedis conn, String sender, SetString 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, SetString 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); HashMapString,Object values = new HashMapString,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; }
注意:登陆成功之后才能进行聊天操作。发消息时,单聊和群聊调用的是统一接口,区别只是要设置下message.isGroup属性。
发消息现在就OK了,剩下的就是用户去拉取未读的消息了。这个比较麻烦,恩,相当的麻烦
异步发送消息接口:
@SuppressWarnings("unchecked") public ListChatMessages fetchPendingMessages(Jedis conn, String recipient) { //获得用户在各个聊天室 已经看到的最新消息的id //有几个聊天室 seenSet的size就是几 SetTuple seenSet = conn.zrangeWithScores("seen:" recipient, 0, -1); ListTuple seenList = new ArrayListTuple(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的长度就是几 ListObject results = trans.exec(); //com.google.gson.Gson jar包自己下载吧 Gson gson = new Gson(); IteratorTuple seenIterator = seenList.iterator(); IteratorObject resultsIterator = results.iterator(); //用户最后成功拉取的未读消息 存放在chatMessages ListChatMessages chatMessages = new ArrayListChatMessages(); ListObject[] seenUpdates = new ArrayListObject[](); ListObject[] msgRemoves = new ArrayListObject[](); //这个大的while循环 用户参与了几个聊天室 就循环几次 while (seenIterator.hasNext()){ Tuple seen = seenIterator.next(); SetString messageStrings = (SetString)resultsIterator.next(); if (messageStrings.size() == 0){ //没有未读的消息 continue; } //代码运行到这里 //说明 我在某个聊天室 还有未读的消息 //seedid记录我已经拉取到的消息 初始为0 int seenId = 0; //当前处理的是哪个聊天室 String chatId = seen.getElement(); ListMapString,Object messages = new ArrayListMapString,Object(); //我在聊天室未读的消息列表 for (String messageJson : messageStrings){ MapString,Object message = (MapString,Object)gson.fromJson( messageJson, new TypeTokenMapString,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 SetTuple 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,咱们看看测试代码:
/*!
@method
@brief 异步方法, 发送一条消息
@discussion 待发送的消息对象和发送后的消息对象是同一个对象, 在发送过程中对象属性可能会被更改. 在发送过程中, willSendMessage:error:和didSendMessage:error:这两个回调会被触发
@param message 消息对象(包括from, to, body列表等信息)
@param progress 发送多媒体信息时的progress回调对象
@result 发送的消息对象(因为是异步方法, 不能作为发送完成或发送成功失败与否的判断)
*/
- (EMMessage *)asyncSendMessage:(EMMessage *)message
progress:(id)progress;
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..."); SetString recipients = new HashSetString(); 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("看看消息库"); //消息库里的所有消息 SetTuple messageFromBase=conn.zrangeWithScores("msgs:" chatId, 0, -1); IteratorTuple 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..."); ListChatMessages r1 = c.fetchPendingMessages(conn, "jeff"); ListChatMessages r2 = c.fetchPendingMessages(conn, "jenny"); //当我拉取了joe的未读信息后 就会删除msgs:1里面的信息 //为什么想明白了么 ListChatMessages 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(MapString,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
/*!
@method
@brief 收到消息时的回调
@param message 消息对象
@discussion 当EMConversation对象的enableReceiveMessage属性为YES时, 会触发此回调
针对有附件的消息, 此时附件还未被下载.
附件下载过程中的进度回调请参考didFetchingMessageAttachments:progress:,
下载完所有附件后, 回调didMessageAttachmentsStatusChanged:error:会被触发
*/
- (void)didReceiveMessage:(EMMessage *)message;
----- testMultiRecipientMessaging -----Let's create a new chat session with some recipients...Now let's send a few messages...
2. 会话Conversation
看看消息库{"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.0And let's get the messages that are waiting for jeff and jenny...要删除的 tuple:jenny--0.0要删除的 tuple:joe--0.0要删除的 tuple:jeff--4.0They are the same trueThose 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}看看还有没
会话是操作聊天消息EMMessage的容器,也就是说我们可以通过会话获得聊天记录。对应的数据结构是EMConversation。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。
SDK中对应会话的操作,包括创建,删除,获取单个或者获取所有会话等操作。下面是创建和8001的会话示例代码:
EMConversation *conversation = [[EaseMob sharedInstance].chatManager conversationForChatter:@"8001"conversationType:eConversationTypeChat];
3. 群组Group
群组分为两大类(公开群组与私有群组),并根据邀请方式进一步细分为四小类。
群组只有一个owner,为其创建者。有人数的限制。数据结构为EMGroup。
操作包括:创建,查询,加入,退出,解散,黑名单管理等操作。
4. 聊天室Chatroom
相对于群组,聊天室比较简单些。环信官网关于“聊天室”模型的说明如下:
进入聊天页面之前,进行加入聊天室操作;
成功进入聊天室之后,服务器会自动给推10条消息;
离开聊天页面之后,进行退出聊天室操作;
聊天室创建者owner可以进行退出聊天室操作;
支持最大成员5000;
环信的聊天室内仅有owner和游客;
不支持客户端建立聊天室;
不支持客户端邀请;
不支持REST邀请;
聊天室内成员离线后,服务器当监听到此成员不在线后不在会给此成员再发推送。
客户端操作包括:获取聊天室,获取聊天室详情,或许成员,加入和离开聊天室等。
5. 好友BuddyList及黑名单BlockedList
因此如果未在黑名单上,使用环信可以给任何人发起聊天,不管是不是好友。
添加好友的作用是我们可以通过查看好友名单,快速找到好友,与之交流。
环信的黑名单体系是独立的,与好友无任何关系。如果将好友加入黑名单,该好友就同时在两个名单中。
黑名单类型EMRelationship,包括取值eRelationshipBoth:双向都不接受消息;eRelationshipFrom:能给黑名单中的人发消息,接收不到黑名单中的人发的消息。
好友操作:获取好友列表,好友申请(发送请求,监听请求,同意请求,拒绝请求),删除好友,
黑名单操作:获取黑名单,加入黑名单,移除黑名单。
启动流程
1. 启动SDK
这个流程涉及到了Demo应用,EaseUI库,环信客户端SDK库,三者间的调用关系。
AppDelegate的application:didFinishLaunchingWithOptions:方法调用了AppDelegate EaseMob(该类扩展负责EaseMob的初始化和推送)中的easemobApplication:didFinishLaunchingWithOptions:appKey:apnsCertName:otherConfig
上面方法调用了EaseUI库定义的EaseSDKHelper类中的初始化方法,去注册和启动easemob sdk
上面的Helper类的方法调用了EaseMobSDK库的接口
#pragma mark - init easemob
- (void)easemobApplication:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
appkey:(NSString *)appkey
apnsCertName:(NSString *)apnsCertName
otherConfig:(NSDictionary *)otherConfig
{
//注册登录状态监听
// [[NSNotificationCenter defaultCenter] addObserver:self
// selector:@selector(loginStateChange:)
// name:KNOTIFICATION_LOGINCHANGE
// object:nil];
//注册AppDelegate默认回调监听
[self _setupAppDelegateNotifications];
//注册apns
[self _registerRemoteNotification];
//注册easemob sdk
[[EaseMob sharedInstance] registerSDKWithAppKey:appkey
apnsCertName:apnsCertName
otherConfig:otherConfig];
// 注册环信监听
[self registerEaseMobLiteNotification];
//启动easemob sdk
[[EaseMob sharedInstance] application:application didFinishLaunchingWithOptions:launchOptions];
[[EaseMob sharedInstance].chatManager setIsAutoFetchBuddyList:YES];
}
2. 显示UI
AppDelegate EaseMob类中,根据状态判断加载主窗口还是登陆页面
- (void)loginStateChange:(NSNotification *)notification
{
UINavigationController *navigationController = nil;
BOOLisAutoLogin = [[[EaseMob sharedInstance] chatManager] isAutoLoginEnabled];
BOOLloginSuccess = [notification.object boolValue];
if(isAutoLogin || loginSuccess) {//登陆成功加载主窗口控制器
//加载申请通知的数据
[[ApplyViewController shareController] loadDataSourceFromLocalDB];
if(self.mainController == nil) {
self.mainController = [[MainViewController alloc] init];
navigationController = [[UINavigationController alloc] initWithRootViewController:self.mainController];
}else{
navigationController = self.mainController.navigationController;
}
// 环信UIdemo中有用到Parse,您的项目中不需要添加,可忽略此处
[self initParse];
} else { //登陆失败加载登陆页面控制器
self.mainController = nil;
LoginViewController *loginController = [[LoginViewController alloc] init];
navigationController = [[UINavigationController alloc] initWithRootViewController:loginController];
[self clearParse];
}
self.window.rootViewController = navigationController;
}
注册和登陆
1. 注册
在LoginViewController中,定义了注册和登陆的实现。它们都调用chatManager的异步方法。作为demo,这里注册的方式是由客户端通过环信客户端SDK来申请账号,而不是由我们的应用服务器端来创建和管理用户。
编辑:计算机教程 本文来源:环信IM
关键词: