在之前的博客中我们知道Okhttp在发起链接请求先从链接池中获取连接,如果链接池中没有链接则创建新的链接RealConnection对象,然后执行其connet方法打开SOCKET链接(详见《 OkHtp之ConnectInterceptor简单分析》):
rivate RealConnection findConnection(。。。){
result = new RealConnection(connectionPool, selectedRoute);
result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
}
看看RealConnection的connect方法都做些了什么:
public void connect(。。。) {
//如果协议不等于null,抛出一个异常
if (protocol != null) throw new IllegalStateException("already connected");
。。 省略部分代码。。。。
while (true) {//一个while循环
//如果是https请求并且使用了http代理服务器
if (route.requiresTunnel()) {
connectTunnel(...);
} else {//
//直接打开socket链接
connectSocket(connectTimeout, readTimeout);
}
//建立协议
establishProtocol(connectionSpecSelector);
break;//跳出while循环
。。省略部分代码。。。
}
//当前route的请求是https并且使用了Proxy.Type.HTTP代理
public boolean requiresTunnel() {
return address.sslSocketFactory != null && proxy.type() == Proxy.Type.HTTP;
}
简单来说上面connect主要做了如下的事儿: 1、如果当前route的请求是https并且使用了Proxy.Type.HTTP代理,就开启一个隧道链接。 2、如果1不成立,则调用connectSocket方法创建Socket链接 3、调用establishProtocol构建协议。
为了方便后续博文的说明,先简单说一下connectSocket方法都干了些什么:
private void connectSocket(int connectTimeout, int readTimeout) {
Proxy proxy = route.proxy();
Address address = route.address();
//1、初始化socket
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
? address.socketFactory().createSocket()
: new Socket(proxy);//使用SOCKS的代理服务器
rawSocket.setSoTimeout(readTimeout);
//2、打开socket链接
Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
//3、对输入和输出做处理
source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
}
1、如果没有使用代理或者使用了HTTP代理,那么通过Address对象的socketFactory来创建一个Socket:注意在构建Address对象时候如果在初始化OkhttpClient的时候没有build SocketFactory,那么把address对象的socketFactory引用初始化为 : SocketFactory.getDefault()。
2、如果使用了SOCKET代理,则初始化一个Socket
3、调用Platform.get().connectSocket实际就是调用socket的connect方法来打开一个连接
4、然后将socket的输入流inputStream交给 Okio.buffer(Okio.source(rawSocket))也就是Source对象,输出流outputStream对象交给 Okio.buffer(Okio.sink(rawSocket))即Sink对象。 由Okio对连接进行读写数据(关键Okio的这两种方法,会另开博文解释),此处只需要简单的理解为source对象为读取服务数据,sink为向服务器发送数据即可。
简单的来说connectSocket方法其作用就是打开了一条Socket链接!
下面就来分析requiresTunnel方法,在分析该方法之前先回顾 HTTP的一些基础知识: 什么是隧道呢?隧道技术(Tunneling)是HTTP的用法之一,使用隧道传递的数据(或负载)可以是不同协议的数据帧或包,或者简单的来说隧道就是利用一种网络协议来传输另一种网络协议的数据。比如A主机和B主机的网络而类型完全相同都是IPv6的网,而链接A和B的是IPv4类型的网络,A和B为了通信,可以使用隧道技术,数据包经过Ipv4数据的多协议路由器时,将IPv6的数据包放入IPv4数据包;然后将包裹着IPv6数据包的IPv4数据包发送给B,当数据包到达B的路由器,原来的IPv6数据包被剥离出来发给B。
SSL隧道:SSL隧道的初衷是为了通过防火墙来传输加密的SSL数据,此时隧道的作用就是将非HTTP的流量(SSL流量)传过防火墙到达指定的服务器。
**怎么打开隧道?**HTTP提供了一个CONNECT方法 ,它是HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器,该方法就是用来建议一条web隧道。客户端发送一个CONNECT请求给隧道网关请求打开一条TCP链接,当隧道打通之后,客户端通过HTTP隧道发送的所有数据会转发给TCP链接,服务器响应的所有数据会通过隧道发给客户端。 (注:以来内容来源参考《计算机网络第五版》和《HTTP权威指南》第八章的有关内容,想深入了解的话可以查阅之。) 关于CONNECT在HTTP 的首部的内容格式,可以简单如下表示: CONNECT hostname:port HTTP/1.1 准备工作完成之后,继续回到OKhttp的代码上来。
private void connectTunnel(int connectTimeout, int readTimeout, int writeTimeout)
throws IOException {
//1、创建隧道请求对象
Request tunnelRequest = createTunnelRequest();
HttpUrl url = tunnelRequest.url();
int attemptedConnections = 0;
int maxAttempts = 21;
//一个while循环
while (true) {
//尝试连接词说超过最大次数
if (++attemptedConnections > maxAttempts) {
throw new ProtocolException("Too many tunnel connections attempted: " + maxAttempts);
}
//2、打开socket链接
connectSocket(connectTimeout, readTimeout);
//3、请求开启隧道并返回tunnelRequest
tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url);
//4、成功开启了隧道,跳出while循环
if (tunnelRequest == null) break; /
//隧道未开启成功,关闭相关资源,继续while循环
//当然,循环次数超限后抛异常,退出wiile循环
closeQuietly(rawSocket);
rawSocket = null;
sink = null;
source = null;
}
}
上面的代码大致做了如下工作: 1、调用createTunnelRequest构建Request对象:
private Request createTunnelRequest() {
return new Request.Builder()
.url(route.address().url())
.header("Host", Util.hostHeader(route.address().url(), true))
.header("Proxy-Connection", "Keep-Alive")
.header("User-Agent", Version.userAgent())
.build();
上面代码是是创建了一个Request对象,只不过这个对象增加了Host、Proxy-Connection、User-Agent首部。 Host首部:该首部主要是为了解决Http/1.0缺少主机信息(主机名和端口号)导致虚拟服务器不可用的问题(详细解释可参考《HTTP权威指南》第18章)。 Proxy-Connection:主要是解决(不支持Keep-alive首部的)代理服务器盲目转发Keep-alive给服务器,造成客户端挂起的问题(详细解释可参考《HTTP权威指南》第4章).
2、进入while循环,首先调用connectSocket打开TCP链接。而后调用createTunnel创建一个隧道,有意思的是这个方法放回一个Requset对象,当返回的Request对象不为null的时候说明隧道打通,退出while循环,否则就关闭当前循环的socket,继续循环直到隧道请求成功或者循环次数超出指定的值而异常退出。
所以看看这个createTunnel方法都做了些神马:
private Request createTunnel(int readTimeout, int writeTimeout, Request tunnelRequest,
HttpUrl url) throws IOException {
// 拼接CONNECT命令
String requestLine = "CONNECT " + Util.hostHeader(url, true) + " HTTP/1.1";
while (true) {//又一个while循环
//对应http/1.1 编码HTTP请求并解码HTTP响应
Http1Codec tunnelConnection = new Http1Codec(null, null, source, sink);
。。。
//发送CONNECT,请求打开隧道链接,
tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
//完成链接
tunnelConnection.finishRequest();
//构建response,操控的是inputStream流
Response response = tunnelConnection.readResponseHeaders(false)
.request(tunnelRequest)
.build();
。。。。。
switch (response.code()) {
case HTTP_OK:
return null;
case HTTP_PROXY_AUTH://表示服务器要求对客户端提供访问证书,进行代理认证
//进行代理认证
tunnelRequest = route.address().proxyAuthenticator().authenticate(route, response);
//代理认证不通过
if (tunnelRequest == null) throw new IOException("Failed to authenticate with proxy");
//代理认证通过,但是响应要求close,则关闭TCP连接此时客户端无法再此连接上发送数据
if ("close".equalsIgnoreCase(response.header("Connection"))) {
return tunnelRequest;
}
break;
}
}
}
上述方法作了如下几个工作: 1、拼接HTTP的 CONNECT信息 2、通过Http1Codec 的 writeRequest向TCP链接发起打开隧道请求。 3、构建服务器响应的Resopnse,如果状态码是200,说明CONNECT请求成功,如果是HTTP_PROXY_AUTH,说明需要进行代理认证,认证失败则抛异常,如果成功且服务器响应 了close,则返回tunnelRequest;如果返回其他状态码,则继续while循环。
到此为止,RealConnection打开隧道链接和打开Socket链接简单分析完毕。 至于最后的establishProtocol方法,目前还在研究中,后续会继续说明。 本篇博文有好几处“详细参考《HTTP权威指南》第xx章和《计算机网络》”的地方,本来也想详细写的,但是写多了觉得太啰里啰嗦,影响文章调理,所以有关网络的相关概念,还是请参考上面两本书,不一定仔细看完,大致翻翻即可。到此本篇博文简单介绍未必,如有不当之处,欢迎批评指正共同学习。