iOS基于Socket.io即時通訊IM實現(xiàn)炼彪,WebRTC實現(xiàn)視頻通話

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)功能

  1. 文本發(fā)送
  2. 圖片發(fā)送(從相冊選取,或者拍攝)
  3. 短視頻
  4. 語音發(fā)送
  5. 視頻通話
  6. 其他一些效果(類似QQ底部tabBar,短視頻拍攝等)
  7. 功能擴展中玖详。。。揽咕。。

先看看實際效果

文字.gif
圖片.gif
定位.gif
語音.gif
IMG_1227.PNG

使用技術(shù)

一套菜、Socket.io

github地址

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)

視頻通話流程圖.png

關(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)功能

  1. 群聊天 后臺已實現(xiàn)姚炕,iOS客戶端待實現(xiàn)
  2. 短視頻發(fā)送與播放
  3. 消息氣泡優(yōu)化
  4. 用戶頭像管理
  5. 離線消息拉取
  6. iOS遠程推送
  7. 未讀消息紅點管理




    第一次發(fā)布文章,還有許多不足。如果您在文章項目中發(fā)現(xiàn)錯誤柱宦,請指正些椒!同時歡迎點贊評論,有更多想法希望多溝通交流掸刊,一起提升免糕。。忧侧。


    聯(lián)系方式:
    qq:954751186
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末石窑,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蚓炬,更是在濱河造成了極大的恐慌尼斧,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件试吁,死亡現(xiàn)場離奇詭異棺棵,居然都是意外死亡,警方通過查閱死者的電腦和手機熄捍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門烛恤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人余耽,你說我怎么就攤上這事缚柏。” “怎么了碟贾?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵币喧,是天一觀的道長。 經(jīng)常有香客問我袱耽,道長杀餐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任朱巨,我火速辦了婚禮史翘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘冀续。我一直安慰自己琼讽,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布洪唐。 她就那樣靜靜地躺著钻蹬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪凭需。 梳的紋絲不亂的頭發(fā)上问欠,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天肝匆,我揣著相機與錄音,去河邊找鬼溅潜。 笑死术唬,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的滚澜。 我是一名探鬼主播粗仓,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼设捐!你這毒婦竟也來了借浊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤萝招,失蹤者是張志新(化名)和其女友劉穎蚂斤,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體槐沼,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡曙蒸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了岗钩。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纽窟。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖兼吓,靈堂內(nèi)的尸體忽然破棺而出臂港,到底是詐尸還是另有隱情,我是刑警寧澤视搏,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布审孽,位于F島的核電站,受9級特大地震影響浑娜,放射性物質(zhì)發(fā)生泄漏佑力。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一棚愤、第九天 我趴在偏房一處隱蔽的房頂上張望搓萧。 院中可真熱鬧,春花似錦宛畦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至那伐,卻和暖如春踏施,著一層夾襖步出監(jiān)牢的瞬間石蔗,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工畅形, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留养距,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓日熬,卻偏偏與公主長得像棍厌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子竖席,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容