前言
在Android开发中,网络请求是非常常用的,因此我们需要对Android开发的网络请求有大致的了解;包括应用到的一些基础原理,以及一些常用的开源库
基本原理
在Android App的开发中,用到最多的通信方式是基于Http协议,如果涉及到消息推送,可能会使用到WebSocket等,不管是Http还是WebSocket,其底层实现都是基于Socket;Socket即套接字,是一个对TCP/IP协议进行封装的编程调用接口(API);对于一般的开发而言,有很成熟的开源库可以使用;但是我们还是要对其原理有基本的了解
TCP
网络是分层的,将网络节点所要完成的数据的发送或转发、打包或拆包、以及控制信息的加载或拆出等工作,分别由不同的硬件和软件模块来完成;常见的是TCP/IP五层分层模型
TCP_IP五层协议.png传输层有TCP(传输控制协议)和UDP(用户数据报协议)两种协议;主要用于保证数据的传输
TCP三次握手和四次挥手
tcp标志位,有6种标示:SYN(synchronous建立联机) ACK(acknowledgement 确认) PSH(push传送) FIN(finish结束) RST(reset重置) URG(urgent紧急)
三次握手
三次握手.png第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。 完成三次握手,客户端与服务器开始传送数据.
四次挥手
四次挥手.png第一次挥手:客户端A发送一个FIN.用来关闭客户A到服务器B的数据传送
第二次挥手:服务器B收到这个FIN. 它发回一个ACK,确认序号为收到的序号+1。和SYN一样,一个FIN将占用一个序号
第三次挥手:服务器B关闭与客户端A的连接,发送一个FIN给客户端A
第四次挥手:客户端A发回ACK报文确认,并将确认序号设置为序号加1
拓展-四次挥手TIME_WAIT机制
我们知道, 在tcp四次挥手中, B发FIN包(第三次挥手)后, A马上回应最后的ACK, 此时, A的socket让然不能立即进入CLOSED的状态, 为什么呢? 其实这就是在问TIME_WAIT状态存在的理由。
理由之一:
A不能保证最后的ACK能达到B, 所以, 还应该观望一段时间, 护送一段时间。 如果最后的ACK丢失, 那么B显然收不到, B于是发起了重传FIN的操作, 此时如果A处于CLOSED的状态, 就没办法给对端发ACK了(实际是发RST), 呜呼哀哉。 所以A应该等一段时间, 这段时间就是所谓的TIME_WAIT, 比如, 等待一个RTT的时间(实际上, 考虑到如下的理由之二就知道, RTT可能不够, 用2MSL更靠谱)。
所以, TIME_WAIT存在的理由之一是尽可能护送最后的ACK达到对端。
理由之二:
假设tcp连接是: A(1.2.3.4:8888)------B(6.7.8.9:9999), 这就是一个tcp四元组。 当tcp连接关闭后, 四元组释放。 后面的新连接可能会重用到这个四元组(有这个可能性), 那么问题就来了: 新四元组和旧四元组完全一致, 他们的网络包会混乱吗? 所以, 可以考虑这样一个机制: 让旧四元组对应的所有网络包都消失后(等一段时间), 才允许新四元组建立, 颇有点锁的味道。 那么这个等一段时间究竟是多久呢? 多久才合适呢? 在前面的文章中, 我们讨论过, 采用2MSL比较合适, 我个人认为, 把TIME_WAIT定义为2MSL只是一个通用的经验方法而已, 无法从理论上百分之百论证。
所以, TIME_WAIT存在的理由之二是新旧四元组互不干扰
Socket
Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议
socket和http的区别
Http协议:简单的对象访问协议,对应于应用层。Http协议是基于TCP链接的。
tcp协议:对应于传输层
ip协议:对应与网络层
TCP/IP是传输层协议,主要解决数据如何在网络中传输;而Http是应用层协议,主要解决如何包装数据。而且前面说了socket不是一种协议,而是对传输层协议tcp/ip的包装;
Http连接:http连接就是所谓的短连接,及客户端向服务器发送一次请求,服务器端相应后连接即会断掉。
socket连接:socket连接及时所谓的长连接,理论上客户端和服务端一旦建立连接,则不会主动断掉;但是由于各种环境因素可能会是连接断开,比如说:服务器端或客户端主机down了,网络故障,或者两者之间长时间没有数据传输,网络防火墙可能会断开该链接已释放网络资源。所以当一个socket连接中没有数据的传输,那么为了位置连续的连接需要发送心跳消息,具体心跳消息格式是开发者自己定义的。
http协议
http协议是应用层协议,重点在数据的包装;TCP/IP重点在于数据的传输
http协议的工作流程
HTTP协议定义了客户端发送请求数据的格式以及服务端返回的数据格式,通信双方只需要按照规定的格式来解析数据即可解读数据。HTTP采用请求/响应模式,客户端向服务端发送一个请求报文,请求报文的内容包括请求的方法、URL、协议版本、请求头部和请求数据;服务端以一个状态行作为响应,响应内容包括协议的版本、请求状态响应码、服务器信息、响应头部和响应内容。这中间的数据传输依赖于TCP协议
传输详细步骤如下:
1、客户端连接到服务器
客户端发起一个TCP连接请求,经过三次握手和服务器建立TCP连接;
2、发送请求数据
按照HTTP协议规定的格式组装请求报文,并通过TCP连接向服务端发送请求报文;
3、服务端接收请求报文并处理
服务端通过TCP连接收到客户端发送过来的请求报文后,按照规定格式解析数据,根据解读的数据生成对应的响应报文,生成的响应报文也要遵循HTTP协议;
4、服务端发送响应报文给客户端
服务端将生成的响应报文通过TCP连接发送给客户端
5、关闭TCP连接
服务端将数据发送给客户端后,如果connection模式为close,则服务端主动关闭TCP连接,客户端被动关闭连接,通信结束;如果connection模式为keepalive,则该连接会保持一段时间,则该时间段内可以继续通过连接传输数据;
6、客户端处理响应数据
客户端收到响应报文后,按照HTTP协议规定格式解析响应报文并处理。
http请求报文
HTTP协议的请求报文由请求行、请求头部、空行、请求数据四个部分组成
请求报文范例
POST /lotto/android/v1.0/order-group/queryOrderGroupPersonInfo HTTP/1.1
cache-control: no-cache
Postman-Token: 800ec750-6ee8-4b2b-a879-f5d854115862
Content-Type: application/json
User-Agent: PostmanRuntime/3.0.11-hotfix.2
Accept: */*
Host: sitapp.2ncai.com
accept-encoding: gzip, deflate
content-length: 38
Connection: close
{"seeType":1,"source":1,"userId":"30"}
请求行
在上面的例子中,请求行如下:
POST /lotto/android/v1.0/order-group/queryOrderGroupPersonInfo HTTP/1.1
http请求方法
GET:请求获得Request-URL所标识的资源
POST:在Request-URL所标识的资源后附加新的数据,即可以向服务端发送请求数据
HEAD:请求获取Request-URL所标识的资源的响应消息报头
PUT:请求服务器存储一个资源,并用Request-URL作为其标识
DELETE:请求服务器删除Request-URL所标识的资源
TRACE:请求服务器回送收到的请求信息,主要用于测试或者诊断
CONNETC:HTTP1.1中预留的能够将连接改为管道方式的代理服务器
OPTIONS:请求查询服务器性能,或者查询与资源相关的选项或需求
对于我们平时的开发来说,用到最多的就是GET和POST
get和post的区别
-
GET在浏览器回退时是无害的,而POST会再次提交请求。
-
GET产生的URL地址可以被Bookmark,而POST不可以。
-
GET请求会被浏览器主动cache,而POST不会,除非手动设置。
-
GET请求只能进行url编码,而POST支持多种编码方式。
-
GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
-
GET请求在URL中传送的参数是有长度限制的,而POST没有。
-
对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
-
GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
-
GET参数通过URL传递,POST放在Request body中
请求报头
在请求行之后会有0个或者多个请求报头,每个请求报头都包含一个名字和一个值,他们之间用英文冒号(:)分隔,例如上面的例子中请求报头如下
cache-control: no-cache
Postman-Token: 800ec750-6ee8-4b2b-a879-f5d854115862
Content-Type: application/json
User-Agent: PostmanRuntime/3.0.11-hotfix.2
Accept: */*
Host: sitapp.2ncai.com
accept-encoding: gzip, deflate
content-length: 38
Connection: close
关于请求报头我们后面说到消息报头的时候在统一说明
请求数据
请求数据不在GET方法中使用,而是在POST中使用,它表示向服务器附加的请求数据。POST方法使用于需要向服务器提交数据的请求,比如客户填写表单需要提交到服务器就可以使用POST方法发起请求
响应报文
HTTP的响应报文是指服务端返回给客户端的报文,其格式为状态行、响应报头、空行、响应正文
范例如下:
HTTP/1.1 200
Date: Sat, 11 Aug 2018 04:24:25 GMT
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Connection: close
X-Application-Context: application:test:8160
Server: my_server
{"success":1,"errorCode":"10001","message":"正确","data":{"userName":"CCCC","userId":30,"headPic":"https://sitres.2ncai.com/_upload_images/user/head/1711181009252.png","winCount":32,"winAmount":2580477.84,"orderCount":249,"orderSucRate":0.36,"customizationCount":0,"winBwCount":0,"winSwCount":7,"winWCount":10,"winQCount":20,"winOtherCount":42,"orderGroupLotteryBOs":[{"lotteryCode":100,"lotteryName":"双色球","lotteryType":1,"grade":0,"orderCount":239,"orderSucRate":0.03,"winCount":3,"winAmount":324800.0}]},"serviceTime":1533961465544}
状态行
状态行的格式:HTTP-Version Status-Code Reason-Phrase CRLF
其中HTTP-Version表示服务器HTTP协议的版本,Status-Code表示响应的状态码,Reason-Phrase表示状态码的文本描述。状态码由三位数字组成,其中首位数字定义了响应的类别,且有5种类别:
100-199:指示信息,收到请求后,需要请求者继续执行操作
200-299:请求成功,请求已被成功接收并处理
300-399:重定向,要完成请求需要进行更进一步操作
400-499:客户端错误,请求有语法错误或者请求无法实现
500-599:服务端错误,服务器执行错误,无法正确处理请求
常见响应状态码
200:请求被正常处理
204:请求被受理但没有资源可以返回
206:客户端只是请求资源的一部分,服务器只对请求的部分资源执行GET方法,相应报文中通过Content-Range指定范围的资源。
301:永久性重定向
302:临时重定向
303:与302状态码有相似功能,只是它希望客户端在请求一个URI的时候,能通过GET方法重定向到另一个URI上
304:发送附带条件的请求时,条件不满足时返回,与重定向无关
307:临时重定向,与302类似,只是强制要求使用POST方法
400:请求报文语法有误,服务器无法识别
401:请求需要认证
403:请求的对应资源禁止被访问
404:服务器无法找到对应资源
500:服务器内部错误
503:服务器正忙
响应报头
与消息报头一起解释
响应正文
服务端返回给客户端的正文数据
消息报头
a、通用首部字段(请求报文与响应报文都会使用的首部字段)
Date:创建报文时间
Connection:连接的管理
Cache-Control:缓存的控制
Transfer-Encoding:报文主体的传输编码方式
b、请求首部字段(请求报文会使用的首部字段)
Host:请求资源所在服务器
Accept:可处理的媒体类型
Accept-Charset:可接收的字符集
Accept-Encoding:可接受的内容编码
Accept-Language:可接受的自然语言
c、响应首部字段(响应报文会使用的首部字段)
Accept-Ranges:可接受的字节范围
Location:令客户端重新定向到的URI
Server:HTTP服务器的安装信息
d、实体首部字段(请求报文与响应报文的的实体部分使用的首部字段)
Allow:资源可支持的HTTP方法
Content-Type:实体主类的类型
Content-Encoding:实体主体适用的编码方式
Content-Language:实体主体的自然语言
Content-Length:实体主体的的字节数
Content-Range:实体主体的位置范围,一般用于发出部分请求时使用
http上层实践-开源库okhhtp
HTTP是现代应用常用的一种交换数据和媒体的网络方式,高效地使用HTTP能让资源加载更快,节省带宽。OkHttp是一个高效的HTTP客户端,它有以下默认特性:
- 支持HTTP/2,允许所有同一个主机地址的请求共享同一个socket连接
- 连接池减少请求延时
- 透明的GZIP压缩减少响应数据的大小
- 缓存响应内容,避免一些完全重复的请求
简单实例
get
String url = "https://www.baidu.com";
OkHttpClient okHttpClient = new OkHttpClient();//创建OkHttpClient实例
final Request request = new Request.Builder()
.url(url)
.get()//默认就是GET请求,可以不写
.build();//创建Request对象
Call call = okHttpClient.newCall(request);//将request对象封装成Call任务对象
call.enqueue(new Callback() {//执行异步任务,call.excute为同步任务
@Override
public void onFailure(Call call, IOException e) {
Log.d(TAG, "onFailure: ");
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.d(TAG, "onResponse: " + response.body().string());
}
});
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.d(TAG, "onResponse: " + response.body().string());
}
});
post
MediaType mediaType = MediaType.parse("text/x-markdown; charset=utf-8");
String requestBody = "I am Jdqm.";
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(mediaType, requestBody))
.build();
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.d(TAG, "onFailure: " + e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.d(TAG, response.protocol() + " " +response.code() + " " + response.message());
Headers headers = response.headers();
for (int i = 0; i < headers.size(); i++) {
Log.d(TAG, headers.name(i) + ":" + headers.value(i));
}
Log.d(TAG, "onResponse: " + response.body().string());
}
});
简单流程分析
Okhttp初始化
public OkHttpClient() {
this(new Builder());
}
public Builder() {
dispatcher = new Dispatcher(); // 由call代表的请求任务分发器
protocols = DEFAULT_PROTOCOLS; // 默认的协议 http2 http1.1
connectionSpecs = DEFAULT_CONNECTION_SPECS; // 设置连接时支持的tls层协议以及不进行数据加密
eventListenerFactory = EventListener.factory(EventListener.NONE);
proxySelector = ProxySelector.getDefault();
cookieJar = CookieJar.NO_COOKIES;
socketFactory = SocketFactory.getDefault(); // socket生产工厂
hostnameVerifier = OkHostnameVerifier.INSTANCE;
certificatePinner = CertificatePinner.DEFAULT;
proxyAuthenticator = Authenticator.NONE;
authenticator = Authenticator.NONE;
connectionPool = new ConnectionPool(); //连接池 支持多路复用
dns = Dns.SYSTEM;
followSslRedirects = true;
followRedirects = true;
retryOnConnectionFailure = true;
connectTimeout = 10_000;
readTimeout = 10_000;
writeTimeout = 10_000;
pingInterval = 0;
}
然后根据请求报文的格式进行Request对象的构造;紧接着通过 OkHttpClient 和 Request 构造一个 Call对象,它的实现是RealCall,代表请求任务
RealCall请求任务
public Call newCall(Request request) {
return RealCall.newRealCall(this, request, false /* for web socket */);
}
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket){
// Safely publish the Call instance to the EventListener.
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.eventListener = client.eventListenerFactory().create(call);
return call;
}
private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
this.client = client;
this.originalRequest = originalRequest;
this.forWebSocket = forWebSocket;
this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
}
可以看到在 RealCall 的构造方法中创建了一个RetryAndFollowUpInterceptor,用于处理请求错误和重定向等,这是 Okhttp 框架的精髓 interceptor chain 中的一环,默认情况下也是第一个拦截器,除非调用 OkHttpClient.Builder#addInterceptor(Interceptor) 来添加全局的拦截器。关于拦截器链的顺序参见 RealCall#getResponseWithInterceptorChain() 方法。
RealCall#enqueue(Callback)
public void enqueue(Callback responseCallback) {
synchronized (this) {
//每个请求只能之执行一次
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
AsyncCall代表异步执行的请求任务,然后调用任务分发器Dispatcher的enqueue方法
Dispatcher
public final class Dispatcher {
private int maxRequests = 64; //最大请求数量
private int maxRequestsPerHost = 5; //每台主机最大的请求数量
private @Nullable Runnable idleCallback;
/** Executes calls. Created lazily. */
private @Nullable ExecutorService executorService; //线程池
/** Ready async calls in the order they'll be run. */
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
/** Running synchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
/** 这个线程池没有核心线程,线程数量没有限制,空闲60s就会回收*/
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
}
任务分发器包括一个线程池,用于执行http请求;三个队列,正在执行的异步任务队列,准备就绪正在排队的异步任务队列,同步任务队列;
Dispatcher#enqueue
synchronized void enqueue(AsyncCall call) {
//正在执行的任务数量小于最大值(64),并且此任务所属主机的正在执行任务小于最大值(5)
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call); //线程池执行任务
} else {
readyAsyncCalls.add(call); //等待队列
}
}
这里的执行就类似于java中Callable和FutureTask的使用,原理上是类似的;最终执行到AsyncCall的execute
AsyncCall#execute
final class AsyncCall extends NamedRunnable {
//省略...
@Override protected void execute() {
boolean signalledCallback = false;
try {
//调用 getResponseWithInterceptorChain()获得响应内容
Response response = getResponseWithInterceptorChain();
if (retryAndFollowUpInterceptor.isCanceled()) {
//这个标记为主要是避免异常时2次回调
signalledCallback = true;
//回调Callback告知失败
responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
signalledCallback = true;
//回调Callback,将响应内容传回去
responseCallback.onResponse(RealCall.this, response);
}
} catch (IOException e) {
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
} else {
eventListener.callFailed(RealCall.this, e);
responseCallback.onFailure(RealCall.this, e);
}
} finally {
//不管请求成功与否,都进行finished()操作
client.dispatcher().finished(this); //做队列清理操作,触发队列下面的任务
}
}
}
getResponseWithInterceptorChain
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>(); //这是一个List,是有序的
interceptors.addAll(client.interceptors());//首先添加的是用户添加的全局拦截器
interceptors.add(retryAndFollowUpInterceptor); //错误、重定向拦截器
//桥接拦截器,桥接应用层与网络层,添加必要的头、
interceptors.add(new BridgeInterceptor(client.cookieJar()));
//缓存处理,Last-Modified、ETag、DiskLruCache等
interceptors.add(new CacheInterceptor(client.internalCache()));
//连接拦截器
interceptors.add(new ConnectInterceptor(client));
//从这就知道,通过okHttpClient.Builder#addNetworkInterceptor()传进来的拦截器只对非网页的请求生效
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
//真正访问服务器的拦截器
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
RealInterceptorChain#proceed()
public Response proceed(Request request) throws IOException {
return proceed(request, streamAllocation, httpCodec, connection);
}
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
//省略异常处理...
// Call the next interceptor in the chain.
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
//省略异常处理...
return response;
}
okhttp对于网络请求采用用了一个类似AOP的的拦截器链,链式调用所有拦截器,最后执行请求返回response,而okhttp内置了5个拦截器,分别为:
1.RetryAndFollowUpInterceptor
在网络请求失败后进行重试
当服务器返回当前请求需要进行重定向时直接发起新的请求,并在条件允许情况下复用当前连 接
2.BridgeInteceptor
设置内容长度,内容编码
设置gzip压缩,并在接收到内容后进行解压。省去了应用层处理数据解压的麻烦
添加cookie
设置其他报头,如User-Agent,Host,Keep-alive等。其中Keep-Alive是实现多路复用的必要步骤
3.CacheInterceptor
当网络请求有符合要求的Cache时直接返回Cache
当服务器返回内容有改变时更新当前cache
如果当前cache失效,删除
4.ConnectInterceptor
为当前请求找到合适的连接,可能复用已有连接也可能是重新创建的连接,返回的连接由连接池负责决定。
5.CallServerInterceptor
负责向服务器发起真正的访问请求,并在接收到服务器返回后读取响应返回。
示意图如下:
okhttp拦截器.png5个拦截器的代码可以参考Okhttp3源码分析,这里不再详细叙述了
后续
本文学习了Android网络编程的一些基础知识点,并从一次简单的网络请求来跟踪源码,梳理的一个大致流程,其中对于每个拦截器只是说明作用,其实每个拦截器设计也很巧妙,后续有机会可以深入学习
网友评论