1 概况 Introduction
在Android上我们用开源项目OkHttp作为客户端的SPDY协议栈链接支持SPDY的网站,如Taobao CDN等。本文主要分析OkHttp是如何在Android的通信层扩展SPDY。先简要介绍一下SPDY。
SPDY的主要目标:
单个tcp连接支持并发的Http请求。这主要是解决目前http协议的一个连接只能服务一次请求的问题。
压缩请求/响应头。
定义spdy为一个相比http容易实现的协议。
使用ssl加密连接,用户数据更加安全的同时也解决了对已有网络体系的兼容问题。
服务器可以主动的向客户端发起连接,并推送数据。
SPDY是在HTTP协议栈的会话层进行的扩展,提供stream机制和消息组帧的实现
SPDY协议定义了Session, Stream和Frame等逻辑概念。请参考协议:
http://www.chromium.org/spdy/spdy-protocol
2 OkHttp
OkHttp是github上的一个开源项目github.com/square/okhttp。本文分析基于1.1.0版本。
2.1 OkHttp类结构图
从概述中可见,SPDY协议是在TCP/IP的协议栈的会话层的扩展。因此实现改协议的OkHttp项目在,架构上也是在已有的Android的网络通信协议栈上扩展。
下图是OkHttp的总体架构图,这里只包含了比较重要的类、成员和方法。其中,淡蓝色部分是对外接口部分,可见OkHttp在实现时保持了对外接口与Android的HttpUrlConnection、InputStream、OutputStream等不变。这样使用HttpUrlConnection的Android应用不必大幅修改代码就可以享受Spdy带来的好处了。白色部分是和Android网络库的架构基本一致,定义了Connection, Stream, Transport等抽象层次。淡黄色部分是spdy的扩展部分,通过实现遵循SPDY协议的Transport实体类来支持SPDY链接, 从这个切入点把SPDY协议的实现加入到Android的通信模块。
2.1.1 OkHttpClient
这个类是对外接口类, 它担当了两个功能。
1. 支持Spdy的HttpURLConneciton的工厂类,通过兼容HttpURLConnection类的方式,开发者的代码不需要修改就可以从HTTP迁移到SPDY。示例代码如下:
OkHttpClient client = new OkHttpClient();
String get(URL url) throws IOException {
HttpURLConnection connection = client.open(url);
InputStream in = null;
try {
// Read the response.
in = connection.getInputStream();
byte[] response = readFully(in);
return new String(response, "UTF-8");
} finally {
if (in != null) in.close();
}
}
2.1.2 HttpURLConnectionImpl
这是接口类HttpURLConnection的实现者。它实际是用HttpEngine来实现的,一个HTTPUrlConnection对应一个HttpEngine。
2.1.3 HttpEngine
这个类担当了链接过程中状态管理,它调度Transport和Connection完成建Socket链接和在链接上的数据收发过程。
2.1.4 Connection
这个类主要维护链接的Socket。它的newTransport函数决定了使用的协议是SPDY或HTTP。
2.1.5 Transport
这个接口定义了HTTP/SPDY等协议遵循的接口契约。
2.1.6 HttpUrlConnectionDelegate
用于HTTPS,代理HTTPS的链接等功能到HttpUrlConnectionImp类。
2.1.7 SpdyTransport
Spdy的实现时通过这个类实现Transport接口来接入的。一个SpdyTransport对应唯一的一个SpdyConnection。
2.1.8 SpdyConnection
Spdy的多路复用是以SpdyStream的形式实现的,即同时可以多个Stream承载在一个Connection上。这个类维护了Stream的列表。在SpdyConnection对应的Socket链接建立起来以后,它的内部类Reader起了一个线程,使用StreamReader从Socketet读取数据并派发。
2.1.9 SpdyReader
这个类负责从Socket读入数据流,并按照SPDY协议进行解析。对应于SPDY协议的Frame Layer,是Frame数据的解析者。
2.1.10 SpdyWriter
这个类负责在把数据发送到Server前按SPDY协议封装。应于SPDY协议的Frame Layer,是Frame数据的组装者。
2.1.11 SpdyStream
这个对应于SPDY协议的Stream,它在逻辑上把底层的输入流分为独立的多路。它的内部类SpdyDataInputStream和SpdyDataOutputStream担当了一个Stream上的输入输出流的角色。
3 场景分析
这里以几个典型的场景来分析第2章中的类是如何配合工作的。
3.1 链接建立过程
一般使用HttpURLConnection接口来完成HTTP链接上的数据读取的代码如下:
//Step1 打开链接
conn = (HttpURLConnection)OkhttpClient.open ();
//Step2 建立链接
conn.connect();
//Step3 获得Response Code
responeCode = conn.getResponseCode();
//Step4 打开输入流
InputStream dis = conn.getInputStream();
//Step5 读取数据
while ((i = dis.read(b, 0, 2048)) != -1)
{
}
//Step6 关闭链接
dis.close();
conn.disconnect();
这里分析这6步中,OKHTTP各做了什么。
3.1.1 Step1
第一步,获得http conneciton的实例。
conn = (HttpURLConnection)OkhttpClient.open ();
可见在Step1中,只是生成了URLConnecton层面的类实例,并没有实际的链接动作发生。
3.1.2 Step2
第二步,调用connect。
conn.connect();
这一步比较复杂,建链的过程也是初始化相关对象的过程。
首先生成了HttpEngine的实例。这个类担当了链接过程中状态管理者。它处理在Connection之上的一次request和Response过程。
接着向服务端发起了链接请求(SendRequest)。这个过程分为几步:
a. 处理HTTP Header(prepareRawRequestHeaders),包括UserAgent,Keep-Alive, gzip, Host, If-Modified-Since。这里还处理了Cookie,从OKHttpClient的cookieHandler中获得该域名对应的Cookie信息,加入到request的头信息中。如果需要自定义okhttp的Cookie策略,可以通过OkhttpClient的setCookieHandler方法提供自己的实现。
b. 处理http层面的Cache(InitResponseSource)。Response Cache的实现也是通过OkHttpClient类的getResponseCache获得可自定义的Cache实现者。如果使用了Cache策略且命中了Cache,则从Cache中获得数据并返回。
c. 发起链接 SendSocketRequest。这里建立起链接(Connection)和输入输出流。这一步是链接的主流程下面详细描述。
通过RouteSelector决定复用一个已有的Connection对象还是新产生一个实例,Connection对象管理了链接的套接字Socket,因此一个Connection对象对应于一个物理上的链接。对于相同Address的链接应该复用一个Connection。然后完成了Socket的链接 并且获得了输入和输出流。这里的流是解析SPDY前的流。
这里Connection过程中的upgradeToTls操作比较重要,他完成了TLS握手和验证。在这个过程中产生了SpdyConnection对象,这个对象的构造过程中,会启动一个线程驱动SpdyReader从输入流循环读入数据(nextFrame),并根据Frame的类型动作或派发到相应的Stream。
@Override public void run() {
int shutdownStatusCode = GOAWAY_INTERNAL_ERROR;
int rstStatusCode = SpdyStream.RST_INTERNAL_ERROR;
try {
while (spdyReader.nextFrame(this)) {
}
shutdownStatusCode = GOAWAY_OK;
rstStatusCode = SpdyStream.RST_CANCEL;
} catch (IOException e) {
shutdownStatusCode = GOAWAY_PROTOCOL_ERROR;
rstStatusCode = SpdyStream.RST_PROTOCOL_ERROR;
} finally {
try {
close(shutdownStatusCode, rstStatusCode);
} catch (IOException ignored) {
}
}
}
下一步,建立SpdyTransport对象。Transportport接口提供了一个用于写Request头和数据的输出流,SPDY协议的实现就是通过实现Transport接口接入到Android的网络协议栈的。
下一步,从Transport获得Request的输出流(CreateRequestBody)。这里首先写入Request的Header信息,这时会在Transport上创建一个spdy stream (spdyConnection.newStream)。 这里为输入和输出类分别创建了符合流接口的SpdyDataInputStream和SpdyDataOutputStream, 用于后续再输入和输出流上的操作
下一步,然后通过spdyWriter向服务端发送一个sync stream,告知服务端建立一个新的Stream。SYN_STREAM的格式可以参考SPDY协议,它向服务端发起了一个创建stream的请求。
c. 回到HttpUrlConnectionImp下一步是readResponse。这里写入剩余的Request的Header和Body部分并读取Response Header和Body。 这里用到了前述的SpdyStream来获得Response Header(getResponseHeaders). 这是一个同步实现,读取线程会wait起来直到获得数据或超时。获得响应头后会通知Http Engine(receiveHeaders),在这里处理种下来的Cookie。
下一步,更新HttpResponseCache,之后就从SpdyTransport 获取输入流, 即前述的SpdyStream下的SpdyDataInputStream,为读取数据做准备了(initContentStream)。
至此Step2 Connection完成。
3.1.3 Step3
获取Response Code
//Step3 获得Response Code
responeCode = conn.getResponseCode();
这一步十分简单,就是从上一步获得的Response Header中取得Response Code,这一步不牵涉到网络读写。
3.1.4 Step4
打开输入流
//Step4 打开输入流
InputStream dis = conn.getInputStream();
这一步也很简单,就是从HttpEngine把Connection时产生的 输入流返回。它实际是SpdyDataInputStream的实例。
3.1.5 Step5
读取服务端返回的数据
//Step5 读取数据
while ((i = dis.read(b, 0, 2048)) != -1)
{
}
这里通过前述的SpdyDataInputStream读取数据,这个读数据线程和前述SpdyConnection的内部Reader的派发线程组成比较典型的生产者/消费者模式。这个Stream由一个窗口大小的循环Buffer,它的Receive从输入流接受到这个Stream上的数据。一方面Read从这个Buffer消费数据。
3.1.6 Step6
关闭连接
//Step6 关闭链接
dis.close();
conn.disconnect();
SpdyDataInputStream上的Close操作会发送一个FIN包,这个Stream会关闭。而Conn的disconnect也没有释放链接,只是把Connection摆到了Pool里面。
3.2 多路复用
HTTP协议至此keep-alive特性部分解决了慢启动的问题,但SPDY比他更具优势的是它的多路复用的特性,即在同一条物理链路上可以并发的发送。
以主客户端下载图片为例,一般会是以一个线程池上同时打开几个HttpUrlConnection来下载图片的。
这里是一个典型的一个生成者/多个消费者的模式。
使用者通过getResponse或者read读取数据时,最终对应的是这个Connection对应的SpdyDataOutputStream实例,这个类中含有一个数据Buffer,如果数据够消费,则读操作顺利完成,否则会Wait在信号量上,直到收到足够数据。
如前所述,SpdyConnection在实例化时,即起了一个线程接管了从物流Socket按Frame方式读取数据。当获得数据帧时,在按协议解包以后它会通知SpdyConnection。SpdyConnection是Stream的管理者,它含有所有Stream的列表。接下来它根据StreamID获得表示Stream的SpdyStream对象,并把数据派发出去(receiveData)。最终收到数据的是SpdyDataOutputStream对象,这是一个输出流的实现类,它包括了一个数据Buffer和一个信号量,派发上的数据通知上来以后被放到数据中,然后Notify信号量,这样等待在这个OutputStream上的读数据者就被唤醒了。