我們組內部搞了一個公眾號,大家支持一下,有不少好文章。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)聯合制定的開放系統互連參考模型,為開放式互連信息系統提供了一種功能結構的框架。
- 物理層:負責機械、電子、定時接口通信信道上的原始比特流的傳輸。
- 數據鏈路層:負責物理尋址,同時將原始比特流轉變成邏輯傳輸線路。
- 網絡層:控制子網的運行,如邏輯編址、分組傳輸、路由選擇。
- 傳輸層:接受上一層的數據,在必要的時候把數據進行分割,并將這些數據交給網絡層,且保證這些數據段有效到達對方。
- 會話層:不同機器上的用戶之間建立以及管理回話。
- 表示層:信息的語法語義以及它們的關聯,如加密解密、轉換翻譯、壓縮解壓縮。
- 應用層:各種應用程序協議,如Http、Ftp、SMTP、POP3。
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實現的客戶端代碼
#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
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連接數