平时用 Chrome 开发者工具抓包时,经常会见到 Proxy-Connection 这个请求头。

之前一直没去了解什么情况下会产生它,也没去了解它有什么含义。

最近看完《HTTP 权威指南》第四章 连接管理 和第六章 代理 之后,终于搞明白了这是因为给浏览器设置了代理(Proxy)。

米扑博客这里顺便推荐《图解HTTP》,250页的小书,几天就看完,还带可爱的漫画,特别轻松就可以了解http的报文,状态码,网络层级等概念,等有了这些基础,再去实践和看权威指南,会容易得多。

而神器 Fiddler 的抓包原理就是让浏览器请求走它开的本地代理,所以开了 Fiddler 必然会产生这个请求头。

Fiddler:The free web debugging proxy for any browser, system or platform

 

Proxy-Connection, 这个字段是干什么的?

从字面上的意思看,这个字段表示和代理的连接。

首先我们看一下设置了浏览器代理之后HTTP的请求有哪些变化

GET / HTTP/1.1
Host: www.mimvp.com
Connection: keep-alive

GET https://www.mimvp.com/ HTTP/1.1
Host: www.example.com
Proxy-Connection: keep-alive

我们看到有两个变化

1. 请求的资源URI变成了完整路径,即由根目录 / 变成了 https://www.mimvp.com/

2. Connection 头变为了  Proxy-Connection头

 

为什么需要完整路径

早期的HTTP设计中(http 1.0),浏览器直接和单个服务器对话,不存在虚拟主机(代理服务器)。

所以单个服务器总知道自己的主机名和端口,这样浏览器发送请求只需要发送相对地址就可以了。

例如:GET / HTTP /1.0  接受主机知道这是要访问我的 / 根路径

 

有了代理就麻烦了,代理服务器不知道GET / HTTP /1.0  这个请求发给哪个主机,无法得知用户想要访问的URI在什么主机上。

因为我们先请求代理,若仍然是根目录 / ,则表示请求代理的根目录,代理下一步就不知道怎么请求谁了(没有请求host url)

为此,HTTP/1.0 要求浏览器为代理请求发送完整的 URI,也就是说规范告诉浏览器的实现者必须这么做。

所以,HTTP 1.0 又要求浏览器给代理发送的时候必须发送完整的路径名称

GET http://www.mimvp.com/ HTTP/1.0

 

HTTP 1.1 规定了必须包含Host主机名这个字段,所以 HTTP 1.1 可以是

GET / HTTP/1.1
Host: www.mimvp.com

 

但是,代理可以出现在连接的任何位置,例如请求 —> 代理1 —> 代理2 —> 代理3 —> ... —> 目标服务器,很多代理对浏览器来说不可见,如反向代理或路由器代理。

所以实际上,几乎所有的浏览器都会为每个请求加上内容为主机名的 HOST 请求头,来彻底解决虚拟主机问题。

1)对于 HTTP/1.1 请求,HOST 请求头必须存在,否则会收到 400 错误;

2)对于 HTTP/1.0 请求,如果连接的是代理服务器,使用相对 URI,并且没有 HOST 请求头,会发生错误。

 

但是由于不清楚代理是1.0 还是1.1,也许代理不认识Host这个头,所以http 1.1 发给代理的最后格式就变为

GET http://www.mimvp.com/ HTTP/1.1
Host: www.mimvp.com

 

Connection 详解

GET / HTTP/1.1
Host: www.mimvp.com
Connection: my-header, close, my-connection
My-Header: xxx

解释说明:

my-header

是本次请求中其它 Header 的名字(不区分大小写),表示这个 Header 只与当前连接有关。实际上,Connection 本身也只有当前连接有关。当客户端和服务端存在一个或多个中间实体(如代理)时,每个请求报文都会从客户端(通常是浏览器)开始,逐跳发给服务器;服务器的响应报文,也会逐跳返回给客户端。通常,即使通过了重重代理,请求头都会原封不动的发给服务器,响应头也会原样被客户端收到。但 Connection,以及 Connection 定义的其它 Header,只是对上个节点和当前节点之间的连接进行说明,必须在报文转给下个节点之前删除,否则可能会引发后面要提到的问题。其它不能传递的 Header 还有 Prxoy-Authenticate、Proxy-Connection、Transfer-Encoding 和 Upgrade

「close」表示操作完成后需要关闭当前连接;

Connection 还允许任何字符串作为它的值,如「my-connection」,用来存放自定义的连接说明。

HTTP/1.0 默认不支持持久连接(即不支持Keep-Alive),很多 HTTP/1.0 的浏览器和服务器使用

「Keep-Alive」这个自定义说明来协商持久连接:浏览器在请求头里加上 Connection: Keep-Alive,服务端返回同样的内容,这个连接就会被保持供后续使用。对于 HTTP/1.1,Connection: Keep-Alive 已经失去意义了,因为 HTTP/1.1 除了显式地将 Connection 指定为 close,默认都是持久连接。

有了上面的背景知识,我们来看问题。互联网上,存在着大量简陋并过时的代理服务器在继续工作,它们很可能无法理解 Connection——无论是请求报文还是响应报文中的 Connection。而代理服务器在遇到不认识的 Header 时,往往都会选择继续转发。大部分情况下这样做是对的,很多使用 HTTP 协议的应用软件扩展了 HTTP 头部,如果代理不传输扩展字段,这些软件将无法工作。

如果浏览器对这样的代理发送了 Connection: Keep-Alive,那么结果会变得很复杂。这个 Header 会被不理解它的代理原封不动的转给服务端,如果服务器也不能理解就还好,能理解就彻底杯具了。服务器并不知道 Keep-Alive 是由代理错误地转发而来,它会认为代理希望建立持久连接,服务端同意之后也返回一个 Keep-Alive。同样,响应中的 Keep-Alive 也会被代理原样返给浏览器,同时代理还会傻等服务器关闭连接——实际上,服务端已经按照 Keep-Alive 指示保持了连接,即使数据回传完成,也不会关闭连接。另一方面,浏览器收到 Keep-Alive 之后,会复用之前的连接发送剩下的请求,但代理已经close,它不认为这个连接上还会有其他请求,请求被忽略。这样,浏览器会一直处于挂起状态,直到连接超时。

这个问题最根本的原因是代理服务器转发了禁止转发的 Header。但是要升级所有老旧的代理也不是件简单的事,所以浏览器厂商和代理实现者协商了一个变通的方案:

首先,显式给浏览器设置代理后,浏览器会把请求头中的 Connection 替换为 Proxy-Connetion。这样,对于老旧的代理,它不认识这个 Header,会继续发给服务器,服务器也不认识,代理和服务器之间不会建立持久连接(不能正确处理 Connection 的都是 HTTP/1.0 代理),服务器不返回 Keep-Alive,代理和浏览器之间也不会建立持久连接。而对于新代理,它可以理解 Proxy-Connetion,会用 Connection 取代无意义的 Proxy-Connection,并将其发送给服务器,以收到预期的效果。

1)浏览器 —> Proxy-Connetion —> 老旧的代理(http 1.0)不认识 —> 转发 —> 目标服务器 —> 不认识Proxy-Connetion字段 —> 关闭 close

2)浏览器 —> Proxy-Connetion —> 新的代理(http 2.0)认识 —> 转发Connection —> 目标服务器 —> 认识Connetion字段 —> keep-alive

显然,如果浏览器并不知道连接中有老旧代理的存在(某个新代理会用 Connection 取代无意义的 Proxy-Connection,而目标服务器认识Connection),或者在老旧代理任意一侧有新代理的情况下(如老旧代理一样),这种方案仍然无济于事。

所以,有时候服务器也会选择彻底忽略 HTTP/1.0 的 Keep-Alive 特性:对于 HTTP/1.0 请求,从不使用持久连接,也从不返回 Keep-Alive。

 

通过上面的内容可以看到,浏览器对代理请求头的修改,都是为了尽可能的兼容网络中各种不规范的中转设备,使网络更健壮。

最后小结,用 Fiddler 和其它工具查看同一个请求头,会发现 Fiddler 显示的是 Connection,而其它工具显示的是 Proxy-Connection。

这是因为大部分情况下,Fiddler 会把 Proxy-Connection 换回 Connection 来显示,只是展现上的差别而已。

 

协议规定Connection可以定义其他只和本次Connection有关的head,比如 上面的 my-header, my-connection

Http/1.1 默认是keepalive,所以如果Connection指明 close的话 表明消息发送完之后,会释放对应的TCP

 

由于代理会识别http消息中的Header并重写明白的Header,对于不能理解的Header则正常转发, 这样的目的就是为了软件功能比较方便,增加一个Header不需要升级代理。

假如Connection发给了代理,代理不认识进行了转发,最后到了目标服务器。

如果目标服务器认识Connection:keep-alive,但是代理不认识(1.0代理),问题就出现了

1)服务器发送完response之后会保持这个会话连接keep-alive

2)代理由于是1.0的默认是close的行为,所以会等待服务器释放连接 close

3)客户端接受到服务器的response之后,由于response中的Connection也是keep-alive

所以,客户端还会继续在连接上发送请求(keep-alive状态),但是代理却认为客户端不会继续发送了,代理已经处于close状态。

 

解决这个问题,就出现了一个新的Header叫 Proxy-Connection用来协商浏览器和代理之间的链接

如果代理是1.1的,那么认识Proxy-Connection,完全没问题,代理会重写为Connection

如果代理是1.0的,那么不认识Proxy-Connection,那么会转发到目标服务器,目标服务器发现Proxy-Connection 并且Http的版本是1.0的,那么就会在response中添加Connection:close 也没有问题了。

但是如果最后一个代理和目标服务器之间还存在一个或多个不可见代理的话,则还有问题.

 

 

代理服务器的作用

1. 过滤,比如过滤儿童不适宜的网站

2. 控制,集中控制内部计算机访问外部网络以及外部网络发送回来的消息,并且可以经常变更策略

3. 缓存,提供缓存给经常访问的页面

4. 内容路由,可以根据内容把请求定位到特定的资源,注:迅雷的会员是否就是这样达到限制网速的?

5.转码,可以根据客户端的语言,甚至区分手机客户端还是PC客户端来对消息进行转码

 

 

参考推荐

检测代理IP匿名程度的方法

匿名、透明、HTTP、SSL、SOCKS代理的区别

米扑代理:CentOS 7安装squid代理服务器

VPS 动态拨号服务器搭建动态代理ip

Tornado+Redis 维护ADSL拨号服务器代理池

爬虫突破网站封禁不能抓取的6种常见方法

爬虫代理IP从哪找

LinuxIP代理筛选系统(shell+proxy)

PHP $_SERVER 和 getenv 获取环境变量

各种脚本语言获取客户端真实IP的方法