So Terrible
很久没有写了。尤其是技术文章,一个多月没写了。本来这篇文章应该早一个月前写出来的。一直想把这篇我文章写好点,所以一直在看,在研究相关的问题。但是迟迟拖到现在也没完成。非常的遗憾,平时琐碎的时间太多。有时候觉得自己弄懂了就行了何必再去写东西呢!写一篇高质量的文章非常耗时,从写Demo到一步一步分析。😔还是没能完成最终的想法。就当给自己一个教训!
瞎倒腾
几大核心对象(Util)如下:
- GCDAsyncSocketPreBuffer:用于当socket有更多可用的数据请求的时候而不是被当前读请求使用。在这种情况下将会从socket中剔除所有数据来最小化系统调用。并且将其他尚未被读取的数据放如preBuffer中。再次从socket读取之前,会填满preBuffer,换句话说也就是大量的数据会被写入preBuffer。接下来通过一系列的一次或多次读取(后续的读取请求)排除preBuffer。之前为了实现这个目的,使用了ring Buffer,但是一个ringBuffer占用的存储是需要的两倍,实际上通常是需要两倍以上的大小,因为所有内容必须四舍五入到vm_page_size。因为preBuffer在写入之后总是完全耗尽,所以不需要完整ringBuffer。目前通过链表实现的
- GCDAsyncReadPacket:用于对可读数据包装,可读包可以确定是否读到了某个长度、短刀了么讴歌分隔符,捉着读到了第一个可用的chunk数据
- GCDAsyncWritePacket:用于对可写数据包装
- GCDAsyncSpecialPacket:对在读写队列中终端的特殊指令包装
- GCDAsyncSocket:最终向外部暴露的类,使用标准的代理模式。在所给的代理队列里面回调。允许设置最大并发,提供了简单的线程安全。
- Class Utilities:常用工具方法
- 根据域名得到ip地址数组:lookupHost:(NSString *)host port:(uint16_t)port error:
- 是否为ipv4、ipv6
连接
连接最终调用如下方法:
|
|
各个参数的意思很清楚,这里说一下参数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中。目的也是为了优化性能。
在开始连接之前做了一个预处理。里面做了几个重要的事情:
- 参数检验:代理是否为空、当前队列是否为socket队列、当前状态是否已经连接、清空读写队列
- 状态更新:用flags |= kSocketStarted;方式更新状态标识为开始Socket连接
- 域名解析(ipv4、ipv6):返回的是一个数组
- 开始去连接:在全局队列里面调用[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
|
|
基本上分为三种,一种是用于监听连接的accept类型,一种是读写read、write,一种是定时器source。通过这几种source,达到监听网络连接、读写进度、定时器计时。
连接
通过代理来看看
|
|
一旦有网络连接连上就会调用dispatch_source_set_event_handler设置的block。当取消连接的时候就会走dispatch_source_set_cancel_handler的block。注意source不会自己启动,需要手动启动。一切的开始都是从这一步开始的。
读写
读和写逻辑上基本一样,同样是开始创建事件源,设置监听回调、设置取消回调,手动启动几个步骤。具体步骤可以看看下面的注释。
|
|
计时器
计时器可以用NSTimer但是精度上没有source那么精确
这里以一个读超时计时器为例:
|
|
source总结
通过上面的代码可以得出source的使用方式,而且整个GCDAsyncSocket都是建立在这个基础之上。设置各个source的回调处理方法。在事件到来的时候调用。如果手动取消则会调用cancle回调。基本上是照着葫芦画瓢的过程。
读
读和写的过程和逻辑判断步骤基本相似的。
写
往socket里面写数据调用writeData: withTimeout: tag:
发送之前先将数据包装成GCDAsyncWritePacket对象。将packet加入到writeQueue当中,然后调用doWriteData方法。最终调用ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite);
中间经过一系列的条件判断。
- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag
[self maybeDequeueWrite];
一系列条件判断,比如TLS,SSL。超时设置[self doWriteData];
- Writing data directly over raw socket。
注意ssize_t write(int fd, const void*buf,size_t nbytes);write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数.失败时返回-1. 并设置errno变量。
在写的时候可能一次写不完全,所以需要GCDAsyncWritePacket记录当前的已经上传多少。
给自己截止的日期已经到了,但是还是没有整理出来。