Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP & TCP #18

Open
374632897 opened this issue Jul 2, 2017 · 4 comments
Open

HTTP & TCP #18

374632897 opened this issue Jul 2, 2017 · 4 comments

Comments

@374632897
Copy link
Owner

374632897 commented Jul 2, 2017

索引

连接管理

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

TCP连接

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

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

  • 一个 IP 分组首部(通常20字节) —— 包含源和目的 IP 地址、长度及其他标记
  • 一个 TCP 段首部(通常20字节)—— 包含 TCP 端口号、TCP 控制标记以及用于数据排序和完整性检查的一些数字值
  • 一个 TCP 数据块(0或者多个字节)

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 慢启动拥塞机制
  • 数据聚集的 Nagle 算法
  • 用于捎带确认的 TCP 延迟确认算法
  • TIME_WAIT 累积与端口耗尽
TCP 连接的握手时延

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

TCP 慢启动拥塞机制

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

数据聚集的 Nagle 算法

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

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

  • 如果 HTTP 报文本身就很小, 不能够填充一个全尺寸的段的时候, 可能会因为等待永远不会来到的数据块而产生时延
  • 与延迟确认算法之间有冲突: 延迟确认算法会对收到的数据块的回应做缓冲处理, 而 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首部是一个由逗号分隔的连接标签列表, 它可以包含以下三个部分:

  • HTTP 首部字段名, 列出了只与此连接有关的首部
  • 任意标签值, 用于描述此连接的非标准选项
  • close, 说明操作之后需要关闭这个持久连接

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

解决串行事务处理时延

串行加载的缺点:

  • TCP 连接建立时延
  • 阻塞
  • 交互不友好
并行连接

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

  • 带宽竞争: 可用带宽是有限的, 在这种情况下会出现多条连接竞争固定带宽, 从而导致每个资源的加载速度都变慢的情况
  • 内存消耗: TCP 连接的建立和维护会消耗内存资源
  • 新建立的连接都会经历三次握手以及 TCP 慢启动阶段
持久连接

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

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

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

  • HTTP/1.0
    HTTP/1.0下持久连接的建立需要客户端添加首部Connection: Keep-Alive, 如果服务端同意建立持久连接, 则需要回送Connection:Keep-Alive
  • HTTP/1.1
    HTTP/1.1默认是开启了持久连接的, 要关闭持久连接, 则需要在首部里添加Connection: close

持久连接的限制和规则:

  • 服务端发送了Connection:close之后, 客户端就无法再那条连接上发送更多请求
  • 只有当连接上所有的报文都是正确的、自定义报文长度时实体部分的长度和响应的 Content-Length 一致或者分块传输(Transfer-encoding: chunked) 的时候, 连接才能持久。
管道化连接

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

限制:

  • 不能确认连接是持久的话, 就不该使用管道

  • 必须按照与请求相同的顺序回送 HTTP 响应(为了保证 TCP连接 的有序性)。

  • HTTP 客户端必须做好连接会在任意时刻关闭的准备, 以及准备好重发所有未完成的管道化请求。

  • HTTP 客户端不应该使用管道化的方式发送回产生副作用的请求(如 POST)

    如果一个事务, 不管是执行一次还是很多次, 得到的结果都相同,这个事务就是幂等的。 通常 GET、HEAD、TRACE、PUT、DELETE、OPTIONS 方法都共享这一特性,而POST 通常都是非 幂等请求。 要发送非幂等请求, 就需要等待来自前一条请求的响应。

复用的连接

交替传送请求和响应报文

关闭连接

完全关闭与半关闭

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

TCP 关闭及重置错误

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

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

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

参考:

@374632897
Copy link
Owner Author

374632897 commented Jul 4, 2017

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

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

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

image

参考

@374632897
Copy link
Owner Author

374632897 commented Jul 4, 2017

管道化连接

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

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

参考:

@374632897
Copy link
Owner Author

374632897 commented Jul 6, 2017

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 测试结果
+ 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
<html>
<meta http-equiv="refresh" content="0;url=http://www.baidu.com/">
</html>
+ 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
<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>
+ 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/

<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>
+ for _url in '$dist_url'
+ url=http://teambition.com
+ curl http://teambition.com
<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>
+ 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
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>302 Moved</TITLE></HEAD><BODY>
<H1>302 Moved</H1>
The document has moved
<A HREF="http://www.google.com.hk/?gfe_rd=cr&amp;ei=JvxdWaL2KrSM8Qe2qJHIAg">here</A>.
</BODY></HTML>
+ 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

<!DOCTYPE html>
<html lang=en>
  <meta charset=utf-8>
  <meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
  <title>Error 404 (Not Found)!!1</title>
  <style>
    *{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
  </style>
  <a href=//www.google.com/><span id=logo aria-label=Google></span></a>
  <p><b>404.</b> <ins>That’s an error.</ins>
  <p>The requested URL <code>/</code> was not found on this server.  <ins>That’s all we know.</ins>
+ for _url in '$dist_url'
+ url=http://meituan.com
+ curl http://meituan.com
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<h1>301 Moved Permanently</h1>
<p>The requested resource has been assigned a new permanent URI.</p>
<hr/>Powered by Tengine</body>
</html>
+ 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

<!doctype html>
<html lang="zh-cn">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1,
    maximum-scale=1, minimum-scale=1, user-scalable=no">
  <link rel="icon" href="//s3.meituan.net/v1/mss_8c96abc444e14a23a220b2bd8d3bbcc8/ape/475d8b07-bfc7-40ee-96b3-d65507450ebf" type="image/x-icon">
+ 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 不一致的时候:

  • 百度会重置连接
  • teambition 空响应(curl: (52) Empty reply from server)
  • google 404
  • 美团 200 提示未找到页面
  • 日事清无处理

以上,测试的网站比较少, 然而就测试数据而言, 大部分的网站在访问的 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 发送给代理。

最后, 感谢杜老师。。

参考:

  • RFC2616
  • 《HTTP 权威指南》第3章 HTTP 报文
  • 《HTTP 权威指南》第6章 代理
  • 《HTTP 权威指南》第18章 Web 主机托管

@374632897
Copy link
Owner Author

374632897 commented Mar 5, 2018

0.0.0.0 127.0.0.1 localhost 和 ip

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant