Socket.io-FLSocketIM-iOS
基于Socket.io iOS即時(shí)通訊客戶端 iOS IM Client based on Socket.io
iOS 代碼地址:https://github.com/fengli12321/Socket.io-FLSocketIM-iOS
服務(wù)器端代碼實(shí)現(xiàn)參照:https://github.com/fengli12321/Socket.io-FLSocketIM-Server
安卓端代碼實(shí)現(xiàn)參照:https://github.com/fengli12321/Socket.io-FLSocketIM-Android
安卓簡(jiǎn)書(shū)介紹:http://www.itdecent.cn/p/cdb3b0301712
實(shí)現(xiàn)功能
- 文本發(fā)送
- 圖片發(fā)送(從相冊(cè)選取,或者拍攝)
- 短視頻
- 語(yǔ)音發(fā)送
- 視頻通話
- 其他一些效果(類似QQ底部tabBar,短視頻拍攝等)
- 功能擴(kuò)展中。。。。。
先看看實(shí)際效果




使用技術(shù)
一、Socket.io
Socket.io是該項(xiàng)目實(shí)現(xiàn)即時(shí)通訊關(guān)鍵所在,非常強(qiáng)大;
Socket.io將Websocket和輪詢 (Polling)機(jī)制以及其它的實(shí)時(shí)通信方式封裝成了通用的接口,并且在服務(wù)端實(shí)現(xiàn)了這些實(shí)時(shí)機(jī)制的相應(yīng)代碼。
先上代碼
1.創(chuàng)建Socket連接,通過(guò)單例管理類FLSocketManager實(shí)現(xiàn)
- (void)connectWithToken:(NSString *)token success:(void (^)())success fail:(void (^)())fail {
NSURL* url = [[NSURL alloc] initWithString:BaseUrl];
/**
log 是否打印日志
forceNew 這個(gè)參數(shù)設(shè)為NO從后臺(tái)恢復(fù)到前臺(tái)時(shí)總是重連,暫不清楚原因
forcePolling 是否強(qiáng)制使用輪詢
reconnectAttempts 重連次數(shù),-1表示一直重連
reconnectWait 重連間隔時(shí)間
connectParams 參數(shù)
forceWebsockets 是否強(qiáng)制使用websocket, 解釋The reason it uses polling first is because some firewalls/proxies block websockets. So polling lets socket.io work behind those.
來(lái)源: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í)時(shí)間設(shè)置為15秒
[socket connectWithTimeoutAfter:15 withHandler:^{
fail();
}];
// 監(jiān)聽(tīng)一次連接成功
[socket once:@"connect" callback:^(NSArray * _Nonnull data, SocketAckEmitter * _Nonnull ack) {
success();
}];
_client = socket;
}
這個(gè)方法是在用戶登錄后調(diào)用,主要作用是初始化Socket連接,關(guān)于socket初始化相關(guān)參數(shù)請(qǐng)參照socket.io文檔。
2.監(jiān)聽(tīng)服務(wù)器向客戶端發(fā)送的消息,通過(guò)單例管理類FLClientManager進(jìn)行管理,然后讓代理實(shí)現(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ù)庫(kù)
[[FLChatDBManager shareManager] addMessage:message];
// 會(huì)話插入數(shù)據(jù)庫(kù)或者更新會(huì)話
BOOL isChatting = [message.from isEqualToString:[FLClientManager shareManager].chattingConversation.toUser];
[[FLChatDBManager shareManager] addOrUpdateConversationWithMessage:message isChatting:isChatting];
// 本地推送,收到消息添加紅點(diǎn),聲音及震動(dòng)提示
[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];
}
}
}
}];
// 視頻通話請(qǐng)求
[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)聽(tīng)方法,這里監(jiān)聽(tīng)的事件包括:
- “chat” 接收到好友消息
- “videoChat” 視頻通話請(qǐng)求
- “onLine” 有好友上線
- “offLine” 有好友離線
- “statusChange” socket.io內(nèi)部提供的,連接狀態(tài)改變
這部分代碼,有個(gè)比較關(guān)鍵的需要說(shuō)明一下,舉個(gè)例子,在接收到“chat”事件后,數(shù)據(jù)庫(kù)管理類需要將消息存放到數(shù)據(jù)庫(kù),會(huì)話列表需要更新UI,聊天列表需要顯示該消息...也就是該事件需要多個(gè)對(duì)象響應(yīng)。對(duì)于這種需求最先想到的就是使用通知的功能,畢竟可以實(shí)現(xiàn)一對(duì)多的消息傳遞嘛!后來(lái)又思考,通過(guò)代理模式能否實(shí)現(xiàn)呢,通過(guò)制定協(xié)議代碼質(zhì)量更高?于是乎將代理存放在一個(gè)數(shù)組中,接收到事件后遍歷數(shù)組中的代理去響應(yīng)事件。 然而出現(xiàn)了一個(gè)問(wèn)題,我們?cè)谝话闶褂么砟J街?,代理都是一個(gè)weak修飾屬性,代理釋放該屬性自動(dòng)置nil,然而將代理放到數(shù)組中,代理被強(qiáng)引用,引用計(jì)數(shù)加1,數(shù)組不釋放,代理永遠(yuǎn)無(wú)法釋放。這該怎么解決呢,后來(lái)仿照一般的代理模式,創(chuàng)建一個(gè)橋接對(duì)象,代理數(shù)組里面存放橋接對(duì)象,然后橋接對(duì)象有一個(gè)weak修飾的屬性指向真正的代理。橋接對(duì)象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];
}
}
}
通過(guò)橋接對(duì)象的方式,完美解決代理無(wú)法釋放的問(wèn)題
3.消息的發(fā)送,通過(guò)管理類FLChatManager實(shí)現(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ù)器沒(méi)有應(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ù)庫(kù)添加或者刷新會(huì)話
[[FLChatDBManager shareManager] addOrUpdateConversationWithMessage:message isChatting:YES];
}];
二、FMDB
主要實(shí)現(xiàn)離線消息存儲(chǔ),F(xiàn)LChatDBManager管理類中實(shí)現(xiàn)
三、WebRTC
WebRTC,名稱源自網(wǎng)頁(yè)實(shí)時(shí)通信(Web Real-Time Communication)的縮寫(xiě),簡(jiǎn)而言之它是一個(gè)支持網(wǎng)頁(yè)瀏覽器進(jìn)行實(shí)時(shí)語(yǔ)音對(duì)話或視頻對(duì)話的技術(shù)。
它為我們提供了視頻會(huì)議的核心技術(shù),包括音視頻的采集、編解碼、網(wǎng)絡(luò)傳輸、顯示等功能,并且還支持跨平臺(tái):windows,linux,mac,android,iOS。
它在2011年5月開(kāi)放了工程的源代碼,在行業(yè)內(nèi)得到了廣泛的支持和應(yīng)用,成為下一代視頻通話的標(biāo)準(zhǔn)。
首先感謝下面大神的無(wú)私分享
作者:涂耀輝
鏈接:http://www.itdecent.cn/p/c49da1d93df4
來(lái)源:簡(jiǎn)書(shū)
本項(xiàng)目視頻通話的核心部分都是源自于此,自己將WebRTC與Socket.io予以整合,添加了部分功能
下圖為視頻通話實(shí)現(xiàn)的流程圖,具體邏輯請(qǐng)參照項(xiàng)目源碼,F(xiàn)LVideoChatHelper工具類中實(shí)現(xiàn)

關(guān)于服務(wù)器部分代碼
該項(xiàng)目服務(wù)器部分是通過(guò)node.js搭建,node.js真的是一門非常強(qiáng)大的語(yǔ)言,而且簡(jiǎn)單易學(xué),如果你有一點(diǎn)點(diǎn)js基礎(chǔ)相信看懂服務(wù)器代碼也沒(méi)有太大問(wèn)題!本人周末在家看了一天node.js就上手寫(xiě)服務(wù)器端代碼,所以有時(shí)間真滴可以認(rèn)真學(xué)習(xí)一下,以后寫(xiě)項(xiàng)目再也不用擔(dān)心沒(méi)有網(wǎng)絡(luò)數(shù)據(jù)了,哈哈
項(xiàng)目安裝
1.iOS
- pod install安裝第三方
- 首先我們需要去百度網(wǎng)盤下載 WebRTC頭文件和靜態(tài)庫(kù).a。下載完成,解壓縮,拖入項(xiàng)目中;
- 切換連接的地址為服務(wù)器的IP地址(RequestUrlConst.h中的baseUrl)
- 想要測(cè)試視頻通話功能需要兩臺(tái)真機(jī),且同時(shí)在線,處于同一局域網(wǎng)內(nèi)
2.服務(wù)器部分
- 首先需要node.js環(huán)境
- 電腦安裝MongoDB
- npm install 安裝第三方
- brew install imagemagick
brew install graphicsmagick(服務(wù)器處理圖片用到)
待實(shí)現(xiàn)功能
- 群聊天 后臺(tái)已實(shí)現(xiàn),iOS客戶端待實(shí)現(xiàn)
- 短視頻發(fā)送與播放
- 消息氣泡優(yōu)化
- 用戶頭像管理
- 離線消息拉取
- iOS遠(yuǎn)程推送
-
未讀消息紅點(diǎn)管理
第一次發(fā)布文章,還有許多不足。如果您在文章項(xiàng)目中發(fā)現(xiàn)錯(cuò)誤,請(qǐng)指正!同時(shí)歡迎點(diǎn)贊評(píng)論,有更多想法希望多溝通交流,一起提升。。。
聯(lián)系方式:
qq:954751186