整体流程
- 创建一个OKhttpClient,请求客户端类,作为一个全局的实例进行保存,全局网络请求都使用这一个单例对象
- 创建一个Request对象,封装了请求报文信息,包含url,path,请求方法get/post,请求参数,请求头,在Request内部是通过build链式生成了request对象
- 通过调用request调用的newCall方法得到Call对象,在okhttp中call的实现类是RealCall,通过这个call能够进行后续的异步同步网络请求操作
- 决定异步同步的是Dispatcher分发器,内部维护了一个线程池,这个线程池用来执行网络请求,realCall会把请求添加到Dispatcher中,Dispatcher中有三个队列来维护同步异步请求
- 不管同步还是异步,在Okhttp内部都是通过拦截器链interceptors来进行真正的服务器数据获取,会构建一个拦截器链,依次执行每一个拦截器来将服务器数据返回,主要拦截器包括
- RetryAndFollow: 负责失败重试及重定向之后直接请求
- Bridge: 设置内容长度,内容编码,gzip压缩,cookie报头等,主要是请求前的操作
- Cache: 负责缓存管理,当有符合要求的缓存时,直接返回缓存,而不需要经过网络端
- Connect:为当前请求找到合适的连接,注意,这里可能会复用已有的连接
- CallServer:向服务器发起真正的访问请求,接收到服务器返回的数据
- 如图
同步与异步请求
同步请求
- 创建OkHttpClient和Request对象:通过build创建了OkHttpClient对象和Request对象
- 将Request封装成Call对象:通过build构建一个Request对象,通过OkHttpClient和Request对象,构建出Call对象
- 调用Call的execute()发送同步请求
注意:发送同步请求后,就会进入阻塞状态,直到收到响应。
异步请求
- 创建OkHttpClient和Request对象
- 将Request封装成Call对象
- 调用Call的enqueue发送同步请求,在onFailure和onResponse回调中做相应的数据处理
注意:onFailure和onResponse都是在工作线程(子线程)中执行的
enqueue方法总结
- 判断当前call
- 封装成了一个AsyncCall对象
- client.dispatcher().enqueue()
与同步请求的区别
- 发起请求的方法调用: 同步execute及异步enqueue
- 阻塞线程与否:同步阻塞异步不阻塞
代码如下
val url = "https://api.github.com/users/dsh/repos"
val hostname = "api.github.com"
val client = OkHttpClient.Builder()
.build()
val request: Request = Request.Builder()
.url(url)
.build()
client.newCall(request)
.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
e.printStackTrace()
}
override fun onResponse(call: Call, response: Response) {
println("Response status code: ${response.code()}")
}
})
任务调度核心类Dispatcher
Q1.okhttp如何实现同步异步请求?
发送的同步/异步请求都会在dispatcher中管理其状态
Q2.到底什么是dispatcher
dispatcher的作用为维护请求的状态,并维护一个线程池,用于执行请求。
Q3.异步请求为什么需要两个队列
- Dispatcher 生产者
- ExecutorService 消费者池
- Deque 缓存
- Deque 正在运行的任务
Dispatcher的几个主要变量
Dispatcher的调度流程图解
OKHttp拦截器流
官网:拦截器是OkHttp中提供一种强大机制,它可以实现网络监听、请求以及响应重写、请求失败重试等功能
OkHttp主要有网络拦截器和应用程序拦截器,如图,我们主要关心应用程序拦截器
Okhttp主要的几个拦截器
核心逻辑:
- 在发起请求前对request进行处理
- 调用下一个拦截器,获取response
- 对response进行处理,返回给上一个拦截器
以同步请求方法execute为例,如图
-
①创建一系列拦截器,并将其放入一个拦截器list中
-
②创建一个拦截器链ReallnterceptorChain,并执行拦截器链的proceed方法
-
③proceed方法创建并调用下一个拦截器,获取response
-
④每一个拦截器对response进行处理,返回给上一个拦截器
RetryAndFollowUpInterceptor 重试重定向拦截器
主要负责失败重连,并不是所有的网络请求失败后都能重连,是有一定限制范围的,Okhttp内部会帮助我们检测网络异常,及响应码判断等,如果是在限制范围内的话,就可以根据条件进行网络请求的重连
- 调用 ReallnterceptorChain.proceed(...)进行网络请求
- 根据异常结果或者响应结果判断是否要进行重新请求
- 调用下一个拦截器,对response进行处理,返回给上一个拦截器
@Override
public Response intercept(Chain chain) throws IOException {
...
//重连不是无限制的,超过20个重连请求,就不会再请求
if (++followUpCount > MAX_FOLLOW_UPS) {
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
...
}
BridgeInterceptor 桥接拦截器
主要负责添加请求头部信息,包括内容长度、编码方式、压缩等,其中的keepAlive是连接复用的基础。
工作流程
-
1.是负责将用户构建的一个Request请求转化为能够进行网络访问的请求
-
2.将这个符合网络请求的Request 进行网络请求
-
3.将网络请求回来的响应Response转化为用户可用的Response。
CacheInterceptor缓存拦截器
缓存put与get方法
put方法
get方法
缓存拦截器操作
ConnectInterceptor (连接拦截器)
连接池ConnectionPool
-
每次http请求都会产生一个Transmitter对象
-
Transmitter对象的弱引用添加到RealConnection对象的transmitters集合
-
从连接池中获取
ConnectionPool的回收:
- OKHttp使用了GC回收算法
- Transmitter的数量会渐渐变成0
- 被线程池监测并回收,这样就可以保持多个健康的keep-alive连接
CallServerInterceptor
- 发起真正的网络请求
- 接收服务器的响应
主要包含以下步骤:
- 写入请求header
- 写入请求body
- 构建响应头
- 构建响应体
- 返回响应response
public final class CallServerInterceptor implements Interceptor {
private final boolean forWebSocket;
public CallServerInterceptor(boolean forWebSocket) {
this.forWebSocket = forWebSocket;
}
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Exchange exchange = realChain.exchange();
Request request = realChain.request();
long sentRequestMillis = System.currentTimeMillis();
//1. 写入请求的header
exchange.writeRequestHeaders(request);
boolean responseHeadersStarted = false;
Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
// 如果请求头的Expect为100-continue时,只发送请求头
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
exchange.flushRequest();
responseHeadersStarted = true;
exchange.responseHeadersStart();
responseBuilder = exchange.readResponseHeaders(true);
}
if (responseBuilder == null) {
if (request.body().isDuplex()) {
// 准备body 一遍应用程序可以稍后发送正文
exchange.flushRequest();
BufferedSink bufferedRequestBody = Okio.buffer(
exchange.createRequestBody(request, true));
request.body().writeTo(bufferedRequestBody);
} else {
// 如果符合100-continue,写入body
BufferedSink bufferedRequestBody = Okio.buffer(
exchange.createRequestBody(request, false));
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
}
} else {
exchange.noRequestBody();
if (!exchange.connection().isMultiplexed()) {
exchange.noNewExchangesOnConnection();
}
}
} else {
exchange.noRequestBody();
}
//结束请求
if (request.body() == null || !request.body().isDuplex()) {
exchange.finishRequest();
}
if (!responseHeadersStarted) {
exchange.responseHeadersStart();
}
//构建响应体
//读取网络响应的头部信息
if (responseBuilder == null) {
responseBuilder = exchange.readResponseHeaders(false);
}
Response response = responseBuilder
.request(request)
.handshake(exchange.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
int code = response.code();
if (code == 100) {
response = exchange.readResponseHeaders(false)
.request(request)
.handshake(exchange.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
code = response.code();
}
exchange.responseHeadersEnd(response);
//读取body信息
if (forWebSocket && code == 101) {
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
response = response.newBuilder()
.body(exchange.openResponseBody(response))
.build();
}
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
exchange.noNewExchangesOnConnection();
}
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
return response;
}
}
Okhttp中一次网络请求的大致过程
- Call对象请求的封装
- dispatcher对请求的分发
- getResponseWithInterceptors()方法
面试相关
1. Android基础网络编程: socket、HttpClient和HttpURLConnection
- Socket
- 是一个对TCP /IP协议进行封装的编程调用接口
- 成对出现,一对套接字:包括ip地址和端口号
- Socket与Http
- Http:采用请求一响应方式。HTTP协议属于应用层
- Socket:采用服务器主动发送数据的方式,Socket属于传输层
- HttpClient
- 6.0之后已经被移除
- HttpURLConnection
class HttpConnectionTest { public void HttpConnectionGet() throws IOException { String getURL = "GET_URL" + " ?username= " + URLEncoder.encode(" fat man ", "utf-8"); URL getUrl = new URL(getURL); HttpURLConnection connection = (HttpURLConnection)getUrl.openConnection(); connection.connect(); //取得输入流,并使用Reader读取,这时才真正的把request发送至服务器端 BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); System.out.println(" ======== "); System.out.println(" Contents of get request "); System.out.println(" ======== "); String lines; while((lines = reader.readLine())!= null){ System.out.println(lines); } reader.close(); //断开连接,底层就是关闭socket连接 connection.disconnect(); System.out.println(" ======== "); System.out.println(" Contents of get request ends "); System.out.println(" ======== "); } }
2. 了解websocket吗?知道和socket的区别吗?okhttp是如何处理websocket的相关问题的?
- 通过client.newWebSocket(request,listener)建立socket建立连接
- 在listener中监听和发送消息
- 记得要调用shutdown()关闭socket连接
public void OkhttpSocketTest(){ OkHttpClient client = new OkHttpClient.Builder().build(); Request request = new Request.Builder().url("www.webSocket.com").build(); WebSocketListener listener = new WebSocketListener() { @Override public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { super.onOpen(webSocket, response); webSocket.send("hello"); webSocket.send(ByteString.decodeHex("absd")); webSocket.close(1000,"bye"); } @Override public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) { super.onMessage(webSocket, text); //和UI交互,需要使用hander setText("onMessage: " + bytes); handler.sendEmptyMessage(0); } @Override public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) { super.onMessage(webSocket, bytes); setText("onMessage byteString: " + bytes); handler.sendEmptyMessage(0); } @Override public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) { super.onFailure(webSocket, t, response); setText("onFailure:"+ t.getMessage()); } @Override public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) { super.onClosing(webSocket, code, reason); webSocket.close(1000, null); setText("onClosing:"+ code + "/" + reason); } @Override public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) { super.onClosed(webSocket, code, reason); setText("onClosed:"+ code + "/" + reason); } }; client.newWebSocket(request,listener); client.dispatcher().executorService().shutdown(); }
3. WebSocker&轮询相关
- 轮询是在特定的的时间间隔,由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。
- 短轮询:Client每间隔一段时间向服务端发送请求,并且接收服务器响应
- 缺点:在某个时间段 server没有更新数据,但client仍然每隔一段时间发送请求来询问,所以这段时间内的询问都是无效的冗余数据,浪费带宽(如直播评论短轮询,永远请求第一页的数据,返回大量重复数据)
- 长轮询:服务端接收到请求后,不会立即返回response,当数据更新时,才会把response返回,如果没有数据更新,则会一直保持请求连接
- 轮询缺点:
- 浪费带宽(HTTP HEAD 是比较大的,有效信息比较少)
- 消耗服务器cpu占用
- webSocket通信模型
- WebSocket与Http区别
- 与Http同等的网络协议
- 双向通信协议
- WebSocket与Socket,完全没有关系
- Socket其实并不是一个协议
- WebSocket是一种协议
- WebSocket总结
- 1.本质上是一个基于TCP的协议
- 2.向服务器发起一个HTTP请求/ “Upgrade:WebSocket"
- 3.服务器端解析这些附加的头信息
4. Http如何处理缓存?Okhttp如何处理缓存相关问题?
- 分强制缓存和对比缓存两种
- 强制缓存:分为Expires和Cache-Control
- Expires
- Expires的值为服务端返回的到期时间,如果小于到期时间,则使用缓存
- Http1.0经常使用,浏览器默认是1.1,所以基于已弃用
- 问题:到期时间是服务器返回的
- Cache-Control(重要)
- Cache-control是由服务器返回的Response中添加的头信息
- 取值:
- private:客户端可以缓存
- public:客户端和代理服务器都可以缓存
- max-age:缓存内容将在多少秒之后失效
- no-cache:对比缓存中用到
- no-store:所有内容都不缓存
- Expires
- 对比缓存
- 首先需要进行比较判断是否可以使用缓存。
- ETag / If-None-Match:成对出现
- Etag:服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识
- If-None-Match:再次请求服务器时,通过此字段通知服务器客户段缓存数据的唯一标识
- Last-Modified / If-Modified-Since
- Last-Modified:服务器在响应请求时,告诉浏览器资源的最后修改时间
- If-Modified-Since:再次请求服务器时,通过此字段通知服务器上次请求时,服务器返回的资源最后修改时间
- 缓存图示
5. 断点续传的原理?如何实现?okhttp中如何实现相关问题?
- 断点续传:从文件已经下载的地方开始继续下载
- 原理是在头部添加RANGE信息
- 都会创建RandomAccessFile对象并调用其seek方法
如图所示:
6. 多线程下载原理okhttp如何实现?
- 多线程下载:每个线程只负责下载文件的一部分
- 首先要获取文件大小totalSize以及每个线程要下载的文件大小partSize = totalSize/threadCount
- 在每个线程中创建请求并创建RandomAccessFile对象,调用seek方法进行指定位置的写入,最后关闭IO流
图示:
7. 文件上传如何做?原理?okhttp如何完成文件上传
- 指定Http头部的ContentType
- 由Content-Type属性指定请求和响应的HTTP内容类型
- 如下图的例子,如果文件太长,一个包无法发送完毕,需要boundry分隔符来实现分片上传
- Okhttp中,主要是通过MultiPartBody来构建请求
图示:
8. json数据如何解析?okhttp如何解析json类型数据?
- json:文本形式的数据交换格式
- json 比 XML 更轻量,也比二进制更容易阅读
- json解析
- 传统的jsonObject解析
- Gson
- FastJson
- Gson解析
方式二Gson mGson = new Gson(); List<User> userList = mGson.fromJson(jsonArray.toString(), new TypeToken<List<User>>(){ } .getType());JsonParser parser = new JsonParser(); JsonArray jsonArray = parser.parse(json1).getAsJsonArray(); Gson gson = new Gson(); Arraylist<UserBean> userBeanList = new Arraylist<>(); for (JsonElement user : jsonArray) { UserBean userBean = gson.fromJson(user,UserBean.class); userBeanList.add(userBean); }
9. okhttp如何处理https?
-
Https是一种基于SSL/TLS的Http协议
-
不对称VS对称加密
- 对称加密所使用的密钥我们可以通过非对称加密的方式发送出去
- 不对称加密速度上不如对称加密,所以实际传输过程中https使用的是对称加密
-
Https算法
- 不对称加密:SSL/TLS握手阶段
- 对称加密:对传输数据进行加密
- Hash:用于验证客户端发送给服务端数据完整性
-
Https通信过程总结:
- 服务端用RSA生成公钥和私钥,把公钥放在证书里发送给客户端,私钥自己保存
- 客户端接收到公钥后,首先向一个权威的服务器检查证书的合法性,如果证书合法,客户端产生一段随机数,这个随机数就作为通信的密钥,我们称之为对称密钥,用公钥加密这段随机数,然后发送到服务器
- 服务器用密钥解密获取对称密钥,然后,双方就已对称密钥进行加密解密通信了