- 进程通信和套接字
应用层解决的是两端应用程序(进程)通信的问题。两端进程是通过套接字通信,为用户进程提供套接字也是应用层的主要功能。网络通信的用户进程是运行在应用层上的。
套接字是同一台主机内应用层与运输层之间的接口,也是提供给应用程序的可编程接口,应用程序可以通过套接字控制和使用应用层的一切功能,但无法通过套接字控制运输层。
应用程序通过套接字对运输层的控制仅限于:选择运输层协议(TCP 或 UDP);能设定几个运输层参数 如最大缓存和最大报文段长度。
- 进程寻址
通过IP地址和端口号寻找接收方进程(具体来说是寻找接收方进程对应的套接字)。
- 运输层协议概述
TCP服务
能提供面向连接和可靠数据传输的服务。
面向连接服务:应用层报文开始流动前,TCP会让两端主机的套接字交换和保存双方的控制信息(IP、端口、窗口大小等),这也是建立连接的本质。该连接是全双工的,两端进程都能收能发。
可靠数据传输服务:即使用TCP协议通信的双方,把数据通过套接字传递给对方后能做到没有字节的差错、丢失、冗余和乱序。
TCP还有拥塞控制机制,该服务不一定能为通信进程带来直接好处,但能为因特网带来整体好处:当通信双方的网络出现拥塞,TCP的拥塞控制会抑制发送进程的发送速度,使占用信道的多个TCP连接公平的共享网络带宽。
UDP服务
是一种不提供不必要服务的轻量级运输协议,只提供最小的服务。UDP是无连接的不可靠数据传送服务,不保证报文到达接收进程,达到的报文可能也是乱序的。UDP协议没有拥塞控制,发送端可以用它选定的任何速率向下层(网络层)注入数据。
- HTTP协议
HTTP使用TCP作为支撑的运输协议,服务器向客户端发送被请求的文件而不存储客户的状态信息,因此HTTP是无状态的协议。
长连接和短连接
短连接是指每个HTTP请求发送前都要与服务端建立连接,响应返回后服务端就关闭连接;长连接是指多个HTTP请求复用一个连接,由客户端决定什么时候关闭连接。
往返时间(RTT)
是指一个分组从客户到达服务端再返回客户端花费的时间。RTT包括分组的传播时延、处理时延和排队时延。
一个短连接的文件下载请求大约会花费2个RTT的时间+文件接收(传输)时间:
其中三次握手占一个半RTT,同时HTTP请求也连带着在这次ACK报文中发给了服务端。最后目标文件的多个分组从服务端到达客户端,假设文件不会太大,文件的所有分组在网络中的时长占半个RTT。总共是2个RTT,外加客户端接收和拷贝文件的时间。
短连接要求为每次请求维护一个独立连接,每个连接的建立都会在两端创建一个新的socket,以及为这个socket分配资源(如缓冲区等),浪费资源增加两端负担,另一方面每次连接都要耗时2个RTT,效率低。
长连接可以优化这些缺点。典型的使用场景就是打开一个Web页面,里面所有的js脚本和其他静态文件的请求都用一个连接传输。
HTTP请求报文的格式
包含请求行(必需)、请求头(非必需)和请求体(非必需)。
同理,HTTP的响应报文包含响应行、响应头部和响应体。
- HTTP的性能
· HTTP事务时延
一个HTTP事务是指,一个HTTP请求从发出到响应到达发送端的过程,如下所示:
上图显示一个HTTP事务的时延主要包括4部分:
1、DNS查询:如果从本地DNS缓存就能得到目标IP地址,则耗时0RTT;如果从本地DNS服务器就能得到目标IP地址,则耗时1个RTT;如果涉及到DNS的递归和迭代请求,则耗时多个RTT。
2、TCP连接的三次握手,占 1 个RTT,如果使用TLS安全连接则占3个RTT。
3、请求到达服务器和响应返回的时间,占1个RTT。
4、服务器应用程序处理请求的时间,假设请求的是一个静态文件,则处理时间可以忽略不记。
与TCP连接相关的时延
HTTP协议是基于TCP协议的,因此一个HTTP事务的性能很大程度取决于TCP通道的性能,下面是一些TCP连接相关的可能会拖慢HTTP的地方:
1、TCP连接的3次握手时延
花费 1个 RTT。越小的HTTP事务在TCP连接下的效率越低。
2、延迟确认
接收方在接收到一个序号的分组后不会马上返回ACK报文,而是允许接收方在有数据发往发送方的分组中“捎带”这个ACK消息(ACK标志位和ACK号),这是考虑到ACK确认报文很小,单独发一个报文会比较浪费(TCP头部就占了40~60字节)。
这也会给HTTP响应带来时延。
3、TCP慢启动
TCP慢启动使 刚打开的连接 传输分组的速度很慢。
4、Nagle算法 和 TCP_NODELAY
Nagle算法是为了防止一次分组发送的实际数据量太少,造成网络效率降低,所以要求缓冲区需要到达MSS及以上大小的数据时才发送分组(或者发送计时器到时)。如果缓冲区里的数据不足一个MSS大小,那么要等到在前一个分组被确认之后或者发送计时器超时才能发下一个分组。
Naggle会引发几种HTTP性能问题:小的HTTP报文无法填满一个MSS,所以只能等到计时器到达才能发送;延迟确认会阻止Naggle发送下一个分组(延迟确认和Naggle共同作用)。
HTTP应用程序会设置参数TCP_NODELAY禁用Naggle算法。当然,应用程序要保证每次向缓冲区输送的数据块得是较大的数据块(如512B)而不能时逐字节发送。
5、TIME_WAIT 累积和端口耗尽
主动关闭连接的一方(不论时服务器还是客户端)进入TIME_WAIT后,主动关闭方会在内存中记录最近所关闭的对端IP地址和端口,记录有效期维持2MSL,2MSL内连接并不会真正关闭。在这段期间内,该端不会与对端建立相同IP和端口的连接。
假如A和B以很快的速率创建和关闭连接,B就没有端口可以和A的80服务建立连接了,这就是端口耗尽(是客户端B的端口耗尽),当然这种情况很少出现,只会出现在性能测试的时候。
对于服务端,服务端如果有大量的TIME_WAIT未关闭连接也会使操作系统的速度严重减慢(如果使用多线程则一个连接可能就是一个线程)和消耗大量内存(毕竟一个连接是一个套接字)。
TCP连接过多意味着什么?
1、大量的套接字会占用客户端、服务器以及代理的内存(输入输出缓冲区要预先分配的)和CPU;
2、并行 TCP 连接接收和发送数据时竞争共享的带宽;
3、应用的并行能力也受限制(管理一个套接字可能就要用一个线程)。
· HTTP优化建议
从大方向上,HTTP优化只会围绕2点进行:减少 网络延迟 和 减少要传输的字节。具体可以通过以下几点进行优化:
1、减少DNS查询次数
尽量命中本地DNS缓存和加长DNS缓存记录的过期时间。
2、减少HTTP请求
任何请求都不如没有请求更快,因此要去掉页面上没有必要的资源,多次请求尽可能合并为1次请求。
3、使用CDN
从地理上把数据放到接近客户端的地方,可以显著减少每次请求的传播时延。
4、添加Expires首部并配置ETag标签
相关资源应该缓存,以避免重复请求每个页面中相同的资源。Expires 首部可用 于指定缓存时间,在这个时间内可以直接从缓存取得资源,完全避免 HTTP 请求。ETag 及 Last-Modified 首部提供了一个与缓存相关的机制,相当于最后一次 更新的指纹或时间戳。
5、Gzip资源
所有文本资源都应该使用 Gzip 压缩,再传输。
6、避免HTTP重定向
HTTP 重定向极其耗时,特别是把客户端定向到一个完全不同的域名的情况下, 还会导致额外的 DNS 查询、TCP 连接延迟,等等。
- HTTP协议演进
· HTTP 1.0
HTTP 1.0 默认使用短连接,一个连接只发送一个请求,而且请求之间串行发送,只有接受到上一个请求的响应才能够发送下一个请求。
此时连接时延和慢启动时延会累加起来。
· HTTP 1.1 的特点
1、并行连接
上面的4个连接不再是串行发起,而是并发的发起,并且里面的请求也在连接建立后马上发送。此时连接、慢启动的时延是重叠的而非累加的,而且请求和响应的传播时延也是重叠的。
该优化的问题:如果浏览器要请求的资源很多(一个页面上百个静态文件),客户端和服务端同一时间打开大量短连接,双端都会消耗过多内存(尤其是服务端),占用的内存主要是指创建的套接字。多个连接会竞争有限的带宽,带宽不足的情况下,其实并行的连接也很慢。
所以,服务端会限制同时打开的连接数,浏览器也会限制对每个主机(域名)的并行连接的数量。
2、持久连接
持久连接(HTTP/1.0+“keep-alive”,以及 HTTP/1.1“persistent”)允许多个 HTTP 请求重用现存的连接。好处是消除多次的连接握手时延和慢启动时延。
持久连接的问题是会使客户端和服务器累积大量的空闲连接,白白耗费机器资源。
对于HTTP 1.0,如果服务器愿意为下一条请求将连接保持在打开状态,就在响应中包含相同的首部 Connection: Keep-Alive ,否则客户端就认为 服务器不支持 keep-alive,会在发回响应报文之后关闭连接。
下面的header头表示 服务器最多还会为 另外 5 个事务(请求)保持连接的打开状态,或者将打开状态保持到连接空闲了 2 分钟之后。
Connection: Keep-Alive
Keep-Alive: max=5, timeout=120
对于 HTTP 1.1,它是默认支持长连接,如果不用持久连接,需要使用Connection: close 首部。
在长连接中,如果客户端不想在连接上发送其他请求了,就应该在最后一条请求中发送一个 Connection: close 请求首部,最后一个响应也会包含 Connection: close 首 部,客户端收到后就会主动关闭持久连接。服务端不发送 Connection: close 并不意味着服务器承诺永远将连 接保持在打开状态。
如果在客户端收到完整响应之前连接关闭了,客户端就必须要重新发起请求。
一个用户客户端对任何服务器或代理最多只能维护2条持久连接,以防服务器过载。
目前浏览器大多结合使用 并发连接 和 持久化连接 来进行http优化。
3、管道化连接
上面的持久连接依旧是串行的发送请求,传播时延是累加的。
HTTP/1.1 允许在持久连接上可选地使用请求管道(是一个请求队列)。连续多个http请求在响应到达之前可以并发的发送,此时多个请求的传播时延和RTT就是重叠的。
管道化连接是建立在持久化连接之上的。
虽然请求1~4是并发发出的,但也是有序发出的,请求1所有报文段一定会早于请求2交付应用层。就算请求2的报文段比请求1早到达服务端,服务端也会缓存请求2,并按先请求1后请求2的顺序交付应用层。
HTTP 1.1 还要求响应严格按请求的顺序返回,不能多个响应交错到达。假设请求1~4到达服务端,应用程序并行处理这4个请求,请求2先处理好了,但响应2不能发送客户端,只能缓存起来等待请求1处理完,先发响应1,后发响应2(响应也是并发的发送,只不过要按请求的顺序发送而已)。这就是所谓的队首阻塞问题。
HTTP队首阻塞原因
队首阻塞发生的根本原因是 HTTP 1.1 要求响应严格按请求的顺序返回,要求按序返回是因为请求和响应没有一一对应的序号或者标识,如果不按序返回响应,就不知道这个响应应该交付给哪个请求。
举个例子:
请求1~4按序交付到了服务端,服务端并发处理,先生成了响应2,如果响应2不等响应1就先发了,当响应2到达客户端,客户端该把响应2交付给请求1~4中的哪一个呢?
如何解决?
解决方法是每个请求都生成一个唯一标识1~4,服务端生成的响应也带上标识1~4,服务端先发出响应2。根据标识,先到客户端的响应2就知道应该要交付给请求2。这样就不必要求响应严格按请求的顺序返回。
可否根据响应的 url 作为交付请求的标识?
不行,因为多个并发的请求的url可能是相同的而非唯一的,例如 请求1~4的url都是 /index.html。 响应2先到达,本来应该交付给响应2,但却交付给了响应1。
队首阻塞会带来的问题
阻塞、占用缓存、引起重复操作。
a、一个慢响应就会阻塞后面响应的返回;
b、先处理好但无法发送的响应会造成缓冲区开销;
c、中间某个响应失败(如500)导致连接断开,会使后面的响应无法发送,这些请求客户端要重发。这可能会引起错误重复操作(例如是POST请求,把用户金币+1,请求处理成功,但响应没法回去,客户单重发导致用户金币重复增加)。
为了缓解这些问题,HTTP1.1可以同时使用 管道化连接 + 多个并行的连接,例如 请求1 和 请求3使用 管道化连接1,请求2和请求4使用 管道化连接2,这样服务器的响应队列就会有2个,请求1和3在一起,请求2和4在一起,1不会阻塞2,但会阻塞3,因此该方案只能缓解,不能根本性解决。
下面是HTTP 1.1的其他特性
4、Host域
HTTP 1.0中认为每台服务器都绑定一个唯一的IP地址,因此请求消息中的URL并没有传递主机名(hostname)。实际上一个物理主机上可能有多个主机名(域名)对应多个项目,这些主机名共用一个IP。HTTP 1.1支持host头部信息告知服务端要请求那个域名,请求消息中如果没有host域会报告一个错误(400 Bad Request)。
5、缓存处理
在HTTP 1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。
· HTTP 2.0 的特点
1、二进制分帧层
首先需要介绍HTTP2的新概念 流 和 帧 ,这里的帧是指应用层的二进制分帧,而非数据链路层的帧。
HTTP2中的一个 流 是指 一对完整HTTP消息(请求和响应)的字节流。
一个流 = 一个请求 + 这个请求的响应
每个流有一个唯一的标识符,这样的一对请求和响应具有相同的流标识。
HTTP1.x中,HTTP消息是纯文本数据(头部肯定是纯文本,数据部分可能是纯文本或者二进制数据)。
HTTP2 在应用层中添加了一个二进制分帧层,作用是把纯文本的HTTP消息分割为更小的帧,并将每个帧的内容(HTTP消息头部和数据部分)编码为二进制,每个帧都有属于自己的头部信息。这就是二进制分帧。
二进制分帧包含多种,最主要的两种为:包含http消息头部的 HEADERS 帧 和 包含 http 数据部分的 DATA 帧。
一个 流 包含多个帧(多个HEADERS 帧(因为http的头部也可能很大,尤其是cookie可能就包含很多字节) 和 多个DATA 帧)。帧头部会包含所属的流标识。
下图是帧的头部:
16 位的长度前缀意味着一帧大约可以携带 64 KB 数据(所以一个帧还要被TCP分成几十个报文段),不包括 8 字节首部。
客户端和一个主机域名只通过一个持久连接并发的可乱序的发送 所有流的所有帧 并在对端根据流标识把帧组装成完整的一个流。
这意味着所有请求和响应都可以通过一个管道化连接并发的交错的不按序的发送,并且响应能正确交付到对应的请求,解决了队首阻塞问题。
下图是客户端和服务端的通信
流和流可以乱序(即不同流里面的帧可以交错),但同一个流里面的帧要按序发送,同一个流里面的二进制分帧序号要和TCP序号对得上。例如 流1的帧1~3,发送顺序是帧123(帧1对应TCP序号 0~1000, 2对应1001~2000, 3对应2001~3000),而不会是帧132(帧1对应TCP序号 0~1000, 3对应1001~2000, 2对应2001~3000)。
2、请求优先级
HTTP 2 可以为每一个流设置优先值。0最高级,2^31 - 1最低级。
服务器优先处理高优先级的流、为这个流分配更多的资源(如带宽)并先发送它的响应。不过这又会引入队首阻塞问题,即某个高优先级的慢请求会阻塞响应的发送。
3、每个来源一个连接
意思是,每一个客户端都只需和服务端的一个域名建立一个持久连接,无需建立多个并行的连接。
好处自然是节省了多次慢启动时延和TCP握手时延。
4、针对流的流量控制
HTTP 2.0 为一个连接内的多个流提供了流量控制和流量分配
在同一个 TCP 连接上传输多个数据流,就意味着要共享带宽。TCP为一个连接提供了流量控制,而HTTP 2.0 为一个连接内的多个流提供了流量控制和流量分配,这是属于HTTP的流量控制,是基于一种“窗口更新帧”的帧进行的。
具体做法是接收方在接收到某个流的多个帧过程中,会向发送方发送一个或多个 WINDOW_UPDATE 帧,这个帧指明流ID和这个流的窗口大小。
任何一端都可以选择禁用个别流或 整个连接的流量控制。
5、服务器推送
HTTP 2.0 允许服务器可以对一个客户端请求发送多个 响应。
除了对最初请求的响应外,服务器还可以额外向客户端推送资源而无需客户端明确地请求。
具体是指:客户端发送一个html的请求,html包含很多css、图片和js,客户端只需发送一个html请求,服务端就可以把html响应返回,并连带着html内所需众多静态资源主动推送给客户端。节省了多个请求的发送。
主动推送的实现有很多策略,比较容易实现的2种是:
1、客户端解析html中所需的其他资源,并将这些希望让服务器主动推送的资源路径放到html请求的一个header头中(如Apache支持的 X-Associated-Content首部)
2、服务器响应html前解析html中所需资源,并主动推送。
当然 服务器不会随意发起推送流,需要和客户端协商过后才被允许主动推送。客户端需要缓存推送的资源,以避免重复请求或重复推送。
6、首部压缩
客户端的多个请求中相同的header不会重复发送。
HTTP众多的header头使每个HTTP消息至少有500~800字节,加上cookie可能上千(首部过长而且重复传输首部是HTTP1.1除了队首阻塞外的另一缺陷)。HTTP2提供首部压缩功能,使用了HPack算法。
首部压缩的实现:
双端为一个连接各自维护自己的“首部表”,记录之前本端发送过的首部信息,这个首部表类似于一个hashmap,包含请求url、请求方法method以及各种header头。
首部表在整个连接存在的期间都存在,并且通信双方都要维护一个相同的首部表,发送方发送的第一个HTTP消息的头部信息让首部表有了初始值。
之后每个HTTP报文发送的时候,如果该报文头部的信息对照首部表发生了变化,则将这个变化了的信息更新到首部表中。发送的时候也只需发送变化了的首部(例如method和path以及一些header头),没有变的首部无需重复发送。
接收方收到报文后读取HTTP消息中改变了的首部信息,并更新到接收方的首部表。其他没有改变的首部直接从首部表中读出来 与 HTTP消息中的首部信息拼成完整的首部。
7、基于TLS
HTTP1.1本身默认不使用TLS安全传输层协议,得使用HTTPS协议可以支持TLS。HTTP2默认使用TLS安全传输。
· HTTP3的特点
HTTP2通过二进制分帧层,使请求和响应可以并发且交错的发送,解决了服务端在应用层的队首阻塞的问题。发送可以乱序,但交付应用层仍需按序。
假设客户端需要发送5个请求,每个请求2个报文段(共10个报文段,报文段1~10,序号1~1000)。假设第2个请求的第2个报文段(即报文段3)丢失,其他9个报文段全都到达。
根据TCP的可靠传输协议,数据必须按序交付应用层,所以只有第一个请求交付给服务器应用层,后面的3~5号请求都要等待 客户端 把第2号请求的报文段3 超时重传。
这是TCP的队首阻塞,是有TCP协议的可靠传输这一性质决定的,HTTP无法改变。
因此 HTTP3 在HTTP2的基础之上(HTTP3也支持二进制分帧层,支持流)把 HTTP 下层的 TCP 协议改成了 UDP来解决这个问题。
HTTP要求的是可靠传输,UDP协议是不可靠传输,但基于 UDP 的 QUIC 协议 可以实现类似 TCP 的可靠性传输。
QUIC 有自己的一套机制可以保证传输的可靠性的。当某个流发生丢包时,只会阻塞这个流的交付,其他流仍可以先交付。
TL3 升级成了最新的 1.3 版本,头部压缩算法也升级成了 QPack。
QUIC 直接把以往的 TCP 连接握手和 TLS握手合并成了共 3 次握手,减少了握手的次数。
QUIC 是新协议,对于很多网络设备,根本不知道什么是 QUIC,只会当做 UDP,这样会出现新的问题。所以 HTTP/3 现在普及的进度非常的缓慢,不知道未来 UDP 是否能够逆袭 TCP。
HTTP 1 ~ 3的演进图如下