iOS 中長連線的那些事
一、長連線在iOS開發中的作用
一般的App的網路請求都是基於HTTP1.0進行的,使用的是NSURLConnection、NSURLSession或者是AFNetworking,HTTP1.0連結最顯著的特點就是客戶端每一次需要主動向服務端傳送請求,都需要經歷建立連結、傳送請求、返回資料、關閉連結這幾個階段,是一種單向請求且無狀態的協議。而有的時候,我們需要服務端主動往客戶端進行推送服務的時候,這個時候長連線就起作用了。蘋果提供的push服務apns就是典型的長連線的應用,IM應用、訂單推送這些也是長連線的典型應用。長連線的特點是一旦通過三次握手建立連結之後,該條鏈路就一直存在,而且該鏈路是一種雙向的通行機制,適合於頻繁的網路請求,避免HTTP每一次請求都會建立連結和關閉連結的操作,減少浪費,提高效率。
二、通訊網路的一些基本概念
長連線的一般實現方式都是基於TCP或者UDP協議完成的。這個時候我們就需要一些基本的通訊網路概念。
2.1 OSI七層網路協議
開放系統互連參考模型 (Open System Interconnect 簡稱OSI)是國際標準化組織(ISO)和國際電報電話諮詢委員會(CCITT)聯合制定的開放系統互連參考模型,為開放式互連資訊系統提供了一種功能結構的框架。
-
物理層:負責機械、電子、定時介面通訊通道上的原始位元流的傳輸。
-
資料鏈路層:負責物理定址,同時將原始位元流轉變成邏輯傳輸線路。
-
網路層:控制子網的執行,如邏輯編址、分組傳輸、路由選擇。
-
傳輸層:接受上一層的資料,在必要的時候把資料進行分割,並將這些資料交給網路層,且保證這些資料段有效到達對方。
-
會話層:不同機器上的使用者之間建立以及管理回話。
-
表示層:資訊的語法語義以及它們的關聯,如加密解密、轉換翻譯、壓縮解壓縮。
2.2、IP、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 請求資訊體。
三、Socket
socket翻譯為套接字,是支援TCP/IP協議的網路通訊的基本操作單元。它是網路通訊過程中端點的抽象表示,包含進行網路通訊必須的五種資訊:連線使用的協議,本地主機的IP地址,本地程序的協議埠,遠地主機的IP地址,遠地程序的協議埠。socket是在應用層和傳輸層之間的一個抽象層,它把TCP/IP層複雜的操作抽象為幾個簡單的介面供應用層呼叫已實現程序在網路中通訊。它不屬於OSI七層協議,它只是對於TCP,UDP協議的一套封裝,讓我們開發人員更加容易編寫基於TCP、UDP的應用。
使用socket進行TCP通訊的基本流程如下:
socket程式設計中我們經常使用到的函式
// 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
4.1、客戶端實現程式碼
// 1、 建立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); // 2、 連線到伺服器 /** 引數 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; } // 3、傳送資料到伺服器 /** 引數 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); // 4、 從伺服器接受資料 /** 引數 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); // 5、 關閉 close(clientSocket);
4.2、服務端Socket使用nc命令代替
開啟mac命令列終端 輸入 nc -lk 12345
4.3、演示結果
五、CocoaAsyncSocket
CocoaAsyncSocket是谷歌基於BSD-Socket寫的一個IM框架,它給Mac和iOS提供了易於使用的、強大的非同步套接字型檔,向上封裝出簡單易用OC介面。省去了我們面向Socket以及資料流Stream等繁瑣複雜的程式設計,而且支援TCP或者UDP協議,支援IPv4和IPv6,支援TLS/SSL安全傳輸,並且是執行緒安全的。開源專案地址為https://github.com/robbiehanson/CocoaAsyncSocket。
5.1、基於CocoaAsyncSocket實現的客戶端程式碼
"GCDAsyncSocket.h"
@ interface ViewController2 ()< GCDAsyncSocketDelegate>
@property (nonatomic, strong) GCDAsyncSocket *clientSocket;
@end
@implementation ViewController2
- (
void)viewDidLoad {
[
superviewDidLoad];
self.view.backgroundColor =[UIColor redColor];
UIButton *btn = [[UIButton alloc]initWithFrame:CGRectMake(
0 , 400 , 300 , 60)];
btn.backgroundColor = [UIColororangeColor];
[btn setTitle:@
" 傳送資料 "forState:UIControlStateNormal];
[btn addTarget:self action:@selector(clickBtn)forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
self.clientSocket =[[GCDAsyncSocket alloc] initWithDelegate:selfdelegateQueue:dispatch_get_main_queue()];
NSError *error = nil;
[self.clientSocketconnectToHost:@
"127.0.0.1" onPort: 12345error:&error];
if(error) {
NSLog(@
"error == %@",error);
}
}
- (
void)clickBtn{
NSString *msg = @
" 傳送資料 : 你好 \r\n ";
NSData *data = [msgdataUsingEncoding:NSUTF8StringEncoding];
//withTimeout -1 : 無窮大 , 一直等 // tag : 訊息標記 [self.clientSocketwriteData:data withTimeout:- 1 tag: 0];
}
- (
void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)hostport:(uint16_t)port
{
NSLog(@
" 連結成功 ");
NSLog(@
" 伺服器 IP:%@------- 埠 : %d",host,port);
}
- (
void )socket:(GCDAsyncSocket *)sock didWriteDataWithTag:( long)tag
{
NSLog(@
" 傳送資料 tag= %zi",tag);
[sock readDataWithTimeout:-
1tag: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);
// 讀取到服務端資料值後 , 能再次讀取 [sockreadDataWithTimeout:- 1tag:tag];
}
- (
void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
NSLog(@
" 斷開連線 ");
self.clientSocket.delegate = nil;
self.clientSocket = nil;
}
@end
5.2、服務端Socket使用nc命令代替
開啟mac命令列終端 輸入 nc -lk 12345
5.3、演示結果
六、補充知識
6.1、長連線為什麼要保持心跳?
國內移動無線網路運營商在鏈路上一段時間內沒有資料通訊後, 會淘汰NAT表中的對應項, 造成鏈路中斷。而國內的運營商一般NAT超時的時間為5分鐘,所以通常我們心跳設定的時間間隔為3-5分鐘。
6.2、長連線選擇TCP協議還是UDP協議?
使用TCP進行資料傳輸的話,簡單、安全、可靠,但是帶來的是服務端承載壓力比較大。
使用UDP進行資料傳輸的話,效率比較高,帶來的服務端壓力較小,但是需要自己保證資料的可靠性,不作處理的話,會導致丟包、亂序等問題。
如果你的技術團隊實力過硬,你可以選擇UDP協議,否則還是使用TCP協議比較好。據說騰訊IM就是使用的UDP協議,然後還封裝了自己的私有協議,來保證UDP資料包的可靠傳輸。
6.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問題。
七、參考文章
IP,TCP 和 HTTP
玩轉iOS開發:iOS中的Socket程式設計
iOS即時通訊,從入門到“放棄”
單機最大tcp連線數