天天看點

iOS IM自研方案

這裡寫自定義目錄标題

前言

本文會用執行個體的方式,将iOS各種IM的方案都簡單的實作一遍。并且提供一些選型、實作細節以及優化的建議。

注:文中的所有的代碼示例,在github中都有demo:https://github.com/tuyaohui/IM_iOS

(demo)

可以打開項目先預覽效果,對照着進行閱讀。

言歸正傳,首先我們來總結一下我們去實作IM的方式

第一種方式,使用第三方IM服務

對于短平快的公司,完全可以采用第三方SDK來實作。國内IM的第三方服務商有很多,類似雲信、環信、融雲、LeanCloud,當然還有其它的很多,這裡就不一一舉例了,感興趣的小夥伴可以自行查閱下。

第三方服務商IM底層協定基本上都是TCP。他們的IM方案很成熟,有了它們,我們甚至不需要自己去搭建IM背景,什麼都不需要去考慮。

如果你足夠懶,甚至連UI都不需要自己做,這些第三方有各自一套IM的UI,拿來就可以直接用。真可謂3分鐘內建…

但是缺點也很明顯,定制化程度太高,很多東西我們不可控。當然還有一個最最重要的一點,就是太貴了…作為真正社交為主打的APP,僅此一點,就足以讓我們望而卻步。當然,如果IM對于APP隻是一個輔助功能,那麼用第三方服務也無可厚非。

另外一種方式,我們自己去實作

我們自己去實作也有很多選擇:

1)首先面臨的就是傳輸協定的選擇,TCP還是UDP?

2)其次是我們需要去選擇使用哪種聊天協定:

基于Scoket或者WebScoket或者其他的私有協定、

MQTT

還是廣為人诟病的XMPP?

3)我們是自己去基于OS底層Socket進行封裝還是在第三方架構的基礎上進行封裝?

4)傳輸資料的格式,我們是用Json、還是XML、還是谷歌推出的ProtocolBuffer?

5)我們還有一些細節問題需要考慮,例如TCP的長連接配接如何保持,心跳機制,Qos機制,重連機制等等…當然,除此之外,我們還有一些安全問題需要考慮。

一、傳輸協定的選擇

接下來我們可能需要自己考慮去實作IM,首先從傳輸層協定來說,我們有兩種選擇:TCPorUDP?

移動端IM/推送系統的協定選型:UDP還是TCP?

這裡我們直接說結論吧:對于小公司或者技術不那麼成熟的公司,IM一定要用TCP來實作,因為如果你要用UDP的話,需要做的事太多。當然QQ就是用的UDP協定,當然不僅僅是UDP,騰訊還用了自己的私有協定,來保證了傳輸的可靠性,杜絕了UDP下各種資料丢包,亂序等等一系列問題。

總之一句話,如果你覺得團隊技術很成熟,那麼你用UDP也行,否則還是用TCP為好。

二、我們來看看各種聊天協定

首先我們以實作方式來切入,基本上有以下四種實作方式:

基于Scoket原生:代表架構CocoaAsyncSocket。

基于WebScoket:代表架構SocketRocket。

基于MQTT:代表架構MQTTKit。

基于XMPP:代表架構XMPPFramework。

當然,以上四種方式我們都可以不使用第三方架構,直接基于OS底層Scoket去實作我們的自定義封裝。下面我會給出一個基于Scoket原生而不使用架構的例子,供大家參考一下。

首先需要搞清楚的是,其中MQTT和XMPP為聊天協定,它們是最上層的協定,而WebScoket是傳輸通訊協定,它是基于Socket封裝的一個協定。而通常我們所說的騰訊IM的私有協定,就是基于WebScoket或者Scoket原生進行封裝的一個聊天協定。

是以說到底,iOS要做一個真正的IM産品,一般都是基于Scoket或者WebScoket等,再之上加上一些私有協定來保證的。

1.我們先不使用任何架構,直接用OS底層Socket來實作一個簡單的IM。

我們用戶端的實作思路也是很簡單,建立Socket,和伺服器的Socket對接上,然後開始傳輸資料就可以了。

我們學過c/c++或者java這些語言,我們就知道,往往任何教程,最後一章都是講Socket程式設計,而Socket是什麼呢,簡單的來說,就是我們使用TCP/IP或者UDP/IP協定的一組程式設計接口。

我們在應用層,使用socket,輕易的實作了程序之間的通信(跨網絡的)。想想,如果沒有socket,我們要直面TCP/IP協定,我們需要去寫多少繁瑣而又重複的代碼。

如果有對socket概念仍然有所困惑的,可以看看這篇文章:

從問題看本質,socket到底是什麼?。

但是這篇文章關于并發連接配接數的認識是錯誤的,正确的認識可以看看這篇文章:

單台伺服器并發TCP連接配接數到底可以有多少

我們接着可以開始着手去實作IM了,首先我們不基于任何架構,直接去調用OS底層-基于C的BSD Socket去實作,它提供了這樣一組接口:

讓我們可以對socket進行各種操作,首先我們來用它寫個用戶端。總結一下,簡單的IM用戶端需要做如下4件事:

用戶端調用 socket(…) 建立socket;

用戶端調用 connect(…) 向伺服器發起連接配接請求以建立連接配接;

用戶端與伺服器建立連接配接之後,就可以通過send(…)/receive(…)向用戶端發送或從用戶端接收資料;

用戶端調用 close 關閉 socket;

根據上面4條大綱,我們封裝了一個名為TYHSocketManager的單例,來對socket相關方法進行調用:

TYHSocketManager.h

#import<Foundation/Foundation.h>

@interfaceTYHSocketManager:NSObject

+(instancetype)share;

-(void)connect;

-(void)disConnect;

-(void)sendMsg:(NSString*)msg;

@end

#import “TYHSocketManager.h”

#import <sys/types.h>

#import <sys/socket.h>

#import <netinet/in.h>

#import <arpa/inet.h>

@interface TYHSocketManager()

@property (nonatomic,assign)int clientScoket;

@end

TYHSocketManager.m

@implementation TYHSocketManager

  • (instancetype)share

    {

    static dispatch_once_t onceToken;

    static TYHSocketManager *instance = nil;

    dispatch_once(&onceToken, ^{

    instance = [[self alloc]init];

    [instance initScoket];

    [instance pullMsg];

    });

    return instance;

    }

  • (void)initScoket

    {

    //每次連接配接前,先斷開連接配接

    if (_clientScoket != 0) {

    [self disConnect];

    _clientScoket = 0;

    }

    //建立用戶端socket

    _clientScoket = CreateClinetSocket();

    //伺服器Ip

    const char * server_ip=“127.0.0.1”;

    //伺服器端口

    short server_port=6969;

    //等于0說明連接配接失敗

    if (ConnectionToServer(_clientScoket,server_ip, server_port)==0) {

    printf(“Connect to server error\n”);

    return ;

    }

    //走到這說明連接配接成功

    printf(“Connect to server ok\n”);

    }

    static int CreateClinetSocket()

    {

    int ClinetSocket = 0;

    //建立一個socket,傳回值為Int。(注scoket其實就是Int類型)

    //第一個參數addressFamily IPv4(AF_INET) 或 IPv6(AF_INET6)。

    //第二個參數 type 表示 socket 的類型,通常是流stream(SOCK_STREAM) 或資料封包datagram(SOCK_DGRAM)

    //第三個參數 protocol 參數通常設定為0,以便讓系統自動為選擇我們合适的協定,對于 stream socket 來說會是 TCP 協定(IPPROTO_TCP),而對于 datagram來說會是 UDP 協定(IPPROTO_UDP)。

    ClinetSocket = socket(AF_INET, SOCK_STREAM, 0);

    return ClinetSocket;

    }

    static int ConnectionToServer(int client_socket,const char * server_ip,unsigned short port)

    {

    //生成一個sockaddr_in類型結構體

    struct sockaddr_in sAddr={0};

    sAddr.sin_len=sizeof(sAddr);

    //設定IPv4

    sAddr.sin_family=AF_INET;

    //inet_aton是一個改進的方法來将一個字元串IP位址轉換為一個32位的網絡序列IP位址

    //如果這個函數成功,函數的傳回值非零,如果輸入位址不正确則會傳回零。

    inet_aton(server_ip, &sAddr.sin_addr);

    //htons是将整型變量從主機位元組順序轉變成網絡位元組順序,指派端口号

    sAddr.sin_port=htons(port);

    //用scoket和服務端位址,發起連接配接。

    //用戶端向特定網絡位址的伺服器發送連接配接請求,連接配接成功傳回0,失敗傳回 -1。

    //注意:該接口調用會阻塞目前線程,直到伺服器傳回。

    if (connect(client_socket, (struct sockaddr *)&sAddr, sizeof(sAddr))==0) {

    return client_socket;

    }

    return 0;

    }

    #pragma mark - 新線程來接收消息

  • (void)pullMsg {

    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(recieveAction) object:nil];

    [thread start];

    }

    #pragma mark - 對外邏輯

  • (void)connect{

    [self initScoket];

    }

  • (void)disConnect {

    //關閉連接配接

    close(self.clientScoket);

    }

    //發送消息

  • (void)sendMsg:(NSString *)msg {

    const char *send_Message = [msg UTF8String];

    send(self.clientScoket,send_Message,strlen(send_Message)+1,0);

    }

    //收取服務端發送的消息

  • (void)recieveAction{

    while (1) {

    char recv_Message[1024] = {0};

    recv(self.clientScoket, recv_Message, sizeof(recv_Message), 0);

    printf("%s\n",recv_Message);

    }

    }

    如上所示:

    我們調用了initScoket方法,利用CreateClinetSocket方法了一個scoket,就是就是調用了socket函數:

    ClinetSocket = socket(AF_INET, SOCK_STREAM, 0);

    然後調用了ConnectionToServer函數與伺服器連接配接,IP位址為127.0.0.1也就是本機localhost和端口6969相連。在該函數中,我們綁定了一個sockaddr_in類型的結構體,該結構體内容如下:

    structsockaddr_in{__uint8_t sin_len;sa_family_t sin_family;in_port_t sin_port;structin_addrsin_addr;charsin_zero[8];};

    裡面包含了一些,我們需要連接配接的服務端的scoket的一些基本參數,具體指派細節可以見注釋。

    連接配接成功之後,我們就可以調用send函數和recv函數進行消息收發了,在這裡,我新開辟了一個常駐線程,在這個線程中一個死循環裡去不停的調用recv函數,這樣服務端有消息發送過來,第一時間便能被接收到。

    就這樣用戶端便簡單的可以用了,接着我們來看看服務端的實作。

    一樣,我們首先對服務端需要做的工作簡單的總結下:

    伺服器調用 socket(…) 建立socket;

    伺服器調用 listen(…) 設定緩沖區;

    伺服器通過 accept(…)接受用戶端請求建立連接配接;

    伺服器與用戶端建立連接配接之後,就可以通過 send(…)/receive(…)向用戶端發送或從用戶端接收資料;

    伺服器調用 close 關閉 socket;

    接着我們就可以具體去實作了

    OS底層的函數是支援我們去實作服務端的,但是我們一般不會用iOS去這麼做(試問真正的應用場景,有誰用iOS做scoket伺服器麼…),如果還是想用這些函數去實作服務端,可以參考下這篇文章:深入淺出Cocoa-iOS網絡程式設計之Socket。

    在這裡我用node.js去搭了一個簡單的scoket伺服器。源碼如下:

    var net = require(‘net’);

    var HOST = ‘127.0.0.1’;

    var PORT = 6969;

    // 建立一個TCP伺服器執行個體,調用listen函數開始監聽指定端口

    // 傳入net.createServer()的回調函數将作為”connection“事件的處理函數

    // 在每一個“connection”事件中,該回調函數接收到的socket對象是唯一的

    net.createServer(function(sock) {

    //我們獲得一個連接配接 - 該連接配接自動關聯一個socket對象

    console.log('CONNECTED: ’ +

    sock.remoteAddress + ‘:’ + sock.remotePort);

    sock.write(‘服務端發出:連接配接成功’);

    //為這個socket執行個體添加一個"data"事件處理函數

    sock.on(‘data’, function(data) {

    console.log('DATA ’ + sock.remoteAddress + ': ’ + data);

    //回發該資料,用戶端将收到來自服務端的資料

    sock.write(‘You said "’ + data + ‘"’);

    });

    //為這個socket執行個體添加一個"close"事件處理函數

    sock.on(‘close’, function(data) {

    console.log(‘CLOSED: ’ +

    sock.remoteAddress + ’ ’ + sock.remotePort);

    });

    }).listen(PORT, HOST);

    console.log(‘Server listening on ’ + HOST +’:’+ PORT);

    看到這不懂node.js的朋友也不用着急,在這裡你可以使用任意語言c/c++/java/oc等等去實作背景,這裡node.js僅僅是樓主的一個選擇,為了讓我們來驗證之前寫的用戶端scoket的效果。如果你不懂node.js也沒關系,你隻需要把上述樓主寫的相關代碼複制粘貼,如果你本機有node的解釋器,那麼直接在終端進入該源代碼檔案目錄中輸入:

    node fileName

    即可運作該腳本(fileName為儲存源代碼的檔案名)。

接着我們用之前寫的iOS端的例子。用戶端列印顯示連接配接成功,而我們運作的伺服器也列印了連接配接成功。接着我們發了一條消息,服務端成功的接收到了消息後,把該消息再發送回用戶端,繞了一圈用戶端又收到了這條消息。至此我們用OS底層scoket實作了簡單的IM。

大家看到這是不是覺得太過簡單了?

當然簡單,我們僅僅是實作了Scoket的連接配接,資訊的發送與接收,除此之外我們什麼都沒有做,現實中,我們需要做的處理遠不止于此,我們先接着往下看。接下來,我們就一起看看第三方架構是如何實作IM的。

這個架構實作了兩種傳輸協定TCP和UDP,分别對應GCDAsyncSocket類和GCDAsyncUdpSocket,這裡我們重點講GCDAsyncSocket。2.我們接着來看看基于Socket原生的CocoaAsyncSocket:

這裡Socket伺服器延續上一個例子,因為同樣是基于原生Scoket的架構,是以之前的Node.js的服務端,該例仍然試用。這裡我們就隻需要去封裝用戶端的執行個體,我們還是建立一個TYHSocketManager單例。

TYHSocketManager.h

#import <Foundation/Foundation.h>

@interface TYHSocketManager : NSObject

  • (instancetype)share;
  • (BOOL)connect;
  • (void)disConnect;
  • (void)sendMsg:(NSString *)msg;
  • (void)pullTheMsg;

    @end

    TYHSocketManager.m

    #import “TYHSocketManager.h”

    #import “GCDAsyncSocket.h” // for TCP

    static NSString * Khost = @“127.0.0.1”;

    static const uint16_t Kport = 6969;

    @interface TYHSocketManager(){

    GCDAsyncSocket *gcdSocket;

    }

    @end

    @implementation TYHSocketManager

  • (instancetype)share {

    static dispatch_once_t onceToken;

    static TYHSocketManager *instance = nil;

    dispatch_once(&onceToken, ^{

    instance = [[self alloc]init];

    [instance initSocket];

    });

    return instance;

    }

  • (void)initSocket{

    gcdSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];

    }

    #pragma mark - 對外的一些接口

    //建立連接配接

  • (BOOL)connect {

    return [gcdSocket connectToHost:Khost onPort:Kport error:nil];

    }

    //斷開連接配接

  • (void)disConnect {

    [gcdSocket disconnect];

    }

    //發送消息

  • (void)sendMsg:(NSString *)msg {

    NSData *data = [msg dataUsingEncoding:NSUTF8StringEncoding];

    //第二個參數,請求逾時時間

    [gcdSocket writeData:data withTimeout:-1 tag:110];

    }

    //監聽最新的消息

  • (void)pullTheMsg {

    //監聽讀資料的代理 -1永遠監聽,不逾時,但是隻收一次消息,

    //是以每次接受到消息還得調用一次

    [gcdSocket readDataWithTimeout:-1 tag:110];

    }

    #pragma mark - GCDAsyncSocketDelegate

    //連接配接成功調用

  • (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {

    NSLog(@“連接配接成功,host:%@,port:%d”,host,port);

    [self pullTheMsg];

    //心跳寫在這…

    }

    //斷開連接配接的時候調用

  • (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err {

    NSLog(@“斷開連接配接,host:%@,port:%d”,sock.localHost,sock.localPort);

    //斷線重連寫在這…

    }

    //寫成功的回調

  • (void)socket:(GCDAsyncSocket*)sock didWriteDataWithTag:(long)tag {

    // NSLog(@“寫的回調,tag:%ld”,tag);

    }

    //收到消息的回調

  • (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {

    NSString msg = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];

    NSLog(@“收到消息:%@”,msg);

    [self pullTheMsg];

    }

    //分段去擷取消息的回調

    //- (void)socket:(GCDAsyncSocket )sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag

    //{

    //

    // NSLog(@“讀的回調,length:%ld,tag:%ld”,partialLength,tag);

    //

    //}

    //為上一次設定的讀取資料代理續時 (如果設定逾時為-1,則永遠不會調用到)

    //-(NSTimeInterval)socket:(GCDAsyncSocket )sock shouldTimeoutReadWithTag:(long)tag elapsed:(NSTimeInterval)elapsed bytesDone:(NSUInteger)length

    //{

    // NSLog(@“來延時,tag:%ld,elapsed:%f,length:%ld”,tag,elapsed,length);

    // return 10;

    //}

    @end

    這個架構使用起來也十分簡單,它基于Scoket往上進行了一層封裝,提供了OC的接口給我們使用。至于使用方法,大家看看注釋應該就能明白,這裡唯一需要說的一點就是這個方法:

    [gcdSocket readDataWithTimeout:-1tag:110];

    這個方法的作用就是去讀取目前消息隊列中的未讀消息。記住,這裡不調用這個方法,消息回調的代理是永遠不會被觸發的。而且必須是tag相同,如果tag不同,這個收到消息的代理也不會被處罰。

    我們調用一次這個方法,隻能觸發一次讀取消息的代理,如果我們調用的時候沒有未讀消息,它就會等在那,直到消息來了被觸發。一旦被觸發一次代理後,我們必須再次調用這個方法,否則,之後的消息到了仍舊無法觸發我們讀取消息的代理。就像我們在例子中使用的那樣,在每次讀取到消息之後我們都去調用:

    //收到消息的回調

    -(void)socket:(GCDAsyncSocket)sock didReadData:(NSData)data withTag:(long)tag{NSStringmsg=[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];NSLog(@“收到消息:%@”,msg);[selfpullTheMsg];}//監聽最新的消息-(void)pullTheMsg{//監聽讀資料的代理,隻能監聽10秒,10秒過後調用代理方法 -1永遠監聽,不逾時,但是隻收一次消息,//是以每次接受到消息還得調用一次[gcdSocket readDataWithTimeout:-1tag:110];}

    除此之外,我們還需要說的是這個逾時timeout

    這裡如果設定10秒,那麼就隻能監聽10秒,10秒過後調用是否續時的代理方法:

    -(NSTimeInterval)socket:(GCDAsyncSocket *)sockshouldTimeoutReadWithTag:(long)tagelapsed:(NSTimeInterval)elapsedbytesDone:(NSUInteger)length

    如果我們選擇不續時,那麼10秒到了還沒收到消息,那麼Scoket會自動斷開連接配接。看到這裡有些小夥伴要吐槽了,怎麼一個方法設計的這麼麻煩,當然這裡這麼設計是有它的應用場景的,我們後面再來細講。

至此我們也用CocoaAsyncSocket這個架構實作了一個簡單的IM。

這個例子我們會把心跳,斷線重連,以及PingPong機制進行簡單的封裝,是以我們先來談談這三個概念:3.接着我們繼續來看看基于webScoket的IM:

首先我們來談談什麼是心跳

簡單的來說,心跳就是用來檢測TCP連接配接的雙方是否可用。那又會有人要問了,TCP不是本身就自帶一個KeepAlive機制嗎?

這裡我們需要說明的是TCP的KeepAlive機制隻能保證連接配接的存在,但是并不能保證用戶端以及服務端的可用性.比如會有以下一種情況:

某台伺服器因為某些原因導緻負載超高,CPU 100%,無法響應任何業務請求,但是使用 TCP 探針則仍舊能夠确定連接配接狀态,這就是典型的連接配接活着但業務提供方已死的狀态。

這個時候心跳機制就起到作用了:

我們用戶端發起心跳Ping(一般都是用戶端),假如設定在10秒後如果沒有收到回調,那麼說明伺服器或者用戶端某一方出現問題,這時候我們需要主動斷開連接配接。

服務端也是一樣,會維護一個socket的心跳間隔,當約定時間内,沒有收到用戶端發來的心跳,我們會知道該連接配接已經失效,然後主動斷開連接配接。

參考文章:為什麼說基于TCP的移動端IM仍然需要心跳保活?

其實做過IM的小夥伴們都知道,我們真正需要心跳機制的原因其實主要是在于國内營運商NAT逾時。

那麼究竟什麼是NAT逾時呢?

原來這是因為IPV4引起的,我們上網很可能會處在一個NAT裝置(無線路由器之類)之後。

NAT裝置會在IP封包通過裝置時修改源/目的IP位址. 對于家用路由器來說, 使用的是網絡位址端口轉換(NAPT), 它不僅改IP, 還修改TCP和UDP協定的端口号, 這樣就能讓内網中的裝置共用同一個外網IP. 舉個例子, NAPT維護一個類似下表的NAT表:

NAT映射

NAT裝置會根據NAT表對出去和進來的資料做修改, 比如将192.168.0.3:8888發出去的封包改成120.132.92.21:9202, 外部就認為他們是在和120.132.92.21:9202通信. 同時NAT裝置會将120.132.92.21:9202收到的封包的IP和端口改成192.168.0.3:8888, 再發給内網的主機, 這樣内部和外部就能雙向通信了, 但如果其中192.168.0.3:8888==120.132.92.21:9202這一映射因為某些原因被NAT裝置淘汰了, 那麼外部裝置就無法直接與192.168.0.3:8888通信了。

我們的裝置經常是處在NAT裝置的後面, 比如在大學裡的校園網, 查一下自己配置設定到的IP, 其實是内網IP, 表明我們在NAT裝置後面, 如果我們在寝室再接個路由器, 那麼我們發出的資料包會多經過一次NAT.

國内移動無線網絡營運商在鍊路上一段時間内沒有資料通訊後, 會淘汰NAT表中的對應項, 造成鍊路中斷。

而國内的營運商一般NAT逾時的時間為5分鐘,是以通常我們心跳設定的時間間隔為3-5分鐘。

接着我們來講講PingPong機制:

很多小夥伴可能又會感覺到疑惑了,那麼我們在這心跳間隔的3-5分鐘如果連接配接假線上(例如在地鐵電梯這種環境下)。那麼我們豈不是無法保證消息的即時性麼?這顯然是我們無法接受的,是以業内的解決方案是采用雙向的PingPong機制。

need-to-insert-img

當服務端發出一個Ping,用戶端沒有在約定的時間内傳回響應的ack,則認為用戶端已經不線上,這時我們Server端會主動斷開Scoket連接配接,并且改由APNS推送的方式發送消息。

同樣的是,當用戶端去發送一個消息,因為我們遲遲無法收到服務端的響應ack包,則表明用戶端或者服務端已不線上,我們也會顯示消息發送失敗,并且斷開Scoket連接配接。

還記得我們之前CocoaSyncSockt的例子所講的擷取消息逾時就斷開嗎?其實它就是一個PingPong機制的用戶端實作。我們每次可以在發送消息成功後,調用這個逾時讀取的方法,如果一段時間沒收到伺服器的響應,那麼說明連接配接不可用,則斷開Scoket連接配接

最後就是重連機制:

理論上,我們自己主動去斷開的Scoket連接配接(例如退出賬号,APP退出到背景等等),不需要重連。其他的連接配接斷開,我們都需要進行斷線重連。

一般解決方案是嘗試重連幾次,如果仍舊無法重連成功,那麼不再進行重連。

接下來的WebScoket的例子,我會封裝一個重連時間指數級增長的一個重連方式,可以作為一個參考。

言歸正傳,我們看完上述三個概念之後,我們來講一個WebScoket最具代表性的一個第三方架構SocketRocket。

我們首先來看看它對外封裝的一些方法:

@interface SRWebSocket : NSObject

@property (nonatomic, weak) id delegate;

@property (nonatomic, readonly) SRReadyState readyState;

@property (nonatomic, readonly, retain) NSURL *url;

@property (nonatomic, readonly) CFHTTPMessageRef receivedHTTPHeaders;

// Optional array of cookies (NSHTTPCookie objects) to apply to the connections

@property (nonatomic, readwrite) NSArray * requestCookies;

// This returns the negotiated protocol.

// It will be nil until after the handshake completes.

@property (nonatomic, readonly, copy) NSString *protocol;

// Protocols should be an array of strings that turn into Sec-WebSocket-Protocol.

  • (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates;
  • (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols;
  • (id)initWithURLRequest:(NSURLRequest *)request;

    // Some helper constructors.

  • (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates;
  • (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols;
  • (id)initWithURL:(NSURL *)url;

    // Delegate queue will be dispatch_main_queue by default.

    // You cannot set both OperationQueue and dispatch_queue.

  • (void)setDelegateOperationQueue:(NSOperationQueue*) queue;
  • (void)setDelegateDispatchQueue:(dispatch_queue_t) queue;

    // By default, it will schedule itself on +[NSRunLoop SR_networkRunLoop] using defaultModes.

  • (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
  • (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;

    // SRWebSockets are intended for one-time-use only. Open should be called once and only once.

  • (void)open;
  • (void)close;
  • (void)closeWithCode:(NSInteger)code reason:(NSString *)reason;

    // Send a UTF8 String or Data.

  • (void)send:(id)data;

    // Send Data (can be nil) in a ping message.

  • (void)sendPing:(NSData *)data;

    @end

    #pragma mark - SRWebSocketDelegate

    @protocol SRWebSocketDelegate

    // message will either be an NSString if the server is using text

    // or NSData if the server is using binary.

  • (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message;

    @optional

  • (void)webSocketDidOpen:(SRWebSocket *)webSocket;
  • (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error;
  • (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean;
  • (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload;

    // Return YES to convert messages sent as Text to an NSString. Return NO to skip NSData -> NSString conversion for Text messages. Defaults to YES.

  • (BOOL)webSocketShouldConvertTextFrameToString:(SRWebSocket *)webSocket;

    @end

    方法也很簡單,分為兩個部分:

    一部分為SRWebSocket的初始化,以及連接配接,關閉連接配接,發送消息等方法。

    另一部分為SRWebSocketDelegate,其中包括一些回調:

    收到消息的回調,連接配接失敗的回調,關閉連接配接的回調,收到pong的回調,是否需要把data消息轉換成string的代理方法。

    接着我們還是舉個例子來實作以下,首先來封裝一個TYHSocketManager單例:

    TYHSocketManager.h

    #import <Foundation/Foundation.h>

    typedef enum : NSUInteger {

    disConnectByUser ,

    disConnectByServer,

    } DisConnectType;

    @interface TYHSocketManager : NSObject

  • (instancetype)share;
  • (void)connect;
  • (void)disConnect;
  • (void)sendMsg:(NSString *)msg;
  • (void)ping;

    @end

    TYHSocketManager.m

    #import “TYHSocketManager.h”

    #import “SocketRocket.h”

    #define dispatch_main_async_safe(block)

    if ([NSThread isMainThread]) {

    block();

    } else {

    dispatch_async(dispatch_get_main_queue(), block);

    }

    static NSString * Khost = @“127.0.0.1”;

    static const uint16_t Kport = 6969;

    @interface TYHSocketManager()

    {

    SRWebSocket *webSocket;

    NSTimer *heartBeat;

    NSTimeInterval reConnectTime;

    }

    @end

    @implementation TYHSocketManager

  • (instancetype)share {

    static dispatch_once_t onceToken;

    static TYHSocketManager *instance = nil;

    dispatch_once(&onceToken, ^{

    instance = [[self alloc]init];

    [instance initSocket];

    });

    return instance;

    }

    //初始化連接配接

  • (void)initSocket {

    if (webSocket) {

    return;

    }

    webSocket = [[SRWebSocket alloc]initWithURL:[NSURL URLWithString:[NSString stringWithFormat:@“ws://%@:%d”, Khost, Kport]]];

    webSocket.delegate = self;

    //設定代理線程queue

    NSOperationQueue *queue = [[NSOperationQueue alloc]init];

    queue.maxConcurrentOperationCount = 1;

    [webSocket setDelegateOperationQueue:queue];

    //連接配接

    [webSocket open];

    }

    //初始化心跳

  • (void)initHeartBeat {

    dispatch_main_async_safe(^{

    [self destoryHeartBeat];

    __weak typeof(self) weakSelf = self;

    //心跳設定為3分鐘,NAT逾時一般為5分鐘

    heartBeat = [NSTimer scheduledTimerWithTimeInterval:3*60 repeats:YES block:^(NSTimer * _Nonnull timer) {

    NSLog(@“heart”);

    //和服務端約定好發送什麼作為心跳辨別,盡可能的減小心跳包大小

    [weakSelf sendMsg:@“heart”];

    }];

    [[NSRunLoop currentRunLoop]addTimer:heartBeat forMode:NSRunLoopCommonModes];

    })

    }

    //取消心跳

  • (void)destoryHeartBeat {

    dispatch_main_async_safe(^{

    if (heartBeat) {

    [heartBeat invalidate];

    heartBeat = nil;

    }

    })

    }

    #pragma mark - 對外的一些接口

    //建立連接配接

  • (void)connect {

    [self initSocket];

    //每次正常連接配接的時候清零重連時間

    reConnectTime = 0;

    }

    //斷開連接配接

  • (void)disConnect {

    if (webSocket) {

    [webSocket close];

    webSocket = nil;

    }

    }

    //發送消息

  • (void)sendMsg:(NSString *)msg {

    [webSocket send:msg];

    }

    //重連機制

  • (void)reConnect {

    [self disConnect];

    //超過一分鐘就不再重連 是以隻會重連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(), ^{

    webSocket = nil;

    [self initSocket];

    });

    //重連時間2的指數級增長

    if (reConnectTime == 0) {

    reConnectTime = 2;

    }else{

    reConnectTime *= 2;

    }

    }

    //pingPong

  • (void)ping{

    [webSocket sendPing:nil];

    }

    #pragma mark - SRWebSocketDelegate

  • (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {

    NSLog(@“伺服器傳回收到消息:%@”,message);

    }

  • (void)webSocketDidOpen:(SRWebSocket *)webSocket {

    NSLog(@“連接配接成功”);

    //連接配接成功了開始發送心跳

    [self initHeartBeat];

    }

    //open失敗的時候調用

  • (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {

    NSLog(@“連接配接失敗…\n%@”,error);

    //失敗了就去重連

    [self reConnect];

    }

    //網絡連接配接中斷被調用

  • (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {

    NSLog(@“被關閉連接配接,code:%ld,reason:%@,wasClean:%d”,code,reason,wasClean);

    //如果是被使用者自己中斷的那麼直接斷開連接配接,否則開始重連

    if (code == disConnectByUser) {

    [self disConnect];

    }else{

    [self reConnect];

    }

    //斷開連接配接時銷毀心跳

    [self destoryHeartBeat];

    }

    //sendPing的時候,如果網絡通的話,則會收到回調,但是必須保證ScoketOpen,否則會crash

  • (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload{

    NSLog(@“收到pong回調”);

    }

    //将收到的消息,是否需要把data轉換為NSString,每次收到消息都會被調用,預設YES

    //- (BOOL)webSocketShouldConvertTextFrameToString:(SRWebSocket *)webSocket

    //{

    // NSLog(@“webSocketShouldConvertTextFrameToString”);

    //

    // return NO;

    //}

    .m檔案有點長,大家可以參照github中的demo進行閱讀,這回我們添加了一些細節的東西了,包括一個簡單的心跳,重連機制,還有webScoket封裝好的一個pingpong機制。

    代碼非常簡單,大家可以配合着注釋讀一讀,應該很容易了解。

    需要說一下的是這個心跳機制是一個定時的間隔,往往我們可能會有更複雜實作,比如我們正在發送消息的時候,可能就不需要心跳。當不在發送的時候在開啟心跳之類的。微信有一種更高端的實作方式,有興趣的小夥伴可以看看:

    微信的智能心跳實作方式

    還有一點需要說的就是這個重連機制,demo中我采用的是2的指數級别增長,第一次立刻重連,第二次2秒,第三次4秒,第四次8秒…直到大于64秒就不再重連。而任意的一次成功的連接配接,都會重置這個重連時間。

    最後一點需要說的是,這個架構給我們封裝的webscoket在調用它的sendPing方法之前,一定要判斷目前scoket是否連接配接,如果不是連接配接狀态,程式則會crash。

    用戶端的實作就大緻如此,接着同樣我們需要實作一個服務端,來看看實際通訊效果。

    webScoket服務端實作

    在這裡我們無法沿用之前的node.js例子了,因為這并不是一個原生的scoket,這是webScoket,是以我們服務端同樣需要遵守webScoket協定,兩者才能實作通信。

    其實這裡實作也很簡單,我采用了node.js的ws子產品,隻需要用npm去安裝ws即可。

    什麼是npm呢?舉個例子,npm之于Node.js相當于cocospod至于iOS,它就是一個拓展子產品的一個管理工具。如果不知道怎麼用的可以看看這篇文章:npm的使用

    我們進入目前腳本目錄,輸入終端指令,即可安裝ws子產品:

    $ npm install ws

    大家如果懶得去看npm的小夥伴也沒關系,直接下載下傳github中的WSServer.js這個檔案運作即可。

    該源檔案代碼如下:

    var WebSocketServer = require(‘ws’).Server,

    wss = new WebSocketServer({ port: 6969 });

    wss.on(‘connection’, function (ws) {

    console.log(‘client connected’);

    ws.send(‘你是第’ + wss.clients.length + ‘位’);

    //收到消息回調

    ws.on(‘message’, function (message) {

    console.log(message);

    ws.send(‘收到:’+message);

    });

    //退出聊天

    ws.on(‘close’, function(close) {

    console.log(‘退出連接配接了’);

    });

    });

    console.log(‘開始監聽6969端口’);

    代碼沒幾行,了解起來很簡單。

    就是監聽了本機6969端口,如果用戶端連接配接了,列印lient connected,并且向用戶端發送:你是第幾位。

    如果收到用戶端消息後,列印消息,并且向用戶端發送這條收到的消息。

MQTT是一個聊天協定,它比webScoket更上層,屬于應用層。4.我們接着來看看MQTT:

它的基本模式是簡單的釋出訂閱,也就是說當一條消息發出去的時候,誰訂閱了誰就會受到。其實它并不适合IM的場景,例如用來實作有些簡單IM場景,卻需要很大量的、複雜的處理。

比較适合它的場景為訂閱釋出這種模式的,例如微信的實時共享位置,滴滴的地圖上小車的移動、用戶端推送等功能。

首先我們來看看基于MQTT協定的架構-MQTTKit:

這個架構是c來寫的,把一些方法公開在MQTTKit類中,對外用OC來調用,我們來看看這個類:

@interface MQTTClient : NSObject {

struct mosquitto *mosq;

}

@property (readwrite, copy) NSString *clientID;

@property (readwrite, copy) NSString *host;

@property (readwrite, assign) unsigned short port;

@property (readwrite, copy) NSString *username;

@property (readwrite, copy) NSString *password;

@property (readwrite, assign) unsigned short keepAlive;

@property (readwrite, assign) BOOL cleanSession;

@property (nonatomic, copy) MQTTMessageHandler messageHandler;

  • (void) initialize;
  • (NSString*) version;
  • (MQTTClient*) initWithClientId: (NSString *)clientId;
  • (void) setMessageRetry: (NSUInteger)seconds;

    #pragma mark - Connection

  • (void) connectWithCompletionHandler:(void (^)(MQTTConnectionReturnCode code))completionHandler;
  • (void) connectToHost: (NSString*)host

    completionHandler:(void (^)(MQTTConnectionReturnCode code))completionHandler;

  • (void) disconnectWithCompletionHandler:(void (^)(NSUInteger code))completionHandler;
  • (void) reconnect;
  • (void)setWillData:(NSData *)payload

    toTopic:(NSString *)willTopic

    withQos:(MQTTQualityOfService)willQos

    retain:(BOOL)retain;

  • (void)setWill:(NSString *)payload

    toTopic:(NSString *)willTopic

    withQos:(MQTTQualityOfService)willQos

    retain:(BOOL)retain;

  • (void)clearWill;

    #pragma mark - Publish

  • (void)publishData:(NSData *)payload

    toTopic:(NSString *)topic

    withQos:(MQTTQualityOfService)qos

    retain:(BOOL)retain

    completionHandler:(void (^)(int mid))completionHandler;

  • (void)publishString:(NSString *)payload

    toTopic:(NSString *)topic

    withQos:(MQTTQualityOfService)qos

    retain:(BOOL)retain

    completionHandler:(void (^)(int mid))completionHandler;

    #pragma mark - Subscribe

  • (void)subscribe:(NSString *)topic

    withCompletionHandler:(MQTTSubscriptionCompletionHandler)completionHandler;

  • (void)subscribe:(NSString *)topic

    withQos:(MQTTQualityOfService)qos

    completionHandler:(MQTTSubscriptionCompletionHandler)completionHandler;

  • (void)unsubscribe: (NSString )topic

    withCompletionHandler:(void (^)(void))completionHandler;

    這個類一共分為4個部分:初始化、連接配接、釋出、訂閱,具體方法的作用可以先看看方法名了解下,我們接着來用這個架構封裝一個執行個體。

    同樣,我們封裝了一個單例MQTTManager。

    MQTTManager.h

    #[email protected]:NSObject+(instancetype)share;-(void)connect;-(void)disConnect;-(void)sendMsg:(NSString)msg;@end

    MQTTManager.m

#import “MQTTManager.h”

#import “MQTTKit.h”

static NSString * Khost = @“127.0.0.1”;

static const uint16_t Kport = 6969;

static NSString * KClientID = @“tuyaohui”;

@interface MQTTManager() {

MQTTClient *client;

}

@end

@implementation MQTTManager

  • (instancetype)share {

    static dispatch_once_t onceToken;

    static MQTTManager *instance = nil;

    dispatch_once(&onceToken, ^{

    instance = [[self alloc]init];

    });

    return instance;

    }

    //初始化連接配接

  • (void)initSocket {

    if (client) {

    [self disConnect];

    }

    client = [[MQTTClient alloc] initWithClientId:KClientID];

    client.port = Kport;

    [client setMessageHandler:^(MQTTMessage *message)

    {

    //收到消息的回調,前提是得先訂閱

    NSString *msg = [[NSString alloc]initWithData:message.payload encoding:NSUTF8StringEncoding];

    NSLog(@“收到服務端消息:%@”,msg);

    }];

    [client connectToHost:Khost completionHandler:^(MQTTConnectionReturnCode code {

    switch (code) {

    case ConnectionAccepted:

    NSLog(@“MQTT連接配接成功”);

    //訂閱自己ID的消息,這樣收到消息就能回調

    [client subscribe:client.clientID withCompletionHandler:^(NSArray *grantedQos) {

    NSLog(@“訂閱tuyaohui成功”);

    }];

    break;

    case ConnectionRefusedBadUserNameOrPassword:

    NSLog(@“錯誤的使用者名密碼”);

    //…

    default:

    NSLog(@“MQTT連接配接失敗”);

    break;

    }

    }];

    }

    #pragma mark - 對外的一些接口

    //建立連接配接

  • (void)connect

    {

    [self initSocket];

    }

    //斷開連接配接

  • (void)disConnect

    {

    if (client) {

    //取消訂閱

    [client unsubscribe:client.clientID withCompletionHandler:^{

    NSLog(@“取消訂閱tuyaohui成功”);

    }];

    //斷開連接配接

    [client disconnectWithCompletionHandler:^(NSUInteger code) {

    NSLog(@“斷開MQTT成功”);

    }];

    client = nil;

    }

    }

    //發送消息

  • (void)sendMsg:(NSString *)msg {

    //發送一條消息,發送給自己訂閱的主題

    [client publishString:msg toTopic:KClientID withQos:ExactlyOnce retain:YES completionHandler:^(int mid) {

    }];

    }

    @end

    #import “MQTTManager.h”

    #import “MQTTKit.h”

    static NSString * Khost = @“127.0.0.1”;

    static const uint16_t Kport = 6969;

    static NSString * KClientID = @“tuyaohui”;

    @interface MQTTManager() {

    MQTTClient *client;

    }

    @end

    @implementation MQTTManager

  • (instancetype)share {

    static dispatch_once_t onceToken;

    static MQTTManager *instance = nil;

    dispatch_once(&onceToken, ^{

    instance = [[self alloc]init];

    });

    return instance;

    }

    //初始化連接配接

  • (void)initSocket {

    if (client) {

    [self disConnect];

    }

    client = [[MQTTClient alloc] initWithClientId:KClientID];

    client.port = Kport;

    [client setMessageHandler:^(MQTTMessage *message)

    {

    //收到消息的回調,前提是得先訂閱

    NSString *msg = [[NSString alloc]initWithData:message.payload encoding:NSUTF8StringEncoding];

    NSLog(@“收到服務端消息:%@”,msg);

    }];

    [client connectToHost:Khost completionHandler:^(MQTTConnectionReturnCode code) {

    switch (code) {

    case ConnectionAccepted:

    NSLog(@“MQTT連接配接成功”);

    //訂閱自己ID的消息,這樣收到消息就能回調

    [client subscribe:client.clientID withCompletionHandler:^(NSArray *grantedQos) {

    NSLog(@"訂閱tuyaohui成功");
              }];
              break;
          case ConnectionRefusedBadUserNameOrPassword:
         NSLog(@"錯誤的使用者名密碼");
          //....
          default:
              NSLog(@"MQTT連接配接失敗");
              break;
      }
               

    }];

    }

    #pragma mark - 對外的一些接口

    //建立連接配接

  • (void)connect {

    [self initSocket];

    }

    //斷開連接配接

  • (void)disConnect {

    if (client) {

    //取消訂閱

    [client unsubscribe:client.clientID withCompletionHandler:^{

    NSLog(@“取消訂閱tuyaohui成功”);

    }];

    //斷開連接配接

    [client disconnectWithCompletionHandler:^(NSUInteger code) {

    NSLog(@“斷開MQTT成功”);

    }];

    client = nil;

    }

    }

    //發送消息

  • (void)sendMsg:(NSString )msg

    {

    //發送一條消息,發送給自己訂閱的主題

    [client publishString:msg toTopic:KClientID withQos:ExactlyOnce retain:YES completionHandler:^(int mid) {

    }];

    }

    @end

    實作代碼很簡單,需要說一下的是:

    1)當我們連接配接成功了,我們需要去訂閱自己clientID的消息,這樣才能收到發給自己的消息。

    2)其次是這個架構為我們實作了一個QOS機制,那麼什麼是QOS呢?

    QoS(Quality of Service,服務品質)指一個網絡能夠利用各種基礎技術,為指定的網絡通信提供更好的服務能力, 是網絡的一種安全機制, 是用來解決網絡延遲和阻塞等問題的一種技術。

    在這裡,它提供了三個選項:

    typedefenumMQTTQualityOfService:NSUInteger{AtMostOnce,AtLeastOnce,ExactlyOnce}MQTTQualityOfService;

    分别對應最多發送一次,至少發送一次,精确隻發送一次。

    QOS(0),最多發送一次:如果消息沒有發送過去,那麼就直接丢失。

    QOS(1),至少發送一次:保證消息一定發送過去,但是發幾次不确定。

    QOS(2),精确隻發送一次:它内部會有一個很複雜的發送機制,確定消息送到,而且隻發送一次。

    更詳細的關于該機制可以看看這篇文章:MQTT協定筆記之消息流QOS。

    同樣的我們需要一個用MQTT協定實作的服務端,我們還是node.js來實作,這次我們還是需要用npm來新增一個子產品mosca。

    我們來看看服務端代碼:

    MQTTServer.js

    varmosca=require(‘mosca’);varMqttServer=newmosca.Server({port:6969});MqttServer.on(‘clientConnected’,function(client){console.log(‘收到用戶端連接配接,連接配接ID:’,client.id);});/*

  • 監聽MQTT主題消息

    **/MqttServer.on(‘published’,function(packet,client){vartopic=packet.topic;console.log(‘有消息來了’,‘topic為:’+topic+’,message為:’+packet.payload.toString());});MqttServer.on(‘ready’,function(){console.log(‘mqtt伺服器開啟,監聽6969端口’);});

    服務端代碼沒幾行,開啟了一個服務,并且監聽本機6969端口。并且監聽了用戶端連接配接、釋出消息等狀态。

至此,我們實作了一個簡單的MQTT封裝。

5.XMPP:XMPPFramework架構

結果就是并沒有XMPP…因為個人感覺XMPP對于IM來說實在是不堪重用。僅僅隻能作為一個玩具demo,給大家練練手。網上有太多XMPP的内容了,相當一部分用openfire來做服務端,這一套東西實在是太老了。還記得多年前,樓主初識IM就是用的這一套東西…

如果大家仍然感興趣的可以看看這篇文章:iOS 的 XMPPFramework 簡介。這裡就不舉例贅述了。

三、關于IM傳輸格式的選擇:

使用 ProtocolBuffer 減少 Payload

滴滴打車40%;

攜程之前分享過,說是采用新的Protocol Buffer資料格式+Gzip壓縮後的Payload大小降低了15%-45%。資料序列化耗時下降了80%-90%。

采用高效安全的私有協定,支援長連接配接的複用,穩定省電省流量

【高效】提高網絡請求成功率,消息體越大,失敗幾率随之增加。

【省流量】流量消耗極少,省流量。一條消息資料用Protobuf序列化後的大小是 JSON 的1/10、XML格式的1/20、是二進制序列化的1/10。同 XML 相比, Protobuf 性能優勢明顯。它以高效的二進制方式存儲,比 XML 小 3 到 10 倍,快 20 到 100 倍。

【省電】省電

【高效心跳包】同時心跳包協定對IM的電量和流量影響很大,對心跳包協定上進行了極簡設計:僅 1 Byte 。

【易于使用】開發人員通過按照一定的文法定義結構化的消息格式,然後送給指令行工具,工具将自動生成相關的類,可以支援java、c++、python、Objective-C等語言環境。通過将這些類包含在項目中,可以很輕松的調用相關方法來完成業務消息的序列化與反序列化工作。語言支援:原生支援c++、java、python、Objective-C等多達10餘種語言。 2015-08-27 Protocol Buffers v3.0.0-beta-1中釋出了Objective-C(Alpha)版本, 2016-07-28 3.0 Protocol Buffers v3.0.0正式版釋出,正式支援 Objective-C。

【可靠】微信和手機 QQ 這樣的主流 IM 應用也早已在使用它(采用的是改造過的Protobuf協定)

如何測試驗證 Protobuf 的高性能?

對資料分别操作100次,1000次,10000次和100000次進行了測試,

縱坐标是完成時間,機關是毫秒,

反序列化

序列化

位元組長度

need-to-insert-img

need-to-insert-img

資料來源。

資料來自:項目thrift-protobuf-compare,測試項為 Total Time,也就是 指一個對象操作的整個時間,包括建立對象,将對象序列化為記憶體中的位元組序列,然後再反序列化的整個過程。從測試結果可以看到 Protobuf 的成績很好.

缺點:

可能會造成 APP 的包體積增大,通過 Google 提供的腳本生成的 Model,會非常“龐大”,Model 一多,包體積也就會跟着變大。

如果 Model 過多,可能導緻 APP 打包後的體積驟增,但 IM 服務所使用的 Model 非常少,比如在 ChatKit-OC 中隻用到了一個 Protobuf 的 Model:Message對象,對包體積的影響微乎其微。

在使用過程中要合理地權衡包體積以及傳輸效率的問題,據說去哪兒網,就曾經為了減少包體積,進而減少了 Protobuf 的使用。

綜上所述,我們選擇傳輸格式的時候:ProtocolBuffer>Json>XML

如果大家對ProtocolBuffer用法感興趣可以參考下這兩篇文章:

ProtocolBuffer for Objective-C 運作環境配置及使用

iOS之ProtocolBuffer搭建和示例demo

三、IM一些其它問題

1.IM的可靠性:

我們之前穿插在例子中提到過:

心跳機制、PingPong機制、斷線重連機制、還有我們後面所說的QOS機制。這些被用來保證連接配接的可用,消息的即時與準确的送達等等。

上述内容保證了我們IM服務時的可靠性,其實我們能做的還有很多:比如我們在大檔案傳輸的時候使用分片上傳、斷點續傳、秒傳技術等來保證檔案的傳輸。

2.安全性:

我們通常還需要一些安全機制來保證我們IM通信安全。

例如:防止 DNS 污染、帳号安全、第三方伺服器鑒權、單點登入等等

3.一些其他的優化:

類似微信,伺服器不做聊天記錄的存儲,隻在本機進行緩存,這樣可以減少對服務端資料的請求,一方面減輕了伺服器的壓力,另一方面減少用戶端流量的消耗。

我們進行http連接配接的時候盡量采用上層API,類似NSUrlSession。而網絡架構盡量使用AFNetWorking3。因為這些上層網絡請求都用的是HTTP/2 ,我們請求的時候可以複用這些連接配接。

更多優化相關内容可以參考參考這篇文章:

IM 即時通訊技術在多應用場景下的技術實作,以及性能調優