Android之网络编程学习

Android网络编程学习(1)

2019-02-27  本文已影响0人  weiinter105

前言

在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两种基本请求方法的区别

请求报头

在请求行之后会有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客户端,它有以下默认特性:

简单实例

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拦截器.png

5个拦截器的代码可以参考Okhttp3源码分析,这里不再详细叙述了

后续

本文学习了Android网络编程的一些基础知识点,并从一次简单的网络请求来跟踪源码,梳理的一个大致流程,其中对于每个拦截器只是说明作用,其实每个拦截器设计也很巧妙,后续有机会可以深入学习

上一篇下一篇

猜你喜欢

热点阅读