0%

tcp队头阻塞是什么?

tcp是按字节有序传输的,如果发送方发送了一连串的报文,中间有个报文没有收到ACK确认,会一直等待超时然后重传报文,发送方的滑动窗口一直不能移动,导致不能发送更多的报文。

这样http请求之间就不是独立的,会互相影响。

http队头阻塞是什么?

多个http请求只能顺序请求,因为http请求和响应没有编号,同时执行多个请求,不按顺序返回响应,客户端无法分辨是哪个响应对应哪个请求,这样如果前面的请求处理较慢,后面的请求都会被阻塞。

http2解决了什么?

  1. http报文分片
  2. http header压缩
  3. 支持服务端推送
  4. 基于TLS实现安全传输(与https相同)

报文分片

http报文是纯文本的,http2报文是二进制字节流,对报文进行了分片并编号,相当于把tcp的报文分段编号实现搬到应用层。

同一个tcp连接可以同时传输多个http报文分片,实现多路复用,解决http请求对头阻塞问题。

服务端推送

客户端请求一个html,服务端可以把相关的css、png等资源一同返回给客户端,减少客户端请求次数。

QUIC解决了什么?

Quick UDP Internet Connection

基于udp实现,在应用层实现传输可靠性,非常的自由灵活。

主要改进:

  1. 快速握手
  2. 避免对头阻塞的多路复用
  3. 连接迁移

快速握手

在HTTPS协议中,由于TCP和TLS都各需要自3次握手,导致连接建立过程较为复杂和耗时,降低了HTTPS的效率。

QUIC选择UDP来作为其底层协议,就可以将连接建立和密钥协商的过程合二为一,简化操作流程,提高连接效率。

在连接建立成功后, 客户端会缓存起来原始的连接信息等。 在接下来与相同的服务器建立连接的过程中, 客户端能够在不增加额外RTT的情况下建立一个加密的连接,数据要发送的数据可以在握手的包中捎带着发送过去,而不用等待服务器的回复,从而实现0RTT。

所以,所谓QUIC的0RTT是指在建立连接之后,后续发送数据都不需要增加额外的RTT时间,最开始的握手还是需要1RTT的时间消耗的。

多路复用

为什么quic能解决队头阻塞?

  • http队头阻塞用http分片解决。
  • tcp队头阻塞用udp协议代替,在应用层重新实现可靠传输机制,http请求之间是独立互不影响的。

为什么不修改tcp协议解决队头阻塞?

  1. tcp在操作系统层面实现,普及比较麻烦。
  2. 各种网络中间设备(如网关、防火墙、代理服务器)为了效率最优化,保持了约定俗成的潜规则,如某些防火墙只允许特定端口,中间代理会删除报头中不认识的选项字段

连接迁移

一个TCP连接由四元组确定:源ip、源端口号、目标ip、目标端口号。

任何一个元素变化了,tcp就要重新建立连接,例如移动手机在移动网络和WiFi切换,重新建立连接会带来数据传输时延。

而QUIC的连接不依赖于四元组,以一个随机ID作为标识,这样IP或端口发生变化,只要连接ID不变,还是认为是一个连接,应用层感知不到底层tcp连接的变化,这样把影响就降到最低。

http传输数据存在哪些问题?

不使用SSL/TLS的HTTP通信,就是不加密的通信。所有信息明文传播,带来了三大风险。
(1) 窃听风险(eavesdropping):第三方可以获知通信内容。
(2) 篡改风险(tampering):第三方可以修改通信内容。
(3) 冒充风险(pretending):第三方可以冒充他人身份参与通信。

Charles抓包就是中间人拦截监听。

https新引入了什么?解决了什么问题?

  1. 数据加密传输,防止窃听
  2. 校验数据,防止篡改
  3. 使用数字证书验证身份,防止身份被冒充

https缺点是什么?

  1. TLS握手增加了建立连接的时间
  2. 每次加密解密数据需要占用额外的时间和CPU资源

https如何保证数据的完整性?

http验证数据完整性存在的问题:

http通信是不加密的,即使对报文做了哈希摘要,并放在报文中传输,http通信内容被拦截篡改后,一并把哈希摘要也改掉,客户端是没办法识别报文是否被篡改。

https改进:

对报文的哈希摘要进行加密,另一方接受到报文并重新计算报文的哈希摘要,再对比解密后的摘要,检查是否一致。

对摘要的加密用对称加密,对称加密的秘钥用非对称加密方式加解密。

https加密过程是怎样的?

采用对称加密方式加密报文,对称加密的秘钥用非对称加密方式加密后传递给通信双方。

因为:

  1. 对称加密速度快,但是通信双方都需要同一个密钥,密钥在传输过程中被窃取泄露后就不安全了
  2. 非对称加密速度慢,但私钥不公开,只传递公钥出去,可以保证安全性

https通信过程是怎样的?

  1. 客户端发起SSL/TLS连接
  2. 客户端获取服务端数字证书,验证服务端身份
  3. 两端生成随机数,由服务端根据随机数,用私钥加密得到对称加密密钥
  4. http的请求和响应报文都用对称加密密钥加密后再传输

SSL/TLS握手过程是怎样的?

有四次握手

  1. 客户端请求建立SSL链接,并向服务端发送一个随机数–Client random和客户端支持的加密方法,比如RSA公钥加密,此时是明文传输。
  2. 服务端回复一种客户端支持的加密方法、一个随机数–Server random、授信的服务器证书和非对称加密的公钥。
  3. 客户端收到服务端的回复后利用服务端的公钥,加上新的随机数–Premaster secret 通过服务端下发的公钥及加密方法进行加密,发送给服务器。
  4. 服务端收到客户端的回复,利用已知的加解密方式进行解密,同时利用Client random、Server random和Premaster secret通过一定的算法生成HTTP链接数据传输的对称加密key – session key。

此后的HTTP链接数据传输即通过对称加密方式进行加密传输。

为什么需要3个随机数作为对称加密的密钥?

不管是客户端还是服务器,都需要随机数,这样生成的密钥才不会每次都一样。由于SSL协议中证书是静态的,因此十分有必要引入一种随机因素来保证协商出来的密钥的随机性。

对于RSA密钥交换算法来说,pre-master-key本身就是一个随机数,再加上hello消息中的随机,三个随机数通过一个密钥导出器最终导出一个对称密钥。

pre master的存在,在于SSL协议不信任每个主机都能产生完全随机的随机数,如果随机数不随机,那么pre master secret就有可能被猜出来,那么仅适用pre master secret作为密钥就不合适了,因此必须引入新的随机因素,那么客户端和服务器加上pre master secret三个随机数一同生成的密钥就不容易被猜出了,一个伪随机可能完全不随机,可是是三个伪随机就十分接近随机了,每增加一个自由度,随机性增加的可不是一。

还有什么密钥交换算法?

ECDHE :“短暂 - 椭圆曲线 - 迪菲 - 赫尔曼”算法(ephemeral Elliptic Curve Diffie–Hellman)。

RSA 是比较传统的密钥交换算法,它不具备前向安全的性质,因此现在很少服务器使用的。而 ECDHE 算法具有前向安全,所以被广泛使用。

客户端和服务端双方在一个可以被窥探的信道下给双方建立起一个相同的密钥。

发送公有的参数,保留私有的参数,双方经过计算可以得到一个一致的结果,一致的结果可以用作会话密钥,这个运算的逆运算复杂度很高,所以不会泄露。

为什么参数被窥探后还能够达到保密的效果?

Deffie-Hellman算法的有效性依赖于计算离散对数的难度。

https总共有多少次握手了?

SSL是在TCP之上,在HTTP之下。

TCP先三次握手建立连接,然后SSL四次握手,最后进行HTTP通信。

https的身份认证是如何实现的?

通过数字证书验证通信方身份。

如何验证证书是否可信?

服务端证书都是向CA这类的权威机构申请的,CA颁发证书时会用根证书的私钥去给申请的证书的哈希摘要做加密作为申请的证书的签名。

客户端收到服务端的证书后,查询证书的颁发者,一直寻找到根证书,然后用根证书里的公钥去解密其颁发的证书的签名,对比证书文件的哈希摘要是否一致,这样就验证了身份没有被篡改。

数字证书验证身份的过程是怎样的?

服务端向CA(数字证书认证机构)提交自己的公钥申请数字证书,CA颁发证书,并把服务端的公钥内嵌在颁发的证书中,再对整个证书计算哈希摘要,用一个私钥加密这个哈希摘要,确保了申请到的证书在传输过程中可以校验证书是否被篡改,校验是通过CA提供的根证书里的公钥解密服务端证书中的哈希摘要,再计算服务端证书文件的哈希摘要,对比两者是否一致,根证书一般已事先内嵌客户端。

证书可以有信任链。

https单向认证是什么?

  1. 客户端发起请求,传送支持的TLS版本、摘要算法、随机数等信息
  2. 服务端返回确认的TLS版本、摘要算法,并再发送生成的随机数、服务端的证书
  3. 客户端验证证书是否可信,不可信则弹窗警告,可信则用随机数生成一个用于对称加密的密钥,用于接下来加密报文,用服务端证书的公钥加密发送给服务端
  4. 服务端接收到会话密钥,用私钥解密,得到会话密钥,返回ACK确认消息

https双向认证是什么?

单向认证是客户端验证服务端的证书来检验服务端身份是否合法。

双向认证是服务端也要求验证客户端的证书来检验客户端的身份是否合法。服务端会在发送证书给客户端时,要求客户端也发送证书给服务端。

双向认证使用场景:

使用网上银行可能需要在电脑上插U盾之类的东西,就是生成客户端证书的。

UDP报文头

UDP 协议头中只包含 4 个字段,分别是源端口、目的端口、长度和校验码,其中每一个字段都占 16 比特,即 2 字节,这 4 个字段的作用如下:

  • 源端口是一个可选字段,它表示发送方进程的端口号,接收方可以使用该字段(不一定准确)向发送方发送信息;
  • 目的端口是数据报接收方的端口号,它只在目标的 IP 地址下才有意义;
  • 长度是协议头和数据报中数据长度的总和,表示整个数据报的大小;
  • 校验码使用 IP 首部、UDP 首部和数据报中的数据进行计算,接收方可以通过校验码验证数据的准确性,发现传输过程中出现的问题。

UDP特点

简单高效:

  1. 面向无连接
  2. 报文头简单,只有8字节
  3. 对应用层数据不分段

关于分段:

应用层交给UDP多长的报文,UDP就照样发送,即一次发送一个报文。若报文太长,则到达IP层需要分片,降低效率。若太短,则传输效率会下降(举个例子,1字节的数据,加上20字节的UDP首部,再加上20字节的IP层首部,结果发了41字节的包只完成了1字节有用数据的交付)。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。

UDP和TCP区别?

UDP 在传送数据之前不需要先建立连接,远地主机在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可靠交付,但在某些情况下 UDP 确是一种最有效的工作方式(一般用于即时通信),比如: QQ 语音、 QQ 视频 、直播等等。

TCP 提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。 TCP 不提供广播或多播服务。由于 TCP 要提供可靠的,面向连接的传输服务(TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源),这一难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增大很多,还要占用许多处理机资源。TCP 一般用于文件传输、发送和接收邮件、远程登录等场景。

最大报文长度(Maximum Segment Size)是什么?

TCP报文数据部分的最大长度,不包含TCP头。

这个MSS指的是TCP报文段中数据部分的最大长度,并不是整个TCP报文段长度。整个TCP报文段长度 = TCP首部长度 + TCP数据部分长度。

如果上层交付下来的数据太大,就对其进行数据分块。这个分块过程是在传输层完成的,在接收端的传输层对分块的TCP报文段的数据部分进行重组。

最大报文长度(Maximum Segment Size)存储在什么地方?

TCP首部中的Options字段

为什么要搞一个最大报文长度(Maximum Segment Size)来限制TCP报文数据长度?

当TCP数据长度超过MSS的时候,TCP会做分段。

这样是为了防止IP层对TCP报文分片。

IP层对TCP报文进行分片会产生什么问题?

IP分片后,接收端需要等到所有IP包到达后,才能确定整个TCP报文收到了,然后发送ACK报文,这样比较低效,也增加了控制的复杂性。

IP分片后,如果有几个分片丢失了,只能重传所有分片,这样是很浪费流量的。

首先因为IP层没有超时重传机制,IP层是面向无连接的,所以IP层没有办法只重传丢失的分片。

其次IP分片后,只有第一片有TCP的报文头,后面的IP分片没有TCP报文头,所以TCP也没办法识别到底是哪一段丢失了,也就没办法做超时重传,也没办法确定顺序,如果TCP对报文分段,每段都有TCP报文头,报文丢失就知道丢失的是哪一个。

为什么IP数据长度过长要分片?

IP分组达到数据链路层,整个IP分组被作为数据链路层以太网帧的数据部分。

以太网帧对数据部分有最大长度的限制,所以IP数据数据过长要分片。

数据链路层的以太网帧的数据部分的最大长度(最大IP包长)称为最大传输单元(Maximum Transmit Unit)

以太网对数据帧的限制一般都是 1500 字节,在一般情况下,IP 主机的路径 MTU 都是 1500,去掉 IP 首部的 20 字节,如果待传输的数据大于 1480 节,那么该 IP 协议就会将数据包分片传输。

TCP拆分数据的意义?

TCP 协议拆分数据是为了保证传输的可靠性和顺序,作为可靠的传输协议,为了保证数据的传输顺序,它需要为每一个数据段增加包含序列号的 TCP 协议头,如果数据段大小超过了 IP 协议的 MTU 限制, 就会带来更多额外的重传和重组开销,影响性能。

最大传输单元(Maximum Transmit Unit)是什么?

数据链路层的以太网帧的数据部分的最大长度(IP包长)

为什么以太网MTU默认值被设定为1500?

MTU为什么不能太大呢?

以太网早期的拓扑结构是总线型结构,总线型传输有个特点就是,同一时刻只能有一个主机占用总线传输,如果一个以太网帧过长,这个主机就会一直占用总线,其他主机只能干等着,对于其他主机来说数据延迟就变大了。

而且传输的数据包越大,数据位出错的可能性越大,出错了就要重传,重传又要占用很长时间的带宽,降低了整体吞吐量。

所以MTU不能太大。

如果MTU太小,各种协议头也要占用数据包大小的,这样每个报文的实际有效传输效率也会很低。

所以MTU要搞一个适中值。

1518这个值是考虑到传输效率以及传输时间而折中选择的一个值,并且由于目前网络链路中的节点太多,其中某个节点的MTU值如果和别的节点不一样,就很容易带来拆包重组的问题,甚至会导致丢包无法发送。

为什么MTU最小为64字节?

以CSMA/CD作为MAC算法的一类LAN称为以太网。CSMA/CD冲突避免的方法:先听后发、边听边发、随机延迟后重发。一旦发生冲突,必须让每台主机都能检测到。关于最小发送间隙和最小帧长的规定也是为了避免冲突。

考虑如下的情况,主机发送的帧很小,而两台冲突主机相距很远。在主机A发送的帧传输到B的前一刻,B开始发送帧。这样,当A的帧到达B时,B检测到冲突,于是发送冲突信号。假如在B的冲突信号传输到A之前,A的帧已经发送完毕,那么A将检测不到冲突而误认为已发送成功。由于信号传播是有时延的,因此检测冲突也需要一定的时间。这也是为什么必须有个最小帧长的限制。

按照标准,10Mbps以太网采用中继器时,连接的最大长度是2500米,最多经过4个中继器,因此规定对10Mbps以太网一帧的最小发送时间为51.2微秒。这段时间所能传输的数据为512位,因此也称该时间为512位时。这个时间定义为以太网时隙,或冲突时槽。512位=64字节,这就是以太网帧最小64字节的原因。

512位时是主机捕获信道的时间。如果某主机发送一个帧的64字节仍无冲突,以后也就不会再发生冲突了,称此主机捕获了信道。

由于信道是所有主机共享的,如果数据帧太长就会出现有的主机长时间不能发送数据,而且有的发送数据可能超出接收端的缓冲区大小,造成缓冲溢出。为避免单一主机占用信道时间过长,规定了以太网帧的最大帧长为1500。

100Mbps以太网的时隙仍为512位时,以太网规定一帧的最小发送时间必须为5.12μs。

1000Mbps以太网的时隙增至512字节,即4096位时,4.096μs。

以太网数据帧(802.3)最大与最小长度。

最大报文长度(Maximum Segment Size)是怎么确定的?

MSS由设备的MTU确定,不能超过MTU,否则TCP报文会被IP层分片

TCP报文的长度在TCP三次握手中那一次确定的?

MSS值只会出现在SYN报文中(不要问我为什么,我也不知道为什么只出现SYN报文中),即SYN=1时,才会有MSS字段值。

当客户端想要以TCP方式从服务器端下载数据时,

(1)首先客户端会发送一个SYN请求报文,这个SYN报文的“选项”字段中会有MSS值(MSS = MTU - IP首部长度 - TCP首部长度)。该MSS值是为了告知对方最大的发送数据大小。

(2)当服务器端收到SYN报文后,会向请求端返回SYN+ACK(同步确认报文)报文,其中的“选项”字段也会有MSS值。

(3)通信双方选择SYN和SYN+ACK报文中最小的MSS最为此次TCP连接的MSS,从而达到通信双发协商MSS的效果。

在第二次握手后就可以确定TCP中最大传输报文(MSS)大小。

MTU是怎么确定的?

每一个物理设备都有自己的 MTU,最终的MTU由整个链路上 MTU 最小的物理设备决定。

路径MTU发现(PMTUD: Path MTU Discovery )。

什么情况下才算发生了网络拥塞?

发送方有TCP报文超时了没有收到ACK确认报文

拥塞控制的过程是怎样的?

慢启动、拥塞避免、快重传、快恢复

拥塞窗口,拥塞窗口大小cwnd(congestion window)表示可以发送的报文字节数,单位是一个MSS(最大报文长度)

慢启动

拥塞窗口初始大小为1,每收到一个ACK,拥塞窗口大小+1,实际效果就是每过一个RTT,拥塞窗口大小为原来的2倍,以指数增长率增长,先慢后快,达到慢开始门限ssthresh则停止慢开始,执行拥塞避免算法。

翻倍后如果大小超过阈值则窗口大小设为阈值。

例如:

初始窗口大小为1

发送第一个报文M0,收到ACK1后窗口大小+1,变为2

发送M1、M2,收到ACK2、ACK3后,窗口大小+2,变为4

再发送4个报文,收到4个ACK后,窗口大小+4,变为8

拥塞避免

每过一个RTT(受到一个ACK),拥塞窗口大小+1

线性增长,避免增长过快

发生拥塞

发送方有报文超时,未收到ACK确认报文

无论是在慢开始阶段还是拥塞避免阶段,做出以下调整:

  1. 把慢开始门限ssthresh设置为出现拥塞时发送方拥塞窗口大小的一半(不小于2)
  2. 拥塞窗口大小设为1,执行慢开始

目的:

迅速减少发送方发送到网络的报文数,在发生拥塞时路由器有足够的时间把队列中挤压的报文发完

快速重传

发送方收到了三个序号重复的ACK报文,认为该序号的报文丢失,则不等待定时器结束就直接重传该序号的报文

快速恢复

发送方收到了三个序号重复的ACK报文,认为该序号的报文丢失,慢开始门限设置为拥塞窗口大小的一半,拥塞窗口不设置为1,也设置为原窗口大小的一半,并执行拥塞避免算法,线性增大拥塞窗口。

因为跳过了拥塞窗口从1开始的慢开始过程,所以称为快恢复。

发送方窗口大小为 流量控制的滑动窗口和拥塞窗口 的较小值

快速重传有什么问题?

与基于定时器的超时重传拥有同样的问题,即:

不确定应该重发哪些序号的报文,如果只重发一个可能会拖慢整体传输时长,如果重发多个可能会浪费流量、增加整体传输时长,需要知道哪些序号的报文是丢失的,只重发丢失的报文。

发送方收到同一个序号的多个ACK确认,发送方并不清楚这些ACK是由哪些序号的报文传输成功后ACK回来的,可能会重发接收方已经收到的报文,浪费流量。

带选择确认的重传(SACK)解决了什么?

一端在发送ACK时通过在TCP报文头部增加SACK(Selective Acknowledgment)选项,告知对方已经收到的包序号区间范围,让对方只重传丢失的报文,并且可以一次性集中重传,避免了网络流量的浪费,也避免了长时间的超时等待。

SACK需要客户端和服务的都开启该功能

为什么要有流量控制?

双端通信的时候,发送方发送数据速度和接收方处理数据速度不一定相等;

如果发送过快,接收方处理不过来,接收方只能把数据存在缓冲区,缓冲区满了后再收到数据就只能丢弃;

所以需要控制发送方发送数据的速率,让接收方与发送方处于一种动态的平衡中才好。

对于发送方发送速率的控制,称为流量控制。

滑动窗口大小是如何确定的?

发送数据的一方收到对方发送的ACK报文,其报文头中的窗口大小表示对方的缓冲区大小还能存放多少字节的数据,发送方拿这个窗口大小与本身的拥塞窗口大小在一起取最小值,作为自己滑动窗口的大小。

达到:

  1. 避免对方缓冲区存不下发送过多的数据而丢弃,进而浪费网络带宽和流量
  2. 避免产生网络拥塞

滑动窗口中的内容

滑动窗口内包含两类报文

  1. 已发送但未收到确认
  2. 允许发送但尚未发送

滑动窗口什么时候开始滑动?

窗口左侧的报文收到了ACK报文确认,窗口就往右滑动

如果发送端滑动窗口大小为0,不发送数据了会怎样?

实际上,为了避免此问题的产生,发送端主机会时不时的发送一个叫做窗口探测的数据段,此数据段仅包含一个字节来获取最新的窗口大小信息。

超时重传的等待时间设置为多少合适?

超时重传的等待时间:Retransmission Time Out, RTO。

等待时间过长,若丢包严重,则传输的吞吐量低。

等待时间过短,会导致频繁的发包,增加网络拥堵,占用带宽。

等待时间只能尽可能的短,按照正常的节奏,一端发送报文到收到ACK报文的时间(往返时间Round Trip Time)就是最短的等待时间,RTO可以略微大于RTT,以增加一点容错范围。

但RTT并不是固定的,因为网络拥堵状况随时在发生变化,所以RTT应当不停的动态计算,有一系列的算法支持。

基于定时器的超时重传存在什么问题?

会重发接收方已经收到的报文,浪费带宽,通信时间被拖长。

接收方发送的ACK报文中确认的报文序号有个特点,就是这个序号之前的报文接收方一定都已经收到了。

这样就会导致一个问题,如果发送方发了一系列序号连续的报文,接收方只有中间的几个报文没有收到,那么收到序号较大的报文后回应的ACK里的确认序号是较小的,这会导致发送方无法确认较大序号的报文接收方有没有收到。

此时发送方该重发哪些报文就成了问题,此时有两种策略:

一种策略是只重传ACK期待的序号的报文,序号较大报文暂时不重发,等待它们的定时器超时了再重发,如果较大序号的报文接收方都接受到了倒没问题,但如果较大序号的报文接收方都没收到,一个个等待报文超时无疑是拖慢了整体的传输时间。

另一种策略就是重传ACK序号后所有的报文,如果丢包严重,这种做法效率挺高,但如果丢包并不严重,这种做法会浪费很多流量。

如果能知道没有收到的报文序号是哪些就很好办了,用SACK可以解决这个问题。

例如发送方发了序号为1、2、3、4、5、6的报文,接收方收到了1、2、4、5、6,唯独为没有收到3(3可能卡在某个网络节点上),在接收到1后回应ACK 2,接收2后回应ACK 3,接收到4、5、6,也是回应ACK 3,此时发送方对3、4、5、6的定时器都会超时,然后都会重发3、4、5、6,但实际4、5、6已经接收到了,重发4、5、6其实是浪费带宽,也增加了整体传输时长。

TCP为什么要给报文编号?不编号会有什么问题?

保证报文的不重复、不丢失、不乱序。

作为一个可靠的传输层协议,TCP 需要在不稳定的网络环境中构建一个可靠的传输层,网络的不确定性可能会导致数据包的缺失和顺序颠倒等问题,常见的问题可能包括:

  • 数据包被发送方多次发送造成数据的重复;
  • 数据包在传输的过程中被路由或者其他节点丢失;
  • 数据包到达接收方可能无法按照发送顺序;

为了解决上述这些可能存在的问题,TCP 协议要求发送方在数据包中加入『序列号』字段,有了数据包对应的序列号,我们就可以:

  • 接收方可以通过序列号对重复的数据包进行去重;
  • 发送方会在对应数据包未被 ACK 时进行重复发送;
  • 接收方可以根据数据包的序列号对它们进行重新排序;

序列号在 TCP 连接中有着非常重要的作用,初始序列号作为 TCP 连接的一部分也需要在三次握手期间进行初始化,由于 TCP 连接通信的双方都需要获得初始序列号,所以它们其实需要向对方发送 SYN 控制消息并携带自己期望的初始化序列号 SEQ,对方在收到 SYN 消息之后会通过 ACK 控制消息以及 SEQ+1 来进行确认。

为什么ACK报文中确认序号一定要是已经接收到的连续报文的最大的序号?改为已经接收到的不连续的报文的最大序号会有什么问题?

TCP把数据视为一个无结构但有序的字节流,序号建立在传输的字节流之上,而不建立在报文段之上。

接收端给发送端的Ack确认只会确认最后一个连续的包,比如,发送端发了1,2,3,4,5一共五份数据,接收端收到了1,2,于是回ack 3,然后收到了4(注意此时3没收到),此时的TCP会怎么办?我们要知道,因为正如前面所说的,SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。

因为TCP有窗口拥塞控制,窗口的拥塞是基于字节的,所以必须给字节编号,如果给数据包编号,拥塞窗口无法调节大小。

TCP三次握手中的初始序号是固定的吗?

不是固定的,是动态增长+随机选取,可以避免相邻的TCP会话的序列号有重叠,否则并不知道报文是旧连接的还是新连接的

如果初始序号是固定的,攻击者就可以很容易伪造TCP报文发起攻击,初始序号需要动态生成,提高攻击成本。

源IP地址、源端口、目标IP地址、目标端口唯一确定一个TCP会话,允许刚释放的TCP端口重用,如果旧会话中的分组报文仍然在网络中,新会话建立后可能会收到旧会话产生的分组报文,如果序号是一直递增的,就可以分辨出报文是属于旧会话的还是新会话的。

关于ISN的初始化。ISN是不能hard code的,不然会出问题的——比如:如果连接建好后始终用1来做ISN,如果client发了30个segment过去,但是网络断了,于是 client重连,又用了1做ISN,但是之前连接的那些包到了,于是就被当成了新连接的包,此时,client的Sequence Number 可能是3,而Server端认为client端的这个号是30了。全乱了。RFC793中说,ISN会和一个假的时钟绑在一起,这个时钟会在每4微秒对ISN做加一操作,直到超过2^32,又从0开始。这样,一个ISN的周期大约是4.55个小时。因为,我们假设我们的TCP Segment在网络上的存活时间不会超过Maximum Segment Lifetime(缩写为MSL – Wikipedia语条),所以,只要MSL的值小于4.55小时,那么,我们就不会重用到ISN。

报文编号和确认应答机制存在什么问题?

队头阻塞。

TCP数据包是有序传输,中间一个数据包丢失,会等待该数据包重传,造成后面的数据包的阻塞。

传统Blocking IO存在什么问题?

传统BIO不管是读入还是写出,缓冲区的存在必然涉及copy的过程,而如果涉及双流操作,比如从一个输入流读入,再写入到一个输出流,那么这种情况下,在缓冲存在的情况下,数据走向是:

-> 从输入流读出到缓冲区

-> 从输入流缓冲区copy到 b[]

-> 将 b[] copy 到输出流缓冲区

-> 输出流缓冲区读出数据到输出流

上面情况存在冗余copy操作。

Okio解决了什么?

Okio减少了冗余的复制。

Okio从输入流读取到的数据用一个Segment链表存储,写入数据会把Segment链表中的结点拿过来,不用做中间拷贝。

Segment 实际上也是对 byte[] 进行封装,再通过各种属性来记录各种状态。在读写时,如果可以,将Segment整体作为数据传授媒介,这样就没有具体数据的copy过程,而是交换了对应的Segment引用,这是减少数据copy进而交换数据的关键。

Okio并没有打算优化底层IO方式以及替代原生IO方式,Okio优化了缓冲策略以减轻内存压力和性能消耗,并且对于部分IO场景,提供了更友好的API。

Okio内部实现

Okio底层还是用Java标准的io流来操作,只是缓存机制是自己实现了一套,避免了减少一次中间拷贝过程。

不管是RealBufferedSource还是RealBufferedSink,内部本质上都是通过Buffer这个类来实现缓存功能,而Buffer内部又是通过Segment这个核心类来缓存读取到的数据,Buffer就是一个管理者用来调度Segment。

Segment是双向链表中的一个结点,Buffer类存储了链表的头结点。

Segment中有个byte数组存储了真正的数据,最大字节数是8K,还有一些指针,表明了有效数据的范围。

有数据超过8K,就会创建新的Segment,跟前面的Segment连起来

1
2
3
4
BufferedSource bufferedSource = Okio.buffer(Okio.source(src));
BufferedSink bufferedSink = Okio.buffer(Okio.sink(dest));
bufferedSink.writeAll(bufferedSource);
bufferedSink.close();
  1. 先从原始的Source对象读数据,到RealBufferedSource的buffer对象,通过尾插法创建Segment,数据依次填满一个个的Segment对象里的byte数组。
  2. RealBufferedSource中的buffer中的Segment转移到RealBufferedSink中的buffer中
  3. 最后遍历RealBufferedSink的buffer中Segment链表写到OutputStream中

(1) Source中需要传递的数据是”满”的情况,也就是8k都是有效数据,这种情况直接从source的buffer中拿到Segment,然后添加到sink的buffer上即可,和java io流相比,省去了中间的一次临时buffer拷贝,从而提高的读写效率

(2) Source中需要传递的数据不”满”的情况,通过pos和limit可以定位到有效数据区间,和Sink中buffer的尾Segment有效数据进行对比,如果两个Segment中的有效数据可以合并到一个Segment中那么会进行数据整理,多余的Segment会被回收到。

如果两个Segment的有效数据总和超过8k,那么直接将Source中的Segment链接到Sink中buffer的尾部即可。

(3) Source的buffer中的Segment只是传递部分数据,如5K的数据值传递其中2K,okio内部会通过split方法将Segment分成2K和3K两个Segment,然后将2K的Segment参照第二种情况和Sink中的Segment进行合并。

TCP断开连接为什么要四次挥手?为什么不能三次挥手?

因为服务端接收到客户端断开连接的请求报文时,服务端可能还有数据要发送,要等待服务端把数据发送完了,才能向客户端发起断开连接的请求,此时服务端只能先回复客户端一个ACK报文,告知客户端我已经收到了断开连接的请求,你可以不用重复发送断开连接的请求了,等我把数据发完再通知你我可以断开连接。服务端的ACK和FIN指令必须分开发送,所以需要四次挥手,而不能三次。

TCP断开连接时,客户端发送最后一个ACK给服务端后,为什么要进入TIME_WAIT等待一段时间才能关闭连接?为什么又要等待2MSL?

  1. 防止延迟的数据段被其他使用相同源地址、源端口、目的地址以及目的端口的 TCP 连接收到;
  2. 保证 TCP 连接的远程被正确关闭,即等待被动关闭连接的一方收到 FIN 对应的 ACK 消息;

阻止延迟报文

TCP断开连接时,如果客户端在接收到服务端的FIN自己发送完ACK后直接关闭连接,服务端可能没有收到ACK,会超时重传FIN,此时由于客户端已经关闭了连接,如果客户端有新的程序需要向该服务端的同一个端口传输数据,会重新建立TCP连接,并且这个新TCP连接无法和上一个关闭的TCP连接区分开来,因为源IP地址、源端口、目标IP地址、目标端口都一样,此时新连接传输数据过程中如果收到上次连接的传来的FIN,新连接的状态就异常了,新连接SYN握手会失败,服务端会发送RST阻止客户端连接的建立,因为旧的连接并没有关闭。。也有可能会收到其他服务端发送的报文。

保证连接关闭

服务端发送FIN报文后,如果没有接收到ACK会超时重传FIN,直到收到ACK了才会关闭连接。服务端要等待什么时候超时重传呢?服务端发送FIN后,最多经过一个MSL到达客户端,客户端发送ACK最多经过一个MSL到达服务端,所以服务端等待2MSL没有收到ACK就重发FIN报文,FIN报文最多经过一个MSL到达客户端,所以从客户端发送ACK给服务端要至少等待2MSL,以取保服务端收到了自己发送的ACK,不会超时重传FIN

为什么TIME_WAIT要等待2MSL?

MSL(Maximum Segment Lifetime,最大报文生存时间)是指任何报文在网络上存在的最长时间,超过这个时间的报文都会被丢弃。

网络中可能存在来自发送方的报文,接收方收到后需要回ACK,一来一回两个MSL,等待两个MSL可以确保网络中一定不存在报文。

服务端发送FIN后,会计时等待接受ACK,等待的时间大约是一个RTT多一点的时间,也就是2MSL左右,如果FIN没丢失,客户端发送ACK,发送ACK时服务端已经等待了一个MSL左右,如果ACK丢失了,服务端再等待一个MSL,超时未收到ACK,则会重传FIN,FIN再次到达客户端也需要一个MSL左右,这样客户端至少得等待2MSL,以让服务端重传的FIN达到,如果2MSL后没有收到服务端的FIN,说明服务端已经接收到ACK了,服务端已经关闭连接,同时网络也不可能有此次连接的任何报文了,客户端就可以安全的关闭连接。

进入TIME_WAIT有什么问题?

大并发量的短时间传输的连接,会造成连接等待时间变长。