美文网首页iOS 开发每天分享优质文章
ios 利用socket实现即时聊天总结01

ios 利用socket实现即时聊天总结01

作者: 大弯弓 | 来源:发表于2018-09-10 15:06 被阅读81次

    也是第一次没有利用第三方即时通讯实现聊天功能,所以在此总结一下。

    主要包括 界面的布局(cell动态计算高度)、SocketRocket第三方库、消息的发送、接收、显示消息发送状态(发送中、发送失败)、表情键盘(与后台通讯表情的文字转换),下面对这些做一下总结:

    1、界面的布局

    肯定是用的TableView,主要说一下cell高度的计算,声明一个FrameModel,FrameModel声明一个ChatModel,根据chatModel的消息内容来计算Frame。

    ChatModel.h如下:

      typedef enum: NSUInteger {
        msgSending,
        msgSendFailed,
        msgSendSuccess,
    } msgSendState;//消息发送状态
    
    @interfaceChatModel :NSObject
    @property (nonatomic,copy) NSString *content;//消息内容
    @property(nonatomic,copy)NSAttributedString*attributedContent;//如果有表情需要将表情转换为文字消息
    @property (nonatomic, copy) NSString *time;//消息时间“yyyy-MM-dd HH:mm”
    @property (nonatomic,strong) NSDate *msgDate;
    @property (nonatomic,assign) NSInteger direction;//消息发送方向
    @property (nonatomic,assign) msgSendState sendState;
    @property (nonatomic,assign) NSInteger createTime;
    @property(nonatomic,assign,getter= isHiddenTime) BOOL hiddenTime;//是否隐藏时间   
    @end
    

    FrameModel.h如下:

    @interface FrameModel :NSObject
    
    @property (nonatomic, assign, readonly) CGRect titleLabelFrame;
    
    @property (nonatomic, assign, readonly) CGRect contentBtnFrame;
    
    @property (nonatomic, assign, readonly) CGRect iconImageViewFrame;
    
    @property (nonatomic, assign, readonly) CGRect msgStateImgViewFrame;
    
    @property (nonatomic, assign) CGFloat cellHeight;//行高
    
    @property (nonatomic, strong) ChatModel *chatModel;
    
    @end
    

    那么FrameModel.m里面主要是setChatModel计算cell的高度 以及根据ChatModel direction属性布局控件。

    FrameModel.m setChatModel主要代码:

    - (void)setChatModel:(ChatModel*)chatModel {
        _chatModel= chatModel;
        if (_chatModel.direction == 1) {//自己发消息时cell的布局
    
            _iconImageViewFrame
            _contentBtnFrame
            _msgStateImgViewFrame 
    
        }else{ //接收到消息时、cell的布局
    
            _iconImageViewFrame
            _contentBtnFrame
            _msgStateImgViewFrame 
        }
        _cellHeight
    }
    

    cell高度主要是根据消息内容来计算 ,在加上其他空间的高度、主要用到的方法如下:

    //根据文字计算高度
    
    - (CGSize)sizeWithText:(NSAttributedString*)text {
        return  [textboundingRectWithSize:CGSizeMake(ScreenWidth - 150, MAXFLOAT)
                           options:NSStringDrawingUsesLineFragmentOrigin
                           context:nil].size;
    
    }
    

    2、SocketRocket
    安装SocketRocket以后、全局建立一个SocketManager。
    SocketManager.h 别忘了导入头文件<SocketRocket.h>

    extern NSString*constkNeedPayOrderNote;
    
    extern NSString*constkWebSocketDidOpenNote;
    
    extern NSString*constkWebSocketDidCloseNote;
    
    extern NSString*constkWebSocketdidReceiveMessageNote;
    
    extern NSString*constkWebSocketErrorNote;
    
    @interface SocketManager :NSObject
    
    @property (nonatomic,copy) NSString *connectUrl;
    
    + (MMDSocketManager*)shareSocketMangerWithUrl:(NSString*)url;
    
    -(void)SRWebSocketClose;//关闭连接
    
    - (void)sendData:(id)data;//发送数据
    
    -(void)openSocket;
    
    @end
    

    SocketManager.m

    #import "SocketManager.h"
    
    NSString*constkNeedPayOrderNote              =@"kNeedPayOrderNote";
    NSString*constkWebSocketDidOpenNote          =@"kWebSocketDidOpenNote";
    NSString*constkWebSocketDidCloseNote          =@"kWebSocketDidCloseNote";
    NSString*constkWebSocketErrorNote          =@"kWebSocketErrorNote";
    NSString*constkWebSocketdidReceiveMessageNote =@"kWebSocketdidReceiveMessageNote";
    
    @interface SocketManager()
    @property (nonatomic,strong) SRWebSocket *socket;
    @end
    
    @implementationMMDSocketManager
    {
        int_index;
        NSTimer* heartBeat;
        NSTimeIntervalre ConnectTime;
    }
    
    + (MMDSocketManager*)shareSocketMangerWithUrl:(NSString*)url{
        MMDSocketManager *socketManager = [[MMDSocketManager alloc] init];
        socketManager.connectUrl= url;
        returnsocketManager;
    }
    
    -(void)openSocket{
        //如果是同一个url return
        if(self.socket) {
            return;
        }
        self.socket = [[SRWebSocket alloc] initWithURLRequest:                   [NSURL RequestrequestWithURL:[NSURLURLWithString:[NSStringstringWithFormat:@"%@",self.connectUrl];
        NSLog(@"请求的websocket地址:%@",self.socket.url.absoluteString);
        self.socket.delegate=self;
       [self.socket open];    //开始连接
    }
    
    -(void)reConnect{
        [self SRWebSocketClose];
        //超过一分钟就不再重连 所以只会重连5次 2^5 = 64
        if (reConnectTime > 64) {
            //您的网络状况不是很好,请检查网络后重试
            return;
        }
    
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(reConnectTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            self.socket=nil;
            self.socket = [[SRWebSocket alloc] initWithURLRequest:
                           [NSURLRequestrequestWithURL:[NSURLURLWithString:[NSStringstringWithFormat:@"%@?careId=%@&token=%@",self.connectUrl,self.careId,[MMDCommonUserModelinstance].accessToken]]]];
            self.socket.delegate=self;
            [self.socketopen];    //开始连接
            NSLog(@"重连");
        });
    
        //重连时间2的指数级增长
        if (reConnectTime == 0) {
            reConnectTime = 2;
        }else{
            reConnectTime *= 2;
        
    }
    
    -(void)SRWebSocketClose{
        if(self.socket){
            [self.socketclose];
            self.socket=nil;
            //断开连接时销毁心跳
            [self destoryHeartBeat];
        }
    }
    
    //取消心跳
    - (void)destoryHeartBeat
    {
        dispatch_main_async_safe(^{
            if(heartBeat) {
                if([heartBeat respondsToSelector:@selector(isValid)]){
                    if([heartBeat isValid]){
                        [heartBeat invalidate];
                        heartBeat =nil;
                    }
                }
            }
        })
    }
    
    - (void)sendData:(id)data {
        NSLog(@"socketSendData --------------- %@",data);
        WS(weakSelf)
        dispatch_queue_tqueue =  dispatch_queue_create("zy", NULL);
        dispatch_async(queue, ^{
            if(weakSelf.socket!=nil) {
                // 只有 SR_OPEN 开启状态才能调 send 方法啊,不然要崩
                if(weakSelf.socket.readyState==SR_OPEN) {
                    [weakSelf.socketsend:data];    // 发送数据
                }elseif(weakSelf.socket.readyState==SR_CONNECTING) {
                    NSLog(@"正在连接中,重连后其他方法会去自动同步数据");
    
                    // 每隔2秒检测一次 socket.readyState 状态,检测 10 次左右
                    // 只要有一次状态是 SR_OPEN 的就调用 [ws.socket send:data] 发送数据
                    // 如果 10 次都还是没连上的,那这个发送请求就丢失了,这种情况是服务器的问题了,小概率的
                    // 代码有点长,我就写个逻辑在这里好了
                    [self reConnect];
                }else if(weakSelf.socket.readyState==SR_CLOSING|| weakSelf.socket.readyState==SR_CLOSED) {
    
                    // websocket 断开了,调用 reConnect 方法重连
                    NSLog(@"重连");
                    [[NSNotificationCenter defaultCenter] postNotificationName:kWebSocketErrorNote object:nil];
                    [self reConnect];
                }
            }else{
                [selfreConnect];
                NSLog(@"没网络,发送失败,一旦断网 socket 会被我设置 nil 的");
                NSLog(@"其实最好是发送前判断一下网络状态比较好,我写的有点晦涩,socket==nil来表示断网");
                [[NSNotificationCenter defaultCenter] postNotificationName:kWebSocketErrorNote object:nil];
            }
        });
    
    }
    
    #pragma mark - SRWebSocketDelegate
    - (void)webSocketDidOpen:(SRWebSocket*)webSocket {
        //每次正常连接的时候清零重连时间
        reConnectTime = 0;
        //开启心跳
        //    [self initHeartBeat];
        if(webSocket ==self.socket) {
            NSLog(@"************************** socket 连接成功************************** ");
            [[NSNotificationCenter defaultCenter] postNotificationName:kWebSocketDidOpenNote object:nil];
        }
    }
    
    - (void)webSocket:(SRWebSocket*)webSocket didFailWithError:(NSError*)error {
        if(webSocket ==self.socket) {
            NSLog(@"************************** socket 连接失败************************** ");
            [self SRWebSocketClose];
            //连接失败就重连
            [self reConnect];
            [[NSNotificationCenter defaultCenter] postNotificationName:kWebSocketErrorNote object:nil];
        }
    }
    
    - (void)webSocket:(SRWebSocket*)webSocket didCloseWithCode:(NSInteger)code reason:(NSString*)reason wasClean:(BOOL)wasClean {
        if(webSocket ==self.socket) {
            NSLog(@"************************** socket连接断开************************** ");
            NSLog(@"被关闭连接,code:%ld,reason:%@,wasClean:%d",(long)code,reason,wasClean);
            [self SRWebSocketClose];
            [[NSNotificationCenter defaultCenter] postNotificationName:kWebSocketDidCloseNote object:nil];
        }
    
    }
    
    /*该函数是接收服务器发送的pong消息,其中最后一个是接受pong消息的,
     在这里就要提一下心跳包,一般情况下建立长连接都会建立一个心跳包,
     用于每隔一段时间通知一次服务端,客户端还是在线,这个心跳包其实就是一个ping消息,
     我的理解就是建立一个定时器,每隔十秒或者十五秒向服务端发送一个ping消息,这个消息可是是空的
     */
    
    -(void)webSocket:(SRWebSocket*)webSocket didReceivePong:(NSData*)pongPayload{
        NSString *reply = [[NSString alloc] initWithData:pongPayload encoding:NSUTF8StringEncoding];
        NSLog(@"reply===%@",reply);
    }
    
    - (void)webSocket:(SRWebSocket*)webSocket didReceiveMessage:(id)message  {
        if(webSocket ==self.socket) {
            NSLog(@"************************** socket收到数据了************************** ");
            NSLog(@"我这后台约定的 message 是 json 格式数据收到数据,就按格式解析吧,然后把数据发给调用层");
            NSLog(@"message:%@",message);
            [[NSNotificationCenter defaultCenter] postNotificationName:kWebSocketdidReceiveMessageNote object:[StringUtils dictionaryWithJSONString:message]];
        }
    }
    

    然后就是创建聊天界面、目前就可以实现通讯了。

    3、消息发送状态

    socket可以通过代码可以监听到是否连接或出现错误、此时消息是发送不成功的,所以此时发送消息就应该显示失败的状态;消息发送之后、应是发送中的状态、当后台接收到消息后、给我们一个回馈即消息 发送成功、显示发送成功状态。

    typedef enum: NSUInteger {
        msgSending,
        msgSendFailed,
        msgSendSuccess,
    } msgSendState;//消息发送状态
    

    既然需要改变消息发送状态、就要更新tableView数据源、就要遍历数据源;

    #pragma mark - 消息发送成功改变状态
    -(void)changeMsgSendSuccessed{
        if(self.dataArray.count>0) {
            NSMutableArray *tempArr = [NSMutableArray arrayWithArray:self.dataArray];
            for(NSIntegeri = (self.dataArray.count-1); i >=0; i--) {
                FrameModel*chatFrameModel = [FrameModelobjectWithKeyValues:self.dataArray[i]];
                if(chatFrameModel.chatModel.sendState==msgSending) {
                    chatFrameModel.chatModel.sendState=msgSendSuccess;
                    [tempArr replaceObjectAtIndex:iwithObject:chatFrameModel];
                    self.dataArray= tempArr;
                    NSIndexPath*indexPath = [NSIndexPathindexPathForRow:iinSection:0];
                    dispatch_async(dispatch_get_main_queue(), ^{
                        [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
                        [self tableViewScrollToBottomWithAnimation:YES];
                    });
    
                    break;
                }
            }
        }}
    

    这里使用reloadRowsAtIndexPaths即可;还有一个细节不知道大家注意到没有,遍历时是从数据源后面开始的,这样会提高不少效率,减少资源浪费。

    断开连接消息发送失败改变状态 类同 消息发送成功改变状态。
    后面内容、下面文章发布,谢谢大家的阅读、写的比较粗糙、忽略了许多细节。

    相关文章

      网友评论

      • saman0:有没有swift 版本的,或者swift demo

      本文标题:ios 利用socket实现即时聊天总结01

      本文链接:https://www.haomeiwen.com/subject/kqysgftx.html