在App与服务器需要高频通信,或者服务器主动推送消息到App的情况下,就需要通过长连接来实现。比如聊天和股票软件。
下面介绍iOS中如何通过GCDAsyncSocket来实现长连接。
GCDAsyncSocket介绍
GCDAsyncSocket是一个开源库
CocoaAsyncSocket的一部分,用于建立可靠的TCP连接。如果想建立UDP连接,可以用GCDAsyncUDPSocket。
建立连接
1、创建socket对象,delegateQueue可以指定代理方法执行的队列。
-(GCDAsyncSocket*)socket
{
if (_socket == nil)
{
_socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
}
return _socket;
}
2、连接到指定服务器
NSError *error = nil;
// host:域名或ip,port:端口号,timeout:超时时间
if (![self.socket connectToHost:host onPort:port withTimeout:timeout error:&error])
{
NSLog(@"socket连接服务器错误:%@", error);
}
#pragma mark- GCDAsyncSocketDelegate
-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
NSLog(@"socket成功建立。");
}
-(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error
{
NSLog(@"链接出错:error:%@", error);
}
3、进行TLS验证(可选)
// 打包到App中的根证书
-(NSData *)certData
{
if(!_certData)
{
_certData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"root_cert"
ofType:@"cer"]];
}
return _certData;
}
#pragma mark- GCDAsyncSocketDelegate
-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
NSLog(@"socket成功建立。");
NSMutableDictionary *settings = [NSMutableDictionary dictionary];
//允许自签名证书手动验证
[settings setObject:@YES forKey:GCDAsyncSocketManuallyEvaluateTrust];
//GCDAsyncSocketSSLPeerName
//[settings setObject:@"example.com" forKey:GCDAsyncSocketSSLPeerName];
// 如果不是自签名证书,而是那种权威证书颁发机构注册申请的证书
// 那么这个settings字典可不传。
NSLog(@"socket开始TLS握手");
[sock startTLS:settings];
}
- (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust completionHandler:(void (^)(BOOL))completionHandler
{
NSLog(@"socket TLS开始校验证书。");
OSStatus status = -1;
SecTrustResultType result = kSecTrustResultDeny;
if(self.certData)
{
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef)self.certData);
// 设置证书用于验证
SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)[NSArray arrayWithObject:(__bridge id)cert]);
// 验证服务器证书和本地证书是否匹配
status = SecTrustEvaluate(trust, &result);
CFRelease(cert);
}
else
{
NSLog(@"local certificates could not be loaded");
completionHandler(NO);
}
if ((status == noErr && (result == kSecTrustResultProceed || result == kSecTrustResultUnspecified)))
{
//成功通过验证,证书可信
completionHandler(YES);
}
else
{
CFArrayRef arrayRefTrust = SecTrustCopyProperties(trust);
TPDiskLog(@"error in connection occured\n%@", arrayRefTrust);
CFRelease(arrayRefTrust);
completionHandler(NO);
}
NSLog(@"socket TLS校验证书完毕。");
}
- (void)socketDidSecure:(GCDAsyncSocket *)sock
{
NSLog(@"socket TLS握手成功,安全通信已经建立连接。");
}
收发数据
1、原理
我理解socket就像是一个互不干扰的双向水管,数据就像是水(这里的水不会混在一起,是有序的)。为了不混乱,只考虑App接收数据,Server发送数据的情况。即App端是出水口,Server端是入水口。此时的socket可以看成是单一管道。
在初始状态,出水口是关闭的,入水口是打开的。Server可以一直往水管注水(发数据),App在需要的时候,就拿桶去出水口接水(接收数据)。这个桶的大小(缓冲区),就是App需要数据的大小,每次接满了就关闭出水口,如果没有接满,就等待。
根据App接水的特性,使得App必须明确知道Server每次注水的大小。否则会出现桶太大,接了好几个批次的水,等待的时间也可能很长;桶太小,同一次的水都还没有接完。
所以我们需要规范Server注水的行为,每次注水之前,都需要先注入固定大小的,带有本次注水大小信息的特质液体。App在接水时,每次都是先用固定大小的桶,接收特质液体,然后从液体中获取后续水的大小,再用对应大小的桶来接收水。
我理解socket就像是一个互不干扰的双向水管,数据就像是水(这里的水不会混在一起,是有序的)。为了不混乱,只考虑App接收数据,Server发送数据的情况。即App端是出水口,Server端是入水口。此时的socket可以看成是单一管道。
在初始状态,出水口是关闭的,入水口是打开的。Server可以一直往水管注水(发数据),App在需要的时候,就拿桶去出水口接水(接收数据)。这个桶的大小(缓冲区),就是App需要数据的大小,每次接满了就关闭出水口,如果没有接满,就等待。
根据App接水的特性,使得App必须明确知道Server每次注水的大小。否则会出现桶太大,接了好几个批次的水,等待的时间也可能很长;桶太小,同一次的水都还没有接完。
所以我们需要规范Server注水的行为,每次注水之前,都需要先注入固定大小的,带有本次注水大小信息的特质液体。App在接水时,每次都是先用固定大小的桶,接收特质液体,然后从液体中获取后续水的大小,再用对应大小的桶来接收水。
2、定义头部
对应回数据,则是将数据格式化:基本头(固定长度)+ 数据(可变长度)。比如下面这样定义,每次App先读取4字节数据,获取数据的长度,然后再读取对应长度的数据,来获取payload真实内容。
对应回数据,则是将数据格式化:基本头(固定长度)+ 数据(可变长度)。比如下面这样定义,每次App先读取4字节数据,获取数据的长度,然后再读取对应长度的数据,来获取payload真实内容。
typedef struct {
uint32_t length;// 数据长度
} header_t;
3、读函数
需要指定读取长度length,tag可以用于区分本次读取是头部,还是数据主体(下面的完整代码会有例子)。这里的超时时间一般要设置成-1,防止socket在指定时间内没有读取够数据,把连接断开。如果业务请求需要设置超时时间,要在外部通过定时器管理。
需要指定读取长度length,tag可以用于区分本次读取是头部,还是数据主体(下面的完整代码会有例子)。这里的超时时间一般要设置成-1,防止socket在指定时间内没有读取够数据,把连接断开。如果业务请求需要设置超时时间,要在外部通过定时器管理。
[self.socket readDataToLength:lenght withTimeout:-1 tag:tag];
#pragma mark- GCDAsyncSocketDelegate
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
// 读取到数据,通过tag来区分是header还是body
}
4、写函数
与读函数类似
与读函数类似
[self.socket writeData:data withTimeout:-1 tag:0];
#pragma mark- GCDAsyncSocketDelegate
-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
NSLog(@"didWriteDataWithTag:%ld",tag);
}
完整代码
typedef struct {
uint32_t length;// 数据长度
} header_t;
#define HeaderLength (sizeof(header_t))
// 通信层的超时时间,都设置成infinite
#define SocketTimeOutNone (-1) // 不超时,防止socket断开
#define ReadTagPacketHeader (101)
#define ReadTagPacketBody (102)
@interface SocketManager() <GCDAsyncSocketDelegate>
@property (nonatomic, strong) GCDAsyncSocket *socket;
@property (nonatomic, strong) NSData *certData;
@end
@implementation SocketManager
-(GCDAsyncSocket*)socket
{
if (_socket == nil)
{
_socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
}
return _socket;
}
-(NSData *)certData
{
if(!_certData)
{
_certData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"root_cert" ofType:@"cer"]];
}
return _certData;
}
#pragma mark- func
-(BOOL)connectToServer
{
NSLog(@"socket开始连接服务器");
NSError *error = nil;
if (![self.socket connectToHost:host onPort:port withTimeout:15 error:&error])
{
NSLog(@"socket连接服务器错误:%@", error);
return NO;
}
return YES;
}
-(void)disconnect
{
[self.socket disconnect];
}
-(void)sendData:(NSData*)data
{
header_t h = {};
h.length = (uint32_t)data.length;
[self.socket writeData:[NSData dataWithBytes:&h length:HeaderLength] withTimeout:SocketTimeOutNone tag:0];
[self.socket writeData:data withTimeout:SocketTimeOutNone tag:0];
}
-(void)readDataLenght:(NSUInteger)lenght tag:(long)tag
{
[self.socket readDataToLength:lenght withTimeout:SocketTimeOutNone tag:tag];
}
#pragma mark- GCDAsyncSocketDelegate
-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
NSLog(@"socket成功建立。");
NSMutableDictionary *settings = [NSMutableDictionary dictionaryWithCapacity:3];
//允许自签名证书手动验证
[settings setObject:@YES forKey:GCDAsyncSocketManuallyEvaluateTrust];
//GCDAsyncSocketSSLPeerName
//[settings setObject:@"example.com" forKey:GCDAsyncSocketSSLPeerName];
// 如果不是自签名证书,而是那种权威证书颁发机构注册申请的证书
// 那么这个settings字典可不传。
NSLog(@"socket开始TLS握手");
[sock startTLS:settings]; // 开始TLS握手
}
- (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust completionHandler:(void (^)(BOOL))completionHandler
{
NSLog(@"socket TLS开始校验证书。");
OSStatus status = -1;
SecTrustResultType result = kSecTrustResultDeny;
if(self.certData)
{
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef)self.certData);
// 设置证书用于验证
SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)[NSArray arrayWithObject:(__bridge id)cert]);
// 验证服务器证书和本地证书是否匹配
status = SecTrustEvaluate(trust, &result);
CFRelease(cert);
}
else
{
NSLog(@"local certificates could not be loaded");
completionHandler(NO);
}
if ((status == noErr && (result == kSecTrustResultProceed || result == kSecTrustResultUnspecified)))
{
//成功通过验证,证书可信
completionHandler(YES);
}
else
{
CFArrayRef arrayRefTrust = SecTrustCopyProperties(trust);
NSLog(@"error in connection occured\n%@", arrayRefTrust);
CFRelease(arrayRefTrust);
completionHandler(NO);
}
NSLog(@"socket TLS校验证书完毕。");
}
- (void)socketDidSecure:(GCDAsyncSocket *)sock
{
NSLog(@"socket TLS握手成功,安全通信已经建立连接。");
[self readDataLenght:kHeaderLength tag:ReadTagPacketHeader];
}
-(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error
{
NSLog(@"链接出错:error:%@\n", error);
}
-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
NSLog(@"didWriteDataWithTag:%ld",tag);
}
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
static header_t h = {};
if (tag == ReadTagPacketHeader)
{
if (data.length == HeaderLength)
{
header_t *header = (header_t*)data.bytes;
h.length = header->length;
if (header->length == 0)// 只有头部
{
[self readDataLenght:HeaderLength tag:ReadTagPacketHeader];
}
else
{
[self readDataLenght:header->length tag:ReadTagPacketBody];
}
}
else
{
NSLog(@"exception occur");
}
}
else if (tag == ReadTagPacketBody)
{
// 这里的data,就是server发送的数据
[self readDataLenght:HeaderLength tag:ReadTagPacketHeader];
}
}
@end
这篇文章也还不错:
CocoaAsyncSocket 文档3:介绍GCDAsyncSocket