把 HTTP 当黑盒子来学

本篇梳理 HTTP 的相关知识,也分享个人学习经验。有认识不对的地方,欢迎指正!

前端开发的很多工作,似乎都是 “所见即所得” 的,这种对知识强大的 “既视感”,是前端容易入门的一个关键因素。但越往后走,一些底层而重要的知识,就没那么容易触及了,比如 HTTP 协议。

一开始,可能就只知道 Ajax 是浏览器端 HTTP 强相关的一套 API;另外,无论是浏览器端,还是服务器端,都有一套对 HTTP 的解析、处理的机制。HTTP可以说是前、后端通信的载体。在我们工作中、或者我们面试时,经常涉及的性能优化问题、跨域问题、安全问题等等,都和它紧密相关。

就是因为它更底层,逻辑对外不可见,知识的 “既视感” 也比较难实现(如果有哪个团队做一款这样的产品,那肯定会在前端圈里圈粉无数)。但无论如何,HTTP 是前端进阶应该攻破的一道坎。

尽管它不像页面 DOM 元素、或者一个 react 组件那样触手可及,但我们仍然应该有办法学习它。我们可以暂且不理会内幕细节,把它当做一个 “黑盒子”,而通过一些其他的途径,旁敲侧击。比如使用一些抓包工具(本文用的是 Fiddler ),也能很好的窥视 HTTP 的形貌。

1、认识 HTTP 报文

以访问“”站点首页http://www.jianshu.com/ 为例,通过 Fiddler 工具抓取到以下报文信息。

请求报文

GET http://www.jianshu.com/ HTTP/1.1
Host: www.jianshu.com
Connection: keep-alive
Cache-Control: max-age=0
User-Agent: Mozilla/5.0 Chrome/58.0.3029.110 Safari/537.36
Accept: text/html;q=0.9,image/webp,*/*;q=0.8
Referer: http://www.jianshu.com
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8

//本行及以下均为报文主体

无论请求报文,还是响应报文,都是有着严格的格式规范的,它们绝不是一堆毫无规范组织的字符串。正式这一套标准规范,让报文的解析、分割、组包可以用一定的算法实现,具体内幕本文不涉及。

响应报文

HTTP/1.1 200 OK
Date: Mon, 14 Aug 2017 12:36:22 GMT
Server: Tengine
Content-Type: text/html; charset=utf-8
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
ETag: W/"fffd6ab16cec9ff5e97d58a4d293073c"
Cache-Control: max-age=0, private, must-revalidate
Set-Cookie: _session_id=--4007a37778519a153aad82b63--; path=/; HttpOnly
X-Request-Id: 9c7aa92b-99b8-4e14-a87a-012d88e40608
X-Runtime: 0.230320
X-Via: 1.1 edianxin19:8 (Cdn Cache Server V2.0)
Connection: keep-alive
Content-Length: 56884





//余下报文主体...

HTTP 报文,基本还是符合 “所见即多得” 的,因为它包含的内容的的确确就是这样的。

值得强调的是 HTTP 报文的 “结构化”,是一个非常重要的特征,尤其是报文头部项,不仅是下文重点关注的对象,也是今后学习的一大重点。下图,以请求报文为例,再次感受这种 “结构化”。

把 HTTP 当黑盒子来学_第1张图片
请求报文结构

2、HTTP 的相关概念

两台计算机之间的通信是通过 TCP/IP 协议在因特网上进行的。可以借用一个 20 多年前的例子来理解,比如两个好友用信件联络,他们能建立通信的条件,一个是互相明确彼此的邮箱地址(IP),然后还需要一个邮差去分类和送达(TCP)。

IP: Internet Protocol 网际协议。每台计算机都有一个·IP,用来在 internet 上标识这台计算机。IP 协议负责将计算机之间传输的每个数据包路由至它的目的地。

TCP:Transmission Control Protocol 传输控制协议。顾名思义,是传输控制。TCP 负责在数据传送之前将它们分割为数据包,然后在它们到达的时候将它们重组,确保数据包以正确的次序到达。

HTTP 协议采用了请求/响应模型。客户端向服务器发送一个请求报文,请求报文包含请求的方法、URL、协议版本、请求头部(header)和请求体(body)数据。

服务器以一个状态行作为响应,响应的内容包括协议的版本、成功或者错误代码、服务器信息、响应头部(header)和响应体(body)数据。

HTTP 是应用层协议,是基于 TCP/IP 协议之上的一种应用。作为应用层的 HTTP 协议,通信过程依次会穿越传输层、网络层、链路层。详见下图:


把 HTTP 当黑盒子来学_第2张图片
http 报文的旅行

3、缩小关注范围

毫不讳言,学习 HTTP比较麻烦,除了它不像前端的其他领域知识那样 “既视感”强之外,还有就是面对一个理论性很强的知识,往往很难找到 “下口” 的地方,更别说轻易消化。而在一个信息泛滥的时代,阅后即焚、收集癖这些 “浅阅读” 形式更是我们深入掌握一些知识的头号敌人。

通常,对于获取知识,个人经验来看有这些层次:

  • 最浅层次莫过于浏览、收藏
  • 次浅层次是复制、截图、粘贴成笔记;
  • 速成(鼓吹)的方式是听课看视频;
  • 较深层次则是整理知识、梳理结构,画原理图、思维导图;
  • 更为深层而牢靠的则是实践、实操中Q&A和技术交流。

我们很难掌握某些知识,包括笔者亲身体会,就是大部分都停留在第 3 阶段,很少越过第 4 阶段。

到了第 4 阶段,就应该去划分边界,缩小关注范围,然后重点突破。就好比本文的梳理,至少可以明确,现在(及今后一段时间)的注意力,就完全可以集中在 HTTP 头部项及其包含的实现原理、客户端的 API (Ajax)、服务端的 API (完全可以通过 Nodejs 模块之http.js 来学习)。

其中,HTTP 头部项,可以慢慢积累,这里暂且不谈。本文下部分主要探讨通过客户端的 Ajax 来了解浏览器和服务器间的 HTTP 通信是怎样的。

浏览器端调试 HTTP

以一个问题开始

当客户端请求被 abort(取消)掉后,浏览器和服务器分别会如何处理这个请求?

这是个好问题,它立刻激发我的探究欲望。对于这个问题的验证,只需用上两个工具——浏览器和 Fiddler 抓包工具——就能搭建一个很好的验证环境。其中,浏览器中运行的 JavaScript 脚本非常简单,如下:

var xhr = new XMLHttpRequest();
xhr.open("GET", 'http://www.vrstarman.com/stack.html', true);
//状态变更
xhr.onreadystatechange = function(e){
    console.log('请求状态值变更:', xhr.readyState);
    if(xhr.readyState == '2'){
        //xhr.abort();
    }
};
//进行中
xhr.onprogress = function(e){
    console.log('请求进行时状态:', xhr.readyState);
};
//中断
xhr.onabort = function(e){
    console.log('请求取消事件:', e', '此时状态值:', xhr.readyState);
};
//加载完成
xhr.onload = function(e){
   console.log('请求完成事件:', e);
};
//错误
xhr.onerror = function(){
    console.log('请求错误时状态:', xhr.readyState);
};
xhr.send();

问题的变量因素有:

  • 在不同状态阶段 readyState 阶段 abort 掉请求;
  • 在程序的不同的位置执行 abort
  • 在这些 abort 测试中,验证的 url 分别为以下情形:
    • [v] 一个实际存在的 url
    • [ ] 一个不存在的 url
    • [ ] 一个跨域的 url

本文就用的首页地址http://www.jianshu.com/ 测试了第一种情形,并且已经收获颇丰。可以推测第二种情形和第一种差不多,但第三种会不同,非常值得验证。

验证结果及分析

readyState 状态含义解析 abort 位置
0 请求未初始化, 即 open() 前的状态 此时 abord,后续方法抛出错误
1 请求已经建立,但是还未send(),此时打印状态是1。 send()之后打印状态,结果还是1 (按理说应该是 2),可见状态变更是滞后的,说明是异步的 open()之后 send()之前 abord,会报错,故用try{ xhr.send(); }catch(e){ }捕捉错误
2 请求已send(),正在处理中,并已经接收到响应数据(通常浏览器可从响应中解析出请求头、响应头) 鉴于异步执行,将 xhr.abord() 放在 onreadystatechange 事件中
3 响应数据仍在接收和处理中,响应 body 已部分解析,但服务器还未全部发送完或浏览器还未全部解析完 readyState == 3abord(),由于异步执行的原因,实际请求已接近(或达到)状态 4
4 响应已完成接收和解析,可获取并使用所有响应内容 从以上操作看,一个请求,只要不在状态 2 之前取消,都会经历完整的响应阶段,即完成状态 4

进一步详细解读

达到了不同的状态值,浏览器和服务器都分别做了哪些动作,以下是上述探索的详细解读。

状态为 0

状态为 0 是被大多浏览器隐藏的。只在浏览器端 new 出了 xhr 对象,但还未建立连接就中断请求是没有意义的。

状态为 1

结果是无任何请求发出,且 打印的readyState0
为什么还是 0?虽然 open()已执行,但因立即打印状态,还没有收到已建立连接的返回消息,故立即打印的状态是变更前的。
Fiddler 此时未显示任何 http 请求。
浏览器显示 Provisional headers are shown,这与浏览器的可视化机制有关,真实情况浏览器并未发送请求。

状态为 2

readyState 状态值变更滞后(因为异步),给判断带来干扰
但测试表明放在 onreadystatechange 事件中的 abord(),会导致 readyState 值从 2 “跳” 到 4
浏览器已成功发送请求,并成功的解析了请求头、响应头。
Fiddler 监测出了 http 请求,表明服务器已成功发送请求数据。
但因为手动 abord(),导致浏览器接收到了(部分甚至可能全部)响应但放弃了解析响应的 body,所以页面无数据显示。

状态为 3

尽管是中途 abord(),但 readyState 完整经历 4 个状态变化。
onprogress 事件只激发了 2 次。
浏览器处于一个不确定是否完全解析响应的状态中,反复测试并打印 xhr.response 会发现有时解析了完整的数据,有时则在中间产生了中断。
服务器已经确定发出了(几乎)所有响应内容。
由于在状态 4 前中断,浏览器未确切是否已完成全部接收和解析,故在浏览器控制台的 Preview 中无法看到内容。

状态为 4

abord 会导致 xhr.responsexhr.responseText 被清空。
服务器端全部响应也发送完毕。
浏览器已经全部接受、并完成解析,这在浏览器控制台的 Preview中能看到。
但还是因为 abord(),导致了对响应 body 内容的情况,所以还是无法最终显示在 document 中。除非在 abord() 前执行 document.write(xhr.response);

其中,对 readyState == 2abord() 的情形,服务器到底有没有响应,若有,响应了什么内容?因为从浏览器的控制台无从确认,对于这一步的存疑,还好可以借助 Fiddler 的拦截,一目了然。

把 HTTP 当黑盒子来学_第3张图片
本图借用了一年前对笔者个人站点 www.vrstarman.com 的抓包截图

对于跨域的 url ,在下不同 HTTP 请求阶段被 abord 情况的探索,就暂不描述了。强烈建议读者一并探索下。

另外,HTTP 头部项、以及 HTTP 服务端的一些特性的学习,后续找时间再写。

你可能感兴趣的:(把 HTTP 当黑盒子来学)