Ⅰ Android socket源码解析(三)socket的connect源码解析
上一篇文章着重的聊了socket服务端的bind,listen,accpet的逻辑。本文来着重聊聊connect都做了什么?
如果遇到什么问题,可以来本文 https://www.jianshu.com/p/da6089fdcfe1 下讨论
当服务端一切都准备好了。客户端就会尝试的通过 connect 系统调用,尝试的和服务端建立远程连接。
首先校验当前socket中是否有正确的目标地址。然后获取IP地址和端口调用 connectToAddress 。
在这个方法中,能看到有一个 NetHooks 跟踪socket的调用,也能看到 BlockGuard 跟踪了socket的connect调用。因此可以hook这两个地方跟踪socket,不过很少用就是了。
核心方法是 socketConnect 方法,这个方法就是调用 IoBridge.connect 方法。同理也会调用到jni中。
能看到也是调用了 connect 系统调用。
文件:/ net / ipv4 / af_inet.c
在这个方法中做的事情如下:
注意 sk_prot 所指向的方法是, tcp_prot 中 connect 所指向的方法,也就是指 tcp_v4_connect .
文件:/ net / ipv4 / tcp_ipv4.c
本质上核心任务有三件:
想要能够理解下文内容,先要明白什么是路由表。
路由表分为两大类:
每个路由器都有一个路由表(RIB)和转发表 (fib表),路由表用于决策路由,转发表决策转发分组。下文会接触到这两种表。
这两个表有什么区别呢?
网上虽然给了如下的定义:
但实际上在Linux 3.8.1中并没有明确的区分。整个路由相关的逻辑都是使用了fib转发表承担的。
先来看看几个和FIB转发表相关的核心结构体:
熟悉Linux命令朋友一定就能认出这里面大部分的字段都可以通过route命令查找到。
命令执行结果如下:
在这route命令结果的字段实际上都对应上了结构体中的字段含义:
知道路由表的的内容后。再来FIB转发表的内容。实际上从下面的源码其实可以得知,路由表的获取,实际上是先从fib转发表的路由字典树获取到后在同感加工获得路由表对象。
转发表的内容就更加简单
还记得在之前总结的ip地址的结构吗?
需要进行一次tcp的通信,意味着需要把ip报文准备好。因此需要决定源ip地址和目标IP地址。目标ip地址在之前通过netd查询到了,此时需要得到本地发送的源ip地址。
然而在实际情况下,往往是面对如下这么情况:公网一个对外的ip地址,而内网会被映射成多个不同内网的ip地址。而这个过程就是通过DDNS动态的在内存中进行更新。
因此 ip_route_connect 实际上就是选择一个缓存好的,通过DDNS设置好的内网ip地址并找到作为结果返回,将会在之后发送包的时候填入这些存在结果信息。而查询内网ip地址的过程,可以成为RTNetLink。
在Linux中有一个常用的命令 ifconfig 也可以实现类似增加一个内网ip地址的功能:
比如说为网卡eth0增加一个IPV6的地址。而这个过程实际上就是调用了devinet内核模块设定好的添加新ip地址方式,并在回调中把该ip地址刷新到内存中。
注意 devinet 和 RTNetLink 严格来说不是一个存在同一个模块。虽然都是使用 rtnl_register 注册方法到rtnl模块中:
文件:/ net / ipv4 / devinet.c
文件:/ net / ipv4 / route.c
实际上整个route模块,是跟着ipv4 内核模块一起初始化好的。能看到其中就根据不同的rtnl操作符号注册了对应不同的方法。
整个DDNS的工作流程大体如下:
当然,在tcp三次握手执行之前,需要得到当前的源地址,那么就需要通过rtnl进行查询内存中分配的ip。
文件:/ include / net / route.h
这个方法核心就是 __ip_route_output_key .当目的地址或者源地址有其一为空,则会调用 __ip_route_output_key 填充ip地址。目的地址为空说明可能是在回环链路中通信,如果源地址为空,那个说明可能往目的地址通信需要填充本地被DDNS分配好的内网地址。
在这个方法中核心还是调用了 flowi4_init_output 进行flowi4结构体的初始化。
文件:/ include / net / flow.h
能看到这个过程把数据中的源地址,目的地址,源地址端口和目的地址端口,协议类型等数据给记录下来,之后内网ip地址的查询与更新就会频繁的和这个结构体进行交互。
能看到实际上 flowi4 是一个用于承载数据的临时结构体,包含了本次路由操作需要的数据。
执行的事务如下:
想要弄清楚ip路由表的核心逻辑,必须明白路由表的几个核心的数据结构。当然网上搜索到的和本文很可能大为不同。本文是基于LInux 内核3.1.8.之后的设计几乎都沿用这一套。
而内核将路由表进行大规模的重新设计,很大一部分的原因是网络环境日益庞大且复杂。需要全新的方式进行优化管理系统中的路由表。
下面是fib_table 路由表所涉及的数据结构:
依次从最外层的结构体介绍:
能看到路由表的存储实际上通过字典树的数据结构压缩实现的。但是和常见的字典树有点区别,这种特殊的字典树称为LC-trie 快速路由查找算法。
这一篇文章对于快速路由查找算法的理解写的很不错: https://blog.csdn.net/dog250/article/details/6596046
首先理解字典树:字典树简单的来说,就是把一串数据化为二进制格式,根据左0,右1的方式构成的。
如图下所示:
这个过程用图来展示,就是沿着字典树路径不断向下读,比如依次读取abd节点就能得到00这个数字。依次读取abeh就能得到010这个数字。
说到底这种方式只是存储数据的一种方式。而使用数的好处就能很轻易的找到公共前缀,在字典树中找到公共最大子树,也就找到了公共前缀。
而LC-trie 则是在这之上做了压缩优化处理,想要理解这个算法,必须要明白在 tnode 中存在两个十分核心的数据:
这负责什么事情呢?下面就简单说说整个lc-trie的算法就能明白了。
当然先来看看方法 __ip_dev_find 是如何查找
文件:/ net / ipv4 / fib_trie.c
整个方法就是通过 tkey_extract_bits 生成tnode中对应的叶子节点所在index,从而通过 tnode_get_child_rcu 拿到tnode节点中index所对应的数组中获取叶下一级别的tnode或者叶子结点。
其中查找index最为核心方法如上,这个过程,先通过key左移动pos个位,再向右边移动(32 - bits)算法找到对应index。
在这里能对路由压缩算法有一定的理解即可,本文重点不在这里。当从路由树中找到了结果就返回 fib_result 结构体。
查询的结果最为核心的就是 fib_table 路由表,存储了真正的路由转发信息
文件:/ net / ipv4 / route.c
这个方法做的事情很简单,本质上就是想要找到这个路由的下一跳是哪里?
在这里面有一个核心的结构体名为 fib_nh_exception 。这个是指fib表中去往目的地址情况下最理想的下一跳的地址。
而这个结构体在上一个方法通过 find_exception 获得.遍历从 fib_result 获取到 fib_nh 结构体中的 nh_exceptions 链表。从这链表中找到一模一样的目的地址并返回得到的。
文件:/ net / ipv4 / tcp_output.c
Ⅱ JDK1.8中的HashMap底层数据结构的图解
hashmap是java开发最常用的一种数据模型,hashmap属于map接口的一种实现。以key-value的这种形式存储数据,其中key是不允许重复的但是允许为空,value是可以重复或为空的。其中key只能使用基本数据类型(int,double...)的封装类(interger,double。。。。)。
JDK1.7中HashMap的底层是由数组+单向链表这两种数据结构组合而成的,而在JDK1.8中HashMap是由数组+单向链表+红黑树三种数据结构组合而成的。
初始化有固定的大小长度,有顺序的下标(下标从0开始),图1只是示例。
由每个节点组成,每个节点包含一个data(存储数据)和指向下一个节点地址的next,如图二。
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
介绍完三种基本结构后,我们再来看看hashmap的数据结构图,如图4。
图5是我截自JDK1.8的HashMap底层源码。
Ⅲ Hermes源码分析(二)——解析字节码
前面一节 讲到字节码序列化为二进制是有固定的格式的,这里我们分析一下源码里面是怎么处理的
这里可以看到首先写入的是魔数,他的值为
对应的二进制见下图,注意是小端字节序
第二项是字节码的版本,笔者的版本是74,也即 上图中的4a00 0000
第三项是源码的hash,这里采用的是SHA1算法,生成的哈希值是160位,因此占用了20个字节
第四项是文件长度,这个字段是32位的,也就是下图中的为0aa030,转换成十进制就是696368,实际文件大小也是这么多
后面的字段类似,就不一一分析了,头部所有字段的类型都可以在 BytecodeFileHeader.h 中看到,Hermes按照既定的内存布局把字段写入后再序列化,就得到了我们看到的字节码文件。
这里写入的数据很多,以函数头的写入为例,我们调用了visitFunctionHeader方法,并通过byteCodeMole拿到函数的签名,将其写入函数表(存疑,在实际的文件中并没有看到这一部分)。注意这些数据必须按顺序写入,因为读出的时候也是按对应顺序来的。
我们知道react-native 在加载字节码的时候需要调用hermes的prepareJavaScript方法, 那这个方法做了些什么事呢?
这里做了两件事情:
1. 判断是否是字节码,如果是则调用createBCProviderFromBuffer,否则调用createBCProviderFromSrc,我们这里只关注createBCProviderFromBuffer
2.通过BCProviderFromBuffer的构造方法得到文件头和函数头的信息(populateFromBuffer方法),下面是这个方法的实现。
BytecodeFileFields的populateFromBuffer方法也是一个模版方法,注意这里调用populateFromBuffer方法的是一个 ConstBytecodeFileFields对象,他代表的是不可变的字节码字段。
细心的读者会发现这里也有visitFunctionHeaders方法, 这里主要为了复用visitBytecodeSegmentsInOrder的逻辑,把populator当作一个visitor来按顺序读取buffer的内容,并提前加载到BytecodeFileFields里面,以减少后面执行字节码时解析的时间。
Hermes引擎在读取了字节码之后会通过解析BytecodeFileHeader这个结构体中的字段来获取一些关键信息,例如bundle是否是字节码格式,是否包含了函数,字节码的版本是否匹配等。注意这里我们只是解析了头部,没有解析整个字节码,后面执行字节码时才会解析剩余的部分。
evaluatePreparedJavaScript这个方法,主要是调用了HermesRuntime的 runBytecode方法,这里hermesPrep时上一步解析头部时获取的BCProviderFromBuffer实例。
runBytecode这个方法比较长,主要做了几件事情:
这里说明一下,Domain是用于垃圾回收的运行时模块的代理, Domain被创建时是空的,并跟随着运行时模块进行传播, 在运行时模块的整个生命周期内都一直存在。在某个Domain下创建的所有函数都会保持着对这个Domain的强引用。当Domain被回收的时候,这个Domain下的所有函数都不能使用。
未完待续。。。
Ⅳ ViewPager系列文章(一)- ViewPager源码分析及加载页面原理图
1>:点击 viewPager.setAdapter进入下边源码,会调用 populate() 方法,这个方法作用是创建和销毁子条目(子item):
在populate()方法中:
创建ItemView:mAdapter.instantiateItem(this, position);
销毁ItemView:mAdapter.destroyItem(this, pos, ii.object);
所以由ViewPager的源码可以看出,ViewPager里边无论放多少个页面都不会内存溢出,它会不断的去创建和销毁view;
和 ListView、RecyclerView不一样,ListView、RecyclerView是会不断的复用view,而viewpager是不断的创建和销毁view
轮播图刚打开默认显示当前页,是第一页,默认会缓存左右两个页面,如果左边没有,只有右边有,那么右边是第0页,当前页是第一页;
如果你滑动到第1页,ViewPager会默认把 左边第0页 和 右边第2页 创建出来;
如果你滑动到第2页,ViewPager会默认把第1页和第3页创建出来,而原来的第0页就会变成需要销毁的页面;
如果想要缓存多页,可以调用setOffscreenPageLimit()方法:
setOffscreenPageLimit(1):ViewPager机制默认就是缓存1,表示左边、右边各缓存1页,加上自己,总共是3页,其余页面全部销毁;
setOffscreenPageLimit(2):表示默认给左右各缓存2页,共4页,加上自己,总共缓存5页,其余页面全部销毁;
setOffscreenPageLimit(3):表示默认给左右各缓存3页,共6页,加上自己,总共缓存7页,其余页面全部销毁;
因为 smoothScrollTo()滑动方法也调用populate(),而populate()方法维护了当前显示页面和 左右缓存的页面,就能做到无限滑动而不出问题;
A:从populate()源码中可知:先判断页面是否在缓存范围内:如果在,则addNewItem添加进来,否则在destroyItem掉;
B:ViewPager会缓存左右两边页面+1(当前显示页面),默认认为当前页面的 左右两边各有1个,用户可以手动调用setOffscreenPageLimit()方法设置数量,如果传的值小于1,就默认设置为1;
ViewPager实际示意图如下:
Ⅳ flutter框架(源码分析)
准备写这一系列的文章自己是下了很大的决心的,自知会遇到很大的困难。因能力有限,如有疏漏之处,还请大家斧正。
相信大家都看过很多遍下面那张图,这个系列的文章就是要分析红框内的源码:
我们看源码的结构其实是这样的:
总共十二个模块,对应的模块我一旦写完就会在下面更新链接:
Ⅵ Category实现的原理一:底层结构及源码分析
❓比如说有如下3个类,思考一下如下图所示的实例方法存放在哪里?
下面我们就来验证一下上面的结论.
我们的分类 HHPerson+eat 被转换为类型为 static struct _category_t ,变量名为: _OBJC_$_CATEGORY_HHPerson_$_eat :
实例方法列表:
协议列表: (HHPerson+eat实现了 NSCopying,NSCoding 协议)
属性列表:
以上就是分类的底层结构,可以看到,分类的信息在编译期间都被分离出来了,下面我们从 runtime 源码研究一下分类.
我们梳理一下 attachCategories (cls,cats,true) 方法. attach 一词是 附加 的意思,从名字上我们可以看出这个方法大概意思是: 附加分类 .事实上它的确如此,下面我开始研究:
本篇主要讲了 category 的底层数据结构,和分析 runtime 如何处理 category 分类信息的.在下篇文章中-- Category实现的原理二:分类信息如何添加到本类中 将介绍 runtime 如何将分类信息添加到本类中.
Ⅶ YTKNetWork源码解析
YTKNetWork是一个开源的第三方网络请求框架,具有比较好的网络请求缓存机制的控制。近期项目中想要采取HTTP Cache来优化网络请求,需要实现以下两点:
经过简单的调研发现,YTKNetWork虽然底层是使用的AFNetWorking的框架,但是使用AFNetWorking能够通过设置缓存空间和缓存协议就能快速简单实现的方式在YTKNetWork中并没有生效。YTKNetWork已经很少维护了,所以,只能自己动手来分析YTKNetWork的实现。
废话不多说,下面就开始吧。
先上两张图
首先,我们从目录中找到YTKNetWork.h的头文件,从中可以看到作者想对我们开放的基础功能模块,根据字面意思可以分为几种,一种是Request类型的,一种是Config,还一种是Agent。很容易理解,request类型的是用来发送网络请求的,config是用来配置请求信息的,agent暂时不清楚,用到时我们再来具体分析。那么项目结构就很清晰了,我们就一步一步来分析就好了。
我们从这个最简单的单体类来入手。
m文件中没什么可以分析的,就是对两个过滤池数组的增删操作,唯一注意的就是AFSecurityPolicy初始化的时候使用了defaultPolicy类方法实例化
总结下来,YTKNetworkConfig这个类的作用就是设置base URL、安全策略、url的过滤、缓存路径的过滤。
但是现在对于两个协议的具体使用方式还有疑问,我们带着这个疑问继续往下看。
Ⅷ iOS-底层探索03:isa底层结构分析
在上一篇的文章 iOS alloc 流程分析 中我们分析了 alloc ,知道了 alloc 创建了对象并且分配内存,同时初始化 isa 属性。我们也知道了 Objective-C 对象在底层本质上是结构体 ,所有的对象里面都会包含有一个 isa ,这篇文章我们来分析探究 isa底层是如何实现的 。
其中的 nonpointer 字面的意思是没有指针的,一般情况下nonpointer是为true的,只有在例如实现了 allocwithzone 方法,retain,release等的时候会是false。如果为false是直接将传进来的cls为isa的关联的cls赋值。
其他的剩下的部分就是对isa的初始化赋值了。但是具体的isa内部是怎样的还是不知道的,从源码中 isa_t 点击进去可以查看。
通过源码可以知道 isa 的 isa_t 类型的内部结构
从中可以知道, isa 是一个 联合体 union ,里面有关联的类cls和long型的bits。
联合体(union)、位域不清楚的可以参考 这篇文章
由上面的概念可以知道, cls 和 bits 之间是互斥的,即有cls就没有bits,有bits就没有cls。这就很好地解释了为什么上面的源码在初始化isa的时候会用nonpointer来区分开。所以isa的大小占8个字节,64位。其中这64位中分别存储了什么呢?通过ISA_BITFIELD位域源码:
这两种是分别 arm64 和 x86 系统架构下isa的内部结构,但是都是64位的。iPhone 采用 arm64 架构,MacOS 采用 x86 架构,本文介绍 x86 架构下 isa的联合体结构 。
通过 objc4-781 苹果官方的源码,使用object_getClass这个方法获取到类。
通过源码找到object_getClass的方法
从源码中可以知道返回的 isa 最终是 (Class)(isa.bits & ISA_MASK)
其中源码有一个判断 isTaggedPointer() ,其中苹果对于 Tagged Pointer 的概念引入,是为了节省内存和提高执行效率,对于 64 位程序,相关逻辑能减少一半的内存占用,以及 3 倍的访问速度提升,100 倍的创建、销毁速度提升。如果想了解这部分的内容可以看看 深入理解 Tagged Pointer
下面就是用 lldb 的指令来验证一下的。通过 x/4gx objc 打印出对象的内存地址
由源码知道类Class的最终返回是 (Class)(isa.bits & ISA_MASK) ,所以将 x/4gx objc 打印出来的 0x001d800100002119 & ISA_MASK 的值得到如下:
由前面的文章知道由于内存的优化对象的其他属性的位置实际会发生变化的,所以对象的第一个属性就是 isa
通过测试我们发现 对象的内存地址 和通过 isa取出来的内存地址 是一样的,所以 isa 是关联着对象与类的
通过上面的介绍,可以知道了isa是关联着对象与类的,并且对象的isa指向类,因为万物皆对象,那么类的isa指向的是谁呢?可以通过苹果官方的isa的走位流程图:
其中虚线是代表 isa 的走位,实线代表的是 继承关系 的走位。
通过以上的源码分析,我们认识到对象的 isa指针 指向了对象所属的类。而类本身也有一个 isa指针 ,它指向的又是什么呢?
此时要引入 meta class (即元类)的概念了。我们先了解一下元类的信息:
验证过程见参考文章
具体思路是, shiftcls 在 x86_64 架构下长度是44位,存储在 isa 的 [3, 46]位上,所以可以通过将isa的 [0, 2]位、[47, 63]位清零,同样能得到 shiftcls 的值,进而确定类。(先右移3位、再左移20位、然后右移17位即可)
如果 Person 类继承的是 NSProxy ,相关 isa 指向是怎样的呢?
答案:跟 NSObject 一样,两者都是 根类 。
OC源码分析之isa
iOS的OC的isa的底层原理
Ⅸ Doris 源码分析 (二) 代码结构分析
注 正如上图所示, FE 端主要是 PaloFe 开启的服务入口, 后端为 doris_main 开启的服务入口,前后端交互主要靠 thrift rpc 进行调用。 FE 中主从选举及元数据操作日志同步等均依托 bdbje 主从方案来实现。
元数据变更日志主要靠 bdbje 的主从复制来完成如下图:
注 元数据的数据流具体过程如上图所示,步骤如下:
Ⅹ coredns源码分析
CoreDNS是使用go语言编写的快速灵活的DNS服务,采用链式插件模式,每个插件实现独立的功能,底层协议可以是tcp/udp,也可以是TLS,gRPC等。默认监听所有ip地址,可使用bind插件指定监听指定地址。
格式如下
SCHEME是可选的,默认值为dns://,也可以指定为tls://,grpc://或者https://。
ZONE是可选的,指定了此dnsserver可以服务的域名前缀,如果不指定,则默认为root,表示可以接收所有的dns请求。
PORT是选项的,指定了监听端口号,默认为53,如果这里指定了端口号,则不能通过参数-dns.port覆盖。
一块上面格式的配置表示一个dnsserver,称为serverblock,可以配置多个serverblock表示多个dnsserver。
下面通过一个例子说明,如下配置文件指定了4个serverblock,即4个dnsserver,第一个监听端口5300,后面三个监听同一个端口53,每个dnsserver指定了特定的插件。
下图为配置的简略图
a. 从图中可看到插件执行顺序不是配置文件中的顺序,这是因为插件执行顺序是在源码目录中的plugin.cfg指定的,一旦编译后,顺序就固定了。
b. .根serverblock虽然指定了health,但是图中却没有,这是因为health插件不参与dns请求的处理。能处理dns请求的插件必须提供如下两个接口函数。
dns请求处理流程
收到dns请求后,首先根据域名匹配zone找到对应的dnsserver(最长匹配优先),如果没有匹配到,则使用默认的root dnsserver。
找到dnsserver后,就要按照插件顺序执行其中配置的插件,当然并不是配置的插件都会被执行,如果某个插件成功找到记录,则返回成功,否则根据插件是否配置了fallthrough等来决定是否执行下一个插件。
plugin.cfg
源码目录下的plugin.cfg指定了插件执行顺序,如果想添加插件,可按格式添加到指定位置。
源码目录下的Makefile根据plugin.cfg生成了两个go文件:zplugin.go和zdirectives.go。
core/dnsserver/zdirectives.go将所有插件名字放在一个数组中。
codedns 主函数
codedns.go 首先导入了包"github.com/coredns/coredns/core/plugin",此包内只有一个文件zplugin.go,此文件为自动生成的,主要导入了所有的插件,执行每个插件的init函数。
接着执行 run.go Run
此文件又引入了包"github.com/coredns/coredns/core/dnsserver",其init函数在 dnsserver/register.go 文件中,如下所示,主要是注册了serverType
剩下的就是解析参数,解析配置文件后,执行caddy.Start。
这里就是根据配置文件中指定的serverblock,执行插件的setup进行初始化,创建对应的server,开始监听dns请求
tcp协议调用Serve,udp协议调用ServePacket
收到DNS请求后,调用ServeDNS,根据域名匹配dnsserver,如果没有匹配不到则使用根dnsserver,然后执行dnsserver中配置的插件
以k8s插件为例
参考
//如何写coredns插件
http://dockone.io/article/9620
//coredns源码分析
https://wenku..com/view/.html
https://blog.csdn.net/zhonglinzhang/article/details/99679323
https://www.codercto.com/a/89703.html
//NodeLocal DNSCache
https://www.cnblogs.com/sanzxcvbnm/p/16013560.html
https://blog.csdn.net/xixihahalelehehe/article/details/118894971