374632897 / blog

前端小虾蟹的个人博客
7 stars 1 forks source link

HTTP & TCP #18

Open 374632897 opened 7 years ago

374632897 commented 7 years ago

索引

连接管理

这部分的内容是在学习《HTTP 权威指南》的过程中进行的一些总结

TCP连接

TCP 连接一旦建立, 在客户端和服务器之间交换的报文就永远不会丢失、受损或失序。

TCP是通过名为 IP 分组(IP 数据报)的小数据块来发送的, 每个 TCP 段都有一个序列号和数据完整性校验和。每个段的接收者收到完好的段时都会想发送者回送小的确认分组(ACK)。 当 HTTP 需要传送一条报文的时候, 会以流的形式将报文数据通过一条打开的 TCP 连接按序传输。 TCP 收到数据流之后会将数据流分成被称作段的小数据块, 并将段封装在 IP 分组中进行传输。每个 IP 分组包括以下几个部分:

TCP 性能

HTTP over TCP over IP

由于 HTTP 紧挨着 TCP 并位于其上层, 所以 HTTP 事务的性鞥呢很大程度上取决于 TCP 通道的性能。

HTTP 事务时延

HTTP 事务时延时延指的是一个请求从发起到接收到响应的过程中所用的时间, 这个过程主要如下:

  1. 针对请求域名做 DNS 解析
  2. 建立到服务器的 TCP 连接
  3. 传送数据
  4. 服务器请求处理
  5. 服务器回送响应。 在大部分的情况下, 第1、2步所花的时间占整个时间的大部分, 除非服务器超载或者服务器处理事务的逻辑有问题或者网络情况比较差。

TCP 网络时延的大小取决于硬件速度网络和服务器的负载请求和响应报文的尺寸客户端和服务器之间的距离, 此外TCP 协议的技术复杂性也会对时延产生巨大的影响。

性能聚焦区域

TCP 连接的握手时延

TCP连接的建立会经历三次握手的过程, 而在这个过程中 TCP 软件之间会交换一系列的 IP 分组, 对连接的相关参数进行沟通, 如果只是用于传送少量数据的话, 这些交换过程就会严重降低 HTTP 的性能

TCP 慢启动拥塞机制

TCP 慢启动拥塞机制是指当一个新的 TCP 连接被建立的时候, 其连接的最大速度会被限制, 如果数据成功传输, 则会逐渐提高传输的速度。 TCP 慢启动主要是通过限制一个 TCP 端点在任意时刻可以传输的分组数来防止网络的突然过载和拥塞。 因而新连接的速度往往会低于已经成功进行过数据传输的 TCP 连接的速度。

数据聚集的 Nagle 算法

如最开始提到的 IP 分组, 每个 TCP 段的传输都会包含至少40字节的内容, 如果 TCP 发送了大量的包含少量数据的分组, 网络的性能将会严重下降。 Nagle 算法则试图在发送一个分组之前, 将大量的 TCP 数据绑定在一起, 以提高网络效率。
Nagle 算法鼓励发送全尺寸的段, 只有当其他分组都被确认之后, Nagle 算法才会允许发送非全尺寸的分组。 如果其他分组在传输过程中, 则将那部分数据缓存起来,只有当其他挂起的分组被确认或者说当前数据达到全尺寸的时候, 才会将缓存的数据发送出去。

造成的问题主要有以下两点:

用于捎带确认的 TCP 延迟确认算法

如前文所提到的一样, 每次进行 HTTP 报文发送的时候, TCP 软件都会将报文数据分隔成小数据块(段), 并将每个段置于 IP 分组中进行发送, 当接收方收到这样的一个 IP 分组的时候, 需要向发送发发送小的确认分组(ACK), 如果发送者没有在指定的窗口时间内接收到这个确认标记的话, 则会认为分组已经被破坏或者损毁, 然后再重新发送数据(为了维持 TCP 连接的可靠性)。

延迟确认算法所做的事情就是在接收到数据段的时候, 不会立即回送确认分组, 而是在一个特定的窗口时间内(100-200ms)内, 将其放到缓冲区中, 期望在下一次进行数据分组传送的时候一起传送过去, 如果这个时间段内没有数据分组发送, 再将其作为单独分组进行回送, 从而实现更有效地利用网络的目的。
然而, 由于这个窗口时间可能不一致, 而数据分组可能很久不能到达, 这样会导致的问题是, 一个确认分组被置于缓冲区中, 期望数据分组的到来, 然而过了窗口时间, 最后依然还是将其作为一个独立分组进行发送, 相当于这部分等待的时间白白的浪费了。 而在 web 优化中, 这部分时间所造成的时延却是相当之大的。

TIME_WAIT 累积与端口耗尽

当 TCP 关闭 TCP 连接的时候, 会在内存中维护一个小的控制块, 用来记录所关闭的连接的 IP 地址和端口号,它通常只会维持所顾忌的最大分段试用期的两倍(2MSL, 通常为2分钟)的时间, 以确保这段时间内不会创建具有相同地址和端口号的新连接, 从而防止在两分钟内创建、关闭并重新创建两个具有相同 IP 地址和端口号的连接。

这个问题通常只会出现在基准测试中。 在基准测试中, 测试机器的数量以及服务器监听的端口数量通常是有限的, 这样也就限制了目标 IP:目标端口 -> 源 IP:源端口之间的可组合数量, 而连接的建立的和断开到可重用之间存在 2MSL 的不可用时间, 这样一来可建立的连接数就会越来越少, 从而造成端口耗尽的问题。

HTTP 连接处理

Connection首部

Connection首部是一个由逗号分隔的连接标签列表, 它可以包含以下三个部分:

Connection首部以及 Connection 首部里面包含的 HTTP首部字段名都不应该被代理转发。

解决串行事务处理时延

串行加载的缺点:

并行连接

同时建立多条 TCP 连接。 缺点:

持久连接

HTTP 事务处理完成之后不关闭 TCP 连接从而使得之后的请求依然能够使用该连接。这样可以避免握手以及慢启动造成的问题。

持久连接和并行连接结合在一起使用可能是最高效的方式。

持久连接的建立主要通过Connection首部来进行控制。 而不同版本的 HTTP 处理方式也有所不同。

持久连接的限制和规则:

管道化连接

HTTP/1.1允许在持久连接上可选地使用管道, 当一个请求成功到达服务端的时候, 下一个请求就可以继续发送了。在高时延的网络下, 这样做可以降低网络的环回时间, 提高性能

限制:

关闭连接

完全关闭与半关闭

完全关闭是指 TCP 连接的输入信道和输出信道都被关闭, 半关闭是指二者之一被关闭。

TCP 关闭及重置错误

简单的 HTTP 程序可以只使用完全关闭。 当应用程序开始与其他类型的 HTTP 客户端、服务器和代理进行对话且开始使用管道化持久连接时, 使用半关闭来防止对等实体收到非预期的写入错误就很重要了。

关闭连接的输出信道总是很安全的。
关闭连接的输入信道则会比较危险。 在管道化连接当中, 当你已经在该连接上发送了10条响应了并且也成功收到响应, 但是这部分响应会先存放在操作系统的缓冲区当中(但并未被读取(为什么不读取?)), 之后服务器单方面的关闭了连接, 那么在你发送第11条请求的时候, 就会发送到一条已关闭的连接上去, 这个时候会被回送一条重置信息, 而该重置信息会清空你的输入缓冲区, 这样你已缓冲但是的数据都不存在了。

所以, 通常情况下, 应该是一端先关闭其输出信道,然后周期性地检查输入信道的状态。

参考:

374632897 commented 7 years ago

一个关于进程、端口号的讨论

在阅读《HTTP 权威指南》的过程中碰到了这样一个问题, 里面提到在使用管道化连接的时候, 收到的响应会存在操作系统的缓冲区里, 在应用程序还没有来得及读取的情况下, 如果要是服务端单方面关闭了 TCP 连接,那么下一次客户端再发送 HTTP 请求的时候, 会收到一条连接已重置的错误, 而这个错误造成的影响就是清空客户端缓冲区, 这样一来之前已经收到的响应也会被清空。 我的疑问是, 管道化连接当中如果服务端已经正确对客户端做出了响应的话, 那么这个响应必然是有序且正确的, 在这种情况下(使客户端使用管道化连接), 为什么应用程序不直接从缓冲区中读取以避免因意外而造成的数据丢失从而需要重新请求的情况呢?

内容太多也比较深入, 之后再慢慢补把

image

参考

374632897 commented 7 years ago

管道化连接

受《HTTP 权威指南》第109页对连接重置的疑问的影响,这个问题似乎需要深入了解一下。

由于管道化连接需要服务端做特殊处理, 并且如果第一个请求的处理时间过长的话, 其他请求也会被阻塞, 所以大多数的浏览器都没有启用这个功能, 而 HTTP/2 的多路复用, 则是引入了流的概念, 通过对流添加标识符, 来解决了这样的问题。 image

参考:

374632897 commented 7 years ago

Host

前段时间在进行调试的时候, 在 node 端检测到某个请求后, 需要根据请求信息向后台服务做请求进行信息校验, 为了保证请求头的一致性, 就直接将本地浏览器向 node 服务器发出的请求头合并到了 node 向后台服务做请求的请求头里面, 然而最后这个接口一直请求不成功, 验证不能通过, 总是报401。

通过wireshark 进行抓包后, 发现主要是两个地方不同, 一个是 BA 认证的 token, 一个是 Host。 BA 认证的 token 是根据工具生成的, 且时间也是作为其影响因素之一, 所以应该排除掉, 我也考虑过是 host 的问题, 然而改了 host 之后, 还是不行, 当然, 最后证明问题还是出在了 Host 上, 而我之前改的时候,大概是没有改对。

在 HTTP 权威指南中, 对 Host 的描述如下,

客户端通过 Host 首部为服务器提供客户端想要访问的那台机器的因特网主机名和端口号。主机名和端口号来自客户端所请求的 URL。 只要服务器能够在同一台机器上提供多个不同的主机名,服务器就可以通过 Host 首部, 根据主机名来区分不同的相对 URL。 此外, 所有的 HTTP/1.1 都必须包含 Host 首 部, 如果没有包含的话, 服务器应该回送400 Bad Request 错误。

所以, 当 URL 为相对 URL 的时候, Host 的作用主要用来标识请求接收方, 而当 URL 为绝对 URL 的时候, 根据RFC2616里面提到的, 服务器应该要忽略首部里面的 Host。

所以, 对 Host 的处理应该是服务器的事情而不是协议的事情, 然而在实际情况下效果却不如人意。

使用了以下的脚本来进行了测试

➜  tmp cat host.sh
set -x
dist_url="baidu.com rishiqing.com teambition.com google.com meituan.com localhost:9000";
for _url in $dist_url; do
  url="http://$_url"
  curl $url
  curl $url -iH "Host: noteawesome.com" | head -n 20
done;
Host 测试结果 ```bash + dist_url='baidu.com rishiqing.com teambition.com google.com meituan.com localhost:9000' + for _url in '$dist_url' + url=http://baidu.com + curl http://baidu.com + curl http://baidu.com -iH 'Host: noteawesome.com' + head -n 20 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 curl: (56) Recv failure: Connection reset by peer + for _url in '$dist_url' + url=http://rishiqing.com + curl http://rishiqing.com 301 Moved Permanently

301 Moved Permanently


nginx
+ curl http://rishiqing.com -iH 'Host: noteawesome.com' + head -n 20 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 178 100 178 0 0 4342 0 --:--:-- --:--:-- --:--:-- 4450 HTTP/1.1 301 Moved Permanently Server: nginx Date: Thu, 06 Jul 2017 09:00:21 GMT Content-Type: text/html Content-Length: 178 Connection: keep-alive Location: http://www.rishiqing.com/ 301 Moved Permanently

301 Moved Permanently


nginx
+ for _url in '$dist_url' + url=http://teambition.com + curl http://teambition.com 301 Moved Permanently

301 Moved Permanently


nginx
+ curl http://teambition.com -iH 'Host: noteawesome.com' + head -n 20 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 curl: (52) Empty reply from server + for _url in '$dist_url' + url=http://google.com + curl http://google.com 302 Moved

302 Moved

The document has moved here. + curl http://google.com -iH 'Host: noteawesome.com' + head -n 20 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 1561 100 1561 0 0 13619 0 --:--:-- --:--:-- --:--:-- 13692 HTTP/1.1 404 Not Found Content-Type: text/html; charset=UTF-8 Referrer-Policy: no-referrer Content-Length: 1561 Date: Thu, 06 Jul 2017 09:00:22 GMT Error 404 (Not Found)!!1

404. That’s an error.

The requested URL / was not found on this server. That’s all we know. + for _url in '$dist_url' + url=http://meituan.com + curl http://meituan.com 301 Moved Permanently

301 Moved Permanently

The requested resource has been assigned a new permanent URI.


Powered by Tengine + curl http://meituan.com -iH 'Host: noteawesome.com' + head -n 20 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 6789 100 6789 0 0 189k 0 --:--:-- --:--:-- --:--:-- 194k HTTP/1.1 200 OK Server: Tengine Date: Thu, 06 Jul 2017 09:00:22 GMT Content-Type: text/html Content-Length: 6789 Last-Modified: Fri, 31 Mar 2017 03:46:52 GMT Connection: keep-alive ETag: "58ddd12c-1a85" Expires: Wed, 06 Jul 2016 09:00:22 GMT Cache-Control: no-cache Cache-Control: private, no-cache, no-store, proxy-revalidate Accept-Ranges: bytes + for _url in '$dist_url' + url=http://localhost:9000 + curl http://localhost:9000 Hello world {"host":"localhost:9000","user-agent":"curl/7.51.0","accept":"*/*"}+ curl http://localhost:9000 -iH 'Host: noteawesome.com' + head -n 20 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 80 100 80 0 0 16253 0 --:--:-- --:--:-- --:--:-- 20000 HTTP/1.1 200 OK Date: Thu, 06 Jul 2017 09:00:22 GMT Connection: keep-alive Content-Length: 80 Hello world {"host":"noteawesome.com","user-agent":"curl/7.51.0","accept":"*/*"} ```

可以看到, 在 首部字段的Host 与URL里的 Host 不一致的时候:

以上,测试的网站比较少, 然而就测试数据而言, 大部分的网站在访问的 URL 为绝对路径的时候, 都没有忽略首部字段里的 Host, 而是做了特殊处理。

在一个请求发出的时候, 客户端会先对这个 URL 进行解析, 获取到对应的 Host, path, 然后和服务端建立 TCP 连接, 连接建立好之后, 就会开始进行报文传输。 HTTP 报文主要包含请求头和请求体, 而请求头里的 Host 则标识了客户端解析出来的目的主机名(在没有手动修改的情况下), 而 request-line 则包含了请求相关的信息, 所以其实最后服务器接收到的来自客户端的请求里面, 它拿到的请求头不会有完整的 URI, 而只是一个请求路径, 通常情况下这不会有什么问题, 但是在一台服务器上托管了多个网站的时候, 就需要通过 Host来进行匹配, 如nginx里面, 当它收到一个来自监听端口上的请求的时候, 会将请求头里的 Host 与相应的 server 块里面的server_name进行匹配, 如果匹配,则使用对应的 server 块来处理请求, 如果不匹配,则使用一个其他块来进行处理。 所以可以看到, 上面的请求里面, 最后修改了 Host 之后大部分的网站返回的信息都有所变动, 应该就是在进行主机匹配的时候没有匹配上。

而至于绝对 URI , 标准里面说它的形式应该是如下的:

GET http://www.example.org/pub/WWW/TheProject.html HTTP/1.1

~~然而一直不知道这个应该怎样出现, 应该还需要继续查阅一些资料才行。 ~~ 7月8日备注 据规范里提到的, 在请求发送方知道自己的请求是发送到代理的情况下, 请求 URI 就需要使用绝对 URI 。 这里的代理必须是客户端代理(而不是将请求发送到远程之后由服务器再进行的请求转发), 比如通过在浏览器或者系统里面进行了代理配置。 如下:

image

然后使用 wireshark 抓包结果如下所示: image

image

可以看到, 这个时候, request-line 里面显示的就是 绝对 URI 了 。

因为代理需要知道目标服务器的名称, 这样才能够和目标服务器建立连接, 如果不使用绝对 URI 的话, 最后代理服务器收到的请求里面就只有一个请求路径和 Host , 而 Host 也只是代理服务器的 Host, 所以需要提供绝对路径的目的应该就是为了让代理服务器能够和目标服务器建立连接。

所以, 我们要将部分 URI 发送给服务器, 将完整 URI 发送给代理。

最后, 感谢杜老师。。

参考:

374632897 commented 6 years ago

0.0.0.0 127.0.0.1 localhost 和 ip

image