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

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

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

先看看實(shí)際效果

文字.gif
圖片.gif
定位.gif
語(yǔ)音.gif
IMG_1227.PNG

使用技術(shù)

一、Socket.io

github地址

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)

視頻通話流程圖.png

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

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




    第一次發(fā)布文章,還有許多不足。如果您在文章項(xiàng)目中發(fā)現(xiàn)錯(cuò)誤,請(qǐng)指正!同時(shí)歡迎點(diǎn)贊評(píng)論,有更多想法希望多溝通交流,一起提升。。。


    聯(lián)系方式:
    qq:954751186
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容