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

也是第一次没有利用第三方即时通讯实现聊天功能,所以在此总结一下。
主要包括 界面的布局(cell动态计算高度)、SocketRocket第三方库、消息的发送、接收、显示消息发送状态(发送中、发送失败)、表情键盘(与后台通讯表情的文字转换),下面对这些做一下总结:
1、界面的布局
【ios|ios 利用socket实现即时聊天总结01】肯定是用的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 别忘了导入头文件
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即可;还有一个细节不知道大家注意到没有,遍历时是从数据源后面开始的,这样会提高不少效率,减少资源浪费。
断开连接消息发送失败改变状态 类同 消息发送成功改变状态。
后面内容、下面文章发布,谢谢大家的阅读、写的比较粗糙、忽略了许多细节。

    推荐阅读