0%

一、重试重定向拦截器-重试限制

首先判断是否取消了,取消了直接抛出异常

之后在 try-catch 代码块里面执行责任链获取 response,并且捕捉 RouteException (路线异常,比如 socket 连接失败)和 IOException (IO 异常),在这 2 种 异常中判断是否需要重试,调用的是 recover 函数:

1
2
3
4
5
6
//检查是否需要重试
if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {
throw e.firstConnectException.withSuppressed(recoveredFailures)
} else {
//。。。。
}

可以看出,检查是否需要重试,如果不需要,直接抛出异常就终止了,否则才继续往后面走重试。那么是否需要重试的条件是什么呢? 有以下几点:

  • OkHttpClient 可以配置是否允许重试,设置为 false 直接不能重试

  • request body 不允许复用,那就不能重试

  • 产生了 FileNotFoundException 异常,也不用重试了

  • 产生了不允许重试的异常(协议异常-如返回204代表无响应体,但是Content-Lenth 不为0,二者产生冲突,就是协议异常了、证书异常、除连接超时之外的 IO 中断异常)

  • 最后一关,是否有拥有更多路线(设置了代理,DNS 返回了多个 ip -应该重试其他ip),即使上面的判断都通过,只要没有更多路线,都不能重试

重定向限制次数,20 次

二、重试重定向拦截器-重定向规则

次数超出 20 次,也抛出异常。

代码中是通过 followUpRequest 方法来确定重定向的,同过 response 的响应码来判断响应码,这个重定向看起来不只是重定向功能,如果只是重定向,可能只需要判断 3xx 这个码就可以了,实际上,这里面判断多个码,包括:

  • 3xx:重定向

  • 401:服务器需要授权,比如某些接口需要登录

  • 407:代理需要授权,比如付费代理,验证身份

  • 408:请求超时

  • 421:当前客户端所在的ip到服务器的连接数超了

  • 503:服务不可用

个人觉得准备面试的话就记住 3xx、需要授权、连接数超了这几个基本上就可以了

代理有 2 种,HTTP 代理和 Socket 代理,Http代理代理的是 Http,Socket 代理它代理的是 TCP/IP

三、桥接拦截器

桥接拦截器主要功能就是在请求前和请求后做一些通用繁琐处理:

  • 请求前补全请求头(设置cookie、设置UA、设置Host字段、content-Length 、Content-Type 、Gzip 压缩等)

  • 得到响应后:接收 Cookies 并回调给用户,在下次请求的时候读取相应 cookies 数据设置到请求头;如果有设置 gzip ,则解析 gzip 数据

四、缓存拦截器

关于缓存的请求头和响应头非常多,所以这里很复杂,Http 的缓存我们可以按照行为分为: 强缓存协商缓存

  • 强缓存: 有缓存的时候,直接将缓存给用户,不会发送请求给服务端,可以通过 Cache-Control 和 Expires 来判断缓存过期时间。

  • 协商缓存:请求还是会发给服务端,但是服务端可能会返回 304 (此时没有请求体的),意味着服务端在这段时间没有修改,使用本地的缓存就好。

如果 networkReqeust 存在,则优先发起网络请求,否则使用 cacheResponse 缓存,若都不存在则请求失败。

五、连接拦截器-新建连接

代码很少,总共就 4 行代码

在这里来判断是 http1 还是 http2。找到连接之后,还需要连接是否健康(未关闭,正常工作),

普通代理:可以想象 Fiddler,它是可以更改你的数据再发送出去的

隧道代理:是无法更改客户端的请求的,将客户端的数据无脑地发送给服务端

Okhttp 支持你创建代理,可以选择是 Http 代理还是 Sockets 代理。老师有写这些代理使用的方式,有现成代码,这里就不贴出来了。

ALPN 是 TLS 的扩展协议,从 与 服务端的 hello 里面可以协商到使用哪一种协议。会存储在 sslSocket 中

六、连接拦截器-连接池

首先从连接池中获取连接,如果没有,才去做上述的新建连接。

每次put 新的连接到池子里面的时候,都会扔进去一个定时执行的 Task,用于执行 clean 任务,清除不可用的连接,无效的连接。不移走会占用内存。比如,你刚创建的连接,那么5分钟后肯定会过期,如果没有使用的话

连接池最多允许 5 个空闲连接;连接池中闲置的连接最多允许闲置 5 分钟。比如,某个连接 baidu.com 只使用了一次,这样就能将其清理掉。清理的时候,将闲置时长最长的清理掉。

连接执行流程

七、请求服务拦截器与面试总结

发送很简单,这里就不写了,有个点需要注意下,如果是发送个比较大的文件,是需要与服务端协商的,Okhttp 是这样做的:

  1. 如果服务器允许则返回 100 ,则客户端继续发送请求体

  2. 如果服务器不允许就直接返回给用户

  3. 如果服务器忽略这个问询的请求头,一直无法读取应答,此时会抛出超时异常

7.1 面试题

Okhttp 的请求过程

构建 Request ,通过 OkhttpClient 获得 Call (RealCall对象)如果:

  • 是同步请求,则在发起请求的线程中直接获取结果(调用5个Interceptor)

  • 如果是异步请求,则通过 Dispatcher 分发到线程池中进行请求获取结果(也是调用5个Interceptor)

拦截器如何工作的?

责任链模式将请求者与执行者解耦,请求者只需将请求发送给责任链即可

应用拦截器与网络拦截器的区别?

  • 他们在 interceptors 中的顺序不一样

  • 网络拦截器不一定执行,因为有可能直接拿了缓存,不需要后续的执行了

Okhttp 如何复用连接?

host 验证规则一样、dns 一样等等,这些属性都一样。当然,也会清理垃圾连接,超过 5 分钟没有使用的连接,超过5个闲置连接后,清理闲置最久的连接

一、Okhttp介绍

okhttp 4 的源码(4.9.3版本,它的包名还是 okhttp3,还是要注意下)

Retrofit 只是封装 Okhttp ,让其更容易使用

http 2.0 的特性:头部压缩、server push、多路复用

关于多路复用,其实Http 1.1 也有 keep-alive 这样的标记位来重复使用这条连接,但是这种保持连接只能第一个消息发送完收到 ack 之后,才能发送第二条数据,这种就是串行的;而 http 2.0 可以在第一条还未返回结果的时候就发送第二条,这种才是真正的多路复用。

因为 http1.1 用的是文本,必须要按照顺序, hello world,必须要按顺序,但是 http2.0 可以先传 r 再传h再传 d 等,它是基于 fragment 的,fragment 有顺序标识。

okhttp 支持 http2.0 可以复用连接,避免每次去连接都要三次握手之类的。Http2 还有个连接池,池子里面有各种连接,比如baidu.com 的连接、taobao.com 的连接,下次需要请求相关域名的时候,直接使用这个池子里面的连接即可。

okhttp 的 post 请求的缓存默认是关闭的,只有 get 请求的缓存开启。如果post 请求想开启缓存,只需要做如下 cache 配置就可以了:

1
OkhttpClient.Builder().cache(Cache(File("/xxx"), 1024))

二、Okhttp 的基本使用与请求流程

Okhttp 的大体流程如下:

  1. 创建一个 OkhttpClient(不管是自己Builder 还是直接new 都可以)

  2. 创建一个 Request ,封装请求数据

  3. Request 交给 OkhttpClient 的 newCall 操作得到一个RealCall

  4. 之后,不管是通过 execute 还是 enqueue 方法,反正最终是将这个call 交给 Dispatcher 去调度

  5. 被调度的时候,经过各种 interceptor 拦截器拦截后,最终可以得到 Response ,流程如下图所示:

Okhttp的执行流程

三、分发器异步分发限制

Dispatchers 里面维护了 3 个队列:

  • 准备执行的异步请求队列

  • 正在执行的异步请求队列

  • 正在执行的同步请求队列

AsyncCall 在 enqueue 的时候,会判断队列里面(不管是准备执行的还是正在执行的)有没有 call 和目前这个 AsyncCall 的host 是一样的,如果是一样的,那就将自己的 callsPerHost 的属性替换成队列中 Call 的 callsPerHost 属性(我个人理解的是,这样方便计数,所有的 Call 都用这一个AtomicInteger 变量来统计数量,这样,后续判断同一个 Host 最多只能5个来同时请求就比较好判断了,只需要取这个值)

所以, callsPerHost 表示的是同一个host 有多少个请求在执行

异步总连接不能超过64个,然后 同一个 host 的请求不能超过5个。

四、分发器异步请求分发流程

如果所有异步请求数超过 64 个 (或者同一个 host 的请求数超过 5个)的情况下,那么 call 将不会被执行,那么他们在什么时候会被触发去执行呢?其实,在 RealCall 里面的 run 方法里面,会在 try-catch 块去获取请求的 response :

1
2
//获取response 结果
response = getResponseWithInterceptorChain()

在最终的 finally 中,会执行 client.dispatcher.finished(this) ,告知 Dispatcher 当前 Call 已经结束。之后在 Dispatcher 中会再次激活这个等待队列:

1
2
3
4
internal fun finished(call: AsyncCall) {
call.callsPerHost.decrementAndGet()
finished(runningAsyncCalls, call)
}

可以看出,首先将 callsPerHost 减1 ,意味着这个 host 在执行的请求数减少一个。在里面还会执行重载的 finished 方法,再次去执行 promoteAndExecute 方法去从队列中获取 Call 去分发执行。所以,整个异步任务执行的工作流程如下所示:

异步请求执行的流程

线程池

Okhttp 异步连接的线程池初始化很有意思:

1
2
executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false)

它指定 corePollSize 为 0,Keep Alive 的时长是 60s ,无界队列,这其实和我们使用 Executors.newCachedThreadPool 是一样的:

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

这种线程池有什么特点呢?

  • 核心线程数为 0 :线程池有个例外的情况,哪怕核心线程数设置为 0 ,在第一个任务的时候,还是会开启线程让执行。

  • 最大线程数设置为 Integer.MAX_VALUE ,几乎不限制任务数量

  • 使用 SynchronousQueue 队列提交任务。提交肯定失败!因为它不存储元素的。那既然提交任务肯定失败,那我们为什么还用这个呢?我们考虑下线程池,如果队列有容量的话,在线程数大于等于corePoolSize 的时候,新的任务会被添加到等待队列,只有在往 队列中添加失败的时候,才会去判断如果小于 maximumPoolSize 的时候,新建线程任务。所以,这种线程池可以让任务马上执行,而不是在那里等待。

所以上述线程池,如果设置corePoolSize 为 0 ,再设置个 LinkedBlockingQueue 的话,第一个任务会执行,第二个任务会放在 LinkedBlockingQueue 中。1 完成后,执行2,2执行后才能3等等。

五、分发器处理同步请求

只需要注意,同步请求执行完成后(finnaly 中也是调用 finish),也会去执行 promoteAndExecute 方法,去重新从等待队列中获取 Call 执行任务。有个细节注意,**同步任务执行完成后,会触发异步任务队列的重新获取!

六、Okhttp拦截器责任链设计模式

Okhttp 里面会有5个默认的拦截器,并且我们也能通过addInterceptor 和 addNetworkInterceptor 方法添加拦截器,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
internal fun getResponseWithInterceptorChain(): Response {
// Build a full stack of interceptors.
val interceptors = mutableListOf<Interceptor>()
//添加我们通过 addInterceptor 方法添加的拦截器
interceptors += client.interceptors
interceptors += RetryAndFollowUpInterceptor(client)
interceptors += BridgeInterceptor(client.cookieJar)
interceptors += CacheInterceptor(client.cache)
interceptors += ConnectInterceptor
if (!forWebSocket) {
//添加我们通过 addNetworkInterceptor 添加的拦截器
interceptors += client.networkInterceptors
}
interceptors += CallServerInterceptor(forWebSocket)
//省略无关代码。。。。。
}

从代码可以看出,通过 addInterceptor 添加的拦截器会首先被 add 进 interceptors 列表中,之后依次添加充实、桥、缓存、连接 拦截器,然后如果不是 websocket 的话,就添加通过 addNetworkInterceptor 添加的拦截器,最后添加的是 CallServerInterceptor 拦截器。

所以,addInterceptor 和 addNetworkInterceptor 方法添加拦截器的区别是什么?在代码中的体现是在 interceptors 数组中的位置不一样,一个是在最开始,一个是在倒是第二个。

但是这个顺序到底会有什么影响?Okhttp 使用责任链设计模式,在发起请求的时候,是从上往下经过这些 Interceptor ,这样就先经过我们 addInterceptor 添加的拦截器,后续才经过 addNetworkInterceptor 添加的拦截器。但是结果返回的时候,是从后面往前面返回的。

老师在课程里面讲了下如果自己去实现责任链的 Demo ,这个可以模拟下。

七、Okhttp 拦截器功能概述

首先总体概述一下各种拦截器的含义:

  • 重试定向拦截器。在交给下一个拦截器之前,判断用户是否取消了请求,如果已经取消了,就不需要将结果交回给用户了。并且,在返回结果之后,根据响应码判断是否需要重定向,如果满足条件就会重启执行所有的拦截器。

  • 桥接拦截器。在交出之前,负责将 HTTP 协议必备的请求头加入(比如host 等),并添加一些默认行为(比如 GZIP 压缩),在获得结果后,调用保存cookie 接口并解析GZIP 数据

  • 缓存拦截器。顾名思义就是交出之前读取并判断是否使用缓存;获得结果后判断是否需要缓存

  • 连接拦截器。从连接池中寻找一个连接,如果没有则创建一个连接,并获得对应的 Socket 流;在获得结果后不进行额外处理

  • 请求服务拦截器。进行真正的服务器通信,向服务器发送数据,解析读取响应数据。

刨根究底 addInterceptor 和 addNetworkInterceptor

当我们只是需要在请求参数里面添加字段,比如 sign 字段,或者在 response 里面修改一点数据,那么我们使用 addInterceptor 和 addNetworkInterceptor 都是可以的。

但是如果我们打印日志,使用了 httpLogingInterceptor 这个拦截器,打印请求数据和返回数据,如果你用 addInterceptor 添加拦截器,那么打印出来的是用户写的请求参数,如果使用的是 addNetworkInterceptor ,那么这个请求参数就还包括 Okhttp 自动补全的一些参数

八、Okhttp 相关的面试题

1、Okhttp 的分发器是如何工作的?

对于同步请求,只是记录一下;对于异步任务,首先添加到 ready 队列中,然后检查所有请求数小于64,以及同 host 请求数小于 5 是否满足,满足执行并添加到 running 队列中。

应用拦截器和网络拦截器有什么区别?

在 interceptors 中的顺序不同。导致接收到的数据完整性是不一样的。

省略第一节的 NIO ,后续有时间看

NIO之单线程Reactor模式

略,后续有时间看

网络编程面试-TCP、UDP与HTTP

DDoS攻击是啥?SYN 洪水攻击?

利用合理的服务请求,占用服务资源:带宽攻击和联通性攻击。带宽攻击需要控制大量的肉鸡,SYN 洪泛攻击属于 连通性攻击,发送半连接。

哪些应用适用 UDP 实现?

DNS,直播,多播,可以上层应用自己实现可靠性传输(新的 UDT 协议可以了解一下,Http3 用 UDT)比如广域网中计算中心光纤连接通信

如果应用场景中大多数是简短信息,那么适合用udp实现。因为 udp 是基于报文段的,直接对上层的应用数据封装成报文段然后丢在网络中,如果信息量太大,在链路层会被分片,影响传输效率

http 和 https 的区别?

http 80端口,https 是443端口

是否需要证书

scheme 不同,http 和 https

如果让你来设计QQ,网络协议上如何设计?

  • 登录采用 TCP 协议和 Http 协议:client采用 TCP 向 server 发送信息,Http 协议下载信息。登录后,会有一个 TCP 连接来保持在线状态

  • 好友之间发送消息,主要采用 UDP 协议:利用上层应用层来保证可靠传输,如果发送失败,提示用户发送失败,并可以重新发送。

  • 内网传文件采用 P2P 技术。内网里面可以采用 P2P 技术,不需要服务器中转

TCP粘包的解决

包可能很小,每次都要发送出去得到确认。可以把这些小包合在一起,发送出去,确认一次就行。解决:

  1. 消息头+消息体,消息头用来描述消息的长度,每次读消息先读消息头

  2. 消息定长,每个消息都是一样长

  3. 特殊的分隔符,比如 Netty使用回车换行符来区分

这个比较抽象,老师在视频里面讲了个例子,比如从客户端用 for 语句循环 100 次发送 “james,hello,hahah” 等这些字符串,但是服务端可能说只收到 2 次,因为数据可能粘包,很多数据合在一起发送了,这时候我们没法区分这些次数和数据。如果用 natty 来做的话,会在每次发送后插入 回车换行符,这样,在接收的时候就能根据回车换行符分割。

序列化

阿里的 FastJson 不建议使用,安全问题比较多

ProtocolBuffer 可读性比较差

老师自己平时用的是 Kyro ,说是只能在 Java 语言中使用

select、poll、epoll 的区别

是不是每次都选择 epoll 才是最好的呢?不是,在连接少,用户都活跃的情况,select 和 poll 可能效率更高

直接内存、零拷贝

直接内存比堆内存快在哪里?

任何语言,你通过socket 发送数据,都需要有一个 缓冲区,先把数据写入缓冲区,之后从缓冲区读取数据到内核发送出去。

如果可以选择,先选直接内存缓冲区,因为直接内存的缓冲区比堆内存的缓冲区肯定是要快的。

那么,为什么会快呢?如果你选择堆上的 Buffer ,那么在发送的时候,还会在直接内存上创建一个 Buffer ,这样一来,首先需要将数据拷贝到堆上的buffer,之后拷贝到 直接内存上的 Buffer ,最后才发送出去;如果是Buffer 直接创建在直接内存上,就没有这个步骤,减少一次拷贝,示意图如下:

直接buffer和堆上buffer

为什么使用堆 Buffer 的时候需要使用到直接内存的 Buffer 呢?因为堆上有GC机制,在 Socket 过程中,数据是不能变化的,如果在中途堆上的 Buffer 被GC 了,那咋办?所以只能再开一个直接内存的 Buffer ,这样 GC 管不着。

在新生代的 GC 是不会去管直接内存的 Buffer 的,只有 Full GC 的时候,才会顺便去回收直接内存的。

所以堆外内存(DirectBuffer)的优劣势在哪?

  • 减少GC 带来的 STW ,增强性能

  • 加快复制速度,如果采用堆内 Buffer 的话,还是需要 DirectBuffer 这个环节,使用 DirectBuffer 可以省略这个步骤

劣势:

  • 产生内存泄漏很难排查

  • 不适合存储很复杂的对象

有哪几种零拷贝方案?

linux 中有好几种:

  • mmap

  • linux 的 sendFile 函数,如果有硬件支持的话,只需要告诉缓存的开始位置和大小,都不用真正将数据复制到 socket 的缓冲区,就能将数据发送出去

  • linux 提供的 splice 方案,不需要硬件支持,使用管道(piple line) 就能将数据直接拷贝到 socket 的缓冲区

不过体现在 JDK 里面就 2 种,一种就是 mmap 方案,还有一种就是NIO 提供的 FIleChannel ,它可以转换为 SocketChannel ,直接将文件通过 socket 发送,这样可以利用 linux 的 mmap 方案做到零拷贝。

问题解答

时间局部性

在一个方法里面使用到了成员变量,在方法里面可能会用局部变量承接这个成员变量,这样就能加快速度

一、网络通信基本常识

wireshark 有一本书

操作系统将 TCP 那些细节全部(拆包之类的,传输层以下的所有都封装)封装在 Socket 中了。开发人员只需要关注ip 和端口号就行了。

web服务,比较适合 短连接,这种可以降低服务器的压力。长连接适合连接数不多,并且请求比较频繁的请求。

二、BIO 详解

BIO 是啥?阻塞式 IO,B 是阻塞 Blocking 的意思,比如 read 方法读取网络数据,如果没有数据,阻塞式网络编程就会阻塞住。

NIO : IO 多路复用

相对的,AIO 是异步 IO,read 和 write 操作都作为系统调用

ServerSocket.accept() ,socket.connect 、InputStream.readUTF 等方法都是阻塞方法。

三、NIO详解

NIO ,这个 N 可以作为 New 解释,或者做 no-blocking 解释。

BIO 面向流,阻塞式的

NIO (IO多路复用)面向缓冲区,非阻塞的,一个线程可以服务多个服务端。

BIO 的效率比 NIO 的效率是要高的。

NIO 三大组件:

  • Selector

  • Channel

  • Buffer

NIO 看了30分钟,后续的就没看了,看得有点蒙,后续再继续

一、TCP 四次挥手

OSI 虽然很学术,但是 要熟悉,一般工程使用 IEEE 的 TCP/IP 的四层模型。

4G 和 5G 这种事在数据链路层及以下的协议

两端都可能发起分手,分手4个流程如下:

  1. 客户端发送 FIN=1,seq = x ,随后进入 FIN_WAIT_1

  2. 服务端收到之后进入 CLOSE_WAIT 状态,随后发送确认 ACK = 1,ack = x + 1,客户端收到之后变为 FINE_WAIT_2 状态

  3. 接着服务端也发送 FIN = 1, seq = y ,随后进入 CLOSE 状态

  4. 客户端收到之后,变为 TIME_WAITING 状态,随后发送确认 ACK = 1,seq = y +1 ,服务端收到之后变为 CLOSED 状态

  5. 最后,客户端在 TIME_WAITING 状态持续了 2* MSL 时长之后,才最终变为 CLOSED 状态

MSL 即 最长报文段寿命,即一个报文在网络上能存活的最长时间,协议规定是 2分钟,一般定义成 30s ,所以 是等待 1~4分钟。

面试官:为什么要 4 次?

因为是TCP 是全双工的,必须要确认双方都没有数据要发送了,才能结束。假如,客户端发送了 FIN 的时候,服务端没有数据了,可以将 ACK 和 FIN 同时发送,所以,这就变成了 3 次了。

为什么 TIME_WAITING ?

  1. 有可能最后一步客户端发送的 ACK 丢失了,这时候服务端以为客户端没收到,可能会重发 FIN 过来,所以必须要等待

  2. A 应用程序用了 123 端口,它关闭了,之后,B应用用了 123 端口,此时 A 应用的服务端还发送了数据,由于是马上重用了 123 端口,所以这个数据很大可能还没过期,此时 B 应用就没法区分了这个数据了。所以TIME_WAITING 可以让数据在网络上过期,便于区分

二、网络工具

wireshark 目前免费,支持的协议比较多,所以比较推荐使用 wireshark。 wireshark 工具可以查看网络情况,在输入栏里面可以做一些过滤,比如:

!udp //不看udp

ip.addr==101.89.23.12 //只看特定的ip

!udp and ip.addr==101.89.23.12 //不看udp 并且 指定ip

除了 and 还有多种符号,or ,xor(异或) 等等

三、一次完整的 Http 请求的过程

一次完整的http请求过程,比如在浏览器中输入一个网址,最后怎么出来网页展示出来的?总共大概有 7 个步骤:

  1. 首先 DNS 解析域名(浏览器缓存、操作系统缓存或者 DNS 服务器)

  2. 三次握手建立 TCP 连接

  3. 客户端发起 Http 请求

  4. 服务端响应 Http 请求

  5. 客户端解析 Html 代码,并请求 Html 代码中的资源

  6. 客户端渲染展示内容

  7. 关闭 TCP 连接

DNS 劫持和 Http劫持:http 劫持是把 html 的内容给替换了,或者http 的请求给你改了,到其他页面了

防止劫持的手段: 用自己的 DNS 不要用点心公司的,使用 https 。

一、计算机网络体系结构

因为最开始是为了在军事上防止一次打击就全部毁灭,所以TCP/IP 设计就是为了去中心化。

OSI 七层模型:应(用层)、表(示层)、会(话层)、传(输层)、网(络层)、数(据链路层)、物(理层)

TCP/IP 四层模型: (应用层)、(传输层)、(网络层)、(数据链路层)

二、TCP/IP协议族(上)

我们所说的 TCP/IP 其实是个协议族,并不单单说的 tcp 或者 ip 协议。覆盖最上层的 应用层(HTTP、DNS等)一直到 物理层。

严格来讲,3G、4G、5G 是哪一层?物理层

我们平时发送消息给对方整个过程可以用如下的图表示:

网络传输中的数据

在应用层使用 UDP 自己实现连接和重传,可以加速网络速度,因为系统自带的 TCP 是很保守的。

三、TCP/IP协议族(下)

端口号是用于区分不同的应用的。属于传输层的东西。

TCP 可靠性是怎么保证的?超时重传,应答确认。正是因为设计之初为了保证若干物理线路被摧毁还能保证正常运行,所以相对而言还是挺复杂的。

流量控制:每次高速对方我最多能接收多少数据。

DNS 主要使用 UDP ,但是有时候也使用 TCP

四、三次握手建立连接

建立连接的三次握手:

  1. 客户端发起连接,SYN = 1 ,seq = x ,之后进入 SYN_SENT 状态

  2. 服务端收到后,返回 SYN = 1,ACK = 1,ack = x +1, seq = y ,之后进入 SYN_RCVD 状态

  3. 客户端收到后,返回 ACK = 1,ack = y + 1,之后客户端进入 ESTABLISHED 状态;服务端收到之后,也进入 ESTABLISHED 状态

其中,大写的 比如 SYN 、ACK 等都表示标记位,小写的如 ack、seq 等表示值

面试官:为什么是 3 次,不是 2 次或者 4 次?

首先回复三次握手的过程。之后,给出答案:通知对方序列号,并且确保对方已经收到。

SYN 洪泛攻击,虚假ip只发送第一次 syn 连接,由于服务器收到之后就进入到了 SYN_RCVD 状态,并且回复 ack 数据,但是永远得不到回复;所以解决的方案:1)无效连接的监控和释放 2) 防火墙处理,真正连接后才分配资源

抓包的原理: 操作系统允许你越过传输层和网络层,直接获取到 链路层 原始Socket的值

如果想了解更多 WebSocket 的内容《Html5 Websocket 权威指南》

大厂面试,我们主要是 AMS ,这个要理解透。

PKMS 是啥? PackageManagerService。这里要区分 PMS (PowerManagerService)

Launcher查询PKMS 就能得到所有的安装应用。PKMS 的主要职责:

  • 解析 AndroidManifest.xml 文件,解析清单文件中的所有节点信息
  • 扫描 apk 文件,安装系统应用,本地应用等
  • 管理本地应用:包括安装、卸载、应用信息查询

细节点:为什么大家看源码不使用 AS ,主要是因为使用 AS 看源码有很多文件是被阉割过的,比如你想找 PKMS 中 Binder 通信用到的 aidl 文件,是找不到的,必须要下载完整源码(10个多G),AS 上的源码是找不到的。

Android 三大命脉——AMS、BInder、Handler

现在的桌面一般都是 Launcher3 。面试题:桌面上的应用是怎么展示的?

Launcher3 是系统应用,在 packages/app/Launcher3.apk 路径下跨进程通信向 PKMS 请求获得

手机开机很慢,大概什么问题?

开机过程service 启动总共有7部曲:

startBootstrapService中做的事情:

  • 启动 installer 服务
  • 获取设备是否加密
  • 调用 PKMS.main 实例化 PKMS (启动 30s ,将近有20s都在这里)
  • 设备的加密操作

startOtherService中做的事情:

  • dex 优化操作
  • 磁盘优化维护操作
  • PKMS准备就绪

上面总共 7 个步骤,7个步骤中PKMS 的构造函数很耗时,dex 优化很耗时,要着重关注。

ROM 开发代码改动很少的,可能看代码2周,改了1行代码。编译都要1晚上,所以我们一般不能用调试的方法去发现问题,需要认真看日志,会看日志,正确打印日志。

为什么PKMS源码?

ROM定制开发

Launcher 开发

PKMS 扫描所有的系统应用,接着扫描用户应用,将这些安装信息保存在内存中,PKMS 就会保存这些安装的应用。

Android 安装APK 的原理?

  • 第一步:Copy apk
  • 第二步:扫描指定目录的 apk

    所以,每次系统启动,都会扫描所有应用,安装的应用越多,启动越慢

MainActivity 跳转 SecondActivity (在 AndroidManifest.xml 中指定了 Launchmode了),请问,是在哪里解析这个 launchMode 的?

答:在 PKMS 扫描APp 的时候,就已经解析出来了

为什么手机开机后,我能收到开机广播?

因为开机时,PKMS 构造函数中已经扫描了所有 APK清单文件了,(静态广播)已经注册了

新应用安装时间很长,是为什么?

安装完成后需要 App 扫描,还有更耗时的 APK 优化

一、MS启动和重要属性

WMS 的职责一览如下图所示:

WMS职责

形象的比喻就是,各个 ViewRoot 是演员的动作和表情,WMS 就是导演,负责舞台效果,演员站位,SurfaceFlinger 就是摄像机,捕捉当前画面,然后呈现给观众。

这里有个隐藏的彩蛋:Handler 的 runWithScissors 方法,这个方法是系统隐藏的,不让我们正常调用,这里可能引发出一个面试题:如何在子线程通过 Handler 向主线程发送一个任务,并等主线程处理完这个任务后,再继续执行子线程的后续内容?

这时候就可以借助 runWithScissors() 方法。其原理也是 wait 和 notify 来实现

二、Surface创建过程

在 ViewRootImpl 创建时会 new 一个 Surface 。Android 中每个 Activity 都有一个画布(在应用端叫做 Surface ,在SurfaceFlinger 端叫做 Layer)

三、Surface 写入数据

略过,以及后面的《加窗口流程》、《件输入原理》都忽略,听着想睡觉,后续听