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即可;还有一个细节不知道大家注意到没有,遍历时是从数据源后面开始的,这样会提高不少效率,减少资源浪费。
断开连接消息发送失败改变状态 类同 消息发送成功改变状态。
后面内容、下面文章发布,谢谢大家的阅读、写的比较粗糙、忽略了许多细节。
推荐阅读
- 2020-04-07vue中Axios的封装和API接口的管理
- iOS中的Block
- 记录iOS生成分享图片的一些问题,根据UIView生成固定尺寸的分享图片
- 2019-08-29|2019-08-29 iOS13适配那点事
- Hacking|Hacking with iOS: SwiftUI Edition - SnowSeeker 项目(一)
- iOS面试题--基础
- spring|spring boot项目启动websocket
- 接口|axios接口报错-参数类型错误解决
- iOS|iOS 笔记之_时间戳 + DES 加密
- Spring|Spring Boot 自动配置的原理、核心注解以及利用自动配置实现了自定义 Starter 组件