IM——技术方案

一. 即时通信技术方案

1. 第三方SDK: 环信, 融云, 网易云信, 腾讯

中小型公司/初创型: 建议使用第三方. 
好处: 快, 符合快速开发的需求, 本身和后台人员不须要作什么操做
缺点: 你的数据会通过人家的服务器, 可能会不安全

2. 使用XMPP: XMPPFramework, 之前作即时通信, 基本都在使用XMPP

好处: 源码开源, 能够自行拓展功能, 网上也有不少案例
缺点: 本身和后台人员须要作不少的操做(后台须要额外提供一些接口), 聊天服务器的稳定性可能不够好(看公司本身的运维人员技术是否够好), XML会耗流量

3. 自定义协议: 大型公司/专业即时通信公司

好处: 接口能够自定义, 可使用低流量的传输格式
缺点: 须要必定的自定义协议的经验, 包括对数据处理的经验, 对技术能力有必定的要求

二. 环信集成

1. 环信SDK介绍

环信V3版本使用了自定义协议
环信以前的版本是基于XMPP封装的数据库

APP 服务器与环信服务器的集成

环信只是即时通信的消息通道。环信自己不提供用户体系,环信既不保存任何 APP 业务数据,也不保存任何 APP 的用户信息。好比说,你的 APP 是一个婚恋交友 APP,那么你的 APP 用户的头像、昵称、身高、体重、三围、电话号码等信息是保存在你本身的 APP 业务服务器上,这些信息不须要告诉环信,环信也不想知道。api

环信这样设计的目的有2个:

  1. 本身公司必定会有后台服务器, 能够存储用户的数据
  2. 用户数据很是核心, 不该该保存, 也不太敢存到其余地方

环信服务器提供了 REST API 服务用来集成用户和好友体系:

  1. 环信提供API, 快速将公司本身的帐号体系, 转换成环信帐号体系
  2. 环信也提供了好友体系(正常开发中, 不要使用.咱们目前为了方便, 可使用)
  3. 集成SDK
  4. 环信初始化&UI搭建
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    //AppKey:注册的AppKey,详细见下面注释。
    //apnsCertName:推送证书名(不须要加后缀),详细见下面注释。
    EMOptions *options = [EMOptions optionsWithAppkey:@"czbk#hmwechat"];
    options.apnsCertName = nil;
    [[EMClient sharedClient] initializeSDKWithOptions:options];
    return YES;
}
 
// APP进入后台
- (void)applicationDidEnterBackground:(UIApplication *)application
{
    //以后的消息, 应该发送远程推送
    [[EMClient sharedClient] applicationDidEnterBackground:application];
}
 
// APP将要从后台返回
- (void)applicationWillEnterForeground:(UIApplication *)application
{
    //以后的消息, 取消远程推送
    [[EMClient sharedClient] applicationWillEnterForeground:application];
}

4. 注册&登陆&退出

注册&登陆

@ #warning 未来注册和登陆, 应该调用服务器的接口, 这里只是为了方便测试, 使用的环信接口缓存

- (IBAction)loginClick:(id)sender {
    EMError *error = [[EMClient sharedClient] loginWithUsername:self.usernameTF.text password:self.passwordTF.text];
    if (!error) {
        NSLog(@"登陆成功");
        
        //跳转根控制器
        UIStoryboard *mainSB = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
        HMTabBarController *tabBarC = [mainSB instantiateViewControllerWithIdentifier:@"HMTabBar"];
        [UIApplication sharedApplication].keyWindow.rootViewController = tabBarC;
    }
}
 
- (IBAction)registerClick:(id)sender {
   /*
    注册模式分两种,开放注册和受权注册。
    
    只有开放注册时,才能够客户端注册。开放注册是为了测试使用,正式环境中不推荐使用该方式注册环信帐号。
    受权注册的流程应该是您服务器经过环信提供的 REST API 注册,以后保存到您的服务器或返回给客户端。
    */
   EMError *error = [[EMClient sharedClient] registerWithUsername:self.usernameTF.text password:self.passwordTF.text];
   if (error==nil) {
       NSLog(@"注册成功");
   }
}

退出&被动退出

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.section == 3) {
        /*
         退出登陆分两种类型:主动退出登陆和被动退出登陆。
         
         主动退出登陆:调用 SDK 的退出接口;
         被动退出登陆:1. 正在登陆的帐号在另外一台设备上登陆;2. 正在登陆的帐号被从服务器端删除。
         logout:YES: (远程推送)是否解除 device token 的绑定,在被动退出时 SDK 内部处理,须要调用退出方法。
         */
        
        EMError *error = [[EMClient sharedClient] logout:YES];
        if (!error) {
            NSLog(@"退出成功");
            
            //切换到登录界面
            UIStoryboard *mainSB = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
            HMLoginViewController *loginVC = [mainSB instantiateViewControllerWithIdentifier:@"HMLogin"];
            [UIApplication sharedApplication].keyWindow.rootViewController = loginVC;
        }
    }
}
//添加回调监听代理:
[[EMClient sharedClient] addDelegate:self delegateQueue:nil];
 
#pragma mark - 被动退出
 
/*!
 *  当前登陆帐号在其它设备登陆时会接收到该回调
 */
- (void)userAccountDidLoginFromOtherDevice {
    EMError *error = [[EMClient sharedClient] logout:NO];
    if (!error) {
        NSLog(@"退出成功");
        
        //切换到登录界面
        UIStoryboard *mainSB = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
        HMLoginViewController *loginVC = [mainSB instantiateViewControllerWithIdentifier:@"HMLogin"];
        [UIApplication sharedApplication].keyWindow.rootViewController = loginVC;
    }
}
 
/*!
 *  当前登陆帐号已经被从服务器端删除时会收到该回调
 */
- (void)userAccountDidRemoveFromServer {
    EMError *error = [[EMClient sharedClient] logout:NO];
    if (!error) {
        NSLog(@"退出成功");
        
        //切换到登录界面
        UIStoryboard *mainSB = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
        HMLoginViewController *loginVC = [mainSB instantiateViewControllerWithIdentifier:@"HMLogin"];
        [UIApplication sharedApplication].keyWindow.rootViewController = loginVC;
    }
}

5. 自动登陆&自动重连

@ #warning 未来注册和登陆, 应该调用服务器的接口, 这里只是为了方便测试, 使用的环信接口安全

- (IBAction)loginClick:(id)sender {
    EMError *error = [[EMClient sharedClient] loginWithUsername:self.usernameTF.text password:self.passwordTF.text];
    if (!error) {
        NSLog(@"登陆成功");
        
        //设置自动登陆
        [[EMClient sharedClient].options setIsAutoLogin:YES];
    }
}
    
    /*
     自动登陆在如下几种状况下会被取消:
     
     用户调用了 SDK 的登出动做;
     用户在别的设备上更改了密码,致使此设备上自动登陆失败;
     用户的帐号被从服务器端删除;
     用户从另外一个设备登陆,把当前设备上登陆的用户踢出。
     因此,在您调用登陆方法前,应该先判断是否设置了自动登陆,若是设置了,则不须要您再调用。
     */
    
    //判断是否有自动登陆
    BOOL isAutoLogin = [EMClient sharedClient].options.isAutoLogin;
    if (!isAutoLogin) {
        //跳转登陆界面
        UIStoryboard *mainSB = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
        HMLoginViewController *loginVC = [mainSB instantiateViewControllerWithIdentifier:@"HMLogin"];
        //若是是AppDelegate里, 切换控制器应该使用self.window
        self.window.rootViewController = loginVC;
    }

@ #pragma mark - 自动重连服务器

/*!
 *  SDK链接服务器的状态变化时会接收到该回调
 *
 *  有如下几种状况,会引发该方法的调用:
 *  1. 登陆成功后,手机没法上网时,会调用该回调
 *  2. 登陆成功后,网络状态变化时,会调用该回调
 *
 *  @param aConnectionState 当前状态
 */
- (void)connectionStateDidChange:(EMConnectionState)aConnectionState {
    NSLog(@"重连状态: %zd", aConnectionState);
    //未来断网了能够弹出一些提示/或者UI发生一些改变, 让用户知道断网了
}

三. 好友关系

注:环信不是好友也能够聊天,不推荐使用环信的好友机制。若是你有本身的服务器或好友关系,请本身维护好友关系。网络

未来开发中, 记得使用公司的接口实现app

1. 添加好友

#pragma mark - UITextFieldDelegate
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    EMError *error = [[EMClient sharedClient].contactManager addContact:textField.text message:@"我想加您为好友"];
    if (!error) {
        NSLog(@"添加成功");
        [self.navigationController popViewControllerAnimated:YES];
    }
    return YES;
}
//注册好友回调
[[EMClient sharedClient].contactManager addDelegate:self delegateQueue:nil];

2. 接受好友

#pragma mark - 好友添加
/*!
 *  用户A发送加用户B为好友的申请,用户B会收到这个回调
 *
 *  @param aUsername   用户名
 *  @param aMessage    附属信息
 */
- (void)friendRequestDidReceiveFromUser:(NSString *)aUsername
                                message:(NSString *)aMessage {
    //这里暂时弹出一个提示框, 让用户选择赞成, 或者拒绝
    
   //赞成好友请求
        EMError *error = [[EMClient sharedClient].contactManager acceptInvitationForUsername:aUsername];
        if (!error) {
            NSLog(@"赞成请求");
        }
 //拒绝好友请求
        EMError *error = [[EMClient sharedClient].contactManager declineInvitationForUsername:aUsername];
        if (!error) {
            NSLog(@"拒绝请求");
        }
}
 
/*!
 @method
 @brief 用户A发送加用户B为好友的申请,用户B赞成后,用户A会收到这个回调
 */
- (void)friendRequestDidApproveByUser:(NSString *)aUsername {
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"好友通知" message:[NSString stringWithFormat:@"%@已经成为您的好友", aUsername] preferredStyle:UIAlertControllerStyleAlert];
    
    [self.window.rootViewController presentViewController:alertController animated:YES completion:nil];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [alertController dismissViewControllerAnimated:YES completion:nil];
    });
}
 
/*!
 @method
 @brief 用户A发送加用户B为好友的申请,用户B拒绝后,用户A会收到这个回调
 */
- (void)friendRequestDidDeclineByUser:(NSString *)aUsername {
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"好友通知" message:[NSString stringWithFormat:@"%@拒绝和您成为好友", aUsername] preferredStyle:UIAlertControllerStyleAlert];
    
    [self.window.rootViewController presentViewController:alertController animated:YES completion:nil];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [alertController dismissViewControllerAnimated:YES completion:nil];
    });
}

3. 好友列表

//暂时模拟实时刷新
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
 
    //已进入就刷新好友数据
    [self reloadContactData];
}
 
#pragma mark -  从服务器获取全部的好友
- (void)reloadContactData {
    EMError *error = nil;
    self.contactArray = [[EMClient sharedClient].contactManager getContactsFromServerWithError:&error];
    [self.tableView reloadData];
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *cellID = @"contactCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID forIndexPath:indexPath];
    
    cell.textLabel.text = self.contactArray[indexPath.row];
    
    return cell;
}

添加好友

//发送通知, 让通信录界面刷新运维

4. 删除好友

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        // 删除好友
        //deleteContact: 要删除的用户
        //isDeleteConversation: 是否删除对应的会话和消息 删除缓存
        EMError *error = [[EMClient sharedClient].contactManager deleteContact:self.contactArray[indexPath.row] isDeleteConversation:YES];
        if (!error) {
            NSLog(@"删除成功");
            
            
            [self reloadContactData];
        }
    }
}

四. 单聊

1. 发送消息

@ #pragma mark 发送消息post

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    //1. 构造文字消息
    EMTextMessageBody *body = [[EMTextMessageBody alloc] initWithText:textField.text];
    
    //来自于谁--我
    NSString *from = [[EMClient sharedClient] currentUsername];
    
    //生成Message
    //ConversationID: 会话ID, 用于表示和某人的聊天记录, 不传, 默认就会使用to的内容
    EMMessage *message = [[EMMessage alloc] initWithConversationID:nil from:from to:self.userName body:body ext:nil];
    message.chatType = EMChatTypeChat;// 设置为单聊消息
    //message.chatType = EMChatTypeGroupChat;// 设置为群聊消息
    //message.chatType = EMChatTypeChatRoom;// 设置为聊天室消息
//2. 发送消息
    [[EMClient sharedClient].chatManager sendMessage:message progress:nil completion:^(EMMessage *message, EMError *error) {
        if (error) {
            NSLog(@"error: %@", error);
        } else {
            NSLog(@"发送成功");
        }
    }];
    
    //3.清空文本框
    textField.text = @"";
    [textField resignFirstResponder];
    
    return YES;
}

2. 读取消息列表

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.tableView.estimatedRowHeight = 90;
    self.tableView.rowHeight = UITableViewAutomaticDimension;
    
    self.messageArrayM = [NSMutableArray array];
    
    [self reloadMessageData:YES];
}
- (void)reloadMessageData:(BOOL)isFirst {
    /*
     会话:操做聊天消息 EMMessage 的容器,在 SDK 中对应的类型是 EMConversation。
     
     新建/获取一个会话
     根据 conversationId 建立一个 conversation。
     
     getConversation:建立与8001的会话
     type:会话类型
     createIfNotExist:不存在是否建立
     EMConversationTypeChat            单聊会话
     EMConversationTypeGroupChat       群聊会话
     EMConversationTypeChatRoom        聊天室会话
     */
    EMConversation *conversation = [[EMClient sharedClient].chatManager getConversation:self.userName type:EMConversationTypeChat createIfNotExist:YES];
    
    /*!
     *  从数据库获取指定数量的消息,取到的消息按时间排序,而且不包含参考的消息,若是参考消息的ID为空,则从最新消息取
     *
     *  @param aMessageId       参考消息的ID
     *  @param count            获取的条数
     *  @param aDirection       消息搜索方向
     *  @param aCompletionBlock 完成的回调
     */
    
    [conversation loadMessagesStartFromId:nil count:isFirst ? 20 : 1 searchDirection:EMMessageSearchDirectionUp completion:^(NSArray *aMessages, EMError *aError) {
        if (aError) {
            NSLog(@"数据库查询消息有问题: %@", aError);
        } else {
            [self.messageArrayM addObjectsFromArray:aMessages];
            NSLog(@"self.messageArray: %@", self.messageArrayM);
            [self.tableView reloadData];
        }
    }];
}
//2. 发送消息
    [[EMClient sharedClient].chatManager sendMessage:message progress:nil completion:^(EMMessage *message, EMError *error) {
        if (error) {
            NSLog(@"error: %@", error);
        } else {
            NSLog(@"发送成功");
            
            //从新读取并刷新
            [self reloadMessageData:NO];
        }
    }];
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    static NSString *leftID = @"leftCell";
    static NSString *rightID = @"rightCell";
    
    EMMessage *message = self.messageArrayM[indexPath.row];
    UITableViewCell *cell;
    
    //消息的方向能够去分, 是谁发的
    if (message.direction) {
        //接收的消息
        cell = [tableView dequeueReusableCellWithIdentifier:leftID forIndexPath:indexPath];
    } else {
        //发送的消息
        cell = [tableView dequeueReusableCellWithIdentifier:rightID forIndexPath:indexPath];
    }
    
    EMMessageBody *msgBody = message.body;
    switch (msgBody.type) {
        case EMMessageBodyTypeText:
        {
            // 收到的文字消息
            EMTextMessageBody *textBody = (EMTextMessageBody *)msgBody;
            UILabel *label = [cell viewWithTag:1002];
            label.text = textBody.text;
        }
            break;
        default:
            break;
    }
    
    return cell;
}

3. 接收消息

//注册消息回调
[[EMClient sharedClient].chatManager addDelegate:self delegateQueue:nil];
#pragma mark - 接收普通消息
/*!
 @method
 @brief 接收到一条及以上非cmd消息
 */
- (void)messagesDidReceive:(NSArray *)aMessages {
    
    //须要发送通知, 告诉正在聊天的控制器进行数据刷新
    //通知时, 能够传递消息的数量 --> 7
    [[NSNotificationCenter defaultCenter] postNotificationName:@"HMMessagesDidReceiveNotification" object:nil userInfo:@{@"messageCount": @(aMessages.count)}];
    
    NSLog(@"aMessages: %@", aMessages);
}
 
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.tableView.estimatedRowHeight = 90;
    self.tableView.rowHeight = UITableViewAutomaticDimension;
    
    self.messageArrayM = [NSMutableArray array];
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(messagesDidReceiveNotification:) name:@"HMMessagesDidReceiveNotification" object:nil];
    
    //首次刷新抓20条数据
    [self reloadMessageDataWithCount:20];
}
//这里还有一个通知的方法, 通知的方法中须要根据消息的个数, 从新读取数据reloadMessageDataWithCount:7
- (void)messagesDidReceiveNotification:(NSNotification *)notification{
    int count = [notification.userInfo[@"messageCount"] intValue];
    [self reloadMessageDataWithCount:count];
相关文章
相关标签/搜索