环信IM

时间:2020-01-11 08:42来源:计算机教程
本文为大家分享了Redis支持多人多聊天室功能的设计代码,供大家参考,具体内容如下 概述 设计原理 环信IM的功能一直在演化,比如推送功能和聊天室功能是不久前的版本中添加的。

本文为大家分享了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

关键词: