iOS 即時通信,從入門到 「放棄」?

本文會用實例的方式,將 iOS 各類 IM 的方案都簡單的實現一遍。而且提供一些選型、實現細節以及優化的建議。 —— 由 宇朋Look分享
前言
  • 本文會用實例的方式,將iOS各類IM的方案都簡單的實現一遍。而且提供一些選型、實現細節以及優化的建議。javascript

  • 注:文中的全部的代碼示例,在github中都有demo:
    iOS即時通信,從入門到「放棄」?(demo)
    能夠打開項目先預覽效果,對照着進行閱讀。css

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

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

對於短平快的公司,徹底能夠採用第三方SDK來實現。國內IM的第三方服務商有不少,相似雲信、環信、融雲、LeanCloud,固然還有其它的不少,這裏就不一一舉例了,感興趣的小夥伴能夠自行查閱下。html

  • 第三方服務商IM底層協議基本上都是TCP。他們的IM方案很成熟,有了它們,咱們甚至不須要本身去搭建IM後臺,什麼都不須要去考慮。
    若是你足夠懶,甚至連UI都不須要本身作,這些第三方有各自一套IM的UI,拿來就能夠直接用。真可謂3分鐘集成...
  • 可是缺點也很明顯,定製化程度過高,不少東西咱們不可控。固然還有一個最最重要的一點,就是太貴了...做爲真正社交爲主打的APP,僅此一點,就足以讓咱們望而卻步。固然,若是IM對於APP只是一個輔助功能,那麼用第三方服務也無可厚非。
另一種方式,咱們本身去實現

咱們本身去實現也有不少選擇:
1)首先面臨的就是傳輸協議的選擇,TCP仍是UDP
2)其次是咱們須要去選擇使用哪一種聊天協議:java

  • 基於Scoket或者WebScoket或者其餘的私有協議、
  • MQTT
  • 仍是廣爲人詬病的XMPP?

3)咱們是本身去基於OS底層Socket進行封裝仍是在第三方框架的基礎上進行封裝?
4)傳輸數據的格式,咱們是用Json、仍是XML、仍是谷歌推出的ProtocolBuffer
5)咱們還有一些細節問題須要考慮,例如TCP的長鏈接如何保持,心跳機制,Qos機制,重連機制等等...固然,除此以外,咱們還有一些安全問題須要考慮。node

1、傳輸協議的選擇

接下來咱們可能須要本身考慮去實現IM,首先從傳輸層協議來講,咱們有兩種選擇:TCP or UDPpython


這個問題已經被討論過無數次了,對深層次的細節感興趣的朋友能夠看看這篇文章:ios

  • 移動端IM/推送系統的協議選型:UDP仍是TCP?nginx

    這裏咱們直接說結論吧:對於小公司或者技術不那麼成熟的公司,IM必定要用TCP來實現,由於若是你要用UDP的話,須要作的事太多。固然QQ就是用的UDP協議,固然不只僅是UDP,騰訊還用了本身的私有協議,來保證了傳輸的可靠性,杜絕了UDP下各類數據丟包,亂序等等一系列問題。
    總之一句話,若是你以爲團隊技術很成熟,那麼你用UDP也行,不然仍是用TCP爲好。c++

2、咱們來看看各類聊天協議

首先咱們以實現方式來切入,基本上有如下四種實現方式:git

  1. 基於Scoket原生:表明框架 CocoaAsyncSocket
  2. 基於WebScoket:表明框架 SocketRocket
  3. 基於MQTT:表明框架 MQTTKit
  4. 基於XMPP:表明框架 XMPPFramework

固然,以上四種方式咱們均可以不使用第三方框架,直接基於OS底層Scoket去實現咱們的自定義封裝。下面我會給出一個基於Scoket原生而不使用框架的例子,供你們參考一下。

首先須要搞清楚的是,其中MQTTXMPP爲聊天協議,它們是最上層的協議,而WebScoket是傳輸通信協議,它是基於Socket封裝的一個協議。而一般咱們所說的騰訊IM的私有協議,就是基於WebScoket或者Scoket原生進行封裝的一個聊天協議。

具體這3種聊天協議的對比優劣以下:


協議優劣對比.png

因此說到底,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 建立並初始化 socket,返回該 socket 的文件描述符,若是描述符爲 -1 表示建立失敗。 int socket(int addressFamily, int type,int protocol) //關閉socket鏈接 int close(int socketFileDescriptor) //將 socket 與特定主機地址與端口號綁定,成功綁定返回0,失敗返回 -1。 int bind(int socketFileDescriptor,sockaddr *addressToBind,int addressStructLength) //接受客戶端鏈接請求並將客戶端的網絡地址信息保存到 clientAddress 中。 int accept(int socketFileDescriptor,sockaddr *clientAddress, int clientAddressStructLength) //客戶端向特定網絡地址的服務器發送鏈接請求,鏈接成功返回0,失敗返回 -1。 int connect(int socketFileDescriptor,sockaddr *serverAddress, int serverAddressLength) //使用 DNS 查找特定主機名字對應的 IP 地址。若是找不到對應的 IP 地址則返回 NULL。 hostent* gethostbyname(char *hostname) //經過 socket 發送數據,發送成功返回成功發送的字節數,不然返回 -1。 int send(int socketFileDescriptor, char *buffer, int bufferLength, int flags) //從 socket 中讀取數據,讀取成功返回成功讀取的字節數,不然返回 -1。 int receive(int socketFileDescriptor,char *buffer, int bufferLength, int flags) //經過UDP socket 發送數據到特定的網絡地址,發送成功返回成功發送的字節數,不然返回 -1。 int sendto(int socketFileDescriptor,char *buffer, int bufferLength, int flags, sockaddr *destinationAddress, int destinationAddressLength) //從UDP socket 中讀取數據,並保存發送者的網絡地址信息,讀取成功返回成功讀取的字節數,不然返回 -1 。 int recvfrom(int socketFileDescriptor,char *buffer, int bufferLength, int flags, sockaddr *fromAddress, int *fromAddressLength)

讓咱們能夠對socket進行各類操做,首先咱們來用它寫個客戶端。總結一下,簡單的IM客戶端須要作以下4件事:

  1. 客戶端調用 socket(...) 建立socket;
  2. 客戶端調用 connect(...) 向服務器發起鏈接請求以創建鏈接;
  3. 客戶端與服務器創建鏈接以後,就能夠經過send(...)/receive(...)向客戶端發送或從客戶端接收數據;
  4. 客戶端調用 close 關閉 socket;

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

TYHSocketManager.h

#import <Foundation/Foundation.h> @interface TYHSocketManager : NSObject + (instancetype)share; - (void)connect; - (void)disConnect; - (void)sendMsg:(NSString *)msg; @end

TYHSocketManager.m

#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 @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類型的結構體,該結構體內容以下:

    struct sockaddr_in { __uint8_t sin_len; sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; char sin_zero[8]; };

    裏面包含了一些,咱們須要鏈接的服務端的scoket的一些基本參數,具體賦值細節能夠見註釋。

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

就這樣客戶端便簡單的能夠用了,接着咱們來看看服務端的實現。

同樣,咱們首先對服務端須要作的工做簡單的總結下:
  1. 服務器調用 socket(...) 建立socket;
  2. 服務器調用 listen(...) 設置緩衝區;
  3. 服務器經過 accept(...)接受客戶端請求創建鏈接;
  4. 服務器與客戶端創建鏈接以後,就能夠經過 send(...)/receive(...)向客戶端發送或從客戶端接收數據;
  5. 服務器調用 close 關閉 socket;
接着咱們就能夠具體去實現了

OS底層的函數是支持咱們去實現服務端的,可是咱們通常不會用iOS去這麼作(試問真正的應用場景,有誰用iOSscoket服務器麼...),若是仍是想用這些函數去實現服務端,能夠參考下這篇文章: 深刻淺出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爲保存源代碼的文件名)。

咱們來看看運行效果:


handle2.gif


服務器運行起來了,而且監聽着6969端口。
接着咱們用以前寫的iOS端的例子。客戶端打印顯示鏈接成功,而咱們運行的服務器也打印了鏈接成功。接着咱們發了一條消息,服務端成功的接收到了消息後,把該消息再發送回客戶端,繞了一圈客戶端又收到了這條消息。至此咱們用OS底層scoket實現了簡單的IM。

你們看到這是否是以爲太過簡單了?
固然簡單,咱們僅僅是實現了Scoket的鏈接,信息的發送與接收,除此以外咱們什麼都沒有作,現實中,咱們須要作的處理遠不止於此,咱們先接着往下看。接下來,咱們就一塊兒看看第三方框架是如何實現IM的。


分割圖.png
2.咱們接着來看看基於Socket原生的CocoaAsyncSocket:

這個框架實現了兩種傳輸協議TCPUDP,分別對應GCDAsyncSocket類和GCDAsyncUdpSocket,這裏咱們重點講GCDAsyncSocket

這裏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()<GCDAsyncSocketDelegate> { 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:-1 tag:110];

這個方法的做用就是去讀取當前消息隊列中的未讀消息。記住,這裏不調用這個方法,消息回調的代理是永遠不會被觸發的。並且必須是tag相同,若是tag不一樣,這個收到消息的代理也不會被處罰。
咱們調用一次這個方法,只能觸發一次讀取消息的代理,若是咱們調用的時候沒有未讀消息,它就會等在那,直到消息來了被觸發。一旦被觸發一次代理後,咱們必須再次調用這個方法,不然,以後的消息到了仍舊沒法觸發咱們讀取消息的代理。就像咱們在例子中使用的那樣,在每次讀取到消息以後咱們都去調用:

//收到消息的回調 - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { NSString *msg = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"收到消息:%@",msg); [self pullTheMsg]; } //監聽最新的消息 - (void)pullTheMsg { //監聽讀數據的代理,只能監聽10秒,10秒事後調用代理方法 -1永遠監聽,不超時,可是隻收一次消息, //因此每次接受到消息還得調用一次 [gcdSocket readDataWithTimeout:-1 tag:110]; }

除此以外,咱們還須要說的是這個超時timeout
這裏若是設置10秒,那麼就只能監聽10秒,10秒事後調用是否續時的代理方法:

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

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

咱們一樣來運行看看效果:

handle3.gif

至此咱們也用CocoaAsyncSocket這個框架實現了一個簡單的IM。


分割圖.png
3.接着咱們繼續來看看基於webScoket的IM:

這個例子咱們會把心跳,斷線重連,以及PingPong機制進行簡單的封裝,因此咱們先來談談這三個概念:

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

簡單的來講,心跳就是用來檢測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機制。

當服務端發出一個Ping,客戶端沒有在約定的時間內返回響應的ack,則認爲客戶端已經不在線,這時咱們Server端會主動斷開Scoket鏈接,而且改由APNS推送的方式發送消息。
一樣的是,當客戶端去發送一個消息,由於咱們遲遲沒法收到服務端的響應ack包,則代表客戶端或者服務端已不在線,咱們也會顯示消息發送失敗,而且斷開Scoket鏈接。

還記得咱們以前CocoaSyncSockt的例子所講的獲取消息超時就斷開嗎?其實它就是一個PingPong機制的客戶端實現。咱們每次能夠在發送消息成功後,調用這個超時讀取的方法,若是一段時間沒收到服務器的響應,那麼說明鏈接不可用,則斷開Scoket鏈接

最後就是重連機制:

理論上,咱們本身主動去斷開的Scoket鏈接(例如退出帳號,APP退出到後臺等等),不須要重連。其餘的鏈接斷開,咱們都須要進行斷線重連。
通常解決方案是嘗試重連幾回,若是仍舊沒法重連成功,那麼再也不進行重連。
接下來的WebScoket的例子,我會封裝一個重連時間指數級增加的一個重連方式,能夠做爲一個參考。

言歸正傳,咱們看完上述三個概念以後,咱們來說一個WebScoket最具表明性的一個第三方框架SocketRocket

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

@interface SRWebSocket : NSObject <NSStreamDelegate> @property (nonatomic, weak) id <SRWebSocketDelegate> 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 <NSObject> // 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()<SRWebSocketDelegate> { 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.jsws模塊,只須要用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,而且向客戶端發送:你是第幾位。
若是收到客戶端消息後,打印消息,而且向客戶端發送這條收到的消息。

接着咱們一樣來運行一下看看效果:


運行咱們能夠看到,主動去斷開的鏈接,沒有去重連,而server端斷開的,咱們開啓了重連。感興趣的朋友能夠下載demo實際運行一下。


分割圖.png
4.咱們接着來看看MQTT:

MQTT是一個聊天協議,它比webScoket更上層,屬於應用層。
它的基本模式是簡單的發佈訂閱,也就是說當一條消息發出去的時候,誰訂閱了誰就會受到。其實它並不適合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

#import <Foundation/Foundation.h> @interface MQTTManager : 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

實現代碼很簡單,須要說一下的是:
1)當咱們鏈接成功了,咱們須要去訂閱本身clientID的消息,這樣才能收到發給本身的消息。
2)其次是這個框架爲咱們實現了一個QOS機制,那麼什麼是QOS呢?

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

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

typedef enum MQTTQualityOfService : NSUInteger { AtMostOnce, AtLeastOnce, ExactlyOnce } MQTTQualityOfService;

分別對應最多發送一次,至少發送一次,精確只發送一次。

  • QOS(0),最多發送一次:若是消息沒有發送過去,那麼就直接丟失。
  • QOS(1),至少發送一次:保證消息必定發送過去,可是發幾回不肯定。
  • QOS(2),精確只發送一次:它內部會有一個很複雜的發送機制,確保消息送到,並且只發送一次。

更詳細的關於該機制能夠看看這篇文章:MQTT協議筆記之消息流QOS

一樣的咱們須要一個用MQTT協議實現的服務端,咱們仍是node.js來實現,此次咱們仍是須要用npm來新增一個模塊mosca
咱們來看看服務端代碼:
MQTTServer.js

var mosca = require('mosca'); var MqttServer = new mosca.Server({ port: 6969 }); MqttServer.on('clientConnected', function(client){ console.log('收到客戶端鏈接,鏈接ID:', client.id); }); /** * 監聽MQTT主題消息 **/ MqttServer.on('published', function(packet, client) { var topic = 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 簡介。這裏就不舉例贅述了。

3、關於IM傳輸格式的選擇:

引用陳宜龍大神文章(iOS程序犭袁 )中一段:
使用 ProtocolBuffer 減小 Payload
滴滴打車40%;
攜程以前分享過,說是採用新的Protocol Buffer數據格式+Gzip壓縮後的Payload大小下降了15%-45%。數據序列化耗時降低了80%-90%。

採用高效安全的私有協議,支持長鏈接的複用,穩定省電省流量
【高效】提升網絡請求成功率,消息體越大,失敗概率隨之增長。
【省流量】流量消耗極少,省流量。一條消息數據用Protobuf序列化後的大小是 JSON 的1/十、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次進行了測試,
縱座標是完成時間,單位是毫秒,
反序列化
序列化
字節長度

數據來源

數據來自:項目 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

3、IM一些其它問題
1.IM的可靠性:

咱們以前穿插在例子中提到過:
心跳機制、PingPong機制、斷線重連機制、還有咱們後面所說的QOS機制。這些被用來保證鏈接的可用,消息的即時與準確的送達等等。
上述內容保證了咱們IM服務時的可靠性,其實咱們能作的還有不少:好比咱們在大文件傳輸的時候使用分片上傳、斷點續傳、秒傳技術等來保證文件的傳輸。

2.安全性:

咱們一般還須要一些安全機制來保證咱們IM通訊安全。
例如:防止 DNS 污染、賬號安全、第三方服務器鑑權、單點登陸等等

3.一些其餘的優化:

相似微信,服務器不作聊天記錄的存儲,只在本機進行緩存,這樣能夠減小對服務端數據的請求,一方面減輕了服務器的壓力,另外一方面減小客戶端流量的消耗。
咱們進行http鏈接的時候儘可能採用上層API,相似NSUrlSession。而網絡框架儘可能使用AFNetWorking3。由於這些上層網絡請求都用的是HTTP/2 ,咱們請求的時候能夠複用這些鏈接。

更多優化相關內容能夠參考參考這篇文章:
IM 即時通信技術在多應用場景下的技術實現,以及性能調優

4、音視頻通話

IM應用中的實時音視頻技術,幾乎是IM開發中的最後一道高牆。緣由在於:實時音視頻技術 = 音視頻處理技術 + 網絡傳輸技術 的橫向技術應用集合體,而公共互聯網不是爲了實時通訊設計的。
實時音視頻技術上的實現內容主要包括:音視頻的採集、編碼、網絡傳輸、解碼、播放等環節。這麼多項並不簡單的技術應用,若是把握不當,將會在在實際開發過程當中遇到一個又一個的坑。

由於樓主本身對這塊的技術理解很淺,因此引用了一個系列的文章來給你們一個參考,感興趣的朋友能夠看看:
即時通信音視頻開發(一):視頻編解碼之理論概述
即時通信音視頻開發(二):視頻編解碼之數字視頻介紹
《 即時通信音視頻開發(三):視頻編解碼之編碼基礎
即時通信音視頻開發(四):視頻編解碼之預測技術介紹
即時通信音視頻開發(五):認識主流視頻編碼技術H.264
即時通信音視頻開發(六):如何開始音頻編解碼技術的學習
即時通信音視頻開發(七):音頻基礎及編碼原理入門
即時通信音視頻開發(八):常見的實時語音通信編碼標準
《 即時通信音視頻開發(九):實時語音通信的迴音及迴音消除概述
即時通信音視頻開發(十):實時語音通信的迴音消除技術詳解
即時通信音視頻開發(十一):實時語音通信丟包補償技術詳解
《 即時通信音視頻開發(十二):多人實時音視頻聊天架構探討
即時通信音視頻開發(十三):實時視頻編碼H.264的特色與優點
即時通信音視頻開發(十四):實時音視頻數據傳輸協議介紹
《 即時通信音視頻開發(十五):聊聊P2P與實時音視頻的應用狀況
即時通信音視頻開發(十六):移動端實時音視頻開發的幾個建議
即時通信音視頻開發(十七):視頻編碼H.26四、V8的前世此生

寫在最後:

本文內容爲原創,且僅表明樓主現階段的一些思想,若是有什麼錯誤,歡迎指正~

相關文章
相關標籤/搜索