
IM 的實現(xiàn)方式
- 使用第三方 IM 服務
在國內有很多的 IM 第三方服務商,底層協(xié)議基本上都是基于 TCP 的,例如:"網易云信、環(huán)信、融云、極光 IM、LeanCloud、云通信(騰訊)、云旺(阿里)、容聯(lián)云、小能、美洽等等",技術也相對比較成熟,提供后臺管理和定制化的 UI,半小時可集成。
缺點也很明顯:定制化程度太高,需要二次開發(fā),很多東西我們不可控,關鍵是太貴了。如果 IM 對于 APP 只是一個輔助功能,如客服系統(tǒng)、消息推送等,也基本夠用。
- 切合業(yè)務自己實現(xiàn)
幾乎所有互聯(lián)網IM產品都采用服務器中轉方式進行消息傳輸。自己實現(xiàn)也會面臨許多選擇:
- 傳輸協(xié)議的選擇:TCP 還是 UDP?
- 選擇哪種聊天協(xié)議進行開發(fā):MQTT、XMPP、基于 Socket 原生或 WebSocket 的私有協(xié)議?
- 傳輸數(shù)據(jù)的格式:用 JSON、還是 XML、還是谷歌推出的 ProtocolBuffer?
- 我們還有一些細節(jié)問題需要考慮,例如 TCP 的長連接如何保持,心跳機制,Qos 機制,重連機制等等。另外,還有一些安全問題需要考慮。
一、傳輸協(xié)議選型
移動端IM的傳輸協(xié)議選型:TCP 還是 UDP?
- TCP:基于連接的可靠協(xié)議的全雙工的可靠信道,有流量控制、差錯控制等,占用系統(tǒng)資源較多,傳輸效率相對低。
- UDP:基于無連接的不可靠協(xié)議,沒有足夠的控制手段,傳輸效率高,有丟包問題。
基于 UDP 協(xié)議開發(fā)成本較高,容易各種丟包或亂序,一般小公司或技術不成熟或即時性要求不高的公司,多用 TCP 開發(fā)。TCP 和 UDP 的最完整的區(qū)別
QQ-IM的私有協(xié)議:登錄等安全性操作使用TCP協(xié)議,好友之間發(fā)消息主要使用UDP協(xié)議,內網傳輸文件采用了P2P技術,另外騰訊還用了自己的私有協(xié)議,來保證傳輸?shù)目煽啃浴?/p>
二、聊天協(xié)議的選擇
首先我們以實現(xiàn)方式來切入,基本上有以下四種實現(xiàn)方式:
基于Socket原生:代表框架 CocoaAsyncSocket。
基于WebSocket:代表框架 SocketRocket。
基于MQTT:代表框架 MQTTKit。
基于XMPP:代表框架 XMPPFramework。
以上四種方式都可以不使用第三方框架,直接基于OS底層Socket去實現(xiàn)我們的自定義封裝。其中MQTT和XMPP為聊天協(xié)議,是最上層的協(xié)議,而WebSocket是傳輸通訊協(xié)議,它是基于Socket封裝的一個協(xié)議。而上面所說的QQ-IM的私有協(xié)議,就是基于WebSocket或者Socket原生進行封裝的一個聊天協(xié)議。

總之,iOS端要做一個真正的IM產品,一般都是基于Socket或WebSocket等,在之上加上一些私有協(xié)議來保證的。
三、實現(xiàn)一個簡單的IM
1.Socket概述
Socket其實并不是一個協(xié)議,Socket通常也稱作”套接字”,是對TCP/IP 或者UDP/IP協(xié)議封裝的一組編程接口,用于描述IP地址和端口,使用socket實現(xiàn)進程之間的通信(跨網絡的)。它工作在 OSI 模型會話層(第5層),Socket是對TCP/IP等更底層協(xié)議封裝的一個抽象層,是一個調用接口(API)。網絡上的兩個程序通過一個雙向的通訊連接實現(xiàn)數(shù)據(jù)的交換,這個雙向鏈路的一端稱為一個Socket,一個Socket由一個IP地址和一個端口號唯一確定。

先看下基于C的BSD Socket提供的接口:
// socket 創(chuàng)建并初始化 socket,返回該 socket 的文件描述符,如果描述符為 -1 表示創(chuàng)建失敗。
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);
// 客戶端向特定網絡地址的服務器發(fā)送連接請求,連接成功返回0,失敗返回 -1。
int connect(int socketFileDescriptor,sockaddr *serverAddress, int serverAddressLength);
// 使用 DNS 查找特定主機名字對應的 IP 地址。如果找不到對應的 IP 地址則返回 NULL。
hostent* gethostbyname(char *hostname);
// 通過 socket 發(fā)送數(shù)據(jù),發(fā)送成功返回成功發(fā)送的字節(jié)數(shù),否則返回 -1。
int send(int socketFileDescriptor, char *buffer, int bufferLength, int flags);
// 從 socket 中讀取數(shù)據(jù),讀取成功返回成功讀取的字節(jié)數(shù),否則返回 -1。
int receive(int socketFileDescriptor,char *buffer, int bufferLength, int flags);
// 通過UDP socket 發(fā)送數(shù)據(jù)到特定的網絡地址,發(fā)送成功返回成功發(fā)送的字節(jié)數(shù),否則返回 -1。
int sendto(int socketFileDescriptor,char *buffer, int bufferLength, int flags, sockaddr *destinationAddress, int destinationAddressLength);
// 從UDP socket 中讀取數(shù)據(jù),并保存發(fā)送者的網絡地址信息,讀取成功返回成功讀取的字節(jié)數(shù),否則返回 -1 。
int recvfrom(int socketFileDescriptor,char *buffer, int bufferLength, int flags, sockaddr *fromAddress, int *fromAddressLength);
我們用基于OS底層的原生Socket來實現(xiàn)一個簡單的IM。Socket擴展閱讀
2.搭建IM服務端
服務端需要做的工作簡單的總結下:
1.服務器調用 socket(...) 創(chuàng)建socket;
2.綁定IP地址、端口等信息到socket上,用函數(shù)bind();
3.服務器調用 listen(...) 設置緩沖區(qū);
4.服務器通過 accept(...)接受客戶端請求建立連接;
5.服務器與客戶端建立連接之后,通過 send(...)/receive(...)向客
戶端發(fā)送或從客戶端接收數(shù)據(jù);
6.服務器調用 close 關閉 socket;
服務端可以電腦或手機等終端,也可以用多種語言c/c++/java/js等去實現(xiàn)后臺,當然OC也可以實現(xiàn)。這里我們借用node.js實現(xiàn)了一個服務端,來驗證socket效果。需要在Mac上安裝node解釋器,node下載,直接下載安裝即可,也可以終端命令安裝node。
開啟服務器:
1.打開終端
2.cd到目錄 服務端(node.js)
3.node Server.js #開啟IM服務器
3.實現(xiàn)IM客戶端
IM客戶端需要做如下5件事:
1.客戶端調用 socket(...) 創(chuàng)建socket;
2.綁定IP地址、端口等信息到socket上,用函數(shù)bind();
3.客戶端調用 connect(...) 向服務器發(fā)起連接請求以建立連接;
4.客戶端與服務器建立連接之后,就可以通過send(...)/receive(...)向客戶端發(fā)送或從客戶端接收數(shù)據(jù);
5.客戶端調用 close 關閉 socket;
代碼實現(xiàn)
我們采用CocoaAsyncSocket框架,封裝一個名為WYKSocketManager的單例,來對socket相關方法進行調用。
為了demo演示方便,代碼中使用的時間都較短,實際開發(fā)中根據(jù)需要設置
#import "WYKSocketManager.h"
#import "GCDAsyncSocket.h" // for TCP
static NSString *Khost = @"127.0.0.1";
static uint16_t Kport = 6969;
static NSInteger KPingPongOutTime = 3;
static NSInteger KPingPongInterval = 5;
@interface WYKSocketManager()<GCDAsyncSocketDelegate>
@property (nonatomic, strong) GCDAsyncSocket *gcdSocket;
@property (nonatomic, assign) NSTimeInterval reConnectTime;
@property (nonatomic, assign) NSTimeInterval heartBeatSecond;
@property (nonatomic, strong) NSTimer *heartBeatTimer;
@property (nonatomic, assign) BOOL socketOfflineByUser; //!< 主動關閉
@property (nonatomic, retain) NSTimer *connectTimer; // 計時器
@end
@implementation WYKSocketManager
- (void)dealloc
{
[self destoryHeartBeat];
}
+ (instancetype)share
{
static dispatch_once_t onceToken;
static WYKSocketManager *instance = nil;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
[instance initSocket];
});
return instance;
}
- (void)initSocket
{
self.gcdSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
}
#pragma mark - 對外的一些接口
//建立連接
- (BOOL)connect
{
self.reConnectTime = 0;
return [self autoConnect];
}
//斷開連接
- (void)disConnect
{
self.socketOfflineByUser = YES;
[self autoDisConnect];
}
//發(fā)送消息
- (void)sendMsg:(NSString *)msg
{
NSData *data = [msg dataUsingEncoding:NSUTF8StringEncoding];
//第二個參數(shù),請求超時時間
[self.gcdSocket writeData:data withTimeout:-1 tag:110];
}
#pragma mark - GCDAsyncSocketDelegate
//連接成功調用
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
NSLog(@"連接成功,host:%@,port:%d",host,port);
//pingPong
[self checkPingPong];
//心跳寫在這...
[self initHeartBeat];
}
//斷開連接的時候調用
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err
{
NSLog(@"斷開連接,host:%@,port:%d",sock.localHost,sock.localPort);
if (self.delegate && [self.delegate respondsToSelector:@selector(showMessage:)]) {
NSString *msg = [NSString stringWithFormat:@"斷開連接,host:%@,port:%d",sock.localHost,sock.localPort];
[self.delegate showMessage:msg];
}
if (!self.socketOfflineByUser) {
//斷線/失敗了就去重連
[self reConnect];
}
}
//寫的回調
- (void)socket:(GCDAsyncSocket*)sock didWriteDataWithTag:(long)tag
{
NSLog(@"寫的回調,tag:%ld",tag);
//判斷是否成功發(fā)送,如果沒收到響應,則說明連接斷了,則想辦法重連
[self checkPingPong];
}
//收到消息
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
NSString *msg = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"收到消息:%@",msg);
if (self.delegate && [self.delegate respondsToSelector:@selector(showMessage:)]) {
[self.delegate showMessage:[NSString stringWithFormat:@"收到:%@",msg]];
}
//去讀取當前消息隊列中的未讀消息 這里不調用這個方法,消息回調的代理是永遠不會被觸發(fā)的
[self pullTheMsg];
}
//為上一次設置的讀取數(shù)據(jù)代理續(xù)時 (如果設置超時為-1,則永遠不會調用到)
- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag elapsed:(NSTimeInterval)elapsed bytesDone:(NSUInteger)length
{
NSLog(@"來延時,tag:%ld,elapsed:%f,length:%ld",tag,elapsed,length);
if (self.delegate && [self.delegate respondsToSelector:@selector(showMessage:)]) {
NSString *msg = [NSString stringWithFormat:@"來延時,tag:%ld,elapsed:%.1f,length:%ld",tag,elapsed,length];
[self.delegate showMessage:msg];
}
return KPingPongInterval;
}
#pragma mark- Private Methods
- (BOOL)autoConnect
{
return [self.gcdSocket connectToHost:Khost onPort:Kport error:nil];
}
- (void)autoDisConnect
{
[self.gcdSocket disconnect];
}
//監(jiān)聽最新的消息
- (void)pullTheMsg
{
//監(jiān)聽讀數(shù)據(jù)的代理,只能監(jiān)聽10秒,10秒過后調用代理方法 -1永遠監(jiān)聽,不超時,但是只收一次消息,
//所以每次接受到消息還得調用一次
[self.gcdSocket readDataWithTimeout:-1 tag:110];
}
//用Pingpong機制來看是否有反饋
- (void)checkPingPong
{
//pingpong設置為3秒,如果3秒內沒得到反饋就會自動斷開連接
[self.gcdSocket readDataWithTimeout:KPingPongOutTime tag:110];
}
//重連機制
- (void)reConnect
{
//如果對一個已經連接的socket對象再次進行連接操作,會拋出異常(不可對已經連接的socket進行連接)程序崩潰
[self autoDisConnect];
//重連次數(shù) 控制3次
if (self.reConnectTime >= 5) {
return;
}
__weak __typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.reConnectTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf.delegate && [strongSelf.delegate respondsToSelector:@selector(showMessage:)]) {
NSString *msg = [NSString stringWithFormat:@"斷開重連中,%f",strongSelf.reConnectTime];
[strongSelf.delegate showMessage:msg];
}
strongSelf.gcdSocket = nil;
[strongSelf initSocket];
[strongSelf autoConnect];
});
//重連時間增長
if (self.reConnectTime == 0) {
self.reConnectTime = 1;
} else {
self.reConnectTime += 2;
}
}
//初始化心跳
- (void)initHeartBeat
{
[self destoryHeartBeat];
// 每隔5s像服務器發(fā)送心跳包
self.connectTimer = [NSTimer scheduledTimerWithTimeInterval:5
target:self selector:@selector(longConnectToSocket)
userInfo:nil
repeats:YES];
// 在longConnectToSocket方法中進行長連接需要向服務器發(fā)送的訊息
[self.connectTimer fire];
}
// 心跳連接
-(void)longConnectToSocket
{
// 根據(jù)服務器要求發(fā)送固定格式的數(shù)據(jù),但是一般不會是這么簡單的指令
[self sendMsg:@"心跳連接"];
}
//取消心跳
- (void)destoryHeartBeat
{
if (self.heartBeatTimer && [self.heartBeatTimer isValid]) {
[self.heartBeatTimer invalidate];
self.heartBeatTimer = nil;
}
}
@end
我們發(fā)了一條消息,服務端成功的接收到了消息后,把該消息再發(fā)送回客戶端,繞了一圈客戶端又收到了這條消息。至此我們用OS底層socket實現(xiàn)了簡單的IM。這里僅僅是實現(xiàn)了Socket的連接并傳輸字符串,我們要做的遠不止于此。
3.四個重要的功能:心跳機制、PingPong機制、斷線重連、消息可達
(1)心跳機制
心跳機制是相對時間內主動向服務器發(fā)送心跳包消息,用來檢測TCP連接的雙方是否可用。TCP的KeepAlive機制只能保證連接的存在,但是并不能保證客戶端以及服務端的可用性。
真正需要心跳機制的原因其實主要是在于國內運營商的網絡地址轉換設備超時,對于家用路由器來說, 使用的是網絡地址端口轉換(NAPT), 它不僅改IP, 還修改TCP和UDP協(xié)議的端口號, 這樣就能讓內網中的設備共用同一個外網IP,造成連接存在,但并不一定可用。
而國內的運營商一般NAT超時的時間為5分鐘,頻繁心跳會帶來耗電和耗流量的弊端,所以通常IM心跳設置的時間間隔為3-5分鐘,甚至10分鐘都行。微信有一種更高端的實現(xiàn)方式,有興趣的小伙伴可以看看:微信的智能心跳實現(xiàn)方式
(2)PingPong機制
心跳機制是不能完全保證消息的即時性的,業(yè)內的解決方案是輔助采用雙向的PingPong機制。

當服務端發(fā)出一個Ping,客戶端沒有在約定的時間內返回響應的ack,則認為客戶端已經不在線,這時我們Server端會主動斷開Socket連接,并且改由APNS推送的方式發(fā)送消息。同樣的是,當客戶端去發(fā)送一個消息,因為我們遲遲無法收到服務端的響應ack包,則表明客戶端或者服務端已不在線,我們也會顯示消息發(fā)送失敗,并且斷開Socket連接。
(3)重連機制
理論上,自己主動斷開的Socket連接(如退出賬號,APP退出到后臺等),不需要重連。其他的連接斷開,我們都需要進行斷線重連。 一般解決方案是嘗試重連幾次,如果仍舊無法重連成功,那么不再進行重連。
(4)消息可達(即QoS機制)
在移動網絡下,丟包、網絡重連等情況非常之多,為了保證消息的可達,一般需要做消息回執(zhí)和重發(fā)機制。
一般有三種類型:
QOS(0),最多發(fā)送一次:如果消息沒有發(fā)送過去,那么就直接丟失。
QOS(1),至少發(fā)送一次:保證消息一定發(fā)送過去,但是發(fā)幾次不確定。
QOS(2),精確只發(fā)送一次:它內部會有一個很復雜的發(fā)送機制,確保消息送到,而且只發(fā)送一次。
參考易信,每條消息會最多會有3次重發(fā),超時時間為15秒,同時在發(fā)送之前會檢測當前連接狀態(tài),如果當前連接并沒有正確建立,緩存消息且定時檢查(每隔2秒檢查一次,檢查15次)。所以一條消息在最差的情況下會有2分鐘左右的重試時間,以保證消息的可達。因為重發(fā)的存在,接受端偶爾會收到重復消息,這種情況下就需要接收端進行去重。通用的做法是每條消息都戴上自己唯一的message id(一般是uuid)。
擴展閱讀:
4.IM的其他實現(xiàn)方式
(1)基于WebSocket最具代表性的一個第三方框架-SocketRocket
實現(xiàn)的思路和基于CocoaAsyncSocket框架類似,需要編寫遵守webSocket協(xié)議的服務端,感興趣的也可以參照實現(xiàn)一下。
(2)基于MQTT協(xié)議的框架-MQTTKit
MQTT是一個聊天協(xié)議,它比webSocket更上層,屬于應用層,它的基本模式是簡單的發(fā)布訂閱,也就是說當一條消息發(fā)出去的時候,誰訂閱了誰就會收到消息。其實它并不適合IM的場景,例如用來實現(xiàn)有些簡單IM場景,卻需要很大量的、復雜的處理。這個框架是c來寫的,把一些方法公開在MQTTKit類中,對外用OC來調用,這個庫有4年沒有更新了。
(3)基于XMPP協(xié)議的框架-XMPPFramework
XMPP是較早的聊天協(xié)議(2000年發(fā)布第一個公開版本),當時主要是用來打通 ICQ、MSN 等 PC 端的聊天軟件而設計的,技術比較成熟,它本身有很多優(yōu)點,如開放、標準、可擴展,并且客戶端和服務器端都有很多開源的實現(xiàn),但是相對于移動端它也有很明顯的缺點,譬如數(shù)據(jù)負載過重、不支持二進制,在交互中有50% 以上的流量是協(xié)議本身消耗的,需要做深度的二次開發(fā)。
三、關于IM通信協(xié)議的選擇
1.序列化與反序列化
移動互聯(lián)網相對于有線網絡最大特點是:帶寬低,延遲高,丟包率高和穩(wěn)定性差,流量費用高。所以在私有協(xié)議的序列化上一般使用二進制協(xié)議,而不是文本協(xié)議。
常見的二進制序列化庫有Protocol Buffers和MessagePack,當然你也可以自己實現(xiàn)自己的二進制協(xié)議序列化和反序列的過程,比如蘑菇街的TeamTalk。但是前面二者無論是可拓展性還是可讀性都完爆TeamTalk(TeamTalk連Variant都不支持,一個int傳輸時固定占用4個字節(jié)),所以大部分情況下還是不推薦自己去實現(xiàn)二進制協(xié)議的序列化和反序列化過程。
一條消息數(shù)據(jù)用Protobuf序列化后的大小是 JSON 的1/10、XML格式的1/20、是二進制序列化的1/10。同 XML 相比, Protobuf 性能優(yōu)勢明顯。它以高效的二進制方式存儲,比 XML 小 3 到 10 倍,快 20 到 100 倍。
同時心跳包協(xié)議對IM的電量和流量影響很大,對心跳包協(xié)議上進行了極簡設計:僅 1 Byte 。
ProtocolBuffer可能會造成 APP 的包體積增大,通過 Google 提供的腳本生成的 Model,會非常“龐大”,Model 一多,包體積也就會跟著變大。
如何測試驗證 Protobuf 的高性能?
對數(shù)據(jù)分別操作100次,1000次,10000次和100000次進行了測試,縱坐標是完成時間,單位是毫秒。


Xml, Json, Hessian, Protocol Buffers序列化對比
選擇傳輸格式的時候:ProtocolBuffer > JSON > XML
2.協(xié)議格式設計
基于TCP的應用層協(xié)議一般都分為包頭和包體(如HTTP),IM協(xié)議也不例外。包頭一般用于表示每個請求/反饋的公共部分,如包長,請求類型,返回碼等。而包頭則填充不同請求/反饋對應的信息。
一個最簡單的包頭可以定義為:
struct PackHeader
{
int32_t length_; //包長度
int32_t serial_; //包序列號
int32_t command_; //包請求類型
int32_t code_; //返回碼
};
以心跳包為例,假設當前的serial為1,心跳包的command為10,那么使用MessagePack做序列化時:length=4,serial=1,command=10,code=0,每個字段各占一個字節(jié),包體為空,僅需要4個字節(jié)。
當然這是最簡單的一個例子,面對真正的業(yè)務邏輯時,包體里面會需要塞入更多地信息,這個需要開發(fā)根據(jù)自己的業(yè)務邏輯總結公共部分,如為了兼容加入的協(xié)議版本號,為了負載均衡加入的模塊id等。
四、IM一些其它問題
1.IM的可靠性:
除了心跳機制、PingPong機制、斷線重連機制這些被用來保證連接的可用,要提高IM服務時的可靠性,能做的還有很多:比如在大文件傳輸?shù)臅r候使用分片上傳、斷點續(xù)傳、秒傳技術、P2P技術等來保證文件的傳輸。
2.安全性:
我們通常還需要一些安全機制來保證我們IM通信安全。如:加密傳輸、防止 DNS 污染、帳號安全、第三方服務器鑒權、單點登錄等。
3.一些其他的優(yōu)化:
精簡心跳包,心跳包只在空閑時發(fā)送,動態(tài)化心跳間隔。文件上傳、下載優(yōu)化等。類似微信,服務器不做聊天記錄的存儲,只在本機進行緩存,這樣可以減少對服務端數(shù)據(jù)的請求,一方面減輕了服務器的壓力,另一方面減少客戶端流量的消耗。
我們進行HTTP連接的時候盡量采用上層API,類似NSUrlSession。而網絡框架盡量使用AFNetWorking3.0以上版本。因為這些上層網絡請求都用的是HTTP/2 ,我們請求的時候可以復用這些連接。
更多優(yōu)化相關請參考這篇文章:
五、實時音視頻通話
IM應用中的實時音視頻技術,幾乎是IM開發(fā)中的最后一道高墻。原因在于:實時音視頻技術 = 音視頻處理技術 + 網絡傳輸技術 的橫向技術應用集合體,而公共互聯(lián)網不是為了實時通信設計的。
實時音視頻技術上的實現(xiàn)內容主要包括:音視頻的采集、編碼、網絡傳輸、解碼、播放等環(huán)節(jié)。這么多項并不簡單的技術應用,如果把握不當,將會在在實際開發(fā)過程中遇到一個又一個的坑。
參考文章
《即時通訊音視頻開發(fā)(一):視頻編解碼之理論概述》
《即時通訊音視頻開發(fā)(二):視頻編解碼之數(shù)字視頻介紹》
《即時通訊音視頻開發(fā)(三):視頻編解碼之編碼基礎》
《即時通訊音視頻開發(fā)(四):視頻編解碼之預測技術介紹》
《即時通訊音視頻開發(fā)(五):認識主流視頻編碼技術H.264》
《即時通訊音視頻開發(fā)(六):如何開始音頻編解碼技術的學習》
《即時通訊音視頻開發(fā)(七):音頻基礎及編碼原理入門》
《即時通訊音視頻開發(fā)(八):常見的實時語音通訊編碼標準》
《即時通訊音視頻開發(fā)(九):實時語音通訊的回音及回音消除概述》
《即時通訊音視頻開發(fā)(十):實時語音通訊的回音消除技術詳解》
《即時通訊音視頻開發(fā)(十一):實時語音通訊丟包補償技術詳解》
《即時通訊音視頻開發(fā)(十二):多人實時音視頻聊天架構探討》
《即時通訊音視頻開發(fā)(十三):實時視頻編碼H.264的特點與優(yōu)勢》
《即時通訊音視頻開發(fā)(十四):實時音視頻數(shù)據(jù)傳輸協(xié)議介紹》
《即時通訊音視頻開發(fā)(十五):聊聊P2P與實時音視頻的應用情況》
《即時通訊音視頻開發(fā)(十六):移動端實時音視頻開發(fā)的幾個建議》
《即時通訊音視頻開發(fā)(十七):視頻編碼H.264、V8的前世今生》
擴展閱讀:
推薦一個專業(yè)IM開發(fā)的網站:
本文來自:iOS端IM開發(fā)從入門到填坑
鏈接:http://www.itdecent.cn/p/b1d54fd570ef
聲明:本文并非由本人完全所創(chuàng)。整理一些開發(fā)技能知識,以作存檔用于學習。
點贊+關注,第一時間獲取技術干貨和最新知識點,謝謝你的支持!
最后祝大家生活愉快~