So Terrible

Author Avatar
纸简书生 8月 01, 2017

很久没有写了。尤其是技术文章,一个多月没写了。本来这篇文章应该早一个月前写出来的。一直想把这篇我文章写好点,所以一直在看,在研究相关的问题。但是迟迟拖到现在也没完成。非常的遗憾,平时琐碎的时间太多。有时候觉得自己弄懂了就行了何必再去写东西呢!写一篇高质量的文章非常耗时,从写Demo到一步一步分析。😔还是没能完成最终的想法。就当给自己一个教训!

瞎倒腾

几大核心对象(Util)如下:

  1. GCDAsyncSocketPreBuffer:用于当socket有更多可用的数据请求的时候而不是被当前读请求使用。在这种情况下将会从socket中剔除所有数据来最小化系统调用。并且将其他尚未被读取的数据放如preBuffer中。再次从socket读取之前,会填满preBuffer,换句话说也就是大量的数据会被写入preBuffer。接下来通过一系列的一次或多次读取(后续的读取请求)排除preBuffer。之前为了实现这个目的,使用了ring Buffer,但是一个ringBuffer占用的存储是需要的两倍,实际上通常是需要两倍以上的大小,因为所有内容必须四舍五入到vm_page_size。因为preBuffer在写入之后总是完全耗尽,所以不需要完整ringBuffer。目前通过链表实现的
  2. GCDAsyncReadPacket:用于对可读数据包装,可读包可以确定是否读到了某个长度、短刀了么讴歌分隔符,捉着读到了第一个可用的chunk数据
  3. GCDAsyncWritePacket:用于对可写数据包装
  4. GCDAsyncSpecialPacket:对在读写队列中终端的特殊指令包装
  5. GCDAsyncSocket:最终向外部暴露的类,使用标准的代理模式。在所给的代理队列里面回调。允许设置最大并发,提供了简单的线程安全。
  6. Class Utilities:常用工具方法
    • 根据域名得到ip地址数组:lookupHost:(NSString *)host port:(uint16_t)port error:
    • 是否为ipv4、ipv6

连接

连接最终调用如下方法:

1
2
3
4
5
- (BOOL)connectToHost:(NSString *)inHost
onPort:(uint16_t)port
viaInterface:(NSString *)inInterface
withTimeout:(NSTimeInterval)timeout
error:(NSError **)errPtr

各个参数的意思很清楚,这里说一下参数inInterface。代表的是网卡接口。常见的如下:

  • en1(Wireless NIC)无线网卡
  • lo0(本机环路),
  • en0有线网卡 (Wired NIC)。
  • 注意:MBP 上没有有线网卡,en0 即为无线网卡 ,可通过 ifconfig 命令自行识别。

    它的值可以是”en1” or “lo0”也可以是ip地址。并且可以包含一个端口,用冒号分开。如果interface有值,则后面会将本机的IPV4 IPV6的 address设置上。

    将参数传递进来之后立即将参数copy了一份保证不被外部修改。然后发起同步的链接。

dispatch_queue_set_specific

因为连接过程是在特定的socket队列中完成,所以用了dispatch_queue_set_specific来实现。

dispatch_queue_set_specific的使用方法比较简单,只要理解它就是为某个队列打个标记,然后通过这个标记取出来。可以简单的把他理解为一个字典。

  • 打标记:dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL);
  • 根据标记取:dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)

连接过程

连接过程的非常多。这里把整个过程简化了一下。

从开始连接到最终连接,最终调用走到int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]);

注意一点连接的过程全部放在了自动释放池@autoreleasepool中。目的也是为了优化性能。

在开始连接之前做了一个预处理。里面做了几个重要的事情:

  1. 参数检验:代理是否为空、当前队列是否为socket队列、当前状态是否已经连接、清空读写队列
  2. 状态更新:用flags |= kSocketStarted;方式更新状态标识为开始Socket连接
  3. 域名解析(ipv4、ipv6):返回的是一个数组
  4. 开始去连接:在全局队列里面调用[strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];

开始连接的过程有如下几个函数:

  • - (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6
    • 根据ip配置过滤,是否禁用ip4/6,报错则关闭连接。
  • - (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr
    • 获取具体的ipv4、ipv6地址。根据配置确定socketFD的值,socketFD为最终连接数据。
  • - (void)connectSocket:(int)socketFD address:(NSData *)address stateIndex:(int)aStateIndex
    • 最终调用connect方法,将上面得到socketFD进行连接。注意这个方法是个同步的,会阻塞线程。

      Source/Timer

      如果了解过runloop底层的同学就知道source这个东西。项目中一共定义了如下几种source
1
2
3
4
5
6
7
8
9
10
dispatch_source_t accept4Source;
dispatch_source_t accept6Source;
dispatch_source_t acceptUNSource;
//连接timer,GCD定时器
dispatch_source_t connectTimer;
dispatch_source_t readSource;
dispatch_source_t writeSource;
dispatch_source_t readTimer;
dispatch_source_t writeTimer;

基本上分为三种,一种是用于监听连接的accept类型,一种是读写read、write,一种是定时器source。通过这几种source,达到监听网络连接、读写进度、定时器计时。

连接

通过代理来看看

1
2
3
4
5
6
7
8
9
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
// 连接
accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue);
int socketFD = socket4FD;
dispatch_source_t acceptSource = accept4Source;
__weak GCDAsyncSocket *weakSelf = self;
//事件句柄
dispatch_source_set_event_handler(accept4Source, ^{ @autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"
__strong GCDAsyncSocket *strongSelf = weakSelf;
if (strongSelf == nil) return_from_block;
LogVerbose(@"event4Block");
unsigned long i = 0;
//拿到数据,连接数
unsigned long numPendingConnections = dispatch_source_get_data(acceptSource);
LogVerbose(@"numPendingConnections: %lu", numPendingConnections);
//循环去接受这些socket的事件
while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections));
#pragma clang diagnostic pop
}});
//取消句柄
dispatch_source_set_cancel_handler(accept4Source, ^{
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"
#if !OS_OBJECT_USE_OBJC
LogVerbose(@"dispatch_release(accept4Source)");
dispatch_release(acceptSource);
#endif
LogVerbose(@"close(socket4FD)");
//关闭socket
close(socketFD);
#pragma clang diagnostic pop
});
LogVerbose(@"dispatch_resume(accept4Source)");
//开启source
dispatch_resume(accept4Source);

一旦有网络连接连上就会调用dispatch_source_set_event_handler设置的block。当取消连接的时候就会走dispatch_source_set_cancel_handler的block。注意source不会自己启动,需要手动启动。一切的开始都是从这一步开始的。

读写

读和写逻辑上基本一样,同样是开始创建事件源,设置监听回调、设置取消回调,手动启动几个步骤。具体步骤可以看看下面的注释。

1
2
3
4
5
6
7
8
9
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
//GCD source DISPATCH_SOURCE_TYPE_READ 会一直监视着 socketFD,直到有数据可读
readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketFD, 0, socketQueue);
//_dispatch_source_type_write :监视着 socketFD,直到写数据了
writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socketFD, 0, socketQueue);
// Setup event handlers
__weak GCDAsyncSocket *weakSelf = self;
#pragma mark readSource的回调
//GCD事件句柄 读,当socket中有数据流出现,就会触发这个句柄,全自动,不需要手动触发
dispatch_source_set_event_handler(readSource, ^{ @autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"
__strong GCDAsyncSocket *strongSelf = weakSelf;
if (strongSelf == nil) return_from_block;
LogVerbose(@"readEventBlock");
//从readSource中,获取到数据长度,
strongSelf->socketFDBytesAvailable = dispatch_source_get_data(strongSelf->readSource);
LogVerbose(@"socketFDBytesAvailable: %lu", strongSelf->socketFDBytesAvailable);
//如果长度大于0,开始读数据
if (strongSelf->socketFDBytesAvailable > 0)
[strongSelf doReadData];
else
//因为触发了,但是却没有可读数据,说明读到当前包边界了。做边界处理
[strongSelf doReadEOF];
#pragma clang diagnostic pop
}});
//写事件句柄
dispatch_source_set_event_handler(writeSource, ^{ @autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"
__strong GCDAsyncSocket *strongSelf = weakSelf;
if (strongSelf == nil) return_from_block;
LogVerbose(@"writeEventBlock");
//标记为可接受数据
strongSelf->flags |= kSocketCanAcceptBytes;
//开始写
[strongSelf doWriteData];
#pragma clang diagnostic pop
}});
// Setup cancel handlers
__block int socketFDRefCount = 2;
#if !OS_OBJECT_USE_OBJC
dispatch_source_t theReadSource = readSource;
dispatch_source_t theWriteSource = writeSource;
#endif
//读写取消的句柄
dispatch_source_set_cancel_handler(readSource, ^{
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"
LogVerbose(@"readCancelBlock");
#if !OS_OBJECT_USE_OBJC
LogVerbose(@"dispatch_release(readSource)");
dispatch_release(theReadSource);
#endif
if (--socketFDRefCount == 0)
{
LogVerbose(@"close(socketFD)");
//关闭socket
close(socketFD);
}
#pragma clang diagnostic pop
});
dispatch_source_set_cancel_handler(writeSource, ^{
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"
LogVerbose(@"writeCancelBlock");
#if !OS_OBJECT_USE_OBJC
LogVerbose(@"dispatch_release(writeSource)");
dispatch_release(theWriteSource);
#endif
if (--socketFDRefCount == 0)
{
LogVerbose(@"close(socketFD)");
//关闭socket
close(socketFD);
}
#pragma clang diagnostic pop
});

计时器

计时器可以用NSTimer但是精度上没有source那么精确
这里以一个读超时计时器为例:

1
2
3
4
5
6
7
8
9
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
//生成一个定时器source
readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue);
__weak GCDAsyncSocket *weakSelf = self;
//句柄
dispatch_source_set_event_handler(readTimer, ^{ @autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"
__strong GCDAsyncSocket *strongSelf = weakSelf;
if (strongSelf == nil) return_from_block;
//执行超时操作
[strongSelf doReadTimeout];
#pragma clang diagnostic pop
}});
#if !OS_OBJECT_USE_OBJC
dispatch_source_t theReadTimer = readTimer;
//取消的句柄
dispatch_source_set_cancel_handler(readTimer, ^{
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"
LogVerbose(@"dispatch_release(readTimer)");
dispatch_release(theReadTimer);
#pragma clang diagnostic pop
});
#endif
//定时器延时 timeout时间执行
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC));
//间隔为永远,即只执行一次
dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0);
dispatch_resume(readTimer);

source总结

通过上面的代码可以得出source的使用方式,而且整个GCDAsyncSocket都是建立在这个基础之上。设置各个source的回调处理方法。在事件到来的时候调用。如果手动取消则会调用cancle回调。基本上是照着葫芦画瓢的过程。

读和写的过程和逻辑判断步骤基本相似的。

往socket里面写数据调用writeData: withTimeout: tag:

发送之前先将数据包装成GCDAsyncWritePacket对象。将packet加入到writeQueue当中,然后调用doWriteData方法。最终调用ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite);中间经过一系列的条件判断。

  1. - (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag
  2. [self maybeDequeueWrite]; 一系列条件判断,比如TLS,SSL。超时设置
  3. [self doWriteData];
  4. Writing data directly over raw socket。

注意ssize_t write(int fd, const void*buf,size_t nbytes);write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数.失败时返回-1. 并设置errno变量。

在写的时候可能一次写不完全,所以需要GCDAsyncWritePacket记录当前的已经上传多少。

给自己截止的日期已经到了,但是还是没有整理出来。