Socket.io-FLSocketIM-iOS
基于Socket.io iOS即時通訊客戶端 iOS IM Client based on Socket.io
iOS 代碼地址:https://github.com/fengli12321/Socket.io-FLSocketIM-iOS
服務(wù)器端代碼實現(xiàn)參照:https://github.com/fengli12321/Socket.io-FLSocketIM-Server
安卓端代碼實現(xiàn)參照:https://github.com/fengli12321/Socket.io-FLSocketIM-Android
安卓簡書介紹:http://www.reibang.com/p/cdb3b0301712
實現(xiàn)功能
- 文本發(fā)送
- 圖片發(fā)送(從相冊選取,或者拍攝)
- 短視頻
- 語音發(fā)送
- 視頻通話
- 其他一些效果(類似QQ底部tabBar,短視頻拍攝等)
- 功能擴展中玖详。。。揽咕。。
先看看實際效果
使用技術(shù)
一套菜、Socket.io
Socket.io是該項目實現(xiàn)即時通訊關(guān)鍵所在亲善,非常強大;
Socket.io將Websocket和輪詢 (Polling)機制以及其它的實時通信方式封裝成了通用的接口逗柴,并且在服務(wù)端實現(xiàn)了這些實時機制的相應(yīng)代碼蛹头。
先上代碼
1.創(chuàng)建Socket連接,通過單例管理類FLSocketManager實現(xiàn)
- (void)connectWithToken:(NSString *)token success:(void (^)())success fail:(void (^)())fail {
NSURL* url = [[NSURL alloc] initWithString:BaseUrl];
/**
log 是否打印日志
forceNew 這個參數(shù)設(shè)為NO從后臺恢復(fù)到前臺時總是重連,暫不清楚原因
forcePolling 是否強制使用輪詢
reconnectAttempts 重連次數(shù)渣蜗,-1表示一直重連
reconnectWait 重連間隔時間
connectParams 參數(shù)
forceWebsockets 是否強制使用websocket, 解釋The reason it uses polling first is because some firewalls/proxies block websockets. So polling lets socket.io work behind those.
來源:https://github.com/socketio/socket.io-client-swift/issues/449
*/
SocketIOClient* socket;
if (!self.client) {
socket = [[SocketIOClient alloc] initWithSocketURL:url config:@{@"log": @NO, @"forceNew" : @YES, @"forcePolling": @NO, @"reconnectAttempts":@(-1), @"reconnectWait" : @4, @"connectParams": @{@"auth_token" : token}, @"forceWebsockets" : @NO}];
}
else {
socket = self.client;
socket.engine.connectParams = @{@"auth_token" : token};
}
// 連接超時時間設(shè)置為15秒
[socket connectWithTimeoutAfter:15 withHandler:^{
fail();
}];
// 監(jiān)聽一次連接成功
[socket once:@"connect" callback:^(NSArray * _Nonnull data, SocketAckEmitter * _Nonnull ack) {
success();
}];
_client = socket;
}
這個方法是在用戶登錄后調(diào)用屠尊,主要作用是初始化Socket連接,關(guān)于socket初始化相關(guān)參數(shù)請參照socket.io文檔耕拷。
2.監(jiān)聽服務(wù)器向客戶端發(fā)送的消息知染,通過單例管理類FLClientManager進行管理,然后讓代理實現(xiàn)功能
// 收到消息
[socket on:@"chat" callback:^(NSArray * _Nonnull data, SocketAckEmitter * _Nonnull ack) {
if (ack.expected == YES) {
[ack with:@[@"hello 我是應(yīng)答"]];
}
FLMessageModel *message = [FLMessageModel yy_modelWithJSON:data.firstObject];
NSData *fileData = message.bodies.fileData;
if (fileData && fileData != NULL && fileData.length) {
NSString *fileName = message.bodies.fileName;
NSString *savePath = nil;
switch (message.type) {
case FLMessageImage:
savePath = [[NSString getFielSavePath] stringByAppendingPathComponent:[NSString stringWithFormat:@"s_%@", fileName]];
break;
case FlMessageAudio:
savePath = [[NSString getAudioSavePath] stringByAppendingPathComponent:fileName];
break;
default:
savePath = [[NSString getFielSavePath] stringByAppendingPathComponent:fileName];
break;
}
message.bodies.fileData = nil;
[fileData saveToLocalPath:savePath];
}
id bodyStr = data.firstObject[@"bodies"];
if ([bodyStr isKindOfClass:[NSString class]]) {
FLMessageBody *body = [FLMessageBody yy_modelWithJSON:[bodyStr stringToJsonDictionary]];
message.bodies = body;
}
// 消息插入數(shù)據(jù)庫
[[FLChatDBManager shareManager] addMessage:message];
// 會話插入數(shù)據(jù)庫或者更新會話
BOOL isChatting = [message.from isEqualToString:[FLClientManager shareManager].chattingConversation.toUser];
[[FLChatDBManager shareManager] addOrUpdateConversationWithMessage:message isChatting:isChatting];
// 本地推送斑胜,收到消息添加紅點控淡,聲音及震動提示
[FLLocalNotification pushLocalNotificationWithMessage:message];
// 代理處理
for (FLBridgeDelegateModel *model in self.delegateArray) {
id<FLClientManagerDelegate>delegate = model.delegate;
if (delegate && [delegate respondsToSelector:@selector(clientManager:didReceivedMessage:)]) {
if (message) {
[delegate clientManager:self didReceivedMessage:message];
}
}
}
}];
// 視頻通話請求
[socket on:@"videoChat" callback:^(NSArray * _Nonnull data, SocketAckEmitter * _Nonnull ack) {
UIViewController *vc = [self getCurrentVC];
NSDictionary *dataDict = data.firstObject;
FLVideoChatViewController *videoVC = [[FLVideoChatViewController alloc] initWithFromUser:dataDict[@"from_user"] toUser:[FLClientManager shareManager].currentUserID type:FLVideoChatCallee];
videoVC.room = dataDict[@"room"];
[vc presentViewController:videoVC animated:YES completion:nil];
FLLog(@"%@============", data);
}];
// 用戶上線
[socket on:@"onLine" callback:^(NSArray * _Nonnull data, SocketAckEmitter * _Nonnull ack) {
for (FLBridgeDelegateModel *model in self.delegateArray) {
id<FLClientManagerDelegate>delegate = model.delegate;
if (delegate && [delegate respondsToSelector:@selector(clientManager:userOnline:)]) {
[delegate clientManager:self userOnline:[data.firstObject valueForKey:@"user"]];
}
}
}];
// 用戶下線
[socket on:@"offLine" callback:^(NSArray * _Nonnull data, SocketAckEmitter * _Nonnull ack) {
for (FLBridgeDelegateModel *model in self.delegateArray) {
id<FLClientManagerDelegate>delegate = model.delegate;
if (delegate && [delegate respondsToSelector:@selector(clientManager:userOffline:)]) {
[delegate clientManager:self userOffline:[data.firstObject valueForKey:@"user"]];
}
}
}];
// 連接狀態(tài)改變
[socket on:@"statusChange" callback:^(NSArray * _Nonnull data, SocketAckEmitter * _Nonnull ack) {
FLLog(@"%ld========================狀態(tài)改變", socket.status);
for (FLBridgeDelegateModel *model in self.delegateArray) {
id<FLClientManagerDelegate>delegate = model.delegate;
if (delegate && [delegate respondsToSelector:@selector(clientManager:didChangeStatus:)]) {
[delegate clientManager:self didChangeStatus:socket.status];
}
}
}];
- (NSUUID * _Nonnull)on:(NSString * _Nonnull)event callback:(void (^ _Nonnull)(NSArray * _Nonnull, SocketAckEmitter * _Nonnull))callback;
socket.io 提供的事件監(jiān)聽方法,這里監(jiān)聽的事件包括:
- “chat” 接收到好友消息
- “videoChat” 視頻通話請求
- “onLine” 有好友上線
- “offLine” 有好友離線
- “statusChange” socket.io內(nèi)部提供的止潘,連接狀態(tài)改變
這部分代碼掺炭,有個比較關(guān)鍵的需要說明一下,舉個例子凭戴,在接收到“chat”事件后涧狮,數(shù)據(jù)庫管理類需要將消息存放到數(shù)據(jù)庫,會話列表需要更新UI么夫,聊天列表需要顯示該消息...也就是該事件需要多個對象響應(yīng)者冤。對于這種需求最先想到的就是使用通知的功能,畢竟可以實現(xiàn)一對多的消息傳遞嘛档痪!后來又思考涉枫,通過代理模式能否實現(xiàn)呢,通過制定協(xié)議代碼質(zhì)量更高腐螟?于是乎將代理存放在一個數(shù)組中愿汰,接收到事件后遍歷數(shù)組中的代理去響應(yīng)事件。 然而出現(xiàn)了一個問題乐纸,我們在一般使用代理模式中衬廷,代理都是一個weak修飾屬性,代理釋放該屬性自動置nil汽绢,然而將代理放到數(shù)組中吗跋,代理被強引用,引用計數(shù)加1宁昭,數(shù)組不釋放跌宛,代理永遠無法釋放。這該怎么解決呢久窟,后來仿照一般的代理模式秩冈,創(chuàng)建一個橋接對象,代理數(shù)組里面存放橋接對象斥扛,然后橋接對象有一個weak修飾的屬性指向真正的代理入问。橋接對象FLBridgeDelegateModel如下:
#import <Foundation/Foundation.h>
@interface FLBridgeDelegateModel : NSObject
@property (nonatomic, weak) id delegate;
- (instancetype)initWithDelegate:(id)delegate;
@end
添加代理:
- (void)addDelegate:(id<FLClientManagerDelegate>)delegate {
BOOL isExist = NO;
for (FLBridgeDelegateModel *model in self.delegateArray) {
if ([delegate isEqual:model.delegate]) {
isExist = YES;
break;
}
}
if (!isExist) {
FLBridgeDelegateModel *model = [[FLBridgeDelegateModel alloc] initWithDelegate:delegate];
[self.delegateArray addObject:model];
}
}
移除代理:
- (void)removeDelegate:(id<FLClientManagerDelegate>)delegate {
NSArray *copyArray = [self.delegateArray copy];
for (FLBridgeDelegateModel *model in copyArray) {
if ([model.delegate isEqual:delegate]) {
[self.delegateArray removeObject:model];
}
else if (!model.delegate) {
[self.delegateArray removeObject:model];
}
}
}
通過橋接對象的方式丹锹,完美解決代理無法釋放的問題
3.消息的發(fā)送,通過管理類FLChatManager實現(xiàn)
方法:
- (OnAckCallback * _Nonnull)emitWithAck:(NSString * _Nonnull)event with:(NSArray * _Nonnull)items SWIFT_WARN_UNUSED_RESULT;
[[[FLSocketManager shareManager].client emitWithAck:@"chat" with:@[parameters]] timingOutAfter:20 callback:^(NSArray * _Nonnull data) {
FLLog(@"%@", data.firstObject);
if ([data.firstObject isKindOfClass:[NSString class]] && [data.firstObject isEqualToString:@"NO ACK"]) { // 服務(wù)器沒有應(yīng)答
message.sendStatus = FLMessageSendFail;
// 發(fā)送失敗
statusChange();
}
else { // 服務(wù)器應(yīng)答
message.sendStatus = FLMessageSendSuccess;
NSDictionary *ackDic = data.firstObject;
message.timestamp = [ackDic[@"timestamp"] longLongValue];
message.msg_id = ackDic[@"msg_id"];
if (fileData) {
NSDictionary *bodies = ackDic[@"bodies"];
message.bodies.fileRemotePath = bodies[@"fileRemotePath"];
message.bodies.thumbnailRemotePath = bodies[@"thumbnailRemotePath"];
}
if (message.type == FLMessageLoc) {
NSDictionary *bodiesDic = ackDic[@"bodies"];
message.bodies.fileRemotePath = bodiesDic[@"fileRemotePath"];
}
// 發(fā)送成功
statusChange();
}
// 更新消息
[[FLChatDBManager shareManager] updateMessage:message];
// 數(shù)據(jù)庫添加或者刷新會話
[[FLChatDBManager shareManager] addOrUpdateConversationWithMessage:message isChatting:YES];
}];
二芬失、FMDB
主要實現(xiàn)離線消息存儲楣黍,F(xiàn)LChatDBManager管理類中實現(xiàn)
三、WebRTC
WebRTC棱烂,名稱源自網(wǎng)頁實時通信(Web Real-Time Communication)的縮寫租漂,簡而言之它是一個支持網(wǎng)頁瀏覽器進行實時語音對話或視頻對話的技術(shù)。
它為我們提供了視頻會議的核心技術(shù)颊糜,包括音視頻的采集哩治、編解碼、網(wǎng)絡(luò)傳輸衬鱼、顯示等功能业筏,并且還支持跨平臺:windows,linux鸟赫,mac蒜胖,android,iOS抛蚤。
它在2011年5月開放了工程的源代碼台谢,在行業(yè)內(nèi)得到了廣泛的支持和應(yīng)用,成為下一代視頻通話的標(biāo)準岁经。
首先感謝下面大神的無私分享
作者:涂耀輝
鏈接:http://www.reibang.com/p/c49da1d93df4
來源:簡書
本項目視頻通話的核心部分都是源自于此朋沮,自己將WebRTC與Socket.io予以整合,添加了部分功能
下圖為視頻通話實現(xiàn)的流程圖蒿偎,具體邏輯請參照項目源碼朽们,F(xiàn)LVideoChatHelper工具類中實現(xiàn)
關(guān)于服務(wù)器部分代碼
該項目服務(wù)器部分是通過node.js搭建,node.js真的是一門非常強大的語言诉位,而且簡單易學(xué),如果你有一點點js基礎(chǔ)相信看懂服務(wù)器代碼也沒有太大問題菜枷!本人周末在家看了一天node.js就上手寫服務(wù)器端代碼苍糠,所以有時間真滴可以認真學(xué)習(xí)一下,以后寫項目再也不用擔(dān)心沒有網(wǎng)絡(luò)數(shù)據(jù)了啤誊,哈哈
項目安裝
1.iOS
- pod install安裝第三方
- 首先我們需要去百度網(wǎng)盤下載 WebRTC頭文件和靜態(tài)庫.a岳瞭。下載完成,解壓縮蚊锹,拖入項目中瞳筏;
- 切換連接的地址為服務(wù)器的IP地址(RequestUrlConst.h中的baseUrl)
- 想要測試視頻通話功能需要兩臺真機,且同時在線牡昆,處于同一局域網(wǎng)內(nèi)
2.服務(wù)器部分
- 首先需要node.js環(huán)境
- 電腦安裝MongoDB
- npm install 安裝第三方
- brew install imagemagick
brew install graphicsmagick(服務(wù)器處理圖片用到)
待實現(xiàn)功能
- 群聊天 后臺已實現(xiàn)姚炕,iOS客戶端待實現(xiàn)
- 短視頻發(fā)送與播放
- 消息氣泡優(yōu)化
- 用戶頭像管理
- 離線消息拉取
- iOS遠程推送
-
未讀消息紅點管理
第一次發(fā)布文章,還有許多不足。如果您在文章項目中發(fā)現(xiàn)錯誤柱宦,請指正些椒!同時歡迎點贊評論,有更多想法希望多溝通交流掸刊,一起提升免糕。。忧侧。
聯(lián)系方式:
qq:954751186