这本书主要包含5个部分:

  • web基础构件与http核心技术
  • web系统结构
  • 识别认证与安全
  • http报文主体与web标准
  • 内容发布与重定向

我目前这个阶段的话,读完整本书的感觉,重点是在第一部分。第一部分重点讲解了http的报文结构,这是非常重要的,后面部分中介绍的很多机制都是建立在首部字段、请求方法、状态码上面的。随后又讲了http的连接管理,这也是非常重要的章节,可能面试被问到的概率不高,反正我没遇到过,但是很值得学习。
web系统结构主要讲代理、缓存代理、隧道、网关、web机器人、应用服务器等web组件。原谅我学的浅,这一部分其实没怎么看懂,只知道个概念,并没有多少实际的感觉。而且像工程实践中的反向代理、正向代理、代理为什么可以跨域,书中知识较少。这点可以理解,书只有几百页,不可能这么全。
识别、认证、安全这部分,识别和认证应该是有点out了,但是后面讲了点数字证书、ssl的东西,还是实用的。

web基础构件与http核心技术

http 是个很叼的协议,现在的web应用能这么火爆,功不可没,那么为什么它可以这么牛逼呢?

肯定少不了天时地利人和,我没有去考古哈,只是自己的一个看法,首先这是一个请求-应答式的应用层协议,底层使用的tcp(http3基于udp,但是也保证了)可靠地传输数据。其次是通过http应用程序可以请求不同协议的资源,极大的方面了人们的使用,有一统江湖的感觉。还有持久连接、结合MIME的多类型传输、缓存机制、幂等性设计、重定向机制、内容协商等http自身核心的功能。最后还有一些扩展的重要功能,结合密码体系的安全能力。

http 协议采用请求-应答模式,http 客户端向服务器发送 http 请求,指定服务器中的某个资源,执行特定操作,服务器处理后将一些信息放在 http 响应中发送给 http 客户端。

资源
web 资源的话不一定是静态文件,还可能是根据需要实时生成内容的软件程序、文件系统、搜索引擎、扫描资源的网关,这些都是web资源。
http 要传输多种类型的数据,需要为各种数据打上标签,标签采用的是mime(多用途因特网邮件扩展)类型,用来描述并标记多媒体内容。http 客户端收到响应后,通过 Content-Type 字段中的 mime 类型值来处理主体。现代浏览器可以处理数百种常见数据类型:图片、html、音频、视频、表格等等。
mime 由一个主要对象类型和一个特定的子类型组成,中间用正斜杠分隔。主要有:

  • text,html、plain ascii文本文档。
  • image,jpeg、gif 图片。
  • video,quicktime 类型。
  • application,vns.ms-powerpoint 类型。

每个服务端的资源都有一个名字,客户端通过 uri 从网络上找到 web 资源。uri 可以在全网唯一标识并定位资源。uri,统一资源标识符,有两个子集,url、urn。
url,统一资源定位符。url 描述了特定服务器上某个资源的特定位置。
urn,统一资源名。更厉害,可以与特定位置解耦。urn 作为特定内容的唯一名称来使用,可以将资源四处搬移。比如网络标准文档,用 urn 命名,urn:ietf:rfc:2141,不论它在什么地方都可以用这个名字找到它。现在的技术很难实现,需要一个牛逼的架构来解析资源的位置。

事务处理
一个 http 事务就是一个请求和一个响应。b/s、c/s 通过 http 报文的格式化数据块进行通信。
http 报文是由一行一行的字符串组成的。http 报文都是纯文本,纯纯的 ASCII 字符,不是二进制代码。解析工作就很容易出错,需要很多技巧。
http 报文由3部分组成,起始行、首部字段、主体。

连接
http 不负责b/s、b/s的网络连接,这部分细节是交给 tcp/ip 进行的。通过解析后的ip和端口号建立socket连接到目标机器对应端口服务上。请求和响应报文都以空行来分隔主体和其他部分。

web结构组件
web 中应用程序不止是 b/c/s,还有代理、缓存、网关、隧道、agent 代理等等应用程序。
代理,是web安全、应用继承、性能优化的重要组成模块。代理主要转发c/s之间的流量,可以对请求、响应进行过滤。
缓存,就是将源web服务器上的web资源存放在距离c端更近的地方,提升用户访问速度,经典的有 cdn。
网关,是一种特殊的服务器,可以连接不同的web应用程序。比如 http/ftp 网关应用,可以接受一条 http 请求,去 ftp 服务器上获取资源,再封装成一条 http 报文,返回给用户。
隧道,在两条连接中间进行盲转发应用程序,确保转发的数据不会被窥探。经典 ssl。
agent 代理,能发起 http 请求的客户端程序。浏览器也是代理。还有网路爬虫、网络机器人。@todo

思考,为什么 http 可以这么火?@todo

资源与url

url 描述了所需资源在网络中的位置信息。“方案://服务器位置/资源路径”。
url 没出来之前,人们要获取不同的资源,需要选择不同的应用程序,比如获取文件,需要用 ftp 客户端。读取邮件,需要电子邮件程序。看新闻需要新闻阅读器。通过 url 这种技术,浏览器可以知道在访问不同资源的时候采用合适的协议和方法,并且使用何种方式处理资源,呈现给用户。简化了用户面对多媒体数据的操作过程。

url 的语法结构。
<scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<query>#<frag>
用户名和密码
有的服务器会要求用户输入账号密码才允许访问,比如ftp服务器。如果没有携带账号密码,会插入一个默认的。
params是啥,和query有啥不同。todo
params
对于很多方案来说,只有主机名、端口和资源路径,还不能准备达到资源所在的位置。应用服务程序需要这部分参数来更精确的定位资源。比如 ftp 有两种传输模式,二进制和文本模式,可以通过这样的方式。
ftp://prep.ai.mit.edu/pu/gnu:type=d
资源路径部分还可以分成路径片段,插入自己的参数。
http://www.joes-hardware.com/hammers;sale=false/index.html;graphics=true
query
比如有的资源,需要数据库服务器,那么需要传递查询参数来缩小、确定查询资源的范围。
frag
有的资源除了资源本身的定位,还可以对资源内部进行定位,比如 html 文件,可以定位到其中的某个章节或者某个位置。http 请求不会包含 frag,浏览器会对这个 frag 进行处理,将资源定位到 frag 指向的位置,显示该部分资源。

相对 url 与绝对 url
相对 url 使用它所在文档的绝对 url 进行解释。通过所在文档的绝对 url,推导出自己的协议名、主机名、资源路径。浏览器要具备这种解析能力。有的浏览器还会对 url 自动扩展,主机名、浏览历史。

字符规则
url 需要满足可移植性、可读、完整性。
不同的网络协议能处理的字符是不同的,比如 smtp 协议会剔除一些特定的字符。为了满足可移植性,就必须选择一个不同网络协议能处理字符的共同子集。
不可见、不可打印的字符虽然处于最小共工字符子集中,可以放在 url 中用于协议之间传输数据,还想可读性好,那么就不能在 url 中使用。
除了最小的通用安全字符子集,人们有时还想传输其他的二进制数据或字符。那么就需要一种转义机制满足这种完整性需求。
最开始采用的 ASCII 字符,方便的是英文打字机案件和少数不可打印控制字符。后来又集成了转义序列,通过转义序列,就可以用 ASCII 字符集对任意字符或数据进行编码

编码的方式就是通过一种转义的表示方式来表示不安全的字符。用一个百分号,后面跟着两个表示字符 ASCII 码的十六进制数。例如,~ 转义为 %7E

报文结构

报文总是从发送端流向接收端,发送端就叫上游、接收端就叫下游。

报文的首行、首部都是ASCII文本,以LF、CRLF分隔。然而主体就可能是ASCII文本或者二进制数据了,也可能为空。
请求行,表示了服务端需要用哪个版本的 http 对指定的资源执行指定的方法。请求行中的值都以空格隔开。
请求方法,常见的有 get / post / head / put / delete / trace / options
状态码,100-101,信息提示。200-206,成功。300-305,重定向。400-415,客户端错误。500-505,服务端错误。

首部有4种,通用、请求、响应、实体。首部中时间日期格式都是GMT格式。
首部字段之间用空格间隔。首部最后一个字段后跟一个CRLF。用这个CRLF表示一个空行,标志首部的结束,主体的开始。http 服务器和客户端就是通过这些间隔符来截取报文的各个部分的。过长的首部可以换行,第二行前是制表符。

主体部分就是 http 报文的负荷,要传输的内容。http 可以承载很多数据类型,图片、视频、音频、脚本、样式表文件、程序、电子邮件等等

请求方法
http 服务器并不一定需要实现所有请求方法,根据自己的需求和功能来实现。
http 定义了安全方法,像 get / head 方法不应该产生动作、副作用,也就是说这个请求不会引起状态的变化。然而具体实现中也有用 get 方法执行其他操作的。

get,请求服务器发送某个资源。
head,服务器响应与 get 相似,除了不返回主体部分。用户可以利用这个方法,在获取实际资源前,对资源类型、资源是否存在、资源是否更新 进行判断。
put,会根据主体的内容去更新或创建由 url 指定的资源。因为允许用户修改服务器内容,很多服务器会要求执行 put 之前用密码验证。
post,向服务器发送数据,服务器可以根据这些数据进行存储、计算、查询操作,不一定像 put 一样只能存数据。
trace,浏览器发出的请求可能经过很多中间代理、网关,这些中间节点都可以修改报文,trace 方法可以判断中间节点是如何修改请求报文的。最后一个收到 trace 请求的服务器会将原始报文添加到响应报文中。就可以查看这个请求的一个请求/响应链,以及中间节点对这个报文进行的修改。todo 描述还有问题。
但是它假设了中间节点对各种方法的处理是相同的。然而很多中间节点应用程序对不同方法的处理方式是不同的。例如代理可能直接转发post请求,但是会将 get 请求转发到缓存节点。trace 方法并不能区分这些方法。
options,询问服务器支持哪些方法,支持哪些首部,就不用实际的去访问资源才知道能不能访问,是一种预处理的优化方式吧。
delete,删除 url 指定的资源,但是服务器并不一定执行。

状态码
总共有 5 种状态码,1开头的信息性状态码,2开头的成功状态码,3开头的重定向状态码,4开头的客户端错误状态码,5开头的服务器错误状态码。状态码方便了客户端理解 http 事务的处理结果。

信息性状态码是 1.1 版本引入进来的,所以要做版本兼容,特别是代理节点。
100,Continue,收到了请求的初始部分,请客户端继续发送主体数据。
101,switching protocols,服务器按照客户端的要求,将协议切换成了 upgrade 首部指定的协议。

100 状态码是一种优化手段,常用于客户端要发送一个很大的消息主体的时候,先发送一个报文,携带上 Expect: 100 Continue 首部,判断服务器是否能处理掉这么大的报文,避免浪费流量。如果服务器能够处理,在收到这条报文后,会返回 100 状态码,不能处理就返回错误状态码。
早期服务器对 Expect: 100 Continue 有一些误解,可能不会返回响应,所以客户端超过一定时间之后,就该把消息主体发送出去。相应的,服务端也有可能在返回状态码之前就收到消息主体。这时候服务器就不用再发送 100 状态码了,只需要发送一个最终的状态码。
todo 服务器不能处理会返回什么错误状态码呢?
代理如果不知道下一跳服务器是 http 哪个版本的,就应该直接转发 Expect: 100 continue 首部,如果它知道下一跳服务器只兼容 1.1 之前的版本,那就向客户端返回 417: Expectation Failed 错误码。更好的做法不是返回 417,而是代理向客户端返回 100 报文,向服务器转发请求报文的时候删除 Expect 首部。
代理决定转发 1.1 版本以前的客户端请求的话,并且做这个优化的话,就应该在请求中加上 Expect: 100 Continue,而且不能将 100 响应报文返回给客户端,因为 1.1 之前的客户端并不知道怎么处理。

200,OK,请求被处理,响应报文中包含了所需资源。
201,Created,服务器资源创建好了,响应报文中包含引用了这个资源的 url,location 首部包含具体的引用 url。
202,Acceptd,请求被接受但未处理,响应报文中应该包含预计处理的时间,或者对这个请求其他的描述。
203,Non-Authoritative Information,返回的资源来自中间节点的资源副本,而且没办法对这个资源相关的元信息进行验证。
todo 元信息如何验证。
204,No Content,请求被成功处理,但是响应报文里没有消息主体。刷新一个表单页面,就不需要显示新的文档。todo 试试。
205,Reset Content,成功执行了客户端发起的一个限定了范围的请求。比如请求一个文档的部分内容。响应报文必须包含 Content-Range / Date / ETag / Content-location 首部。
todo 客户端配置哪些首部可以做到范围内请求?

重定向状态码
告诉客户端资源被移走了,或者资源未更新,仍然可以使用客户端的缓存的副本,响应报文中 location 可以表示资源新的地址。
url 可能指向多个列表,此时S端会返回一个列表。或者必须使用代理来访问,location 会标明代理的 url。
300,Multiple Choices, 某个 url 可能指向多个资源,比如一份文档,有多个语言版本。返回的报文中有一个可选的列表。客户端可以提前沟通好,响应中 location 会标识出首选的 url。
todo 客户端如何提前沟通,17章。
301,Moved Permanently,请求的资源被永久移走了,location 中应该包含了资源现在所在的 url。
302,Found,资源也是被移走了,但是 location 中的 url 可以定位资源。询问用户是否用相同的方法向新 url 发送请求。1.0 版本的定义,没有保证事务的幂等性,实际实现中考虑到了幂等性,造成定义与实现的差异。
303,See Other,资源被移走了,下次请求用 location 中最新的 url 来请求。post 请求的响应会将客户端定位到新 url 上去。
304,Not Modified,资源验证请求验证服务端资源是否有更新,如果没有更新就返回 304,并且不带主体。
305,Use Proxy,这个请求必须通过代理,代理的地址在 location 给出了。这个状态码只针对该条资源请求,不是说所有对源服务器的资源请求都要通过代理,只是这条请求必须经过代理。
307,Temporary Redirect,同302。

302、303、307 有一些交叉,这是因为 1.0 / 1.1 不同版本对这些状态码的处理方式不同。1.0 中定义客户端发送 post 请求,收到 302 之后,会询问用户是否以相同的方法向新 url 发送请求。然而实际实现中,为了保证事务的幂等性,像 post 方法,浏览器一般以 get 方法向新的 url 发送请求。所在在 1.1 中规定了 303,解决了 1.0 中存在的二义性问题,非幂等的方法收到 302 后,用 get 方法发送新请求。然后用 307 状态码取代了 302 状态码,指示客户端请求方法幂等的时候,向新 url 发送请求时请求方法不变。
这样实现之后,服务器就要根据客户端的 http 版本来选择返回哪个重定向状态码了。

客户端错误状态码
400,Bad Request,客户端发送了一个错误的请求。
401,Unauthorized,客户端请求应该携带认证信息。
403,Forbidden,请求被服务端拒绝,服务端一般不会告知原因的时候使用。
404,Not Found,服务器没有找到 url 定位的资源。
405,Method Not Allowed,请求方法对于 url 指定的资源不能用。在 Allow 字段中提示这个资源应该用什么请求方法。
406,Not Accepteable,客户端请求的资源类型,服务端没有相匹配的。
407,Proxy Authentication Required,也是需要认证信息,不过是代理服务器返回的响应。
408,Request Timeout,客户端请求花费的时间太长,服务端结束了这次 http 事务。

服务端错误状态码
服务端或者中间节点出现错误,代理一般有用到502、504。
500,Internal Server Error,服务器内部错误。
501,Not Implemented,客户端请求超出了服务器的能力范围,比如服务器不支持的方法。
todo 那不是和405一样了吗?
502,Bad Gateway,代理或网关没法连接上下游。
503,Service Unavailable,服务暂时不可用。响应中可以包含 Retry-After 首部,提示什么时候资源可用。
504,Gateway Timeout,代理或网关等待上游的响应超时了,返回504给客户端。
505,HTTP Version Not supported,服务器收到的请求使用了它不支持的方法。
todo 那和501 有啥区别呢?
首部

通用首部
提供了报文的基本信息,在客户端、服务端、其他中间应用程序之间提供通用的功能。

  • Connection,指定与连接相关的选项,upgrade、keep-alive、close。
  • Date,报文创建的时间。
  • MIME-Version,MIME 版本。todo 与 Content-type 有什么关系?mime 还有版本的说法?
  • Trailer,与 http 分块传输编码有关。todo 15.6.3详讲了分块传输编码。
  • Transfer-Encoding,为了保证报文的可靠传输,告知接收端,报文采用了什么编码方式。
  • Upgrade,想要升级的协议或版本。
  • Via,报文经过的中间节点。
  • 通用缓存首部,Cache-Control、Pragma。

请求首部
针对请求报文的内容向服务端进行一些说明,说明了是谁在发送请求、请求源自何处、客户端的一些信息。有信息性首部、Accept 首部、条件请求、安全首部、代理首部。

信息性首部

  1. - Host,给了能接收请求的服务都的主机名和端口号。
  2. - Referer,提供了包含当前请求 uri 的文档的 url

Accpet 首部
将客户端的喜好、能力告诉给服务器。想要什么、能用什么、不想要什么。涵盖字符集、编码方式、数据类型、语言。

条件请求首部。
为请求加上限制,有用于缓存控制、资源范围限定。

安全请求首部。
在获取特定的资源前,需要对自身进行认证,增加安全性。Authorization、Cookie、Cookie2。

代理请求首部。
Max-Forward、Proxy-Authorization、Proxy-Connection。

响应首部
向客户端提供一些信息,比如谁在发送响应、响应的功能。比如 Server、Age。

  1. - 协商首部,如果资源有很多种表示方式,比如同一篇文章有法语版、德语版。http/1.1 提供了对资源进行协商的能力。Accept-Ranges,对此资源来说,服务器可接受的范围类型。Vary,一个首部列表,服务器会根据这些首部的内容挑选出最合适的资源版本返回。todo
  2. - 安全响应首部,Proxy-AuthenticateSet-CookieSet-Cookie2WWW-Authenticate

实体首部
主要用来描述报文主体,提供了有关实体及其内容的大量信息,包含对象的类型、编码、语言、长度、位置、校验和、资源的协商缓存控制。

  1. - 内容首部,Content-(BaseEncodingLanguageLengthLocationMD5RangeType)。
  2. - 实体缓存首部,ETagExpiresLast-Modified

扩展首部,用处也很多,虽然不是标准。

状态码、首部字段,还有基于这些语法结构,是建立起来的诸多重要功能的基石,比如持久连接、协议升级、分块传输、请求完成标志、同源检测、状态管理、缓存、支持不同数据类型。

连接管理

浏览器收到 url,先将主机名从url中解析出来,查询主机名对应的 ip 地址,根据 ip 地址和端口号,建立一条到 ip:port 的 tcp 连接。然后发送一条 http 报文,接收服务端的响应报文,浏览器关闭连接。tcp 会按序、无差错的传输 http 数据。

tcp 的数据是通过 ip 分组这种小数据块来发送的。http 要发送一条报文的时候,会以的形式将报文数据的内容通过一条打开的 tcp 连接按序传输。tcp 收到数据流之后,会将数据流砍成被称为段的小数据块,并将小数据块封装在 ip 分组中,通过因特网进行传输。这些工作都是 tcp/ip 软件完成的,http 程序员只需要将报文内容以流的形式写进 tcp 连接中。
ip 分组中包含一个ip分组首部(20字节)、一个tcp段首部(20字节)、一个tcp数据块(0或多个字节)。

tcp 连接通过端口号来保持这些连接的正确运行,通过端口号可以确保连接到正确的应用程序上。tcp 连接通过4个值来唯一定义一条连接<源 ip、源 port、目的 ip、目的 port>。不同连接的部分值可以相同。
socket 套接字,是操作系统提供用来操作 tcp 连接的工具。这套 api 向 http 程序员隐藏了 tcp 和 ip 的细节,包含 tcp 的握手过程、tcp数据流的分段、ip分组的重装。通过这套 api,可以与远端服务器的 tcp 端点建立连接,并对数据进行读写。

  1. - s = socket
  2. - bind(s, <local ip:port>)
  3. - connect(s, <remote ip:port>)
  4. - listen(s, ...)
  5. - s2 =accept(s)
  6. - n = read(s, buffer, n)
  7. - n = write(s, buffer, n)
  8. - close(s)
  9. - shutdown(s, <side>)
  10. - getsockopt(s, ...)
  11. - setsockopt(s, ...)

image.png
tcp 性能问题
http 紧挨 tcp,http 事务的性能很大程度上取决于 tcp 通信通道的性能。从 tcp 的性能问题出发,了解到 tcp 的连接特性后,才能更好地理解 http 的连接优化特性。

http 事务时延分析,从浏览器收到 url 到关闭连接,一共包含这 6 部分的时间,dns 查询、tcp 握手、发送http请求报文、服务器处理请求、服务器返回响应报文、关闭 tcp 连接后的 time_await 时间
tcp 网络时延与硬件速度、网络与服务器的负载、请求与响应报文的尺寸、两端之间的距离有关。

性能问题主要聚焦在5个方面tcp 握手、tcp 慢启动拥塞控制、数据聚集的 Nagle 算法、tcp 延迟确认算法、time_await 时延和端口耗尽

tcp 三次握手主要有两个职责,通过交换报文段,确认双方的通信能力、同步各自的确认序列号。如果 tcp 连接只用来传输很少的数据,这些交换过程将会占据传输数据中的大部分。一般的 http 事务不会交换太多的数据,一个 ip 分组就可以承载整个 http 响应报文,而且很多 http 服务器响应报文都可以放进一个 ip 分组中去。在因特网流量中,ip 分组通常是几百字节,本地流量中 ip 分组为 1500 字节左右。那么小的 http 事务可能会在 tcp 建立上花费 50% 甚至更多的时间。todo 那么大文件如何传输呢?
通过复用 tcp 连接可以解决这个问题。

tcp 通过确认机制确保数据成功传输。每个 tcp 段都有一个序列号和完整性校验和。 接收者在收到完好的段时,会向发送者回送小的确认分组。如果发送者在窗口时间内没有收到确认消息,会认为数据段被损毁或丢弃,重发这个数据段。由于确认消息很小,所以将确认消息放在要返回的数据分组中,这样的方式可以更有效的利用网络。延迟确认算法的做法就是在特定的窗口时间(一般100~200毫秒)到了之后,将确认消息存放在发送缓冲区中,找一个能够捎带它的输出数据分组。
http 的请求-应答行为具有双峰特征。这种特征降低了向输出数据分组捎带确认消息的可能性。当客户端发送完请求报文后,就一直接收分组,也没有需要发的数据分组,延迟算法甚至会产生负作用,导致较大的时延。可以在操作系统中调整或禁用延迟确认算法。

tcp 慢启动也会影响 tcp 的传输性能。tcp 刚建立连接的时候发送窗口为1,每成功传输一个分组,拥塞窗口就增大一倍,下次发送成功两个分组,就增大到4,随着成功传输的数据越多,窗口会打开的越大,呈现一种指数增长的趋势。这样的拥塞控制是为了防止网络的突然过载和拥塞。但是这样的话,新建立的 tcp 连接肯定会比工作了一段时间的 tcp 连接慢。
复用 tcp 连接解决了这个问题。

Nagle 算法与 Tcp_Nodelay
tcp 有一个数据流接口,应用程序可以通过这个接口将任意大小的字节数据放进 tcp 栈,即时只有1个字节。但是如果每个 tcp 段都至少要装载 40 个字节的首部,那么如果 tcp 发送了大量包含少量数据的分组时,网络的性能会严重下降。
Nagle 算法延迟了发送数据的时机,将数据分组缓存起来,新数据到来时判断缓存数据的长度以及是否存在发送未确认分组,将大量的 tcp 数据绑定在一起发送。Nagle 算法鼓励发送全尺寸长度的段。这种算法分为3种情况,第一,如果发送缓冲区中待发送分组达到全尺寸长度,就直接发送,第二,如果没达到全尺寸,并且发送缓存区中有发送未确认的分组,那么将那部分数据缓存起来,在达到几百字节后再发送,第三,没有发送未确认分组,就会直接发送数据,尽管不是全尺寸的。Nagle 算法解决了大量小数据包场景的性能问题。这个算法不止这一种形式,还有多种变体。
Nagle 算法有1个问题,首先,小的 http 报文没法填满一个分组,而且没有数据要发了,还会迟迟等待上一个发送未确认分组的确认消息,而不是立即发送,延迟确认的话时延就是100~200毫秒。
粘包就是从此而来。

time_await 累积和端口耗尽
time_await 累积造成的性能影响最严重。当 tcp 连接关闭时,会在内存中维护一个小的控制块,用来记录最近关闭连接的 ip/port。这类信息会维持 2msl 时间,一般2分钟,现在时间会小很多,这是因为以前的路由器速度很慢。
这样做的目的是为了防止在 2
msl 时间内创建、关闭又重新创建两个完全相同的连接。因为在 2*msl 时间内,关闭又重新创建的连接可能会收到历史fin报文,会照常处理掉,那么新开的 tcp 连接又开始关闭,tcp 正常通信过程会被严重干扰。所以关闭后的这段时间内不能立即重用这个连接,要利用这段时间去消化掉在网络中迷路的历史报文,确保连接正常关闭。这个正常关闭的意思,也就是挥手的报文双方都要正常收到并处理掉。
time_wait 状态限制了端口的复用,那么就只有分配新的端口。根据4元组,只有源端口可以发生变化。假如客户端可用端口有6w个,time_wait 时间是 120s,那么一秒最多发 500 次请求。如果服务器没有做高并发处理,客户端每秒请求数在这之上,那么就遇到这个问题了。特别是在单台客户端对服务端做压测的时候。
time_wait 大量累积,证明内存中也会有大量的控制块,有的操作系统的速度也会变缓。

http 连接
http 允许c/s 之间有多个中间节点。connection 字段有3种值,其他首部字段名、任意标签值、close。如果是第一种值,那么中间节点在转发的时候必须删除报文中对应的首部字段。
中间应用程序收到带有 connection 字段的请求报文后会将其解析并应用。但是转发给下一跳的时候会删除 connection 中列出的首部字段。而且还有一些 Proxy-*、Transfer-Encoding、Upgrade 字段会被删除。
以上的操作被称为“对首部的保护”。

http 仅使用 tcp 短链接的话,多个事务串联的时延是叠加的。有4种技术可以降低时延,并行连接、持久连接、管道化连接、复用连接。

并行连接不一定快,比如现在网速慢,已经有一个连接占用了较多的带宽,此时又来3个并行加载,那么这3个连接去平分剩下的一点点带宽,也没有什么大的提升。通常来说,并行连接并没有真正的提速,只是利用了客户端剩余的可用带宽,只不过实现了多个资源对象的并行加载,让用户可以看到同时在进行多个加载,给人感觉要快一些。

持久连接机制
在 http 事务处理完之后,仍保持 tcp 连接状态为打开,未来的 http 事务仍重用这个 tcp 连接,这种保持打开的 tcp 连接就是持久连接。与并行连接相比,持久连接减少了 tcp 建立连接的时延和开销,将连接保持在已调谐状态,避免了慢启动过程。现在一般的都是持久连接与并行连接一起用。
持久连接有两种,http/1.0 的 keep-alive 和 http/1.1 的 persistent。keep-alive 连接在早期实现中受到一些互操作性设计方面问题的困扰,在 persistent 中得到修正。

keep-alive 减少了创建连接和关闭连接的开销,而且避免了慢启动阶段,请求和响应的时间也得到了缩减。c 端通过 connection: keep-alive 首部头将一条连接保持在打开状态。如果 s 端愿意为下一条请求将连接保持在打开状态,就在响应中包含相同的首部,如果响应头中没有,c 端就认为服务器不支持 keep-alive,在发回响应报文后就关闭连接。
connection:keep-alive 首部只是请求将连接保持在活跃状态,发起 keep-alive 请求后,c/s 端并不一定会保持连接,可能会在任意时刻关闭空闲的 keep-alive 连接,并且可以限制这个 tcp 连接能处理的 http 事务的数量。timeout 在响应头部的 keep-alive 字段中设置,估计 s 端希望连接保持打开状态的时间,并不是承诺值。max 估计 s 端希望这个 tcp 连接处理的 http 事务数。还支持一些自定义的属性。
存在的限制与规则

  1. - 想使用 keep-alive,必须设置相应的首部字段 connection: keep-alive。每一条 http 报文都要带上这个首部字段,要不然客户端或服务会在收到某个不带这个首部的报文后将连接关闭。
  2. - 需要带上正确的 content-length 或使用分块传输编码方式,才可以保持连接在打开状态。如果没有正确的长度提示,那么无法确定 http 事务是否已完成,只能等待服务端主动断开连接来表示这次事务结束。那么想要使用 keep-alive,就需要给出明确的报文长度和传输编码方式,不然根本无法知道这次事务何时结束,下次事务何时开始,甚至造成数据混乱。
  3. - 代理和网关必须在将报文转发出去或资源缓存前,将报文的 connection 首部命中的所有首部和 connection 首部自身删除掉。todo 为什么呢?删除了的话,如何和源服务器建立持久连接。不应该与不知道是否支持 connection 首部的代理服务器建立持久连接,可能会出现哑代理问题。应该忽略掉 http/1.0 中的 connection 首部,很有可能是老代理误转发的。
  4. - 如果在客户端收到响应之前连接就关闭了,只要请求不会产生其他富足用,那么客户端应该做好充实请求的准备。

哑代理
connection: keep-alive 只能支持单跳,也就是客户端和服务端输出方向上的 tcp 连接。为什么只能支持单跳呢,原因就在代理节点。很多的老代理或简单代理都只是盲中继,只是将数据盲目的转发出去,并不会做首部进行处理。这就导致了老代理不认识 connection: keep-alive 的真正奥义,并没有建立起代理到双方的持久连接。
带有 connection:keep-alive 的报文被代理转发到服务器,服务器将这条连接保持在打开状态,客户端在收到代理转发的报文,也将自己的连接保持在打开状态。然而无情的代理,在转发完之后,就以及关闭的连接,傻傻的客户端马上发送下一个http事务,代理直接丢弃,客户端只有等待,直到超时自己把连接关了。为了避免哑代理问题,现代的代理绝对不能转发 connection 包含的首部以及自身,还有一些 proxy-authenticate、proxy-connection、transfer-encoding、upgrade 等字段。
为了解决盲中继问题,网景公司让浏览器发送 proxy-connection 首部给代理,如果代理是盲中继代理,那么服务端收到这个首部,判断不出意义所在,就会忽略掉,如果代理是现代代理,那么会将 proxy-connection 替换为 connection,服务端就会建立与代理的持久连接。这样的作法只能解决只有一个代理的情况。然而现在网络中中间节点太多了,防火墙、拦截缓存、反向代理加速器等等,最重要的是这些代理服务器正确识别并建立持久连接,也就是说还是要从代理本身出发,才能根治哑代理问题。

http/1.1 persistent
对 keep-alive 进行了修正。

  1. - http/1.1 持久连接默认激活持久连接,要想不建立持久连接,要在首部中显示设置 connection: close。就算不设置,http/1.1 设备也可以在任意时刻关闭连接。
  2. - http/1.1 代理必须能够管理自己的持久连接。http/1.1 的代理不应该和 http/1.0 的客户端建立持久连接,除非它连接客户端的处理能力。
  3. - 一个客户端对服务器或代理最多只能维护两条持久连接,防止服务器过载。
  4. - 其他的规则和限制与1.0无异。

管线化请求
持久连接消除了 tcp 连接时延,管线化的目的是消除传输时延。

  1. - 必须首先确定这个 tcp 连接是持久连接,才能用管线化。
  2. - 回送的响应报文的顺序应该和请求报文发送的顺序相对应,如果失序了就没法和请求相匹配。
  3. - 如果 tcp 连接突然被关闭,要准备好重发所有未完成的管线化请求。
  4. - 管线化方式没法安全重发 post 这样的非幂等请求,那么通过管线化发出去的非幂等请求可能在出错时永远不会被执行。todo 为什么不能发送?

关闭连接
何时、如何关闭连接是 http 非常重要且实用的点。任意关闭连接、cotent-length及截尾操作、连接关闭容限,重试以及幂等性、正常关闭。
http 设备可以在任意时刻关闭 tcp 连接。通常是在一条 http 请求结束时关闭。否则只能是服务器怀疑客户端网络故障,才能在请求中关闭连接。服务器可以在任意时刻关闭持久连接,但是关闭的时候并不知道对端有没有数据要发送,或许就在这一刻,对端刚好有数据发送了过来。那么客户端在写入数据的时候会报错,连接错误。所以 http 设备应该做好这种突然关闭的准备。如果请求没有副作用,就应该重新打开连接,重发请求。如果是管线化方式,那么要重发的量就大了。
副作用的意思是改变了其他资源的状态,获取资源就是没有副作用的,删除文件就是具有副作用的。如果是订单就不行了。幂等性的意思就是说一个事务,反复执行的副作用都是相同的,那么这个事务就是幂等的。get / head / put / delete / options / tracke 方法都是幂等的。而 post 不是幂等的。
如果连接出错,一些已经发出去的请求不知道执行了哪些,在重发非幂等方法之前需要等待前一条请求的响应。所以管线化的方式不行难搞。
响应中应该包含正确的 content-length,告诉客户端,这条 http 报文何时结束,否则只有服务关闭连接才知道这个 http 事务结束了。如果是缓存代理收到了不正确的 content-length,就不要缓存这个报文,原封不动的转发出发。
close 会关闭输入和输出通道,shutdown 可以单独关闭输入或输出通道。分别对应“完全连接”和“半连接“。简单的应用可以只是用完全关闭。使用持久连接时,使用半连接防止对端收到非预期的写入错误。先关闭输出通道是安全的。如果在不知道对端是否还有数据要发送的时候,关闭了输入通道是危险的,对端还在往里面写输入数据,突然收到一条 rst 报文,对端操作系统就会将接收缓冲区中未读的数据都删了。最好的作法是先关闭输出通道,在周期性检查输入通道,如果一段时间内没有数据,那么可以强制关闭连接,节省资源。

web系统构成

web服务器

服务器与操作系统负责管理 tcp 连接。操作系统负责底层硬件细节,提供 tcp/ip 网络支持、装在web资源的文件系统、进程管理功能。
web服务器处理请求过程:

  1. - 建立连接
  2. - 接收请求
  3. - 解析 http 报文
  4. - 访问资源
  5. - 创建 http 响应报文
  6. - 发送响应
  7. - 记录事务处理过程到日志文件中

建立连接,就涉及到识别技术,通过 ip 地址或客户端主机名识别。ident 协议携带了客户端用户名,可以通过这种方式确定客户端身份,但是 ident 协议安全性不好。
接收请求,从网络中读取到数据后,将报文中的内容解析出来,先是解析请求行,方法、资源标识符、版本以空格分割,请求行与首部以换行回车符隔开,检测到有以换行回车结尾的空行时,以 content-length 为准,读取报文主体。通过 tcp 传输的数据是一个字节流的形式,解析的时候会不断从网络中接收到数据,在读取的时候需要将数据临时存放在内存中,直接收满为止。
考虑到服务的高并发场景,有4种实现方式,单线程 io,多线程 io,复用 io,多线程复用 io。也可以是单进程或多进程的。
处理请求,根据据方法、资源、首部、主体对请求进行处理。
访问资源,将请求中的 url 映射到服务器上。服务器上有一个根目录,将 url 转换为这个根目录下的绝对路径,来访问对应绝对路径下的资源。一个服务器可以有多个web站点,根据 host 设置站点自己的文档根目录 docroot,通过 host 的 ip 地址来识别要访问的站点的根目录。动态内容资源的映射。
构建响应,如果产生了响应主体,就将内容放在响应报文中,响应头就要包含 content-type、content-length。如果是重定向,location 包含了资源的新地址或者优选出来的 uri,永久删除、临时删除、负载均衡。
发送响应,存在多个连接的情况,需要记录连接的状态。非持久连接,服务端应该在发送完报文后关闭这一端的连接。

代理

代理就是客户端与服务端通信的中间人,负责中间转发,同时作为 http 客户端和服务端两个角色。代理有两种,私人代理、公共代理。
http 代理是相同协议间的通信,而网关可以进行不同协议间的通信,连接两个或多个使用不同协议的端点。实际上代理和网关的区分并不需要很明确。有的代理也需要转换 http/1.0 和 http/1.1,也有的代理会实现网关的功能来支持 ssl 协议、防火墙、ftp 访问。
由于代理的特殊位置,处于通信的中间地带,可以用来做过滤响应内容、统一访问控制、在代理上限制应用层协议的流量、web缓存、反向代理、内容路由器、转码器、匿名者。
反向代理和一般代理有什么区别?
内容路由器可以将流量导向特定的web服务器。
代理服务器在将数据发送给客户端之前可以转换数据的编码格式。
匿名者代理服务器会删除报文中的 cookie / referer / from 等信息。

代理可以部署在本地网络的出口,isp 访问点,网络边缘作为web服务器的替代,因特网对等交换点上,合理的使用代理可以降低带宽费用,提高访问速度。
代理之间一般是3层结构,靠近客户端的代理是子代理,靠近服务器的是父代理。

web缓存

4大优点,减少了冗余的数据传输、缓解了带宽瓶颈、降低了原始服务器的要求、降低了传输时延。
本地网络和因特网的带宽差异是很明显的。从一个局域网的缓存中获取一份副本比从远程服务器中获取的速度快得多,使用缓存可以有效改善广域网有限的带看造成的网络瓶颈。
使用缓存才可以缓解瞬间拥塞。一些突发事件按可能会导致很多人同时访问一个 web 服务器,这种情况造成的峰值流量可能会让网络和服务器出现崩溃。
即时带宽不是问题,服务器也很优秀,但是距离太远依然存在问题。每台路由器都会增加时延,在信道上的传输也会有时延。从旧金山传数据到东京,如果是很复杂的页面,可能就需要几秒钟的时延。

命中与未命中
缓存服务器会在未命中的情况下,发起一个小的再验证请求,检测副本的新鲜度,如果没有变化,服务器会返回一个小的 304 报文。这种叫做”再验证命中“或“缓慢命中”,因为要和原始服务器通信,所以比直接命中慢一些,但是没有获取对象数据,所以要比未命中快一些。
缓存命中率,又叫文档命中率,指的是缓存处理的服务请求所占的比例,中等规模的web缓存来说,40% 的命中率很合理。但是由于文档的尺寸是不一样的,有的文档很大,但是被请求的次数比较少,但是贡献度是很大的。提出了“字节命中率”作为度量值,缓存提供的字节占所有字节的占比。
缓存直接命中和从原始服务器获取,响应报文状态码都是 200。客户端可以通过 Date 首部来判断是否用的缓存。客户端也可以通过 Age 首部来检测报文是否来自缓存。Age 首部表示报文在多久前被创建,单位是秒。

缓存的结构
缓存也分为私有缓存和公有缓存。浏览器就内置了私有缓存。公有缓存的是就是一个服务器了。公有缓存可以有效减少冗余流量,对于流行的对象,代理只会向原始服务器请求一次,后续用户都是请求代理获取到对象数据。
在靠近客户端的一侧使用小一点、廉价的缓存,靠近服务端的一侧使用功能更加强大的缓存来存放更多的资源。算上浏览器上的私有缓存,一般采用三级层次结构。

缓存的处理
对一条 get 报文的基本缓存处理包含7个步骤,接收、解析、查询、新鲜度检测、创建响应、发送、日志。
接收,缓存从网络中读取出请求报文。
解析,缓存对报文进行解析,提取出其中的 url、首部。
查询,缓存检查本地是否有可用的副本,如果没有,就请求原始服务器获取副本,并保存在本地。缓存会将从原始服务器获得的响应主体和响应首部都保存下来,在命中时,用来构建正确的响应报文,还会保存一些元数据,比如被使用次数、缓存了多久。
新鲜度检测,缓存查看已缓存副本是否新鲜,如果不新鲜,就询问服务器是否有更新。
创建响应,用新的首部和已缓存的副本构建一条响应报文。
发送,通过网络发送给客户端。
日志,创建一条日志来描述这个事务。

新鲜度的检测
服务器并不需要知道哪些缓存拥有自己资源的副本,就可以实现资源副本和资源的一致性。通过文件过期、服务器再验证机制。

文档过期
通过 Cache-Control / Expires 首部给文档添加过期时间。在过期时间前,就直接使用缓存。
Expires 指定的是服务器的具体时间,如果改变电脑的时钟,两者之间时间造成偏差,会导致副本过期,向服务器请求资源。Cache-Control: max-age 是相对时间,以秒为单位。

服务器再验证
仅仅是时间过期,并不一定证明这个副本与服务器的资源就不一致了。时间过期之后,就要进行资源的核对,询问服务器资源是否发生了变化。如果源服务器上资源发生变化,缓存节点会获得新的资源,并缓存下来,发送给客户端。如果没有变化,那么只需要产生一个新的过期时间,更新副本的过期时间。
缓存节点就只有客户端请求的副本过期的时候才会想服务器请求验证。节省了服务器的流量,加快用户访问速度。
http 允许缓存向原始服务器发送一个条件get,只有服务器资源与副本不同的时候,才返回资源。总共有5个条件请求首部,其中最有用的是 if-modified-since / if-none-match,其余有 if-match / if-unmodified-since / if-range。
if-modified-since 为真,也就是资源在这个日期后被修改了,那么就会返回新的资源,包括新的过期时间。如果为假,那么返回304,包括新的过期时间,不包括报文。可以和 last-modified 配合使用,将资源最后的修改日期添加到响应报文中。有的服务器并不是真的基于时间做比较,而是直接进行字符串匹配。
if-none-match 进行实体标记验证。根据最后修改时间有两个问题,有的资源可能改变后内容还是一样的,但是最后修改时间变了,或者有的改变可能在毫秒间发生,秒的时间单位内没法判断。http 允许给资源添加一个标记,这个标记可以是版本号、序列号、内容校验和、指纹信息。服务端改变资源后,更新资源相应的标记,这时 if-none-match 为真,执行 get 方法,客户端获取到修改后的资源,不为真的话,就收到一条 304 报文,接着用本地副本。

控制缓存
http 主要定义了 cache-control、expires 首部来控制缓存的行为,资源是否缓存、缓存的有效期。根据优先级排列,no-store / no-cache / must-revalidate / max-age / expires
no-store,禁止对资源进行缓存,每次都向服务器请求。no-cache,会将资源缓存在本地,不过要请求服务器再验证。
todo pragma:no-cache。
must-revalidate,响应首部。有些时候缓存也可以提供一些过期的资源,但是如果服务器想按照严格的来,那就让资源过期后必须要进行再验证请求,就给指定资源添加这个响应首部。
max-age,响应首部,从服务器将资源传来的时候开始算,这个资源有效的秒数。s-max-age 一样的作用,不过只作用于公有缓存。max-age = 0,类似于 no-cache,会进行再验证。
expires,响应首部,设置过期时间,以秒为单位。有两个缺点。
如果没有设置这些缓存配置,浏览器会采用一种试探性过期算法,如果文档中有最后的修改时间,那么以这个时间为开始截取一段时间间隔,以这个时间间隔的 20% 作为这个文档的有效时间。一般的时间间隔是一周。如果在时间间隔内文档有新的 Date 时间,那么就用 Date 时间减去上次最后修改时间,取 20% 作为新的有效期。
如果最后修改时间都没有,那可以设置一小时的有效期,或者设置有效期为0,每次都去验证资源是否新鲜。
通过一些指令放松或强化新鲜度的要求,max-stale / min-fresh / no-cache / no-store / only-if-cached。max-stale,缓存可以提供过期的文件,可以提供秒数,在这段时间内就不算过期。min-fresh,至少在未来多少秒之内应该保持有效。

网关

用HTTP访问不同资源的方法,开发者如何将HTTP作为框架启动其他协议和应用程序通信。
在HTTP 和其他协议及应用程序之间起到接口作用的网关;允许不同类型的Web应用程序互相通信的应用程序接口;允许用户在 HTTP 连接上发送非HTTP流量的隧道;作为一种简化的HTTP代理,一次将数据转发一跳的中继。

网络中的资源从最开始的静态文档,到数据库内容或者动态生成的html页面。http 应用程序提供了一种统一的方式来访问不同类型的资源。但是单个应用程序没法处理所有类型的资源。网关就像一个翻译器,作为资源和应用程序之间的粘合剂。应用程序可以通过 http 接口或其他协议的接口请求网关,网关返回相应的资源。
客户端网关统一不同协议客户端的请求,用 http 协议与服务器通信。而服务端网关则统一了不同协议的服务端,客户端用 http 协议与服务端网关通信。

协议网关
将HTTP流量导向网关时所使用的方式与将流量导向代理的方式相同。最常见的方式是,显式地配置浏览器使用网关,对流量进行透明的拦截,或者将网关配置为替代者(反向代理)。

可以通过服务端网关加密所有输入服务器的请求,提供安全保护。客户端使用普通的http浏览文档,服务端网关使用 https 加密用户与原始服务器之间的通信。

还可以用 http/https 网关做安全加速器,放在原始服务器之前,作为拦截网关或反向代理。接收安全的 https 流量,将其解密为普通 http 请求,发给原始服务器。

资源网关
协议网关是通过网络连接客户端和服务器。而最常用的网关,应用程序服务器,是将原始服务器与网关结合在一个服务器中实现,应用程序服务器就作为服务端网关,与客户端通过 http 通信,并与服务器端的应用程序相连。

客户端通过 http 连接到应用程序服务器,应用程序服务器没有回送文件,而是将请求通过网关应用编程接口发送给运行在服务器上的应用程序。由这个接口收集应用程序的输出数据,放在 http 响应中回送。

第一个流行的应用程序网关 api 就是通用网关接口,CGI,common gateway interface。

请求需要用到应用程序网关的时候,服务器会辅助应用程序处理请求。比如将整条 url 或者查询参数发送给应用程序。应用程序处理完之后会将响应或者响应数据发送给应用程序服务器,由这个网关回送给客户端。

CGI应用程序是独立于服务器的。基本的运行机制就是网关将输入的请求转交给CGI,然后将应用程序输出的数据回送给网关。
通过CGI分离服务器与应用程序,为服务器和众多资源间提供了简单、函数形式的粘合方式,用来处理各种需要的转换。还可以很好的保护服务器,降低了程序对服务器的影响。
然而,这种分离会造成性能的耗费。服务器需要为每个 CGI 请求开启一个新进程,新进程的开销是很高的,会限制服务器的性能。快速CGI,通过接口模拟CGI,作为持久守护进程,避免了为每个请求创建新进程。todo 那么他是如何调度处理多请求的呢?

大多数服务器会提供扩展 api,绑定在服务器自身的结构上,是服务器自身特有的。开发者可以通过这些接口改变服务器的行为,或者尽可能的提升服务器的性能。

应用程序接口与web服务
http 连接不同应用程序,棘手的问题在于如何协商两个应用程序之间的协议接口,只有商议好协议接口,才能进行数据的交换。
应用程序之间配合工作,所需的信息比 http 首部复杂的多。todo 19章

隧道

web 隧道允许用户通过 http 连接发送非 http 流量,可以在 http 连接中嵌入非 http 流量,这样,这类流量就可以穿过质询与 web 流量通过的防火墙。

隧道通过 http 的 connect 方法建立,该方法请求隧道网关创建一条到达任意目的服务器和端口的 tcp 连接,并对客户端和服务器之间的后续数据进行盲转发。

ssl 隧道,其中的 ssl 流量就包含在普通的 http 报文中。客户端发送 connect 报文给隧道网关,网关与指定ip和端口的服务器建立好 tcp 连接后,返回 200 报文,原因短语一般为 connection established。
加密的 ssl 流量无法通过传统的代理进行转发,ssl 隧道网关可以接收普通 http 报文,解析其中的 ssl 流量,穿过端口80 的 http 防火墙。将原始的加密数据放在普通 http 报文中,通过 http 连接传送。ssl 隧道网关将其中的 ssl 流量解析出来后与服务器建立普通的 ssl 连接。

ssl 隧道网关与 http/https 安全网关不同的是,ssl 会话是建立在客户端与服务器之间的,不需要中间的隧道代理实现 ssl,只是将加密的数据经过隧道传输。

隧道网关无法验证目前使用的协议是否就是它原本打算经过隧道传输的协议。为了降低对隧道的滥用,可以限定网关可以打开隧道的端口。

中继

中继是没有完全遵循 http 规范的简单 http 代理,只负责处理 http 中建立连接的部分,然后对字节进行盲转发。中继这样的代理,并不会执行任何首部和方法逻辑,有可能会提供简单的过滤、诊断和内容转换。但是这样的代理方式存在严重的互操作问题。比如哑代理问题。

web机器人

web 机器人也就是自活跃的用户代理。能够在用户不干预的情况下,自动发起请求,处理响应的程序。
爬虫从根集出发,获取根集中的页面,解析出其中的超链接,再获取超链接关联的页面,递归的处理下去。根集可以包含几个页面,从这几个页面出发,应该能最大限度的覆盖爬虫想要爬取的所有页面。

在解析的过程中,需要注意2个方面的问题,url 的处理、避免陷入循环。
url的处理。相对 url 需要转换为绝对 url。超链可能组成一个循环,需要记录 url 避免这种循环。记录大量 url 需要用到适合快速查询的数据结构,比如搜索树、散列表。单台服务器可能还没有足够的内存、磁盘、计算能力,需要用到集群。两个不同的 url 指向同一个页面的情况,这两个 url 互为别名,可以通过规范化 url 来解决,规范化只能消除基本的语法别名。
除了超链造成的循环,文件系统的符号连接也会造成循环。机器人没法通过 url 判断出文档是否相同,直到请求的 url 超出爬虫或服务器的限制才会结束。网关应用程序可能会构造出带有虚假 url 的页面或者 符号连接,组成一个循环,让爬虫陷进去。

为了尽量避免这2个问题,可以采用以下手段,规范化 url、广度优先爬取、节流、限制 url 的长度、url 黑名单、模式匹配(不允许url某部分内容重复出现)、内容指纹(对内容进行压缩,计算出校验和)、人工监视。

爬虫也是遵守 http 规范的,应该带有能识别自身基本信息的首部。为了避免获取重复的内容,还可以实现条件请求。还需要处理常见的状态码。

没有约束或存在问题的机器人会造成严重的后果,使服务器过载,拒绝服务。请求大量无效内容(url 失效)。请求很长但是无效的 url,消耗服务器的性能。容易访问到敏感的数据(数据筛选机制,丢弃敏感数据)。数据来源可能是开销较高的网关应用程序。

拒绝机器人访问标准,针对机器人制定的一个约定,哪种机器人可以访问,可以访问哪些内容。站点有一个robots.txt文件,遵守标准的机器人应该先获取这个文件。这个文件包含多个记录,每个记录由user-agent、disallow、allow三种行组成。用 get 方法获取这个文件,需要对不同响应做相应处理。robots.txt 这个文件是可以缓存的。
robots.txt 这个文件只能用来控制站点的不同页面的访问权限。不能控制页面中各个部分的访问权限。想要限制机器人对页面具体内容的访问有两种方法,meta 标签发起 robots 指令,<meta name="robots" content=directive-list>,directive-list 很多指令。
还有 description meta 标签和 keywords meta 标签,对内容搜索引擎机器人很有用。

搜索引擎中的爬虫为引擎提供信息。引擎创建好与文档相关的索引,现在的 web 规模非常大,页面可能都有几十亿个,同时搜索引擎的用户量也很大。如果请求串行处理,那处理时间就高了去了。需要具备并行处理的能力,并使用集群来分担任务量。
现代搜索引擎会建立全文索引数据库,用户通过搜索网关,使用关键字查询全文索引数据库。

搜索引擎会根据web页面中特定的单词,对搜索的结果进行相关性排名。搜索引擎对搜索结果的排序是很重要的,严重影响到用户的使用。从站点的角度出发,当然是想让用户搜到最满意的结果。从其他页面站点的角度来讲,希望自己的排名尽可能的靠前。
这样的一种情况,这就引发了搜索引擎的实现者和其他页面的实现者之间的博弈。恶意的行为有可能发生,页面实现者可能使用很多与页面内容不相干的关键字,或者假冒页面来欺骗搜索引擎的相关性排序算法。

识别机制、认证机制、安全机制

识别技术

个性化需求。希望对用户有更多的了解,或者记录下用户浏览页面的足迹信息。站点可以为一个用户提供个性化的配置,比如个性化的问题、针对性的推荐、关键信息的存档、记录会话的持续性状态。

用户识别机制有这5种,http首部、客户端ip、用户登录认证、胖url、cookie。

from / user-agent / referer / authorization / client-ip / x-forwarded-for / cookie。from 包含了用户的邮箱地址,但是可能会被不道德的服务器搜集起来发垃圾邮件,所以这个首部很少用。user-agent 包含的是浏览器、操作系统的信息,与特定的用户并没有多大联系。referer 首部提供了用户来源页面的 url,并不能完全标识用户。但是提供了用户浏览页面的行为,以及一些附加信息。

client-ip,用客户端的 ip 唯一标识一个用户不太现实。ip 定位的一台机器,而不是用户。很多机器的 ip 是动态分配的。而且很多机器的出口可能都是同一个 ip,真实的 ip 隐藏在出口 ip 后面。中间代理也会自己的 ip 地址。
在内网可能可以这么做。

服务器可以主动要求用户输入账号密码进行认证。http 用 www-authenticate 首部和 authenorization 首部向站点发送用户的登录信息,账号和密码是需要加密的,但是还是不够安全。登录上之后,后续的每一条请求都会带上登录信息,这样就在整个会话期间维持了用户的身份。如果一个站点在用户访问时要让用户先登录,一般就会返回一个 401 报文。
todo 存在各种攻击。

胖url,站点为每个用户生成特定的 url 来标识用户的身份。这样做的话就需要对每一个链接做动态修改,维护 url 中的状态信息。这种 url 如果被盗用,信息会泄露出去。服务器需要重写页面中的链接 url,以及返回的一些 url。如果用户跳转站点或者请求了一个特定的 url,那么这次会话也就到此结束了。这种方式在会话期是非持久的,就是说关闭了这个页面再访问站点,会要求重新登录,只有打开之前用的 url 才不会丢失会话。

cookie,一般与前面的技术共用,实现额外的价值。cookie 主要有两种,会话 cookie 和持久 cookie,主要的区别是他们的过期时间不同。用来维护用户访问站点的配置文件和登录信息的一般都是持久 cookie。cookie 的基本思想是让浏览器积累一组服务器特有的信息,每次访问服务器都将这些信息提供给它。
cookie 的存储方式,属性,域属性,控制这个cookie允许发送给哪些站点或站点的指定路径。
浏览器只会向访问站点发送这个站点产生的 cookie。很多第三方厂商会在站点内植入持久 cookie,用户访问另一个由同一家广告商提供服务的站点时,浏览器就会发送早先设置好的第三方厂商的持久 cookie。广告商公司就可以将 cookie 和 referer 首部结合起来,在内部构建起针对某个用户的浏览记录存档。

会话跟踪,通过一系列的重定向、url 重写、cookie 附加标识信息。

cookie 与缓存
如果响应报文中有 set-cookie 首部。有的缓存节点会将 set-cookie 首部删除再缓存,客户端就没有 cookie 了。最好服务器在向缓存节点的报文中添加cache-control: must-revalidate, max-age=0,强制要求缓存节点向原始服务器再验证这个请求。
缓存节点在处理带有 cookie 首部的请求时,响应内容应该是不可缓存的。缓存带有 cookie 首部的图片,将过期时间设置为 0,强制每次都要进行再验证。

基本认证机制

认证就是要出示身份证明,比如身份证件、自己的密码、口令。这些方式策略都不能绝对信任,只能提高信任度。
http 利用自身请求-应答的特点,通过一组控制首部,提供了基本的认证机制。第一条请求没有认证信息,服务器发送 401 响应报文,包含 www-authenticate 首部,要求用户携带上认证信息。客户端重新发送请求,报文包含 authorization 首部,包含了登录认证信息。服务器验证成功,返回 200 响应报文,附带 authentication-info 首部。
在 www-authenticate 首部中可能包含安全域。服务器会限制不同用户能访问的资源,将不同访问权限限制的资源组织为不同的安全域。比如说普通员工不能访问公司的财务信息,那么普通员工访问的时候就会要求 401 进行身份认证。

代理节点有自己的一套认证首部,如果需要客户端进行身份验证,返回 407 报文。其他首部依次加上 proxy- 。认证步骤一样的。

基本认证的缺陷就有点多,有 4 个缺陷。用户账号密码信息一般用 base64 编码,规则很简单,很容易截获明文信息。就算采用非对称加密,也防不住重放攻击。基本认证无法防止报文被篡改。假冒服务器很容易欺骗用户,获取到用户的账号密码。

一般将基本认证和 ssl 配合使用。但是也拦不住被篡改、中间人攻击、非法服务器冒充。

摘要认证机制

针对基本认证的缺点,做了4点改进。永远不以明文传输密码。防重放。可选择性的防止报文被篡改。可防范常见的攻击。todo 哪几种?

摘要认证比基于公钥的机制弱一些。并不能完全满足安全的需求。但是比基本认证强。然而摘要认证还是没有被广泛应用。

客户端不发送密码,只发送密码的摘要。客户端发送摘要,服务端提前知道所有用户的密码,通过相同摘要计算,比较两个摘要是否一致。摘要是不可逆的。
然而防不住词典攻击。

随机数是一种特殊的令牌,这个数会随每毫秒或每次认证发送变化,在密码中加入随机数,能让值摘要随着随机数一起变化。

摘要认证的握手过程使用了和基本认证一样的控制首部,authorization-info 是用于摘要认证的。三步握手机制。服务端要求客户端进行认证,返回 401 报文,随机数、安全域、算法,放在 www-authenticate 首部中,客户端从算法中选出自己支持的算法,将所选算法、密码摘要、客户端随机数放在 authorization 首部中,服务器对摘要进行计算验证,生成自己的摘要、下一个随机数,发送给客户端进行验证。

摘要计算。
随机数的选择,rfc 2617 规范建议使用时间戳+ETag+private-key生产随机数,可以通过实践戳限制随机数的有效持续时间,ETag 可以防止对已更新资源的重放请求。private-key 是服务器自己才知道的数据。实际实现中可以让 post、put 请求使用一次性的随机数或摘要,get 请求使用时间戳。
todo 13.5章节继续介绍随机数的选择。

安全机制

http 是不安全的,为了处理一些重要的事务(包含敏感信息、支付、转账等等),将 http 与数字加密技术结合起来。数字加密技术比基本认证和摘要认证都要强大。
什么是安全呢。无非就是提供以下功能,认证功能、报文完整性、加密防窃听。同时还要保证效率、普适性、扩展性。

数字加密是一个体系,从小到大的有,密码、密钥、对称密钥加密系统、非对称密钥加密系统、混合加密系统、数字签名、数字证书。
密钥对于密码来说,就是加密算法的一个输入参数,可以改变加密的过程,让每次密文的输出都不一样。

对称密钥加密,就是加密和解密使用相同的密钥,常见的有DES、triple-DES、RC2、RC4
密钥的长度是对称密钥加密算法是否可以被破解的一个关键因素。枚举攻击就是通过尝试所有可能的密钥值来破解加密算法的方法。
可用密钥的数量和密钥的位数有关,就是2的位数次方,有的加密算法的密钥不一定所有位数都用来生成密钥。40位的对称密钥已经可破解,最好用128位的。

对称密钥有2个问题,通信双方需要在通信前交换相同的密钥,每对通信实体都需要一对密钥。服务器与n个人通信,相当于要管理2的n次方个密钥。

非对称密钥加密,加密和解密使用不同的密钥,每对通信实体没有单独的密钥。避免了对称密钥的管理问题。非对称密钥面临的问题是,确保私钥的保密性,不能泄露、不能被推算。公钥公开的、明文、密文都知道了,也不能推算出私钥。
rsa 算法可以满足私钥保密性。rsa 的有效密钥数量不一定是所有位数次方,密钥和质数计算有关。

混合加密,一般采用对称和非对称策略,首先通过非对称加密建立好安全通信,然后通过安全通道传输参数值,用于双方产生相同的对称密钥。todo 这样的话还是面临管理密钥的问题。

数字签名,对报文进行标记,是谁发送的报文,并能保证报文的完整性。签名是一个加了密的校验和。只有发送者才有加密的私钥。校验和只有发送者的私钥才能加密生成。
用于数字签名加密的私钥可能被盗用。证书机构有“吊销列表”,记录被盗走的私钥。伪造私钥也是不可能的,因为客户端一般会内置证书,证书验证就通不过。

数字证书。包含了受信任的个人、组织的信息,主要信息有,证书有效期、证书拥有者的公钥、证书的发布机构、数字签名。客户端可以通过证书验证服务端的身份,还从证书中获取公钥。

https 就是将数字签名、数字证书、ssl 结合起来。ssl 是二进制协议,使用 443 端口号。https 客户端与服务器 443 端口建立tcp连接,然后初始化 ssl,生成会话密钥。一般都会经过代理转发报文。http 代理是防火墙唯一允许进行 http 流量交换的设备,可能会进行病毒检测或其他内容控制工作。报文在客户端加密后发送出去,代理是看不到报文首部的,报文的所有信息都被加密。只有握手时候的服务器域名可以被看到。
为了让代理能正常转发 https 流量,那么可以使用 ssl 隧道协议。提前告知代理,想要连接的主机和端口。
todo,客户端到代理的 tcp 连接肯定只能到代理的 ip 和 port,代理到服务器又要重新获取服务器的 ip 和 port。所以代理确实没办法从加密的报文中知道目标服务器的 ip 和 port。只能用隧道协议。
todo,但是b乎上又说ssl隧道协议和 ssl 协议不是一回事。

证书的校验主要包括日期检测、证书发布者检测、签名验证、域名验证,还会访问 ca 服务器,查询吊销列表和证书在线状态。

实体、编码、国际化

这部分主要讲 http 报文主体,以及主体装载的消息内容。消息内容的格式、语法,允许世界各地的人互换内容的标准(包含不同语言的字符集构成),协商内容的机制。

实体与编码

http 可以传输图片、文本、视频、音频、html、程序,各种类型的数据,这些数据被转换成一样的 ascii 字符发送,那么在接收端要能正确识别消息类型、大小,才能正确解析出数据。除此之外,还可能需要内容协商、快速传输、防止不经意的篡改。
todo 不一定是ascii 字符吧,抓包得到的结果是 16进制数值。
通过一系列首部可以让接收端正确识别消息类型、确定数据边界、得到自己想要的数据,并且让数据快速传输,防止不经意的篡改。

http 报文要提供大小、类型、编码,Content-Length / Content-Type / Content-Encoding。其外还通过一些首部,可以实现范围请求、分块编码、摘要、差异编码,Range / Transfer-Encoding / Content-MD5 / A-IM

image.png
http 报文实体由实体首部和实体主体组成。http 主体才是实际的原始数据,实体首部描述了原始数据的相关信息。

主体大小
content-length 表示主体的字节大小,如果进行了压缩那就是压缩过后的大小。早期 http 采用关闭连接的方式来表示报文传输结束。如果没有 content-length 字段,客户端没法区分正常结束,还是异常关闭连接(报文截尾)。报文结尾对缓存代理尤为严重,没有识别出是异常导致的连接关闭的话,会将不完整的报文缓存下来,为用户提供服务。通常缓存代理不会缓存没有 content-length 首部的报文主体。错误的 content-length 比不带这个首部更糟糕。

持久连接普及后,这个首部是必不可少的。持久连接不能通过关闭连接来标识报文结束。响应会一条一条接着到达,客户端通过 cl 就可以知道报文在哪里结束,下一条报文从哪儿开始。或者没有 cl 首部,但是有 te 首部,报文是分块编码传过来的。

为了满足安全性,减小主体大小。报文主体可以编码之后再传输。因为 http 没有标识未编码主体的长度的首部,所以客户端很难验证解码后的主体是否完整。如果主体编码了的话,content-md5 是对编码后的主体进行的摘要计算,也没法用摘要来验证。

确定报文的开始和结束位置,有一个从上到下的匹配规则。如果特定的报文类型中不允许带主体,那就忽略 cl 首部。有的情况下,cl 首部只是提示,报文可能并没有主体,比如 head 响应、1xx、204、304 响应。如果报文是分块编码传输的,那么报文以一个零字节块结束。如果报文中只有 cl,那么就以 cl 作为主体长度。如果报文是多部份/字节范围的媒体类型(multipart/byteranges),并且没有 cl 首部,那么就以报文给出的边界为准。如果都不匹配,那就等待连接关闭,表示报文传输结束。

媒体类型
content-type 首部表示主体的媒体类型,mime 类型是标准化的媒体类型。客户端通过 mime 类型来解释和处理内容,mime 由一个主媒体类型后面跟一条斜线和一个子类型组成。
mime 主要包括这几大类text / image / audio / application / multipart
charset 参数说明的是将主体中的比特转换为字符的方法。Content-Type: text/html; charset=ios-8859-4

multipart 多部分,表示多个报文可以合在一起发送,每个部分独立出来,有各自的首部,通过分界字符串连接。http 中 多部分请求主要有2种应用,多部分表格提交和范围响应。
content-type: multipart/form-data; boundary=[absdfad],boundary 参数携带的字符串就是用来分割主体中不同的部分。通过这种方式,客户端可以将边长的文本字段、或者图片、文件合并在一个报文中发送。
content-type: multipart/x-byteranges; boundary=--[asdfadf]--,多部分范围响应。

内容编码
content-encoding 表示编码方式。为了减少主体传输时间,以及防止未授权的第三者看到主体内容,会对报文主体进行编码。
常用的ce值有 gzip / compress / defalte / identity。前3个都是无损压缩算法。identity 说明没有对实体进行编码。

为了防止服务端使用客户端无法解码的编码方式,可以通过 accept-encoding 请求首部将自己支持的编码方式发送给服务端。可以给每种编码附带权重值 q 说明编码的优先级。
image.png

传输编码与分块编码
传输编码与内容编码不同,传输编码是对报文进行编码,改变了报文在网络中传输的方式。
要传输的数据大小不可知的时候,又想要先将生成的部分数据发送出去。cl 首部携带的是准确的数据大小,所以没法解决这种需求。传输编码就是为此而生。
transfer-encoding / te 两个首部用来描述和控制传输编码。传输编码有多种方式,chunked 是最新的一种编码方式,这种方式不能设置权重值。
image.pngimage.png
分块编码将报文划分为若干个块。不是非持久连接的话,可以不必知道报文的大小,服务器关闭连接就表示报文结束。但是在持久连接中,必须要表示报文的结束位置。所在在持久连接中想发送动态生成的数据,那么就可以分块传输。
客户端也可以将请求报文分块编码发送给服务端。但是可能会收到 411 响应。

te 中的 trailers 说明客户端可以接收“拖挂”。服务端就可以在报文最后加上 trailer 首部表示的首部。拖挂可以包含附带的首部,这些首部在报文传输时没办法确定最终的值,就可以放在最后传输。
image.pngimage.png

范围请求
某个资源发送给用户后,用户掌握的资源副本就叫做实例。资源是会变化的,所以需要更新用户的实例。其中对实例的两个主要操作就是范围请求和差异编码。这两个方法都可以用条件请求,获取资源对象的部分内容。

通过范围请求,还可以恢复断开的下载?但是断开的时间内,资源发生变化就不行。请求报文使用 range 首部表示请求的范围,响应使用 accept-range 说明可以接受的请求范围,两个都是字节单位。范围请求在点对点传输中用的挺广的。范围请求不仅能请求同一个服务器,还能请求不同的服务器。

差异编码
只传输资源变化的那部分内容来优化传输性能。A-IM 首部告诉服务端客户端想要接受资源的差异部分。服务器返回 226 响应,说明传回来的是差异部分,IM 首部表示计算差异的算法,还会返回新的 ETag 首部,Delta-Base 首部与 if-none-match 值一致。
但是这玩意不是很常用,需要保存资源变化的不同版本,而且如果资源变化频繁,那么需要的存储空间会很大。

国际化

不同的地区或国家的人使用不同的语言编写文档,网络中的资源非常多样化。http 要能处理多种语言和字符编写的文档的能力。网站国际化的问题主要有2个,字符集编码和语言。而且包含了对资源内容的字符编码、语言标记、url,还有日期、域名的格式问题。

http 能够传输不同语言编写的资源内容,因为实际传输过程中都是二进制数据。服务器只需要告诉客户端内容的语言和字符集,客户端就可以将二进制数据解析为正确的字符。
服务器通过 content-type 的 charset 参数和 content-language 告诉客户端,主体用的字符集、编码方案、语言。客户端可以发送 accept-charset 首部和 accept-language 首部,告诉服务器它理解哪些字符集、编码方案、语言,以及各自的优先顺序。

字符集就是一张表,整数值作为索引,可以查找到整数对应的字符。编码方案负责将报文中的二进制数据转换为整数,不同的编码方案划分二进制数据的方式不同。charset 参数指的是这两者。

如果没有 content-type 的 charset 参数,检查 meta 标签,<meta http-equiv="content-type" content="text/html; charset=iso-2022-jp">,如果 meta 标签都没有,会扫描实际文本,通过语言和编码的常见模式,来推断字符编码。

字符集的索引又叫字符代码、码点,是一个范围内的整数。代码宽度是这个整数占据的二进制位数。不同的编码方案采用的代码宽度不同,有3种,“固定宽度”、“可变宽度-无模态”、“可变宽度-有模态”。utf-8 是无模态的可变宽编码方案。utf-16 更进一步,采用了定长和不定长的编码方案,2个字节表示基本平面的字符,4个字节表示辅助平面的字符。

http 首部使用的字符集是 us-ascii,代码范围是0-127,只需要7个二进制位就可以覆盖完这个字符集的所有字符。

语言标记,是一个标准化的字符串短语,能够描述语言的地区变种和方言,巴西葡萄牙语是pt-BR,美式英语是en-US,湖南话是zh-xiang。
响应报文的 content-language 描述了主体所属的语言。请求报文中可以用 accept-language 首部,将客户端支持的语言或者想要的语言发送给服务器。

uri 的字符包含在 us-ascii 的一个子集里。全球总有人不知道 ascii 的字符,uri 的设计者为了让全球的人都能简单地操作、记忆 uri,选择了 ascii 的一部分。这是他们的一种理念,可转抄能力和共享能力。
uri 中的字符分为3类,保留、未保留、转义。保留字符是 uri 的关键组成部分,如果想在uri 中某部分使用保留字符,可以将保留字符编码为转义字符。
image.png
在传输和转发 uri 的时候应该保持转义不变。只有在需要数据的时候才对 uri 转义。客户端确保只对 uri 转义一次。

令人不满的是,总有一些人不遵守标准,url 、报文首部可能有超出 ascii 字符集范围的字符。http 中也定义了日期的 gmt 格式,但是也要小心有人不遵守约定的行为。

内容协商

一个 url 可能指向几个资源,这几个资源可能是某一个资源的不同语言版本,这样不同的版本叫做变体。服务端如何知道客户端想要的是具体哪个资源呢。http 提供了内容协商的方式,允许客户端和服务端决定要传输的内容。

内容协商有3种技术,客户端驱动、服务器驱动、中间代理决定。客户端驱动需要与服务器通信两次,服务端给客户端一堆可选列表,让客户端选它到底要哪一个。服务器驱动方式,服务器根据客户端提供的首部做决定。还有一种方式,代理代表客户端与服务器进行协商。
在客户端驱动实现中,服务器有两种方法提供选项,一是返回一个html文档,里面包含各个版本的链接和描述信息,二是返回300响应,客户端弹出对话窗口让用户选。
服务器驱动方式中,有 accept-* 一系列首部。客户端想要的版本服务器没有,此时服务器可以选择猜测、回退到客户端驱动模型、转码。猜测的话是根据质量值来推测的,如果质量值也没有设置,那么可以对文档进行转码,匹配客户端的喜好。

内容发布与分发

web 主机托管

对内容资源进行存储、协调、管理的职责叫做“web主机托管”。主机托管是web服务器的主要功能之一。有的人没有服务器,或者想利用别人的平台存储、管理自己的资源,就需要主机托管服务。托管者出租自己的服务器和管理维护平台。

重定向与负载均衡

wpad 是怎么自动发现 pac 文件的?curl 又是从哪儿来的呢?