iOS中网络编程长连接
1、长连接在iOS开发中的应用
常见的短连接应用场景: 一般的App的网络请求都是基于 Http1.0 进行的,使用的是 NSURLConnection、NSURLSession 或者是 AFNetworking,Http1.0 链接最显著的特点就是客户端每一次需要主动向服务端发送请求,都需要经历建立链接、发送请求、返回数据、关闭链接这几个阶段,是一种单向请求且无状态的协议。
长连接的应用场景: 有的时候,我们需要服务端主动往客户端进行推送服务的时候,这个时候长连接就起作用了。苹果提供的 push 服务 apns 就是典型的长连接的应用,IM 即时通讯应用、订单消息推送这些也是长连接的典型应用。长连接的特点是一旦通过三次握手建立链接之后,该条链路就一直存在,而且该链路是一种双向的通行机制,适合于频繁的网络请求,避免 Http 每一次请求都会建立链接和关闭链接的操作,减少浪费,提高效率。
2、通信网络的一些基本概念
长连接的一般实现方式都是基于TCP或者UDP协议完成的。这个时候我们就需要一些基本的通信网络概念。
2.1 OSI七层网络协议
开放系统互连参考模型 (Open System Interconnect 简称 OSI)是国际标准化组织(ISO)和国际电报电话咨询委员会(CCITT)联合制定的开放系统互连参考模型,为开放式互连信息系统提供了一种功能结构的框架。
image.png如上图所示:
- 物理层:负责机械、电子、定时接口通信信道上的原始比特流的传输;
- 数据链路层:负责物理寻址,同时将原始比特流转变成逻辑传输线路;
- 网络层:控制子网的运行,如逻辑编址、分组传输、路由选择;
- 传输层:接受上一层的数据,在必要的时候把数据进行分割,并将这些数据交给网络层,且保证这些数据段有效到达对方;
- 会话层:不同机器上的用户之间建立以及管理回话;
- 表示层:信息的语法语义以及它们的关联,如加密解密、转换翻译、压缩解压缩;
- 应用层:各种应用程序协议,如 Http、Ftp、SMTP、POP3。
2.2 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、Socket概念
socket 翻译为套接字,是支持 TCP/IP 协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。socket 是在应用层和传输层之间的一个抽象层,它把 TCP/IP 层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。它不属于 OSI 七层协议,它只是对于 TCP,UDP 协议的一套封装,让我们开发人员更加容易编写基于 TCP、UDP 的应用。
image.png使用 socket 进行 TCP 通信的基本流程如下:
image.pngsocket编程中我们经常使用到的函数:
// 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)
4、实现一个简单的基于 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 演示结果
image.png5、开源 CocoaAsyncSocket 库
CocoaAsyncSocket 是谷歌基于 BSD-Socket 写的一个 IM 框架,它给 Mac 和 iOS 提供了易于使用的、强大的异步套接字库,向上封装出简单易用 OC 接口。省去了我们面向 Socket 以及数据流 Stream 等繁琐复杂的编程,而且支持 TCP 或者 UDP 协议,支持 IPv4 和 IPv6,支持 TLS/SSL 安全传输,并且是线程安全的。
5.1 基于 CocoaAsyncSocket 实现的客户端代码
#import "GCDAsyncSocket.h"
@interface ViewController ()<GCDAsyncSocketDelegate>
@property (nonatomic, strong) GCDAsyncSocket *clientSocket;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(0, 400, 300, 60)];
btn.backgroundColor = [UIColor blueColor];
[btn setTitle:@"发送数据" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(didClickToSendDataButton:) 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)didClickToSendDataButton:(UIButton *)button{
NSString *msg = @"发送数据: 你好\r\n";
NSData *data = [msg dataUsingEncoding:NSUTF8StringEncoding];
// withTimeout -1 : 无穷大,一直等
// tag : 消息标记
[self.clientSocket writeData:data withTimeout:-1 tag:0];
}
#pragma mark - GCDAsyncSocketDelegate
- (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 演示结果
image.png6、补充知识
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 问题。