HTTP1.1 是如何解决队头阻塞问题
书接上文,HTTP 传输是基于请求-应答的模式进行的,报文必须是一发一收,任务被放在一个任务队列中串行执行,而所谓的HTTP队头阻塞问题。是指一旦队首的请求处理太慢,就会阻塞后面请求的处理。
- 并发连接
对于一个域名允许分配多个长连接,那么相当于增加了任务队列,不至于一个队伍的任务阻塞其它所有任务。在RFC2616规定过客户端最多并发 2 个连接,不过、现在的浏览器标准中,这个上限要多很多,就比如Chrome 中是 6 个。 - 域名分片
一个域名不是可以并发 多个长连接吗?那我就多分几个域名。
比如 content1.test.com 、content2.test.com。
这样一个test.com域名下可以分出非常多的二级域名,而它们都指向同样的一台服务器,能够并发的长连接数更多了。事实上也更好地解决了队头阻塞的问题。
即使是提高了并发连接,也满足不了对性能的需求,而只有能够并发的长连接数更多了,才能更好地解决了队头阻塞的问题。
HTTP 代理
HTTP 是基于请求-响应模型的协议,一般由客户端发请求,服务器来进行响应。
但是也有代理服务器的情况。引入代理之后,代理的服务器相当于一个中间人的角色,对于客户端而言,表现为服务器进行响应;而对于源服务器,表现为客户端发起请求,具有双重身份。
- 能干嘛?
负载均衡。客户端的请求只会先到达代理服务器,后面到底有多少源服务器,IP 都是多少,客户端是不知道的。因此,代理服务器可以拿到这个请求之后,可以通过特定的算法分发给不同的源服务器,让各台源服务器的负载尽量平均。
保障安全。利用心跳机制监控后台的服务器,一旦发现故障机就将其踢出集群。并且对于上下行的数据进行过滤,对非法 IP 限流,这些都是代理服务器的工作。
缓存代理。将内容缓存到代理服务器,使得客户端可以直接从代理服务器获得而不用到源服务器那里。
- 相关头部字段
Via
代理服务器需要标明自己的身份,假如有两台代理服务器,在客户端发送请求后会经历这样一个过程:
客户端 => 代理1 => 代理2 => 源服务器
源服务器收到请求后,会在请求头拿到这个字段:
Via: proxy_server1, proxy_server2
而源服务器响应时,最终在客户端会拿到这样的响应头:
Via: proxy_server2, proxy_server1
Via中代理的顺序即为在 HTTP 传输中 报文传达的顺序。X-Forwarded-For
是为谁转发, 它记录的是请求方的IP地址(和Via区分开只记录请求方这一个IP)。X-Real-IP
是一种获取用户真实 IP 的字段,不管中间经过多少代理,这个字段始终记录最初的客户端的IP。
相应的,还有X-Forwarded-Host和X-Forwarded-Proto,分别记录客户端(注意哦,不包括代理)的域名和协议名。X-Forwarded-For产生的问题
前面可以看到,只记录了请求方的 IP,这意味着每经过一个不同的代理,这个字段的名字都要变,由此产生了问题:
- 意味着代理必须解析 HTTP 请求头,然后修改,比直接转发数据性能下降。
- 在 HTTPS 通信加密的过程中,原始报文是不允许修改的。
为了解决这个问题产生了代理协议,一般使用明文版本,只需要在 HTTP 请求行上面加如下文本即可:
// PROXY + TCP4/TCP6 + 请求方地址 + 接收方地址 + 请求端口 + 接收端口
PROXY TCP4 0.0.0.1 0.0.0.2 1111 2222
GET / HTTP/1.1
什么是跨域
scheme(协议)、host(主机)和port(端口)都相同则为同源
说到http 就得说到跨域了,在前后端分离的开发模式中,经常会遇到跨域问题,即 Ajax 请求发出去了,服务器也成功响应了,前端就是拿不到这个响应。
非同源站点有这样一些限制
- 不能读取和修改对方的 DOM
- 不读访问对方的 Cookie、IndexDB 和 LocalStorage
- 限制 XMLHttpRequest 请求。
跨域请求是被浏览器拦截,响应其实是成功到达客户端了。那这个拦截是如何发生呢?
- 浏览器是多进程的,以 Chrome 为例,进程组成如下:
当Ajax 请求准备发送的时候,其实还只是在渲染进程的处理。为了防止黑客通过脚本触碰到系统资源,浏览器将每一个渲染进程装进了沙箱,采取了站点隔离的手段。 - 在沙箱当中的渲染进程只能通过网络进程来发送网络请。在数据传递给了浏览器主进程,主进程接收到后,才真正地发出相应的网络请求。
在服务端处理完数据后,将响应返回,主进程检查到跨域,且没有cors响应头,将响应体全部丢掉,并不会发送给渲染进程。这就达到了拦截数据的目的。
解决方案
CORS
CORS 就是是跨域资源共享。它需要浏览器和服务器的共同支持,具体来说,非 IE 和 IE10 以上支持CORS,服务器需要附加特定的响应头,后面具体拆解。
简单请求和非简单请求。
- 简单请求:
请求方法为 GET、POST 或者 HEAD
请求头的取值范围: Accept、Accept-Language、Content-Language、Content- Type(只限于三个值application/x-www-form-urlencoded、multipart/form-data、 text/plain)
请求发出去之前,浏览器会自动在请求头当中,添加一个Origin字段,用来说明 请求来自哪个源。服务器拿到请求之后,在回应时对应地添加Access-Control-Allow-Origin字段,如果Origin不在这个字段的范围中,那么浏览器就会将响应拦截。
Access-Control-Allow-Origin字段是服务器用来决定浏览器是否拦截这个响应,这是必需的字段。
Access-Control-Allow-Credentials表示是否允许发送 Cookie,对于跨域请求,浏览器对这个字段默认值设为 false,而如果需要拿到浏览器的 Cookie,需要添加这个响应头并设为true, 并且在前端也需要设置withCredentials属性:
let xhr = new XMLHttpRequest();xhr.withCredentials = true;
-
Access-Control-Expose-Headers。这个字段是给 XMLHttpRequest 对象赋值,让它不仅可以拿到基本的 响应头字段,还能拿到这个字段声明的响应头字段。比如这样设置:
Access-Control-Expose-Headers: test
前端可以通过 XMLHttpRequest.getResponseHeader('test') 拿到 test 这个字段的值。
-
非简单请求
除了上述的就是非简单请求,针对这种请求进行不同的处理。
主要体现在两个方面: 预检请求和响应字段。
我们以 PUT 方法为例。var url = 'http://xxx.com'; var xhr = new XMLHttpRequest(); xhr.open('PUT', url, true); xhr.setRequestHeader('X-Custom-Header', 'xxx'); xhr.send()
- 预检请求
OPTIONS / HTTP/1.1 Origin: 当前地址 Host: xxx.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-Custom-Header
通过OPTIONS,同时会加上Origin源地址和Host目标地址,同时也会加上两个关键的字段:
Access-Control-Request-Method
, 列出 CORS 请求用到哪个HTTP方法
Access-Control-Request-Headers
,指定 CORS 请求将要加上什么请求头- 响应字段
- 预检请求的响应
HTTP/1.1 200 OK Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Access-Control-Allow-Credentials: true Access-Control-Max-Age: 1728000 Content-Type: text/html; charset=utf-8 Content-Encoding: gzip Content-Length: 0
Access-Control-Allow-Origin
: 表示可以允许请求的源,可以填具体的源名,也可以填*
Access-Control-Allow-Methods
: 表示允许的请求方法列表
Access-Control-Allow-Credentials
: 表示是否允许发送 Cookie
Access-Control-Allow-Headers
: 表示允许发送的请求头字段
Access-Control-Max-Age
: 预检请求的有效期,在此期间,不用发出另外一条预检请求在预检请求的响应返回后,如果请求不满足响应头的条件,则触发XMLHttpRequest的onerror方法,当然后面真正的CORS请求也不会发出去了。
之后和简单请求一样浏览器自动加上Origin字段,服务端响应头返回Access-Control-Allow-Origin。
JSONP
虽然XMLHttpRequest对象遵循同源政策,但script标签可以通过 src 填上目标地址从而发出 GET 请求,实现跨域请求并拿到响应。这也就是 JSONP 的原理
和CORS相比,JSONP 最大的优势在于兼容性好,IE 低版本不能使用 CORS 但可以使用 JSONP,缺点也很明显,请求方法单一,只支持 GET 请求。
Nginx
Nginx通过反向代理服务器来轻松解决跨域问题。
- 正向代理帮助客户端访问客户端自己访问不到的服务器(梯子),然后将结果返回给客户端。
反向代理拿到客户端的请求,将请求转发给其他的服务器,主要的场景是维持服务器集群的负载均衡,反向代理帮其它的服务器拿到请求,然后选择一个合适的服务器,将请求转交给它。
那 Nginx 是如何来解决跨域的呢?
客户端的域名为client.com,服务器的域名为server.com,客户端向服务器发送 Ajax 请求,会跨域产生跨域问题了,通过下面这个配置:
server {
listen 80;
server_name client.com;
location /api {
proxy_pass server.com;
}
}
Nginx 相当于起了一个跳板,这个跳板的名字也是client.com,让客户端首先访问 client.com/api,这当然没有跨域,然后 Nginx 服务器作为反向代理,将请求转发给server.com,当响应返回时又将响应给到客户端,这就完成整个跨域请求的过程。就像是在饭馆点餐时,服务员来回穿梭顾客和厨房是一样的道理,服务员记录了我们点的菜品,然后也将厨房的食物送到我们的餐桌上。