HttpURLConnection 的API是阻塞是的API,通过创建一个写入阻塞发送一个请求,通过一个读取阻塞接受响应。
[源码分析:4.2] 框架连接FramedConnection
阻塞式API因其自上而下的程序代码而显得方便实用。网络调用跟其他普通方法调用一样:请求数据,返回。如果请求失败,则获得一个指向调用的堆栈跟踪。
阻塞式API可能会很低效,因为等待网络期间,线程处于闲置状态。但是线程是昂贵的,因为其建立在内存和上下文之上。
框架协议根spdy/3和http/2一样,不会导致自身阻塞API。每个应用层线程都希望因为特定流阻塞I/O,但是在Socket中,流是多路复用的。线程不允许直接和socket对话,而是要与共享这个socket的其他应用层线程合作。在单个阻塞线程上,直接实现spdy/3或者http/2的框架规则是不切实际的。流控制特性在读取和写入之间引进反馈,要求写入告知读取,读取节制写入。
OkHttp中,在框架协议上暴露了阻塞API,这篇文档解释了使其工作的代码和原理。
注:SPDY(读作“SPeeDY”): 是Google开发的基于TCP的应用层协议,用以最小化网络延迟,提升网络速度,优化用户的网络使用体验。SPDY并不是一种用于替代HTTP的协议,而是对HTTP协议的增强。新协议的功能包括数据流的多路复用、请求优先级以及HTTP报头压缩。谷歌表示,引入SPDY协议后,在实验室测试中页面加载速度比原先快64%。(参见附件《SPDY》)
应用层必须阻塞写入I/O,在我们将字节写入到socket之前,不能从一个写入中返回。否则,如果写入失败,我们将无法把一个IOException传递到应用中。我们应该告知应用层是否写入成功或者失败。
应用层也可以阻塞写入操作。如果应用程序请求从一个没有任何东西的地方读取数据,则需要持有那个线程,直到字节到达,流关闭,或者超时消逝。如果持有一些字节,但是没有任何其他地方需要他们,则应该将其放置到缓冲区中。从流量控制方面讲,在他们被应用消费掉之前,不应认为被传递出去了。假设有一个基于http/2的视频流。假设用户暂停了媒体播放,应用停止从流中读取字节。这个时候缓冲区会被填满,流控制会阻止服务器再往这个流中发送数据。当用户继续播放视频流时,缓冲区会被排放,读取被确认,服务端继续流出数据。
不能依赖应用线程从socket中读取数据。应用线程是短暂的: 有时候,他们正在读取或者写入数据,有时候,他们停止做有关于应用层的事情。但是socket是永久的,并且需要持续的关注,比如我们分发所有进入的框架帧,以使应用层需要它时连接正常。
所以,我们为每个socket创建专用线程,专门读取框架帧并分发他们。
读取线程始终不应运行应用层代码。否则,一个缓慢的流就会拦截整个连接。
同样的,读取线程始终不应该阻塞写操作,否则可能导致锁死连接。试想,客户端和服务端都违反了这个规则。那么,如果运气不好的话,他们会填满整个TCP缓冲区(导致写阻塞),然后使用它们的读取线程进行去写入帧。没有其他地方从另一端读取数据,缓冲区也会被消耗完。
有时候,会存在调用应用层或者响应一个ping操作的动作,而发现这个工作的线程和应该处理这些工作的线程不一致。我们就会在线程池中加入一个线程对象,并且由线程池中的另一个线程进行处理。
有三种不同的事件同步。
这种锁会守护每个连接的内部状态。这种锁不阻塞操作。这就意味着,我们获取一个锁,读取或者写入一些属性,然后释放锁。而没有I/O和应用层回调。
这种锁会守护每个流的内部状态。跟上面一样,是不阻塞操作。当我们需要持有一个应用线程阻塞读操作时,我们必须通知或者等待这个锁。因为当wait()处于等待状态时,这个锁会被释放。
Socket的写是被FrameWriter守护的。同一时刻只有一个流可以写入,所以消息不交叉。写操作要么由应用层线程完成,要么由填充池完成。
持有一个FrameWriter锁的同时,你可以持有一个SpdyConnection锁。但是,反过来不可以。因为持有一个FrameWriter锁可能引起阻塞。对于记账本来说,在创建一个流的时候这样做是有必要的。好的框架,要求在socket中,流的ID是连续的,所以我们需要和发送的流框架绑定设定的ID。