iOS 开发每天分享优质文章

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

2018-09-10  本文已影响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即可;还有一个细节不知道大家注意到没有,遍历时是从数据源后面开始的,这样会提高不少效率,减少资源浪费。

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

上一篇下一篇

猜你喜欢

热点阅读