# iOS项目——IM聊天工具(集成融云SDK)

宋宏毅
2023-12-01

iOS项目——IM聊天工具(集成融云SDK)

1 项目介绍

  • 本项目由4人小团队开发,4人均无iOS编程基础,历时一个月边学边做完成的,其间遇到了各种奇怪的问题,有的硬啃解决了,有的用别的方案代替了,最终完成了勉强让人满意的IM聊天项目。

  • 本博客以我的工作视角进行介绍,主要介绍集成融云SDK实现相应功能部分。

  • 在做项目的过程中,我发现网上IM项目特别是iOS的IM项目相关资料是很少的,iOS客户端集成融云SDK的文章只有集成单聊的比较多,开发中遇到的绝大部分问题最后都是靠融云的官方文档解决的(但官方文档某些部分真的很难理解~)。

  • 由于博主只有一个月的iOS客户端学习与开发经历,项目中可能存在不合理的部分,秉承开放交流的原则,希望大佬能指出我的错误,希望入门开发者能通过本博客获得帮助。

注:本项目使用OC开发。

2 具体功能介绍

2.1集成准备

  • 进入融云官网:https://www.rongcloud.cn
  • 注册开发者账号,进入控制台,选择“服务管理”,点击加号创建自己的应用,然后你会获得所创建应用的App Key和App Secret
  • 其实这部分官方文档里面已经介绍得很详细了,我建议直接看官方文档的,地址:https://www.rongcloud.cn/docs/ios.html

2.2集成SDK

集成:

  • 集成首先就需要将SDK导入项目之中,此处我强烈建议使用CocoaPods导入,千万不要手动导入,手动导入需要配置很多很多东西,一个错误都会导致项目编译直接出错。
  • 这部分同样建议看官方文档:https://www.rongcloud.cn/docs/ios.html
  • 不过其实也很简单,使用CocoaPods官网的podfile管理工具会更加方便,在项目对应的podfile中加入以下代码,然后install就行。(注:博主Xcode版本太低,只能选择低版本的SDK)
pod 'RongCloudIM/IMLib', '~> 2.8.32'
pod 'RongCloudIM/IMKit', '~> 2.8.32'

初始化SDK:

  • 在需要使用融云SDK功能的类中,import该头文件
#import <RongIMKit/RongIMKit.h>
  • 使用融云SDK任何功能之前,需要先初始化SDK。在App生命周期中只需要初始化一次,所以我们可以把它放在AppDelegate类中进行初始化(请确保你的AppDelegate类已经导入融云头文件):
//应用第一次运行时执行,APP从后台激活不会执行该方法
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  
  //初始化融云SDK,sharedRCIM是RCIM的单例,会用到很多,AppKey获取请查看“集成准备”
  [[RCIM sharedRCIM] initWithAppKey:@"你的AppKey"];
  
  return YES;
}

集成部分其实网上教程较多,本博客不赘述集成SDK的流程了。

2.3登录

登录逻辑:

  • 用户信息管理应使用我们自己的服务器。客户端发送验证请求,服务器接收请求并验证数据库中该用户信息是否正确,若正确则发送验证成功的信息。客户端接收到验证成功信息后,调用融云的连接函数,与融云服务器建立连接。
  • 此处我在连接完成之后做了三个工作:
  1. 查询用户好友对象列表,并将好友对象列表赋给AppDelegate类全局变量,方便后续通讯录初始化的使用。当获取好友信息之后,就可以允许App跳转到主界面,这里通过block"success"通知界面跳转
  2. 查询用户好友请求信息列表,即别人对本登录用户的好友请求信息列表
  3. 设置当前用户信息,并返回给融云,让融云知道有这样一个人,并根据该信息显示该用户的头像和昵称
[[RCIM sharedRCIM] connectWithToken:token  //token获取方法见下文
     success:^(NSString *userId) {
         NSLog(@"登录成功。当前登录的用户ID:%@", userId);
       
         // 查询用户好友对象列表,查询完再允许跳转主界面
         [TOAFHttpUtils findUserFriends:userId
                              withBlock:^(NSString * _Nonnull result) {
                                  if([result isEqualToString:@"success"]){
                                      block(@"success");
                                  }
                              }];
         
         // 查询用户好友请求信息表
         [TOAFHttpUtils findNewFiendRequestInfo:userId];
         //设置当前用户信息,用于显示自己的头像和昵称
         [[TORongUtils alloc] setCurrentUserInfo:userId];
         
     } error:^(RCConnectErrorCode status) {
         NSLog(@"登陆的错误码为:%ld", (long)status);
         block(@"error");
     } tokenIncorrect:^{
         NSLog(@"token错误");
         block(@"error");
}];

获取Token:

  • token有两个获取途径:
  1. 可以在融云开发者后台的“Api调试”中手动输入userId获取对应的用户token。一般在测试集成阶段使用,测试比较方便。但很明显,token不能写死,所以我们需要根据用户ID动态获取token。
  2. 使用融云的Server Api获取用户token。流程:向我们自己的App Server发送请求获取token,App Server通过融云的Server Api获取token并返回给客户端,客户端接收该token并以此连接到融云服务器。
  • 为什么必须在服务器端请求 Token,客户端不提供获取 Token 的接口?

因为获取 Token 时需要提供 App Key 和 App Secret。如果在客户端请求 Token,假如您的 App 代码一旦被反编译,则会导致您的 App Key 和 App Secret 泄露。所以,务必在您的服务器端获取 Token。

  • 按开发文档的说法是要求使用App服务端来调用Server Api的,避免信息泄漏,但在本项目开发中,由于对所使用的服务端操作不是很熟悉,我是在客户端请求的。

Api调用签名规则:

  • 调用融云的Server Api时,需要提供4个HTTP Request Header,用于验证App信息,具体如下:

    • App-Key : 开发者平台分配的App Key
    • Nonce : 随机数,无长度限制
    • Timestamp : 时间戳,从1970年1月1日0点0分0秒开始到现在的毫秒数
    • Signature : 数据签名,计算方法是将系统分配的App Secret(注意不是App Key!!!)、Nonce(随机数)、Timestamp(时间戳)三个字符按先后顺序拼接成一个字符串并进行SHA1哈希计算。如果数据签名调用失败,会报错401,若出该错误,请优先检查App Secret是否用了App Key,再检查加密算法是否有错,通常是这两个错误

通用API接口签名规则详情见官方文档:https://www.rongcloud.cn/docs/server.html

  • 客户端请求Token的代码如下,使用了AFNetWorking进行网络请求:
#pragma mark 获取用户token
+ (void)requestRCIMToken:(NSString *)userId
                withName:(NSString *)userName
         withPortraitUri:(NSString *)userPortraitUri
               withBlock:(HttpSuccess)block{
    
    NSString *urlstr = @"https://api-cn.ronghub.com/user/getToken.json";
    
    NSDate *date = [NSDate date];
    NSTimeInterval times =  [date timeIntervalSince1970]; 
    // 时间戳
    NSString *timestamp = [[NSString alloc] initWithFormat:@"%@",[NSString stringWithFormat:@"%.0f",times]];
    
    // 随机数
    NSString *nonce = [NSString stringWithFormat:@"%d",arc4random()];
    
    // 系统分配的AppKey、Nonce、Timestamp三个字符串拼接好的字符串进行SHA1哈希计算
    NSString *signature = [self sha1:[NSString stringWithFormat:@"%@%@%@",AppSecret,nonce,timestamp]];
    
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    
    // 每次请求 API接口时,均需要提供 4个 HTTP Request Header
    [manager.requestSerializer setValue:AppKey forHTTPHeaderField:@"App-Key"];
    [manager.requestSerializer setValue:nonce forHTTPHeaderField:@"Nonce"];
    [manager.requestSerializer setValue:timestamp forHTTPHeaderField:@"Timestamp"];
    [manager.requestSerializer setValue:signature forHTTPHeaderField:@"Signature"];
    
    // 表单参数
    NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
    parameters[@"userId"] = userId;
    parameters[@"name"] = userName;
    parameters[@"portraitUri"] = userPortraitUri;
    
    [manager POST:urlstr
       parameters:parameters
          success:^(NSURLSessionDataTask * _Nullable operation,id _Nullable responseObject){
              NSLog(@"token获取成功");
            
              NSError *error = nil;
              // Json转为NSData
              NSData *responseData = [NSJSONSerialization dataWithJSONObject:responseObject options:kNilOptions error:&error];
              // NSData转为NSDictionary
              NSDictionary *responseDict = [NSJSONSerialization
                                           JSONObjectWithData:responseData options:kNilOptions error:&error];
              // NSDictionary转为NSString
              NSString *token = [responseDict objectForKey:@"token"];
              block(token);
          }
          failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
              NSLog(@"token获取失败");
              block(@"error");
          }];
}
  • SHA1哈希加密:
/*使用下面方法需要导入 CommonCrypto/CommonDigest.h,很多别的博客都不说这一点*/
//#import <CommonCrypto/CommonDigest.h>
// SHA1哈希计算
+ (NSString *)sha1:(NSString *)key{
  
    NSData *data = [key dataUsingEncoding:NSUTF8StringEncoding];

    uint8_t digest[CC_SHA1_DIGEST_LENGTH];

    CC_SHA1(data.bytes, (unsigned int)data.length, digest);

    NSMutableString *output = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH *2];

    for(int i=0; i<CC_SHA1_DIGEST_LENGTH; i++) {
        [output appendFormat:@"%02x", digest[i]];
    }
    return output;
}

用户信息提供:

  • 用户登录后,需要实现用户信息提供者,融云的界面才会显示正确的用户信息

这个很简单,看一下融云的代码就明白了:

/*!
当前登录的用户的用户信息
@discussion 与融云服务器建立连接之后,应该设置当前用户的用户信息,用于SDK显示和发送。
@warning 如果传入的用户信息中的用户ID与当前登录的用户ID不匹配,则将会忽略。
*/
@property(nonatomic, strong) RCUserInfo *currentUserInfo;
  • 实现代码如下,这里请求了自己的App Server获取了相应用户信息,并将用户信息存到了App全局变量中,方便后续调用,再将相应用户信息传给融云:
- (void)setCurrentUserInfo:(NSString *)userId{
    //设置当前用户信息,用于显示自己的头像和昵称
    [TOAFHttpUtils findUserById:userId
                      withBlock:^(UserInfoModel * _Nonnull userModel) {
                          //组装融云用户信息对象
                          RCUserInfo *userInfo = [[RCUserInfo alloc]
                                                  initWithUserId:userModel.userId
                                                  name:userModel.userName
                                                  portrait:userModel.userPortraitUri];
                          //获取主线程队列
                          dispatch_async(dispatch_get_main_queue(),^{
                              
                              AppDelegate *myDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
                              //将用户对象存到全局变量中
                              myDelegate.user = userModel;
                            
                          });
                          //设置当前用户信息
                          [RCIM sharedRCIM].currentUserInfo = userInfo;
                      }];
}

至此,已经成功连接到融云服务器~

2.4集成会话列表

会话列表:

  • 融云 IMKit 已经实现了一个默认的会话列表视图控制器,可以直接使用或继承 RCConversationListViewController,即可快速启动和使用会话列表界面,我推荐自己新建一个类继承融云的类,方便自定义开发。
  • 在viewDidLoad中设置会话列表相关属性:
//实现用户信息提供者、群组信息提供者协议
@interface TOConversationListViewController()<RCIMUserInfoDataSource,RCIMGroupInfoDataSource>

@end
  
@implementation TOConversationListViewController
  
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 设置用户信息代理
    [RCIM sharedRCIM].userInfoDataSource = self;
    // 设置群组信息代理
    [[RCIM sharedRCIM] setGroupInfoDataSource:self];
    //设置用户信息子本地持久化存储。SDK获取过的用户信息将保存着数据库中,即使APP重启也能再次读取
    [RCIM sharedRCIM].enablePersistentUserInfoCache = YES;
    
    //设置会话列表,禁止了系统信息显示
    //系统信息用于好友验证与各种通知信息,适合做信息拦截处理,不适合直接显示
    [self setDisplayConversationTypes:@[@(ConversationType_PRIVATE),
                                        @(ConversationType_DISCUSSION),
                                        @(ConversationType_CHATROOM),
                                        @(ConversationType_GROUP)]];
     
}
  
@end
  • 会话列表类型属性,也可以设置聚合显示,聚合显示见开发文档:
/*!
 会话类型
 */
typedef NS_ENUM(NSUInteger, RCConversationType) {
  /*!
   单聊
   */
  ConversationType_PRIVATE = 1,

  /*!
   讨论组
   */
  ConversationType_DISCUSSION = 2,

  /*!
   群组
   */
  ConversationType_GROUP = 3,

  /*!
   聊天室
   */
  ConversationType_CHATROOM = 4,

  /*!
   客服
   */
  ConversationType_CUSTOMERSERVICE = 5,

  /*!
   系统会话
   */
  ConversationType_SYSTEM = 6,

  /*!
   应用内公众服务会话

   @discussion
   客服2.0使用应用内公众服务会话(ConversationType_APPSERVICE)的方式实现。
   即客服2.0会话是其中一个应用内公众服务会话,这种方式我们目前不推荐,请尽快升级到新客服,升级方法请参考官网的客服文档。
   */
  ConversationType_APPSERVICE = 7,

  /*!
   跨应用公众服务会话
   */
  ConversationType_PUBLICSERVICE = 8,

  /*!
   推送服务会话
   */
  ConversationType_PUSHSERVICE = 9
};

用户信息提供者:

  • 融云认为,每一个设计良好且功能健全的 App 都应该能够在本地获取、缓存并在合适的时机更新 App 中的用户信息。所以,融云不维护和管理用户基本信息(用户ID、昵称、头像)的获取、缓存、变更和同步。

  • 融云 IMKit 设计了用户信息提供者、群组信息提供者和群名片信息提供者,App 只需要实现该协议并提供正确的数据,IMKit 会在会话列表或会话页面需要展示用户头像和昵称的时候,去调用对应协议的代理函数,即可显示用户和群组的名称与头像。

  • 老规矩,看看SDK代码,注释还是很详细的:

// RCIM Class
/*!
 用户信息提供者
 @discussion SDK需要通过您实现的用户信息提供者,获取用户信息并显示。
 */
@protocol RCIMUserInfoDataSource <NSObject>

/*!
 获取用户信息
 @param userId                  用户ID
 @param completion              获取用户信息完成之后需要执行的Block
 @param userInfo(in completion) 该用户ID对应的用户信息
 @discussion SDK通过此方法获取用户信息并显示,请在completion中返回该用户ID对应的用户信息。
 在您设置了用户信息提供者之后,SDK在需要显示用户信息的时候,会调用此方法,向您请求用户信息用于显示。
 */
- (void)getUserInfoWithUserId:(NSString *)userId
                   completion:(void (^)(RCUserInfo *userInfo))completion;

@end

/*!
 群组信息提供者
 @discussion SDK需要通过您实现的群组信息提供者,获取群组信息并显示。
 */
@protocol RCIMGroupInfoDataSource <NSObject>
/*!
 获取群组信息

 @param groupId                     群组ID
 @param completion                  获取群组信息完成之后需要执行的Block
 @param groupInfo(in completion)    该群组ID对应的群组信息
 @discussion SDK通过此方法获取群组信息并显示,请在completion的block中返回该用户ID对应的群组信息。
 在您设置了群组信息提供者之后,SDK在需要显示群组信息的时候,会调用此方法,向您请求群组信息用于显示。
 */
- (void)getGroupInfoWithGroupId:(NSString *)groupId
                     completion:(void (^)(RCGroup *groupInfo))completion;

@end
  • 很明显,只要实现两个协议:RCIMUserInfoDataSourceRCIMGroupInfoDataSource并在相应函数中提供用户信息,就能实现用户信息的正确显示。

在每次进入会话时,融云都会调用用户信息提供者与群组信息提供者函数,也就是说,只要我们保证提供给这两个函数的信息的正确的、实时的,我们就可以保证用户与群组信息的实时显示,那只要使用自己的App Server获取信息就能保证信息实时更新了

以用户信息提供者函数为例:

// 实现用户信息提供者的代理函数
- (void)getUserInfoWithUserId:(NSString *)userId
                   completion:(void (^)(RCUserInfo *))completion {
                     
		//调用App Server实时获取用户信息
    [TOAFHttpUtils findUserById:userId
                      withBlock:^(UserInfoModel * _Nonnull userModel) {
                          
                          RCUserInfo *userInfo = [[RCUserInfo alloc]
                                                  initWithUserId:userModel.userId
                                                  name:userModel.userName
                                                  portrait:userModel.userPortraitUri];
                          if (completion) {
                              completion(userInfo);
                          }
                      }];
    
}

会话列表cell点击事件:

  • 相当于开启会话,可以在此处加上自定义的效果,如果只需要普通的会话功能,按我这样写准没错(当然会话界面类名要改成自己的类名)。
// 会话列表cell点击事件
// RCConversationModelType : 融云会话类型
// RCConversationModel : 融云会话模型
- (void)onSelectedTableRow:(RCConversationModelType)conversationModelType
         conversationModel:(RCConversationModel *)model
               atIndexPath:(NSIndexPath *)indexPath {
                 
    //创建继承了融云会话界面类的自己的会话界面类
    TOConversationViewController *conversationVC = [[TOConversationViewController alloc]initWithConversationType:model.conversationType targetId:model.targetId];
                 
    //设置需要打开的会话属性,三个属性分别对应:会话类型、对方ID(群组的话就是群组ID),会话标题
    conversationVC.conversationType = model.conversationType;
    conversationVC.targetId = model.targetId;
    conversationVC.title = model.conversationTitle;
     
    //窗口跳转 
    [self.navigationController pushViewController:conversationVC animated:YES];
}

会话列表UI自定义:

  • 融云提供了部分函数提供我们自定义会话列表界面,同时我们也可以在继承类中直接添加自己所需要的UI组件。

至此,会话列表集成完成~

2.5集成会话(单聊)

单聊:

  • 指两个用户一对一进行聊天,两个用户间可以是好友也可以是陌生人,融云不对用户的关系进行维护管理,会话关系由融云负责建立并保持,当 App 在后台运行或者 App 进程被杀死后,有新消息时会收到推送通知。
  • 也就是说,融云不维护好友关系,只要你发送了单聊请求,请求携带了正确的对方ID, 就可以开启单聊。
  • 其实只要你仔细看,就会发现集成会话列表会话列表cell点击事件中就包含了开启单聊的方法,可以把这段代码放在任何需要开启单聊的地方开启单聊(注意,开启单聊类中要导入融云头文件,会话界面类用自己的类或者直接用融云的RCConversationViewController都行):
TOConversationViewController *conversationVC = [[TOConversationViewController alloc] init];
conversationVC.conversationType = ConversationType_PRIVATE; //会话类型
conversationVC.targetId = @"对方ID";
conversationVC.targetId = @"会话标题";
               
[self.navigationController pushViewController:conversationVC animated:YES];

至此,单聊集成完成~

2.6集成群聊

后续更新!!

2.7好友添加

后续更新!!

3 总结

  • 总的来说,这个项目做出来还是比较粗糙的
  • 进度也比较赶,一直注重功能的实现,而代码构建方面有很多问题,数据传递也有很多问题,也缺少了对用户体验的考量
  • 好的地方,功能还是成功实现了,起码是一个“能用”的App,以后继续努力,不断进步吧~
 类似资料: