HTTP是使用TCP连接的,HTTPS是在HTTP与TCP之间插入一层SSL的加密层保证传输安全,最后还是以TCP进行下层的传输。
TCP为HTTP提供可靠的比特传输,并且保证传输的有序性和正确性。
显然TCP还需要更下层的IP协议进行协助传输数据,TCP将HTTP报文的内容以流的形式进行有序传输,并将其划分为更小的TCP段,装到IP分组中交由IP协议进行传输,这些操作都是对上层透明的。 与HTTP报文类似,IP分组和TCP段也是含有首部的,于是一个IP分组中包含三个部分,IP分组与TCP段的首部,还有一个TCP数据块。其中TCP段首部与TCP数据块构成IP分组的数据部分。
IP分组的首部包含源与目的地址的IP,还有整个分组的相关信息,TCP首部则包含了相应的端口号,TCP控制标记等信息。
使用端口号来对同一时间内多个TCP连接进行区分,并以此保证其正确运行。相应的连接的表示以IP地址+端口号的组合来进行标记,一条连接对应一个源IP地址+端口号和一个目的IP地址+端口号。 相应的连接之间可以使用相同的目的端口号,或者使用相同的源IP地址,但不存在两个连接使用的四个部分完全相同。
通过系统中的相关API(如UNIX中的套接字API),可以实现对于TCP连接的编程,而相应的API都隐藏了TCP与IP的细节(包括底层协议的握手细节,TCP流与IP分组间分段重装的细节);其允许操作进行TCP端点数据结构的创建,并与远程的服务器TCP端点连接与数据流的读写。 建立连接是需要时间的,从服务器监听开始等待连接,到建立TCP连接完成,其时间花费不仅取决于传输的距离,还有服务器的负载,网络的拥挤程度等。
由于TCP于HTTP在协议栈中的位置精密相连,所以TCP的性能会直接影响HTTP事务的性能。
显然HTTP处理事务的时间在整个过程中的占比是极小的,除非服务器内部出现长时间的等待,否则HTTP的时延主要由TCP时延构成。
URL到IP+端口需要通过DNS服务器解析,相应的时间会有花费。客户端与服务器间建立TCP连接会有时间消耗(三次握手),如果有多个HTTP连接的话,就会有多个TCP的连接过程,对应的时间延迟会更大。连接建立后相应的请求发送也需要时间,请求到达后Web服务器会对其处理,相应的响应返回也需要时间。总的来说时延的原因是复杂的,有硬件的因素,网络与服务器的负载,请求与响应报文的大小,传输距离等等,与TCP协议的复杂程度也有很大关系。
TCP的相关延迟大致可以包含以下几个因素:
TCP连接的握手延时TCP慢启动拥塞控制数据聚集的Nagle算法用于捎带确认的TCP确认算法TIME_WAIT时延与端口耗尽TCP通过一系列的IP分组来进行参数沟通,连接有关的参数会在这些交互中确定,如果传输的数据量过小,对应的有效传输率就会很低,HTTP性能就会下降。
第一次握手:报文的SYN字段被置为1,客户端选取一个起始序号放到报文中一并发过去。连接的一方向对方说明自己的存在。第二次握手:服务器收到了数据包,从中提取SYN字段,并且进行相应的处理,发送一个数据包告知客户端。数据报中的确认字段为客户端传来的序号加一,同时服务器会选一个自己的起始序号放入报文序号段处,并将SYN字段置1。第三次握手:客户端收到服务端的报文后时,进行相应的处理,并发送第三次的握手包,其中的序号字段为之前的加一,确认字段为接受到的序号加一。在最后一个握手包中就可以进行相应的数据捎带传输了。这些握手分组对上层都是透明的,上层只能察觉相应的时延,HTTP事务中的数据一般都不多,相应的用于数据传输的只有第三次握手与之后的应答响应,TCP连接的建立过程占据传输的绝大部分时间。HTTP可以通过复用TCP连接来提高数据传输率。
确认机制是当发送方每发送一个报文,接收方收到后都返回一个应答报文,如果发送方在一定时间没有收到应答报文,则认为之前的传输失败,报文会重新发送。 用于确认的应答报文是很小的,可以将确认放到数据传输时的报文中,进行捎带确认。为了提高捎带的利用率,TCP相应实现了延迟确认的算法,其规定在一定的时间内(极小的时间段),确认报文都会存放到缓存区中,等待数据传输来相应的进行捎带确认,如果该时间段没有数据传输,则单独将该确认放到一个分组中进行传输。 但是有些时候,捎带确认的可能性很小时,延迟确认算法就会带来很大的延迟,根据系统相应可以调整禁止使用确认延迟算法。
由于TCP开始进行连接时无法弄清楚当前网络的负载能力,所以使用一种拥塞窗口的结构,每次发送的数据分组个数受拥塞窗口限制,初始时拥塞窗口大小为1,一次只能发送一个数据分组,当所有分组的确认报文返回时,相应的将窗口扩大一倍,所以一个新建立的TCP连接往往比已经建立的连接慢。
拥塞窗口的大小也不是无限的增加的,到达一定的值之后会根据网络的拥塞情况进行判断,如果网络情况还行,就会进行线性增长,而不再像之前一样进行指数增长,如果网络出现拥塞则相应的进行拥塞处理。
通过TCP进行连接的数据传输并不是每次都可以将分组塞的满满的,所以为了提高有效传输率,会将一些小的TCP数据放在一起交由一个分组进行传输,减少不必要的首部数据消耗。相应的规则为:
当分组的长度达到分组的最大尺寸时,允许发送。如果分组中含FIN字段(用于连接释放),则允许发送。当所有的小数据包(小于最大尺寸的)均被确认后,允许发送。如果发送了超时,则立刻发送。TCP_NODELAY参数可以禁用Nagle算法由此可得Nagle算法对HTTP影响还是比较大的,为了凑够数据量来发送分组会带来额外的等待延迟。与延迟确认联系一下,由于延迟确认算法,确认报文无法及时到达,Nagle算法由于没有收到确认报文又会阻止小的分组传输,而去等待不会到来的后续数据,所以只有等到超时在进行发送,相应的延迟又会增大。
TIME_WAIT状态时在四次挥手时,第四个挥手包发送之后服务器进入的状态,在这个状态中服务器要等待两个MSL(报文最大生存时间),来保证最后一个挥手包能准确的被客户端接收,防止其重传第三个挥手包,此外经过两个MSL之后上一次连接的所有报文在网络中都会消失。因此在这段时间内除非时之前释放的连接重连,否则该主机其他的TCP连接都无法使用对应的(源)端口号。
于是四个部分中的源端口号就被限制了,如果该主机有多个TCP连接访问HTTP服务(80端口),对应的双方的IP,目的端口都被限制了,请求主机的端口就会使用大量的临时端口(大致在1024-65535间),当这些端口在这段时间都被服务器转到TIME_WAIT状态(或者处在连接打开状态,也占用了端口)时,端口就耗尽了,客户端主机就无法建立TCP连接来访问服务器相应服务了。
端口耗尽相应的还是比较难遇到的,但是大量的端口开启也会占用许多的系统资源造成系统速度下降。
HTTP在TCP连接的基础上进行相应的优化,以提升性能;相应的HTTP的连接可以分为以下五种:
串行连接并行连接持久连接管道化连接复用的连接HTTP在客户端到服务器间建立一条逻辑连接(包括中间的代理与高速缓存等),从客户端开始逐个结点的将报文传输到服务器。 Connection首部主要承载三种不同类型的标签,标签之间使用逗号分隔:
HTTP首部字段,仅用于描述两个相邻结点间的连接用于描述连接的非标准选项,可以是一些自定义的标签close值,关闭持久连接,当这条报文发送完之后就关闭连接Connection首部提供了对首部的保护,在Connection首部的标签与首部都只用于相邻两个结点之间的连接,相应的结点收到报文后,将相应的报文与信息留下进行处理,并将该结点到下一结点间连接的相关控制信息建立新的Connection首部,并将报文继续传向下一个结点。 除了Connection首部外,Proxy-Authenticate、Proxy-Connection、Transfer-Encoding和Upgrade等首部也是不一定会传到下一结点的,一般在代理连接中经常出现。
串行连接中每个HTTP事务都占用一个独立的TCP连接,每个资源的请求都放到一个事务中,各个事务依次进行处理,每次只能处理一个连接,相应的连接延迟开销巨大。
在界面渲染上,由于图像尺寸在事务处理之前无法知晓,所以在加载足够多的对象之前,界面上无法展示相关的内容。
并行连接可以一次性处理多个TCP连接,多个事务在这些连接中并行处理。 并行连接中的时延是有重叠的,所以并行连接在处理事务时可能会提升界面的加载速度。
并行连接对于界面加载速度的提升并不是一定的,在带宽不大且有限的情况下,多个并行连接竞争有限的带宽资源,连接数量过多时,每个连接的传输性能就会大打折扣,并且连接数量的增加会导致系统内存资源的紧张与性能的下降,多个连接的控制也比串行更复杂。
尽管并行连接是进行多个连接处理,但是单个连接的处理时间可能远超串行。然而多个事务连接的同时处理却给我带来比串行处理更快的感觉。
站点本地性是指在客户端在请求资源的时候,资源的分布具有空间上的局部性,客户端请求的资源可能都在一个web服务器上,并且在请求这些资源的时间分布上也存在局部性。
因此提出了连接持久化的概念,如果处理完事务后关闭当前的TCP连接,那么这样的连接属于非持久连接,如果在事务结束之后,仍保持TCP连接的打开状态,那么对应的连接属于持久连接。
重用已建立的对目标服务器的TCP连接,可以减少建立连接的时间开销,而且已开启的连接可以避开慢启动时传输速度的限制。 相比于串行连接,持久连接减少了连接建立的过程;虽然并行连接在建立连接时并行操作,相当于一条连接的延迟(近似),但是相应连接的处理开销还是无法避免;而且并行连接在慢启动时会受到极大的限制,连接数量增多也会增加系统负担。
持久化连接在管理时如果处理不当会导致空闲的持久连接,给客户端与服务器带来不必要的开销。
相应的可以将并行连接与持久连接相结合,通过持久化连接可以减少并行连接的数量。
持久连接可以分为两种类型,一个是HTTP/1.0+(扩展的HTTP/1.0)中的keep-alive连接,一个是HTTP/1.1中的persistent连接。
keep-alive连接
keep-alive握手: 主要是在报文中的Connection首部中使用keep-alive,如果是客户端发送就是请求保持连接,若服务器愿意保留连接就在响应中也包含这一字段。 keep-alive首部: Connection中的keep-alive可以对应到首部中的keep-alive首部,进一步来调节持久连接,相关的参数间通过","间隔。常用的参数有: timeout(希望连接在空闲时的保持时间(估计值),超过这一值连接可能会超时)max(希望为多少个事务保持连接(估计值)),参数中还支持任意的键值对参数,用于测试与诊断。 限制规则: HTTP/1.0中的keep-alive不是默认使用的,必须使用Connection:keep-alive首部进行激活。所有希望保持持久连接的报文中都应含有Connection:keep-alive首部,否则服务器在接收到相应的报文之后就会在响应后断开连接。客户端收到服务器的报文通过Connection:keep-alive首部就可以得知服务器在处理完请求后断开连接了。报文中的Content-Length,编码,媒体类型等信息必须是准确的,这样接受方可以准确的确定各个报文间的分界,防止出现报文间数据的错乱。代理与网关在处理Connection:keep-alive首部时必须按照规则,先删除Connection:keep-alive首部与其内部的相应字段,再进行转发或者缓存。不应与无法确定是否支持Connection首部的服务器建立keep-alive连接,以防出现哑代理的问题。尽量忽略来自HTTP/1.0设备的Connection首部,因为其可能来自于较老的代理服务器转发而来。客户端如果不是重复发送请求,那么在收到完整响应之前连接关闭的话,客户端要准备好重试请求。哑代理
盲中继: 其描述了某些不支持Connection首部的代理的,其在处理时仅仅只进行转发,不对Connection首部进行特殊处理。
首先客户端请求与代理建立keep-alive连接,并等待响应。哑代理收到请求后,直接沿着链路转给服务器。服务器收到报文后,以为是代理要与自己建立keep-alive连接,于是返回Connection:keep-alive首部,建立与代理的keep-alive连接。代理收到后直接转发给客户端,客户端以为与代理建立的keep-alive连接,但是实际是代理完成了一次请求的转发,会等待服务器关闭连接或者继续转发服务器的响应,而服务器与客户端都以为自己与代理建立了keep-alive连接。于是代理的传输就变成了单向的,由服务器到客户端的转发,代理认为客户端对服务器不会再进行请求,而客户端认为建立了keep-alive连接,所以后续的请求会被挂起;而服务器也认为建立了keep-alive连接,不会关闭连接,哪怕响应报文完成传输也不会关闭。最终服务器与客户端的keep-alive连接都超时后,服务器会关闭连接。Proxy-Connection首部: 客户端可以使用Proxy-Connection:keep-alive首部来处理哑代理,转发到服务器后,服务器会忽略该首部,如果代理不是哑代理,其会删除Proxy-Connection并根据情况建立Connection首部。 但是当哑代理与正常代理混合为多层代理时就会出现问题。 上面的图中,在响应返回时,正常的代理可以看作服务器,与哑代理时的情况就一致了;下面的图中,正常的代理可以看作服务器,就与哑代理时的情况就一致了。
persistent持久连接
Connection:close首部: HTTP/1.1使用持久连接来代替keep-alive连接,与keep-alive连接不同,持久连接不是可选的,而是默认支持的,除非使用Connection:close首部,表示在事务完成之后就关闭相应的连接,否则连接默认为持久连接。但是也并非只有通过Connection:close首部进行连接的关闭,服务器不保证永久的保持连接的打开状态,客户端与服务器可以随时的关闭空闲连接。限制与规则: 客户端在没有请求需要发送的时候就在最后一个请求中加入Connection:close首部,之后客户端就无法在该连接上发送更多的请求了。报文中的Content-Length首部必须时正确的,这样在接收时才能区分各个报文的边界。HTTP/1.1中的代理必须能分别管理与客户端和服务器的持久连接,每个持久连接只用于两个结点之间。由于HTTP/1.0中有些代理存在哑代理的问题,所以在不了解具体的客户端处理能力的情况下,不应与HTTP/1.0客户端建立持久连接。虽然希望服务器在传输报文时不关闭连接,并且在关闭连接之前至少要响应一条请求,但是不论Connection是何取值,HTTP/1.1设备可以在任意时刻关闭连接。只要不产生积累的副作用,HTTP/1.1程序都应积极的从异步关闭中恢复,或者是在收到整个响应之前连接关闭了,客户端都应尝试重新请求。一个客户端对任意服务器或者代理,最多只能维护两条持久连接,防止服务器过载。同样代理作为客户端访问时,将客户端的持久连接累积,N个客户端就是最大2N条连接(到服务器或者父代理的连接),一并来支持用户的通信。管道连接是在持久连接的基础上建立的,是一个可选的方式,管道化连接在一条请求的响应到达之前,可以将后续的多条请求放入请求队列进行发送,充分利用请求发送之后到响应到来之前的空闲时间段。 管道化连接也有相应的限制规则:
管道的使用必须建立在持久连接的基础上,如果无法保证持久连接,就不应使用管道。由于响应报文中没有对应编号的设置,所以响应发送的顺序要与请求发送的顺序相同,如果接收响应报文时出现失序,就无法与请求进行匹配了。由于持久连接允许服务器任意时刻关闭连接,所以客户端要准备好重新发送服务器没能处理完的请求。客户端在使用管道时不应发送可能产生错误的请求,因为在出错的时候,管道化连接会阻碍客户端对服务器执行情况的了解;同时在重新请求时,对于一些非幂等的请求(每次请求产生的结果不同,幂等就是无论何时请求得到的结果都一样),无法进行安全的请求重试。HTTP的程序不论是客户端还是服务器或者代理,都可以在任意的时刻关闭一条TCP连接,一般是在报文传输完成之后,但是也存在传输中途关闭的情况,特别是在管道化的持久连接中,连接空闲一段时间后被服务器关闭。但是,服务器永远无法确认在其连接关闭的时候客户端后续有没有数据发送,可能在客户端请求写入一半时就出现了连接错误。
Content-Length首部: 每个HTTP响应都应该有准确的Content-Length首部,用来描述报文的边界,否则就仅仅只能依靠服务器发出的关闭连接来说明数据的边界。如果接收端收到的随连接关闭而结束传输的响应报文,其Content-Length首部与实际长度不匹配时:
(接收端是客户端时)应该对报文长度的正确性进行相关的矫正。(接收端是一般代理时)不用进行矫正,直接转发由之后的结点处理,以保证报文内容在传输中的透明性。(接收端是缓存代理时)不缓存这条响应。连接重试: 在非错误的情况下,连接也是可以在任意时刻关闭的,如果客户端仍在执行事务,在考虑可能的副作用的前提下,客户端应该重新进行连接,对于管道化的连接来说,这种处理会更加有难度,客户端会将留下的大量未处理的请求放入队列中等待重新调度。
幂等性: 对于不同的事务请求,如果其结果与执行的次数与时间无关,无论执行多少次,何时执行其得到的结果都相同,那么其具有幂等性,相应的具有幂等性的方法有:GET,HEAD,PUT,DELETE,TRACE,OPTION;而像POST方法等属于非幂等方法,非幂等方法在管道化连接重传中会出现不确定的后果,所以不用于管道化连接,并且非幂等(如POST)方法在一般的重新请求中,一定不是自动进行的,可以让用户来参与决断(浏览器弹出的是否重新请求的对话框)。
正常关闭
TCP的双向连接 TCP的连接是双向的,每一端都含有一个输入队列(缓冲区)与一个输出队列(缓冲区),可以进行数据的读写,一端将数据放到输入队列中,相应的数据会在另一端的输出队列中出现。 半关闭: 相对于关闭两个方向的信道(完全关闭),半关闭是仅关闭输入或者输出信道。 可以关闭输出信道说明数据流传输结束,以此来防止对方收到意外的错误数据,但是对于输入信道的关闭就具有相应的风险,除非可以确定另一端已经完成了数据传输,否则对方就会收到TCP连接被对方重置的报文,于是其就会删除在发送队列中的所有数据;相应的在管道化连接中,这种错误代价巨大,因为可能由于最后一条请求的间隔时间超时,导致连接断开,返回连接重置,缓冲区清空,对应的就会丧失之前收到的所有响应数据。正常关闭 建议使用的关闭方式是双方在传输完成后都主动的关闭输出信道,对应的再关闭自己的输入信道;但是确保对方实现了半关闭是很难实现的,只有在关闭自身输出信道后,周期性的检查输入信道;如果在一定的时间内对方没有关闭输入信道,可以通过强制关闭连接来结束通信,释放相应资源。