聊聊iOS中網絡編程長鏈接的那些事

 

一、長鏈接在iOS開發中的應用


常見的短鏈接應用場景:通常的App的網絡請求都是基於Http1.0進行的,使用的是NSURLConnection、NSURLSession或者是AFNetworking,Http1.0連接最顯著的特色就是客戶端每一次須要主動向服務端發送請求,都須要經歷創建連接、發送請求、返回數據、關閉連接這幾個階段,是一種單向請求且無狀態的協議。

長鏈接的應用場景:有的時候,咱們須要服務端主動往客戶端進行推送服務的時候,這個時候長鏈接就起做用了。蘋果提供的push服務apns就是典型的長鏈接的應用,IM即時通信應用、訂單消息推送這些也是長鏈接的典型應用。長鏈接的特色是一旦經過三次握手創建連接以後,該條鏈路就一直存在,並且該鏈路是一種雙向的通行機制,適合於頻繁的網絡請求,避免Http每一次請求都會創建連接和關閉連接的操做,減小浪費,提升效率。php

(本文同步發佈於:http://www.52im.net/thread-1493-1-1.htmlhtml

二、本文原做者


<ignore_js_op>聊聊iOS中網絡編程長鏈接的那些事_3fae4ce5-1e84-4db3-aefb-2efb583d61b1.jpg 

做者畢業於南京郵電大學,現供職於滴滴出行平臺技術部。
Github:https://github.com/yixiangboy
博客:https://www.jianshu.com/users/c3c893a27097/timeline

三、通訊網絡的一些基本概念


長鏈接的通常實現方式都是基於TCP或者UDP協議完成的。這個時候咱們就須要一些基本的通訊網絡概念。

3.1OSI七層網絡協議


開放系統互連參考模型 (Open System Interconnect 簡稱OSI)是國際標準化組織(ISO)和國際電報電話諮詢委員會(CCITT)聯合制定的開放系統互連參考模型,爲開放式互連信息系統提供了一種功能結構的框架。

<ignore_js_op>聊聊iOS中網絡編程長鏈接的那些事_1.jpg 

如上圖所示:

  • 物理層:負責機械、電子、定時接口通訊信道上的原始比特流的傳輸;
  • 數據鏈路層:負責物理尋址,同時將原始比特流轉變成邏輯傳輸線路;
  • 網絡層:控制子網的運行,如邏輯編址、分組傳輸、路由選擇;
  • 傳輸層:接受上一層的數據,在必要的時候把數據進行分割,並將這些數據交給網絡層,且保證這些數據段有效到達對方;
  • 會話層:不一樣機器上的用戶之間創建以及管理回話;
  • 表示層:信息的語法語義以及它們的關聯,如加密解密、轉換翻譯、壓縮解壓縮;
  • 應用層:各類應用程序協議,如Http、Ftp、SMTP、POP3。

3.2IP、TCP和Http


本小節主要講一下在網絡層的IP協議、傳輸層的TCP協議和網絡層的Http協議。這也是咱們平時接觸到最多的三個網絡協議。

IP協議:TCP/IP 中的 IP 是網絡協議 (Internet Protocol) 的縮寫。從字面意思便知,它是互聯網衆多協議的基礎。IP 實現了分組交換網絡。在協議下,機器被叫作 主機 (host),IP 協議明確了 host 之間的資料包(數據包)的傳輸方式。所謂數據包是指一段二進制數據,其中包含了發送源主機和目標主機的信息。IP 網絡負責源主機與目標主機之間的數據包傳輸。IP 協議的特色是 best effort(盡力服務,其目標是提供有效服務並盡力傳輸)。這意味着,在傳輸過程當中,數據包可能會丟失,也有可能被重複傳送致使目標主機收到多個一樣的數據包。

TCP協議:TCP 層位於 IP 層之上,是最受歡迎的因特網通信協議之一,人們一般用 TCP/IP 來泛指整個因特網協議族。剛剛提到,IP 協議容許兩個主機之間傳送單一數據包。爲了保證對所傳送數據包達到盡力服務的目的,最終的傳輸的結果多是數據包亂序、重複甚至丟包。TCP 是基於 IP 層的協議。可是 TCP 是可靠的、有序的、有錯誤檢查機制的基於字節流傳輸的協議。這樣當兩個設備上的應用經過 TCP 來傳遞數據的時候,總可以保證目標接收方收到的數據的順序和內容與發送方所發出的是一致的。TCP 作的這些事看起來稀鬆日常,可是比起 IP 層的粗曠處理方式已是有顯著的進步了。應用程序之間能夠經過 TCP 創建連接。TCP 創建的是雙向鏈接,通訊雙方能夠同時進行數據的傳輸。鏈接的雙方都不須要操心數據是否分塊,或者是否採用了盡力服務等。TCP 會確保所傳輸的數據的正確性,即接受方收到的數據與發出方的數據一致。

HTTP協議:HTTP 是典型的 TCP 應用。用戶瀏覽器(應用 1)與 web 服務器(應用 2)創建鏈接後,瀏覽器能夠經過鏈接發送服務請求,web 服務器能夠經過一樣的鏈接對請求作出響應。1989 年,Tim Berners Lee 在 CERN(European Organization for Nuclear Research 歐洲原子核研究委員會) 擔任軟件諮詢師的時候,開發了一套程序,奠基了萬維網的基礎。HyperText Transfer Protocol(超文本轉移協議,即HTTP)是用於從 WWW 服務器傳輸超文本到本地瀏覽器的傳送協議。HTTP 採用簡單的請求和響應機制。在 Safari 輸入 http://www.apple.com 時,會向 www.appple.com 所在的服務器發送一個 HTTP 請求。服務器會對請求作出一個響應,將請求結果信息返回給 Safari。每個請求都有一個對應的響應信息。請求和響應聽從一樣的格式。第一行是請求行或者響應狀態行。接下來是 header 信息,header 信息以後會有一個空行。空行以後是 body 請求信息體。

3.3更多文章推薦閱讀


若是你以爲本節文字對網絡通訊的基礎知識講的有點蒙逼的話,可繼續看看下面這些精華文章大餐。

➊ 網絡編程基礎知識:


➋ 若是以爲上面的文章枯燥,則《網絡編程懶人入門》系列多是你的菜:


➌ 若是感到自已已經很牛逼了,《鮮爲人知的網絡編程》應該是你菜:


➍ 若是看完上面的文章仍是躁動不安,那看看《高性能網絡編程系列》吧(你不放棄我會一直推薦下去的,哈哈...):


四、Socket概念


socket翻譯爲套接字,是支持TCP/IP協議的網絡通訊的基本操做單元。它是網絡通訊過程當中端點的抽象表示,包含進行網絡通訊必須的五種信息:鏈接使用的協議,本地主機的IP地址,本地進程的協議端口,遠地主機的IP地址,遠地進程的協議端口。socket是在應用層和傳輸層之間的一個抽象層,它把TCP/IP層複雜的操做抽象爲幾個簡單的接口供應用層調用已實現進程在網絡中通訊。它不屬於OSI七層協議,它只是對於TCP,UDP協議的一套封裝,讓咱們開發人員更加容易編寫基於TCP、UDP的應用。

<ignore_js_op>聊聊iOS中網絡編程長鏈接的那些事_2.jpg 

使用socket進行TCP通訊的基本流程以下:
<ignore_js_op>聊聊iOS中網絡編程長鏈接的那些事_3.jpg 

socket編程中咱們常用到的函數:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// socket()函數用於根據指定的地址族、數據類型和協議來分配一個套接口的描述字及其所用的資源。若是協議protocol未指定(等於0), 則使用缺省的鏈接方式。
socket(af,type,protocol)
 
// 將一本地地址與一套接口捆綁。本函數適用於未鏈接的數據報或流類套接口,在connect()或listen()調用前使用。當用socket()建立套接口後,它便存在於一個名字空間(地址族)中,但並未賦名。bind()函數經過給一個未命名套接口分配一個本地名字來爲套接口創建本地捆綁(主機地址/端口號).
bind(sockid, local addr, addrlen)
 
// 建立一個套接口並監聽申請的鏈接.
listen( Sockid ,quenlen)
 
// 用於創建與指定socket的鏈接.
connect(sockid, destaddr, addrlen)
 
// 在一個套接口接受一個鏈接.
accept(Sockid,Clientaddr, paddrlen)
 
// 用於向一個已經鏈接的socket發送數據,若是無錯誤,返回值爲所發送數據的總數,不然返回SOCKET_ERROR。
send(sockid, buff, bufflen)
 
// 用於已鏈接的數據報或流式套接口進行數據的接收。
recv()
 
// 指向一指定目的地發送數據,sendto()適用於發送未創建鏈接的UDP數據包 (參數爲SOCK_DGRAM)
sendto(sockid,buff,…,addrlen)
 
// 用於從(已鏈接)套接口上接收數據,並捕獲數據發送源的地址。
recvfrom()
 
// 關閉Socket鏈接
close(socked)

五、實現一個簡單的基於TCP的Socket通訊Demo


5.1客戶端實現代碼


01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// 一、 建立socket
/**
  參數
  domain: 協議域,AF_INET --> IPV4
  type: Socket 類型, SOCK_STREAM(TCP)/SOCKET_DGRAM(報文 UDP)
  protocol: IPPROTO_TCP,若是傳入0,會自動根據第二個參數,選擇合適的協議
  
  返回值
  socket
  */
int clientSocket = socket(AF_INET, SOCK_STREAM, 0);
 
// 二、 鏈接到服務器
/**
  參數
  1> 客戶端socket
  2> 指向數據結構sockaddr的指針,其中包括目的端口和IP地址
  3> 結構體數據長度
  
  返回值
  0 成功/其餘 錯誤代號
  */
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
//端口
serverAddr.sin_port = htons(12345);
//地址
serverAddr.sin_addr.s_addr = inet_addr( "127.0.0.1" );
 
int connResult = connect(clientSocket, ( const struct sockaddr *)&serverAddr, sizeof (serverAddr));
if (connResult == 0) {
     NSLog ( @"鏈接成功" );
} else {
     NSLog ( @"鏈接失敗 %zi" ,connResult);
     return ;
}
 
// 三、發送數據到服務器
/**
  參數
  1> 客戶端socket
  2> 發送內容地址
  3> 發送內容長度
  4> 發送方式標誌,通常爲0
  返回值
  若是成功,則返回發送的字節數,失敗則返回SOCKET_ERROR
  */
NSString *sendMsg = @"Hello" ;
ssize_t sendLen = send(clientSocket, sendMsg.UTF8String, strlen(sendMsg.UTF8String), 0);
NSLog ( @"發送了 %zi 個字節" ,sendLen);
 
 
// 四、 從服務器接受數據
/**
  參數
  1> 客戶端socket
  2> 接受內容緩衝區地址
  3> 接受內容緩衝區長度
  4> 接收方式,0表示阻塞,必須等待服務器返回數據
  返回值
  若是成功,則返回讀入的字節數,失敗則返回SOCKET_ERROR
  */
uint8_t buffer[1024]; //將空間準備出來
 
ssize_t recvLen = recv(clientSocket, buffer, sizeof (buffer), 0);
NSLog ( @"接收到了 %zi 個字節" ,recvLen);
 
NSData *data = [ NSData dataWithBytes:buffer length:recvLen];
NSString *str = [[ NSString alloc] initWithData:data encoding: NSUTF8StringEncoding ];
NSLog ( @"接收到數據爲 %@" ,str);
 
// 五、 關閉
close(clientSocket);

5.2服務端Socket使用nc命令代替


打開mac命令行終端 輸入 nc -lk 12345

5.3演示結果


<ignore_js_op>聊聊iOS中網絡編程長鏈接的那些事_4.jpg

六、開源CocoaAsyncSocket庫


CocoaAsyncSocket是谷歌基於BSD-Socket寫的一個IM框架,它給Mac和iOS提供了易於使用的、強大的異步套接字庫,向上封裝出簡單易用OC接口。省去了咱們面向Socket以及數據流Stream等繁瑣複雜的編程,並且支持TCP或者UDP協議,支持IPv4和IPv6,支持TLS/SSL安全傳輸,而且是線程安全的。

6.1基於CocoaAsyncSocket實現的客戶端代碼


01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#import "GCDAsyncSocket.h"
 
@interface ViewController2 ()<GCDAsyncSocketDelegate>
 
@property ( nonatomic , strong) GCDAsyncSocket *clientSocket;
 
@end
 
@implementation ViewController2
 
- ( void )viewDidLoad {
     [ super viewDidLoad];
     self .view.backgroundColor = [UIColor redColor];
     UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(0, 400, 300, 60)];
     btn.backgroundColor = [UIColor orangeColor];
     [btn setTitle: @"發送數據" forState:UIControlStateNormal];
     [btn addTarget: self action: @selector (clickBtn) forControlEvents:UIControlEventTouchUpInside];
     [ self .view addSubview:btn];
     
     self .clientSocket = [[GCDAsyncSocket alloc] initWithDelegate: self delegateQueue:dispatch_get_main_queue()];
     
     NSError *error = nil ;
     [ self .clientSocket connectToHost: @"127.0.0.1" onPort:12345 error:&error];
     if (error) {
         NSLog ( @"error == %@" ,error);
     }
}
 
- ( void )clickBtn{
     NSString *msg = @"發送數據: 你好\r\n" ;
     NSData *data = [msg dataUsingEncoding: NSUTF8StringEncoding ];
     // withTimeout -1 : 無窮大,一直等
     // tag : 消息標記
     [ self .clientSocket writeData:data withTimeout:-1 tag:0];
}
 
- ( void )socket:(GCDAsyncSocket *)sock didConnectToHost:( NSString *)host port:(uint16_t)port
{
     NSLog ( @"連接成功" );
     NSLog ( @"服務器IP: %@-------端口: %d" ,host,port);
}
 
- ( void )socket:(GCDAsyncSocket *)sock didWriteDataWithTag:( long )tag
{
     NSLog ( @"發送數據 tag = %zi" ,tag);
     [sock readDataWithTimeout:-1 tag:tag];
}
 
- ( void )socket:(GCDAsyncSocket *)sock didReadData:( NSData *)data withTag:( long )tag
{
     NSString *str = [[ NSString alloc] initWithData:data encoding: NSUTF8StringEncoding ];
     NSLog ( @"讀取數據 data = %@ tag = %zi" ,str,tag);
     // 讀取到服務端數據值後,能再次讀取
     [sock readDataWithTimeout:- 1 tag:tag];
 
}
 
- ( void )socketDidDisconnect:(GCDAsyncSocket *)sock withError:( NSError *)err
{
     NSLog ( @"斷開鏈接" );
     self .clientSocket.delegate = nil ;
     self .clientSocket = nil ;
}
@end

6.2服務端Socket使用nc命令代替


打開mac命令行終端 輸入 nc -lk 12345

6.3演示結果


<ignore_js_op>聊聊iOS中網絡編程長鏈接的那些事_5.jpg

七、補充知識


7.1長鏈接爲何要保持心跳?


國內移動無線網絡運營商在鏈路上一段時間內沒有數據通信後, 會淘汰NAT表中的對應項, 形成鏈路中斷。而國內的運營商通常NAT超時的時間爲5分鐘,因此一般咱們心跳設置的時間間隔爲3-5分鐘。

相關文章請參見:


>> 更多同類文章 … http://www.52im.net/forum.php?mod=collection&action=view&ctid=17

7.2長鏈接選擇TCP協議仍是UDP協議?


使用TCP進行數據傳輸的話,簡單、安全、可靠,可是帶來的是服務端承載壓力比較大。

使用UDP進行數據傳輸的話,效率比較高,帶來的服務端壓力較小,可是須要本身保證數據的可靠性,不做處理的話,會致使丟包、亂序等問題。

若是你的技術團隊實力過硬,你能夠選擇UDP協議,不然仍是使用TCP協議比較好。聽說騰訊IM就是使用的UDP協議,而後還封裝了本身的是有協議,來保證UDP數據包的可靠傳輸。

相關文章請參見:


7.3服務端單機最大TCP鏈接數是多少?


理論最大值:server一般固定在某個本地端口上監聽,等待client的鏈接請求。不考慮地址重用的狀況下,即便server端有多個ip,本地監聽端口也是獨佔的,所以server端tcp鏈接4元組中只有remote ip(也就是client ip)和remote port(客戶端port)是可變的,所以最大tcp鏈接爲客戶端ip數×客戶端port數,對IPV4,不考慮ip地址分類等因素,最大tcp鏈接數約爲2的32次方(ip數)×2的16次方(port數),也就是server端單機最大tcp鏈接數約爲2的48次方。

實際最大值:上面給出的是理論上的單機最大鏈接數,在實際環境中,受到機器資源、操做系統等的限制,特別是sever端,其最大併發tcp鏈接數遠不能達到理論上限。在unix/linux下限制鏈接數的主要因素是內存和容許的文件描述符個數(每一個tcp鏈接都要佔用必定內存,每一個socket就是一個文件描述符),另外1024如下的端口一般爲保留端口。對server端,經過增長內存、修改最大文件描述符個數等參數,單機最大併發TCP鏈接數超過10萬,甚至上百萬 是沒問題的,國外 Urban Airship 公司在產品環境中已作到 50 萬併發 。在實際應用中,對大規模網絡應用,還須要考慮C10K ,C100k問題。

相關文章請參見:


(原文連接:https://www.jianshu.com/p/85535a17372b,有改動)

附錄:更多網絡編程技術文章


[1] 網絡編程基礎資料:
計算機網絡通信協議關係圖(中文珍藏版)
UDP中一個包的大小最大能多大?
P2P技術詳解(一):NAT詳解——詳細原理、P2P簡介
P2P技術詳解(二):P2P中的NAT穿越(打洞)方案詳解
P2P技術詳解(三):P2P技術之STUN、TURN、ICE詳解
通俗易懂:快速理解P2P技術中的NAT穿透原理
技術掃盲:新一代基於UDP的低延時網絡傳輸層協議——QUIC詳解
讓互聯網更快:新一代QUIC協議在騰訊的技術實踐分享
現代移動端網絡短鏈接的優化手段總結:請求速度、弱網適應、安全保障
聊聊iOS中網絡編程長鏈接的那些事
>> 更多同類文章 ……

[2] NIO異步網絡編程資料:
Java新一代網絡編程模型AIO原理及Linux系統AIO介紹
有關「爲什麼選擇Netty」的11個疑問及解答
開源NIO框架八卦——究竟是先有MINA仍是先有Netty?
選Netty仍是Mina:深刻研究與對比(一)
選Netty仍是Mina:深刻研究與對比(二)
NIO框架入門(一):服務端基於Netty4的UDP雙向通訊Demo演示
NIO框架入門(二):服務端基於MINA2的UDP雙向通訊Demo演示
NIO框架入門(三):iOS與MINA二、Netty4的跨平臺UDP雙向通訊實戰
NIO框架入門(四):Android與MINA二、Netty4的跨平臺UDP雙向通訊實戰
Netty 4.x學習(一):ByteBuf詳解
Netty 4.x學習(二):Channel和Pipeline詳解
Netty 4.x學習(三):線程模型詳解
Apache Mina框架高級篇(一):IoFilter詳解
Apache Mina框架高級篇(二):IoHandler詳解
MINA2 線程原理總結(含簡單測試實例)
Apache MINA2.0 開發指南(中文版)[附件下載]
MINA、Netty的源代碼(在線閱讀版)已整理髮布
解決MINA數據傳輸中TCP的粘包、缺包問題(有源碼)
解決Mina中多個同類型Filter實例共存的問題
實踐總結:Netty3.x升級Netty4.x遇到的那些坑(線程篇)
實踐總結:Netty3.x VS Netty4.x的線程模型
詳解Netty的安全性:原理介紹、代碼演示(上篇)
詳解Netty的安全性:原理介紹、代碼演示(下篇)
詳解Netty的優雅退出機制和原理
NIO框架詳解:Netty的高性能之道
Twitter:如何使用Netty 4來減小JVM的GC開銷(譯文)
絕對乾貨:基於Netty實現海量接入的推送服務技術要點
Netty乾貨分享:京東京麥的生產級TCP網關技術實踐總結
>> 更多同類文章 ……

[3] 有關IM/推送的通訊格式、協議的選擇:
如何選擇即時通信應用的數據傳輸格式
強列建議將Protobuf做爲你的即時通信應用數據傳輸格式
全方位評測:Protobuf性能到底有沒有比JSON快5倍?
移動端IM開發須要面對的技術問題(含通訊協議選擇)
簡述移動端IM開發的那些坑:架構設計、通訊協議和客戶端
理論聯繫實際:一套典型的IM通訊協議設計詳解
58到家實時消息系統的協議設計等技術實踐分享
詳解如何在NodeJS中使用Google的Protobuf
技術掃盲:新一代基於UDP的低延時網絡傳輸層協議——QUIC詳解
>> 更多同類文章 ……
相關文章
相關標籤/搜索