TCP连接
TCP/IP 是全球计算机及网络设备都在使用的一种常用的分组交换网络分层协议集。
TCP 为HTTP 提供了一条可靠的比特传输管道。从TCP连接一端填入的字节会从另一端以原有的顺序/正确的传送出来。
1浏览器解析出主机名
2 浏览器查询这个主机名的IP地址
3 浏览器获得端口
4 浏览器发起到IP端口的连接
5 浏览器向服务器发送一条HTTP GET 报文
6 浏览器从服务器读取HTTP 响应报文
7 浏览器关闭连接
TCP流是分段的/由IP分组传送:TCP的数据是通过名为IP分组 或IP数据报的小数据块来发送的。
https就是在http和tcp之间插入了一个 称为 TLS或SSL的 密码加密层
每个TCP段都是由IP分组承载,从一个IP地址发送到另一个IP地址 。每个IP分组中都包括:
一个IP分组首部 通常为20字节
一个TCP段首部 通常为20字节
一个TCP数据块 0个或多个字节
IP首部包含了源和目的IP地址、长度和其他一些标记。TCP段的首部包含了TCP端口号、TCP控制标记,以及用于数据排序和完整性检查的一些数字值。
在任意时刻计算机都可以有几条TCP连接处于打开状态。TCP是通过端口号来保持所有这些连接的正确运行的。
IP地址可以将你连接到正确的计算机,而端口号则可以将你连接到正确的应用程序上去。TCP连接是通过4个值来识别的:
<源IP地址、源端口号、目的IP地址、目的端口号>
这4个值一起唯一定义了一条连接。两条不同 的TCP连接不能拥有4个完全相同的地址组件值,但不同连接的部分组件可以拥有相同的值。
用TCP套接字编程:
操作系统提供了一些操纵其TCP连接的工具。套接字API允许用户创建TCP的端点数据结构,将这些端点与远程服务器的TCP端点进行连接,并对数据流进行读写 。TCP API隐藏了所有底层网络协议的握手细节,以及TCP数据流与IP分组之间的分段和重装细节。
web服务器等待连接,客户端根据URL判定出IP地址和端口号,并建立一条到服务器的TCP连接。一旦建立了连接,客户端就会发送HTTP请求,服务器则会读取请求。一旦服务器获取了整条请求报文,就会对请求进行处理,执行所请求的动作,并将数据写回客户端。客户端读取数据,并对响应数据进行处理。
HTTP事务的时延
与建立TCP连接,以及传输请求和响应报文的时间相比,事务处理时间 可能是很短的。除非客户端或服务器超载,或正在处理复杂的动态资源,否则HTTP时延就是由TCP网络时延构成的。
HTTP事务 的时延主要原因:
1 客户端首先需要根据URI确定Web服务器的IP地址和端口号。如果最近没有对URI中的主机名进行访问,通过DNS解析系统将URI中的主机名转换成一个IP地址可以要花费数十秒的时间 。
2 接下来,客户端会向服务器发送一条TCP连接请求,并等待服务器回送一个请求接受应答。每条新的TCP连接都会有连接建立时延。这个值通常最多只有一两秒,但如果有数百个HTTP事务的话,这个值会快速地叠加上去。
3 一量连接建立起来了,客户端就会通过新建立的TCP管道来发送HTTP请求。数据到达时,Web 服务器会从TCP连接中读取请求报文,并对请求进行处理。因特网传输请求报文,以及服务器处理请求报文都需要时间。
4 然后,web服务器会回送http响应,这也需要花费时间。
这些TCP网络时延 大小取决于硬件速度、网络和服务器的负载,请求和响应报文的尺寸,以及客户端和服务器之间的距离。TCP协议的技术复杂性也会对时延产生巨大的影响。
TCP连接的握手时延
建立一条新的TCP连接时,甚至是在发送任意数据之前,TCP软件之间会交换一系列的IP分组,对连接的有关参数进行沟通。如果连接只用来传送少量数据,这些交换过程就会严重降低HTTP的性能。
1 请求新的TCP连接时,客户端要向服务器发送一个小的TCP分组 通常是40到60个字节。这个分组中设置了一个特殊的SYN标记,说明这是一个连接请求。
2 如果服务器接受了连接,就会对一些连接参数进行计算,并向客户端回送一个TCP分组,这个分组中的SYN和ACK标记都被置位,说明连接请求已被接受。
3 最后,客户端向服务器回送一条确认信息,通知它连接已成功建立。现代的TCP栈都允许客户端在这个确认分组中发送数据。
通常HTTP事务都不会交换太多数据,些时SYN、SYN+ACK握手会产生一个可测量的时延。TCP连接的ACk分组通常都足够大,可以承载整个HTTP请求报文,而且很多HTTP服务器响应报文都可以放入一个IP分组中去。
小的HTTP事务可能会在TCP建立上花费50%或更多的时间。
延迟确认:
由于因特网自身无法确保可靠的分组传输,因特网路由器超负荷的话,可以随意丢弃分组,所以TCP实现了自己的确认机制来确保数据的成功传输。
每个TCP段都有一个序列号和数据完整性校验和。每个段的接收者收到完好的段时,都会向发送者回送小 的确认分组。如果发送者没有在指定的窗口时间 内收到确认信息,发送者就认为分组已被破坏或损毁,并重发数据。
由于确认报文很小,所以TCP允许在发往相同方向的输出 数据分组中对其进行 "捎带“。TCP 将返回的确认信息与输出的数据分组结合在一起,可以更有效地利用网络。为了增加确认报文找到同向传输数据分组的可能性,很多TCP栈都实现了一种”延迟确认“算法。延迟确认算法会在一个特定的窗口时间 通常 100到200毫秒 内将输出确认存放在缓冲区中,以寻找能够捎带它的输出数据分组。如果在那个时间段内没有输出数据分组,就将确认信息放在单独的分组中传送 。
但是Http 具有双峰特征的请求-应答行为降低了捎带信息的可能。当希望有相扳方向回传分组的时候,偏偏没有那么多。通常,延迟确认算法会引入相当大的时延。根据所使用操作系统的不同,可以调整或禁止延迟确认算法。
TCP慢启动
TCP数据传输的性能还取 决于TCP连接的使用期。TCP连接会随着时间进行自我 调谐 ,超初会限制连接的最大速度,如果数据成功传输,会随着时间的推移提高传输的速度。这种调谐被称为TCP慢启动 slow start,用于防止因特网的突然过载和拥塞。
TCP 慢启动限制了一个TCP端点在任意时刻可以传输的分组数。简单来说,每成功接收一个分组,发送端就有了发送另外两个分组的权限。如果某个HTTP事务有大量数据要发送,是不能一次将所有分组都发送出去的。必须发送一个分组,等待确认,然后可以发送两个分组,每个分组都必须确认,这样就可以发送四个分组了。以此类推。这种方式被称为 打开拥塞窗口。
Nagle算法与TCP_NODELAY
TCP有一个数据流接口,应用程序可以通过它将任意尺寸的数据放入TCP栈中,每个TCP段中都至少装载了40个字节的标记和首部,所以如果TCP发送了大量包含少量数据的分组,网络的性能就会严重下降。
Nagle算法,试图在发送一个分组之前,将大量TCP数据绑定在一起,以提高网络效率。
Nagle算法鼓励发送全尺寸 LAN 上最大尺寸的分组大约是1500字节,在因特网上是几百个字节 的段。只有当所有其他分组都被确认之后,Nagle算法才允许发送非全尺寸的分组。如果其他分组仍然在传输过程中,就将那部分数据缓存起来,只有当挂超分组被确认,或者缓存中积累了足够发送一个全尺寸分组的数据时,才会将缓存的数据发送出去。
Nagle算法会引发几种Http性能问题。首先,小的http报文可能无法填满一个分组,可能会因为等待那些永远不会到来的额外数据 而产生时延。其次,Nagel算法与延迟确认之间的交互存在的问题。Nagle算法会阻止灱的发送,直到有确认分组抵达为止,但确认分组自身会被延迟确认算法延迟 100-200毫秒。
HTTP应用程序常常会在自己的栈中设置参数TCP_NODELAY,禁用Nagle算法,提高性能。如果要这么做 的话,一定要确保会向TCP写入大块的数据,这样就不会产生一堆小分组了。
TIME_WAIT累积与端口耗尽
TIME_WAIT端口耗尽是很严重的性能问题,会影响到性能基准,但在现实中相对较少出现。大多数遇到性能基准问题的人最终都会碰到这个问题,而且性能都会变得出乎意料地差。
当某个TCP端点关闭TCP连接时,会在内在中维护一个小的控制块,用来记录最近所关闭连接的IP地址和端口号。这类信息只会维持一小段时间,通常是所估计的最在分段使用期的2倍,称为 2MSL,通常为2分钟左右,以确保在这段时间内不会创建具有相同地址和端口号的新连接。
现在高速路由器的使用,使得重复分组几乎不可能在连接关闭的几分钟之一,出现在服务器上。
如果来自之前连接的复制分组插入了具有相同连接值的新TCP流,会破坏TCP数据
客户端每次连接到服务器上去时,都会获得一个新的源端口,以实现连接的唯一性。但由于可用源端口的数量有限 ,而且在2MSL秒 内连接是无法重用的,连接率就被限制在了60000/120=500次/秒。如果再不断进行优化,并且服务器的连接率不高于500次/秒,就可确保不会遇到time_wait端口耗尽问题。要修正这个问题,可以增加客户端负载一成机器的数量,或者确保客户端和服务器在循环使用几个虚拟IP地址以增加更多的连接组合。
在大量打开连接或控制块的情况下,有些操作系统的速度会严重减缓。
并行连接 可能会提高页面的加载速度
包含嵌入对象的组合页面如果能通过并行连接克服单条连接的空载时间和带宽限制,加载速度也会有所提高。时延可以重叠起来,而且如果单条连接没有充分利用客户端的因特网带宽,可以将未用带宽分配来装载其他对象。
并行连接不一定更快,客户端的网络带宽不足时, 大部分的时间可能都是用来传送数据的。在这种情况下,一个连接到速度较快服务器上的HTTP事务就会很容易地耗尽所有可用的带宽。如果并行加载多个对象,每个对象都会去竞争这有限的带宽,每个对象都会以较慢的速度按比例加载,这样带来的性能提升就很小,甚至没什么提升。
而且,打开大量连接会消耗很多内在资源,从而引发自身的性能问题。
实际上,浏览器确实使用了并行连接,但它们会将并行连接的总数限制为一个较小的值,通常是4个。服务器可以随意关闭来自特定客户端的超量连接。
持久连接
Web客户端经常会打开到同一个站点的连接。比如一个web页面上的大部分内嵌图片通常都来自同一个web端点,而且相当一部分指向其他对象的超链通常都指向同一个站点。因此,初始化了对某服务器HTTP请求的应用程序很可能会在不久的将来对那台服务器发起更多的请求。这种性质 被称为站点本地性site locality
HTTP/1.1 1.0各增强版本 允许http设备在事务处理结束之后将TCP连接保持在打开状态,以便为未来的HTTP请求重用现存的连接。在事务处理结束之后仍然保持在打开状态的TCP连接被称为持久连接。非持久连接会在每个事务结束之后关闭。持久连接会在不同事务之间保持打开状态,直到客户端或服务器决定将其关闭为止。
重用已对目标服务器打开的空闲持久连接,就可以避开缓慢的连接建立阶段。而且,已经打开的连接还可以避免慢启动的拥塞适应阶段,以便更快速地进行数据的传输。
持久以及并行连接
并行连接有一些缺点:
1 每个事务都会打开/关闭一条新的连接,会耗费时间和带宽。
2 由于TCP慢启动特性的存在,每条新连接的性能都会有所降低。
3 可打开的并行连接数量实际上是有限的。
持久连接 降低了时延和连接建立 的开销,将连接保持在已调谐状态,而且减少了打开连接的潜在数量。但是管理持久连接进要特别小心,不然就会累积出大量的空闲连接,耗费本地以及远程客户端和服务器上的资源。
持久连接与并行连接配合使用可能是最高效的方式。现在很多web应用程序都会打开少量的并行连接,其中每一个都是持久连接。持久连接有两种类型:比较老的Http/1.0+ keep-alive 连接,以及现代 http/1.1 persistent 连接。
keep-alive 首部只是请求将连接保持在活跃状态。发出keep-alive请求之后,客户端和服务器冻一定会同意进行keep-alive会话。它们可以在任意时刻关闭空闲的keep-alive连接,并可随意限制keep-alive连接所处理事务的数量。
Keep-Alive首部完全是可选的,但只有在提供connection:keep-Alive时才能使用它
connection:Keep-Alive
Keep-Alive:max=5,timeout=120
只有在无需检测到连接的关闭即可确定报文实体主体部分长度的情况下,才能将连接保持在打开状态,也就是说实体的主体部分必须有正确 Content-Length
代理不理解connection首部时,会使用浏览器再次发送请求时,一直处于挂起状态。
为避免此类代理通信问题的发生,现代的代理都绝不能转发connection首部和所有名字出现在connection值中的首部。
客户端和服务器之间只有一个代理时:插入Proxy-Connection ,如果代理是盲中继,它会将无意义的Proxy-Connection首部转发给Web服务器,服务器会忽略此首部,不会带来任何问题。但如果代理是个聪明的代理,就用一个Connection首部取代无意义的Proxy-Connection首部,然后将其发送给服务器,以收到预期的效果。
HTTP/1.1 持久连接,默认情况下是激活的。除非特别指明,否则 HTTP/1.1假定所有连接都是持久的。要在事务处理结束之后将连接关闭,HTTP/1.1应用程序必须向报文中显式地添加一个Connection:close首部。
客户端和服务器仍然可以随时关闭空闲的连接。不发送connection:close并不意味着服务器承诺永远将连接保持在打开状态。
发送了Connection:close请求首部之一,客户端就无法在那条连接上发送更多的请求了。
只有当连接上所有的报文都有正确的、自定义报文长度时,也就是说,实体主体部分的长度和相应的Content-Length一致,或者是用分块传输编码方式编码的--连接才能持久保持。
HTTP/1.1的代理必须能够分别管理与客户端和服务器的持久连接,每个持久连接都只适用于一跳传输。
Http/1.1设备可以在任意时刻关闭连接
HTTP/1.1应用程序必须能够从异步的关闭中恢复出来。只要不存在可能会累积起来的副作用,客户端都应该重试这条请求。
除非重复发起请求会产生副作用,否则如果在客户端收到整条响应之前连接关闭了,客户端就必须要重新发起请求。
一个用户客户端对任何服务器或代理最多只能维护两条持久连接,以防服务器过载。
如果有N个用户试图访问服务器的话,代理最多要维护2N 条到任意服务或父代理 的连接。
管道化连接(如何使用管道化)
HTTP/1.1允许在持久连接上可选地使用请求管理。在响应到达之前,可以将多条请求放入队列。当第一条请求通过网络流向服务器时,第二条和第三条请求也可以开始发送了。在高时延网络条件下,这样做可以降低网张的环回时间,提高性能。
对管理首化连接有几条限制
1 如果HTTP客户端无法确认连接是持久的,就不应该使用管道。
2 必须按照与请求相同的顺序达HTTP响应。HTTP报文中没有序列号标签,因此如果收到的响应失序了,就没办法将其与请求匹配起来了。
3 HTTP客户必须做好连接会在任意时刻关闭的准备,还要准备好重发所有未完成 管道化请求。
4 HTTP客户端不应该用管道连接方式发送会产生副作用的请求 如:POST,出错的时候,管道化方式会阻碍客户端了解服务器执行的是一系列管道化请求中的哪些。
任意解除连接
所有HTTP客户端、服务器或代理都可以在任意时刻关闭一条TCP传输连接。通常会在一条报文结束时关闭连接, 但出错的时候,也可能在首部行的中间,或其他奇怪的地方关闭连接。
Content-Length及截尾操作
每条HTTP响应都应该有精确的Content-Length首部,用以描述响应主体的尺寸。一些老的HTTP服务会省略Content-Length首部,或者包含错误的长度指示,这样就要依赖服务器发出的连接关闭来说明数据的真实末尾。
客户端或代理收到一条随连接关闭而结束的HTTP响应,且实际传输的实体长度与Content-Length并不匹配时,接收端就应该质疑长度的正确性。
如果接收端是个缓存代理,就不应该缓存这条响应。代理应该将有问题的报文原封不动地转发出去,而不应该试图去 校正 Content-Length,以维护语义的透明性。
如果一个事务,不管是执行一次还是很多次,得到的结果都相同,这个事务就是幂等的。客户端不应该以管道化方式传送非幂等请求。
尽管用户Agent代理可能会让操作员来选择是否对请求进行重试,但一定不能自动重试非幂等方法或序列。
正常关闭连接
TCP连接是双向的。TCP连接的每一端都有一个输入队列和一个输出队列,用于数据的读或写。放入一端的数据最终会出现在别一端的输入队列中。
应用程序可以关闭TCP输入 和输出信道中的任意一个,或者将两者都关闭了。套接字调用close()会将TCP连接的输入 和输出信道都关闭了。这被称作 完全关闭,还可以用套接字调用shutdown()单独关闭输入或输出信道。这被称为 半关闭
关闭连接的输出信道总是很安全的。连接另一端的对等实体会在从其缓冲区中读出所有数据之后收到一条通知,说明流结束了,这样它就知道你将连接关闭了。
关闭连接的输入信道比较危险。如果另一端向你已关闭的输入信道发送数据,操作系统就会向另一端的机器回送一条TCP 连接被对端重置 的报文,大部分操作系统都会将这种情况作为很严重的错误来处理,删除对端还未读取的所有缓存数据。对管道化连接来说,这是非常糟糕的事情。
HTTP规范建议,当客户端或服务器突然要关闭一条连接时,应该 正常地关闭传输连接 ,但它并没有说明应该如何去做。
总之实现正常关闭的应用程序首先应该关闭它们的输出信道,然后等待连接另一端的对等实体关闭它的输出信道。当两端都告诉对方它们不会再发送任何数据之后,连接就会被完全关闭,而不会 有重置 危险。
但无法确保对等实体会实现半关闭,或对其进行检查。因此想要正常关闭连接的应用程序应该先半关闭其输出信道,然后周期性地检查其输入信道的状态,查找数据,或流的末尾。如果在一定时间区间内对端没有关闭输入信道,应用程序可以强制关闭连接,以节省资源。